diff --git a/boa_ast/Cargo.toml b/boa_ast/Cargo.toml index 1945d56222..52bd97a4b2 100644 --- a/boa_ast/Cargo.toml +++ b/boa_ast/Cargo.toml @@ -13,7 +13,6 @@ rust-version.workspace = true [features] serde = ["dep:serde", "boa_interner/serde", "bitflags/serde", "num-bigint/serde"] arbitrary = ["dep:arbitrary", "boa_interner/arbitrary", "num-bigint/arbitrary"] -temporal = [] [dependencies] boa_interner.workspace = true diff --git a/boa_ast/src/lib.rs b/boa_ast/src/lib.rs index 5f76c0f539..921eea0392 100644 --- a/boa_ast/src/lib.rs +++ b/boa_ast/src/lib.rs @@ -91,8 +91,6 @@ pub mod operations; pub mod pattern; pub mod property; pub mod statement; -#[cfg(feature = "temporal")] -pub mod temporal; pub mod visitor; use boa_interner::{Interner, ToIndentedString, ToInternedString}; diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index f4525fd9cb..e3b8474368 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -47,7 +47,7 @@ trace = ["js"] annex-b = ["boa_parser/annex-b"] # Stage 3 proposals -temporal = ["boa_parser/temporal", "dep:icu_calendar"] +temporal = ["dep:icu_calendar"] # Enable experimental features, like Stage 3 proposals. experimental = ["temporal"] diff --git a/boa_engine/src/builtins/temporal/error.rs b/boa_engine/src/builtins/temporal/error.rs index 3b86b21a17..0781b6bd31 100644 --- a/boa_engine/src/builtins/temporal/error.rs +++ b/boa_engine/src/builtins/temporal/error.rs @@ -8,6 +8,7 @@ impl From for JsNativeError { ErrorKind::Range => JsNativeError::range().with_message(value.message()), ErrorKind::Type => JsNativeError::typ().with_message(value.message()), ErrorKind::Generic => JsNativeError::error().with_message(value.message()), + ErrorKind::Syntax => JsNativeError::syntax().with_message(value.message()), } } } diff --git a/boa_engine/src/builtins/temporal/plain_date/mod.rs b/boa_engine/src/builtins/temporal/plain_date/mod.rs index 517e02f162..a9679c8c1a 100644 --- a/boa_engine/src/builtins/temporal/plain_date/mod.rs +++ b/boa_engine/src/builtins/temporal/plain_date/mod.rs @@ -1,8 +1,6 @@ //! Boa's implementation of the ECMAScript `Temporal.PlainDate` builtin object. #![allow(dead_code, unused_variables)] -use std::str::FromStr; - use crate::{ builtins::{ options::{get_option, get_options_object}, @@ -16,14 +14,8 @@ use crate::{ string::{common::StaticJsStrings, utf16}, Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, }; -use boa_parser::temporal::{IsoCursor, TemporalDateTimeString}; use boa_profiler::Profiler; -use boa_temporal::{ - calendar::{AvailableCalendars, CalendarSlot}, - date::Date as InnerDate, - datetime::DateTime, - options::ArithmeticOverflow, -}; +use boa_temporal::{date::Date as InnerDate, datetime::DateTime, options::ArithmeticOverflow}; use super::calendar; @@ -517,32 +509,17 @@ pub(crate) fn to_temporal_date( }; // 6. Let result be ? ParseTemporalDateString(item). - let result = TemporalDateTimeString::parse( - false, - &mut IsoCursor::new(&date_like_string.to_std_string_escaped()), - ) - .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; - // 7. Assert: IsValidISODate(result.[[Year]], result.[[Month]], result.[[Day]]) is true. // 8. Let calendar be result.[[Calendar]]. // 9. If calendar is undefined, set calendar to "iso8601". - let identifier = result.date.calendar.unwrap_or("iso8601".to_string()); - // 10. If IsBuiltinCalendar(calendar) is false, throw a RangeError exception. - let _ = AvailableCalendars::from_str(identifier.to_ascii_lowercase().as_str())?; - // 11. Set calendar to the ASCII-lowercase of calendar. - let calendar = CalendarSlot::Identifier(identifier.to_ascii_lowercase()); - // 12. Perform ? ToTemporalOverflow(options). - let _ = get_option::(&options_obj, utf16!("overflow"), context)?; - // 13. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar). - Ok(PlainDate::new(InnerDate::new( - result.date.year, - result.date.month, - result.date.day, - calendar, - ArithmeticOverflow::Reject, - )?)) + let result = date_like_string + .to_std_string_escaped() + .parse::() + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + Ok(PlainDate::new(result)) } diff --git a/boa_engine/src/builtins/temporal/time_zone/mod.rs b/boa_engine/src/builtins/temporal/time_zone/mod.rs index 2cf188c8c1..efbc307344 100644 --- a/boa_engine/src/builtins/temporal/time_zone/mod.rs +++ b/boa_engine/src/builtins/temporal/time_zone/mod.rs @@ -364,10 +364,10 @@ pub(super) fn create_temporal_time_zone( /// [spec]: https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring #[allow(clippy::unnecessary_wraps, unused)] fn parse_timezone_offset_string(offset_string: &str, context: &mut Context) -> JsResult { - use boa_parser::temporal::{IsoCursor, TemporalTimeZoneString}; + use boa_temporal::parser::{Cursor, TemporalTimeZoneString}; // 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset). - let parse_result = TemporalTimeZoneString::parse(&mut IsoCursor::new(offset_string))?; + let parse_result = TemporalTimeZoneString::parse(&mut Cursor::new(offset_string))?; // 2. Assert: parseResult is not a List of errors. // 3. Assert: parseResult contains a TemporalSign Parse Node. diff --git a/boa_parser/Cargo.toml b/boa_parser/Cargo.toml index 0207b59d84..bed682cb48 100644 --- a/boa_parser/Cargo.toml +++ b/boa_parser/Cargo.toml @@ -25,7 +25,6 @@ icu_properties.workspace = true [features] annex-b = [] -temporal = ["boa_ast/temporal"] [lints] workspace = true diff --git a/boa_parser/src/lib.rs b/boa_parser/src/lib.rs index ab6370c516..3649d4a2e9 100644 --- a/boa_parser/src/lib.rs +++ b/boa_parser/src/lib.rs @@ -28,8 +28,6 @@ pub mod error; pub mod lexer; pub mod parser; mod source; -#[cfg(feature = "temporal")] -pub mod temporal; pub use error::Error; pub use lexer::Lexer; diff --git a/boa_temporal/src/date.rs b/boa_temporal/src/date.rs index 054940790c..026e654d20 100644 --- a/boa_temporal/src/date.rs +++ b/boa_temporal/src/date.rs @@ -1,14 +1,15 @@ //! The `PlainDate` representation. use crate::{ - calendar::CalendarSlot, + calendar::{AvailableCalendars, CalendarSlot}, datetime::DateTime, duration::{DateDuration, Duration}, iso::{IsoDate, IsoDateSlots}, options::{ArithmeticOverflow, TemporalUnit}, - TemporalResult, + parser::parse_date_time, + TemporalError, TemporalResult, }; -use std::any::Any; +use std::{any::Any, str::FromStr}; /// The `Temporal.PlainDate` equivalent #[derive(Debug, Default, Clone)] @@ -218,3 +219,28 @@ impl Date { self.contextual_difference_date(other, largest_unit, &mut ()) } } + +// ==== Trait impls ==== + +impl FromStr for Date { + type Err = TemporalError; + + fn from_str(s: &str) -> Result { + let parse_record = parse_date_time(s)?; + + let calendar = parse_record.calendar.unwrap_or("iso8601".to_owned()); + let _ = AvailableCalendars::from_str(calendar.to_ascii_lowercase().as_str())?; + + let date = IsoDate::new( + parse_record.date.year, + parse_record.date.month, + parse_record.date.day, + ArithmeticOverflow::Reject, + )?; + + Ok(Self::new_unchecked( + date, + CalendarSlot::Identifier(calendar), + )) + } +} diff --git a/boa_temporal/src/datetime.rs b/boa_temporal/src/datetime.rs index cca2164929..02c9d63669 100644 --- a/boa_temporal/src/datetime.rs +++ b/boa_temporal/src/datetime.rs @@ -1,10 +1,13 @@ //! Temporal implementation of `DateTime` +use std::str::FromStr; + use crate::{ calendar::CalendarSlot, iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime}, options::ArithmeticOverflow, - TemporalResult, + parser::parse_date_time, + TemporalError, TemporalResult, }; /// The `DateTime` struct. @@ -93,3 +96,39 @@ impl DateTime { &self.calendar } } + +// ==== Trait impls ==== + +impl FromStr for DateTime { + type Err = TemporalError; + + fn from_str(s: &str) -> Result { + let parse_record = parse_date_time(s)?; + + let calendar = parse_record.calendar.unwrap_or("iso8601".to_owned()); + + let time = if let Some(time) = parse_record.time { + IsoTime::from_components( + i32::from(time.hour), + i32::from(time.minute), + i32::from(time.second), + time.fraction, + )? + } else { + IsoTime::default() + }; + + let date = IsoDate::new( + parse_record.date.year, + parse_record.date.month, + parse_record.date.day, + ArithmeticOverflow::Reject, + )?; + + Ok(Self::new_unchecked( + date, + time, + CalendarSlot::Identifier(calendar), + )) + } +} diff --git a/boa_temporal/src/duration.rs b/boa_temporal/src/duration.rs index fa2c9f4962..a324af4f82 100644 --- a/boa_temporal/src/duration.rs +++ b/boa_temporal/src/duration.rs @@ -6,11 +6,12 @@ use crate::{ date::Date, datetime::DateTime, options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit}, + parser::{duration::parse_duration, Cursor}, utils, zoneddatetime::ZonedDateTime, TemporalError, TemporalResult, NS_PER_DAY, }; -use std::any::Any; +use std::{any::Any, str::FromStr}; // ==== `DateDuration` ==== @@ -1688,3 +1689,55 @@ impl Duration { Ok(result) } } + +// ==== FromStr trait impl ==== + +impl FromStr for Duration { + type Err = TemporalError; + + fn from_str(s: &str) -> Result { + let parse_record = parse_duration(&mut Cursor::new(s))?; + + let minutes = if parse_record.time.fhours > 0.0 { + parse_record.time.fhours * 60.0 + } else { + f64::from(parse_record.time.minutes) + }; + + let seconds = if parse_record.time.fminutes > 0.0 { + parse_record.time.fminutes * 60.0 + } else if parse_record.time.seconds > 0 { + f64::from(parse_record.time.seconds) + } else { + minutes.rem_euclid(1.0) * 60.0 + }; + + let milliseconds = if parse_record.time.fseconds > 0.0 { + parse_record.time.fseconds * 1000.0 + } else { + seconds.rem_euclid(1.0) * 1000.0 + }; + + let micro = milliseconds.rem_euclid(1.0) * 1000.0; + let nano = micro.rem_euclid(1.0) * 1000.0; + + let sign = if parse_record.sign { 1f64 } else { -1f64 }; + + Ok(Self { + date: DateDuration::new( + f64::from(parse_record.date.years) * sign, + f64::from(parse_record.date.months) * sign, + f64::from(parse_record.date.weeks) * sign, + f64::from(parse_record.date.days) * sign, + ), + time: TimeDuration::new( + f64::from(parse_record.time.hours) * sign, + minutes.floor() * sign, + seconds.floor() * sign, + milliseconds.floor() * sign, + micro.floor() * sign, + nano.floor() * sign, + ), + }) + } +} diff --git a/boa_temporal/src/error.rs b/boa_temporal/src/error.rs index 75957a2520..18f38873b9 100644 --- a/boa_temporal/src/error.rs +++ b/boa_temporal/src/error.rs @@ -12,6 +12,8 @@ pub enum ErrorKind { Type, /// RangeError Range, + /// SyntaxError + Syntax, } impl fmt::Display for ErrorKind { @@ -20,6 +22,7 @@ impl fmt::Display for ErrorKind { Self::Generic => "Error", Self::Type => "TypeError", Self::Range => "RangeError", + Self::Syntax => "SyntaxError", } .fmt(f) } @@ -61,6 +64,18 @@ impl TemporalError { Self::new(ErrorKind::Type) } + /// Create a syntax error. + #[must_use] + pub fn syntax() -> Self { + Self::new(ErrorKind::Syntax) + } + + /// Create an abrupt end error. + #[must_use] + pub fn abrupt_end() -> Self { + Self::syntax().with_message("Abrupt end to parsing target.") + } + /// Add a message to the error. #[must_use] pub fn with_message(mut self, msg: S) -> Self diff --git a/boa_temporal/src/iso.rs b/boa_temporal/src/iso.rs index 113730b340..32760bd176 100644 --- a/boa_temporal/src/iso.rs +++ b/boa_temporal/src/iso.rs @@ -209,12 +209,12 @@ impl IsoDate { /// time slots. #[derive(Debug, Default, Clone, Copy)] pub struct IsoTime { - hour: i32, // 0..=23 - minute: i32, // 0..=59 - second: i32, // 0..=59 - millisecond: i32, // 0..=999 - microsecond: i32, // 0..=999 - nanosecond: i32, // 0..=999 + pub(crate) hour: i32, // 0..=23 + pub(crate) minute: i32, // 0..=59 + pub(crate) second: i32, // 0..=59 + pub(crate) millisecond: i32, // 0..=999 + pub(crate) microsecond: i32, // 0..=999 + pub(crate) nanosecond: i32, // 0..=999 } impl IsoTime { @@ -281,6 +281,28 @@ impl IsoTime { } } + /// Returns an `IsoTime` based off parse components. + pub(crate) fn from_components( + hour: i32, + minute: i32, + second: i32, + fraction: f64, + ) -> TemporalResult { + let millisecond = fraction * 1000f64; + let micros = millisecond.rem_euclid(1f64) * 1000f64; + let nanos = micros.rem_euclid(1f64).mul_add(1000f64, 0.5).floor(); + + Self::new( + hour, + minute, + second, + millisecond as i32, + micros as i32, + nanos as i32, + ArithmeticOverflow::Reject, + ) + } + /// Checks if the time is a valid `IsoTime` pub(crate) fn is_valid(&self) -> bool { if !(0..=23).contains(&self.hour) { diff --git a/boa_temporal/src/lib.rs b/boa_temporal/src/lib.rs index fb5149aac0..eec77841d9 100644 --- a/boa_temporal/src/lib.rs +++ b/boa_temporal/src/lib.rs @@ -36,6 +36,7 @@ pub mod fields; pub mod iso; pub mod month_day; pub mod options; +pub mod parser; pub mod time; pub(crate) mod utils; pub mod year_month; diff --git a/boa_parser/src/temporal/annotations.rs b/boa_temporal/src/parser/annotations.rs similarity index 70% rename from boa_parser/src/temporal/annotations.rs rename to boa_temporal/src/parser/annotations.rs index f9c0ae13ce..37e9c7d27e 100644 --- a/boa_parser/src/temporal/annotations.rs +++ b/boa_temporal/src/parser/annotations.rs @@ -1,20 +1,17 @@ /// Parsing for Temporal's `Annotations`. use crate::{ - error::{Error, ParseResult}, - lexer::Error as LexError, - temporal::{ + parser::{ grammar::{ is_a_key_char, is_a_key_leading_char, is_annotation_close, is_annotation_key_value_separator, is_annotation_value_component, is_critical_flag, }, time_zone, time_zone::TimeZoneAnnotation, - IsoCursor, + Cursor, }, + TemporalError, TemporalResult, }; -use boa_ast::{Position, Span}; - use super::grammar::{is_annotation_open, is_hyphen}; /// A `KeyValueAnnotation` Parse Node. @@ -37,20 +34,14 @@ pub(crate) struct AnnotationSet { /// Parse a `TimeZoneAnnotation` `Annotations` set pub(crate) fn parse_annotation_set( zoned: bool, - cursor: &mut IsoCursor, -) -> ParseResult { + cursor: &mut Cursor, +) -> TemporalResult { // Parse the first annotation. let tz_annotation = time_zone::parse_ambiguous_tz_annotation(cursor)?; if tz_annotation.is_none() && zoned { - return Err(Error::unexpected( - "Annotation", - Span::new( - Position::new(1, cursor.pos() + 1), - Position::new(1, cursor.pos() + 2), - ), - "iso8601 ZonedDateTime requires a TimeZoneAnnotation.", - )); + return Err(TemporalError::syntax() + .with_message("iso8601 ZonedDateTime requires a TimeZoneAnnotation.")); } // Parse any `Annotations` @@ -77,12 +68,11 @@ pub(crate) struct RecognizedAnnotations { } /// Parse any number of `KeyValueAnnotation`s -pub(crate) fn parse_annotations(cursor: &mut IsoCursor) -> ParseResult { +pub(crate) fn parse_annotations(cursor: &mut Cursor) -> TemporalResult { let mut annotations = RecognizedAnnotations::default(); let mut calendar_crit = false; while cursor.check_or(false, is_annotation_open) { - let start = Position::new(1, cursor.pos() + 1); let kv = parse_kv_annotation(cursor)?; if &kv.key == "u-ca" { @@ -93,13 +83,12 @@ pub(crate) fn parse_annotations(cursor: &mut IsoCursor) -> ParseResult ParseResult ParseResult { +fn parse_kv_annotation(cursor: &mut Cursor) -> TemporalResult { debug_assert!(cursor.check_or(false, is_annotation_open)); - let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?; + let potential_critical = cursor.next().ok_or_else(TemporalError::abrupt_end)?; let (leading_char, critical) = if is_critical_flag(potential_critical) { - (cursor.next().ok_or_else(|| Error::AbruptEnd)?, true) + (cursor.next().ok_or_else(TemporalError::abrupt_end)?, true) } else { (potential_critical, false) }; if !is_a_key_leading_char(leading_char) { - return Err(LexError::syntax( - "Invalid AnnotationKey leading character", - Position::new(1, cursor.pos() + 1), - ) - .into()); + return Err(TemporalError::syntax().with_message("Invalid AnnotationKey leading character")); } // Parse AnnotationKey. @@ -147,7 +132,7 @@ fn parse_kv_annotation(cursor: &mut IsoCursor) -> ParseResult ParseResult { +fn parse_annotation_key(cursor: &mut Cursor) -> TemporalResult { let key_start = cursor.pos(); while let Some(potential_key_char) = cursor.next() { // End of key. @@ -157,19 +142,15 @@ fn parse_annotation_key(cursor: &mut IsoCursor) -> ParseResult { } if !is_a_key_char(potential_key_char) { - return Err(LexError::syntax( - "Invalid AnnotationKey Character", - Position::new(1, cursor.pos() + 1), - ) - .into()); + return Err(TemporalError::syntax().with_message("Invalid AnnotationKey Character")); } } - Err(Error::AbruptEnd) + Err(TemporalError::abrupt_end()) } /// Parse an `AnnotationValue`. -fn parse_annotation_value(cursor: &mut IsoCursor) -> ParseResult { +fn parse_annotation_value(cursor: &mut Cursor) -> TemporalResult { let value_start = cursor.pos(); while let Some(potential_value_char) = cursor.next() { if is_annotation_close(potential_value_char) { @@ -182,24 +163,19 @@ fn parse_annotation_value(cursor: &mut IsoCursor) -> ParseResult { .peek_n(1) .map_or(false, is_annotation_value_component) { - return Err(LexError::syntax( - "Missing AttributeValueComponent after '-'", - Position::new(1, cursor.pos() + 1), - ) - .into()); + return Err(TemporalError::syntax() + .with_message("Missing AttributeValueComponent after '-'")); } cursor.advance(); continue; } if !is_annotation_value_component(potential_value_char) { - return Err(LexError::syntax( - "Invalid character in AnnotationValue", - Position::new(1, value_start + cursor.pos() + 1), - ) - .into()); + return Err( + TemporalError::syntax().with_message("Invalid character in AnnotationValue") + ); } } - Err(Error::AbruptEnd) + Err(TemporalError::abrupt_end()) } diff --git a/boa_parser/src/temporal/date_time.rs b/boa_temporal/src/parser/date_time.rs similarity index 61% rename from boa_parser/src/temporal/date_time.rs rename to boa_temporal/src/parser/date_time.rs index b147a89416..5405e925e8 100644 --- a/boa_parser/src/temporal/date_time.rs +++ b/boa_temporal/src/parser/date_time.rs @@ -1,20 +1,19 @@ //! Parsing for Temporal's ISO8601 `Date` and `DateTime`. use crate::{ - error::{Error, ParseResult}, - lexer::Error as LexError, - temporal::{ + parser::{ annotations, grammar::{is_date_time_separator, is_sign, is_utc_designator}, + nodes::TimeZone, time, time::TimeSpec, - time_zone, IsoCursor, IsoParseRecord, + time_zone, Cursor, IsoParseRecord, }, + TemporalError, TemporalResult, }; -use boa_ast::{temporal::TimeZone, Position, Span}; - use super::grammar::{is_annotation_open, is_hyphen}; +use bitflags::bitflags; #[derive(Debug, Default, Clone)] /// A `DateTime` Parse Node that contains the date, time, and offset info. @@ -38,6 +37,16 @@ pub(crate) struct DateRecord { pub(crate) day: i32, } +bitflags! { + /// Parsing flags for `AnnotatedDateTime` parsing. + #[derive(Debug, Clone, Copy)] + pub struct DateTimeFlags: u8 { + const ZONED = 0b0000_0001; + const TIME_REQ = 0b0000_0010; + const UTC_REQ = 0b0000_0100; + } +} + /// This function handles parsing for [`AnnotatedDateTime`][datetime], /// [`AnnotatedDateTimeTimeRequred`][time], and /// [`TemporalInstantString.`][instant] according to the requirements @@ -47,27 +56,22 @@ pub(crate) struct DateRecord { /// [time]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTimeTimeRequired /// [instant]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString pub(crate) fn parse_annotated_date_time( - zoned: bool, - time_required: bool, - utc_required: bool, - cursor: &mut IsoCursor, -) -> ParseResult { - let date_time = parse_date_time(time_required, utc_required, cursor)?; + flags: DateTimeFlags, + cursor: &mut Cursor, +) -> TemporalResult { + let date_time = parse_date_time( + flags.contains(DateTimeFlags::TIME_REQ), + flags.contains(DateTimeFlags::UTC_REQ), + cursor, + )?; // Peek Annotation presence // Throw error if annotation does not exist and zoned is true, else return. let annotation_check = cursor.check_or(false, is_annotation_open); if !annotation_check { - if zoned { - return Err(Error::expected( - ["TimeZoneAnnotation".into()], - "No Annotation", - Span::new( - Position::new(1, cursor.pos() + 1), - Position::new(1, cursor.pos() + 1), - ), - "iso8601 grammar", - )); + if flags.contains(DateTimeFlags::ZONED) { + return Err(TemporalError::syntax() + .with_message("ZonedDateTime must have a TimeZoneAnnotation.")); } return Ok(IsoParseRecord { @@ -84,7 +88,8 @@ pub(crate) fn parse_annotated_date_time( tz = tz_info; } - let annotation_set = annotations::parse_annotation_set(zoned, cursor)?; + let annotation_set = + annotations::parse_annotation_set(flags.contains(DateTimeFlags::ZONED), cursor)?; if let Some(annotated_tz) = annotation_set.tz { tz = annotated_tz.tz; @@ -108,17 +113,14 @@ pub(crate) fn parse_annotated_date_time( fn parse_date_time( time_required: bool, utc_required: bool, - cursor: &mut IsoCursor, -) -> ParseResult { + cursor: &mut Cursor, +) -> TemporalResult { let date = parse_date(cursor)?; // If there is no `DateTimeSeparator`, return date early. if !cursor.check_or(false, is_date_time_separator) { if time_required { - return Err(Error::general( - "Missing a required TimeSpec.", - Position::new(1, cursor.pos() + 1), - )); + return Err(TemporalError::syntax().with_message("Missing a required Time target.")); } return Ok(DateTimeRecord { @@ -139,10 +141,7 @@ fn parse_date_time( Some(time_zone::parse_date_time_utc(cursor)?) } else { if utc_required { - return Err(Error::general( - "DateTimeUTCOffset is required.", - Position::new(1, cursor.pos() + 1), - )); + return Err(TemporalError::syntax().with_message("DateTimeUTCOffset is required.")); } None }; @@ -155,9 +154,11 @@ fn parse_date_time( } /// Parses `Date` record. -fn parse_date(cursor: &mut IsoCursor) -> ParseResult { +fn parse_date(cursor: &mut Cursor) -> TemporalResult { let year = parse_date_year(cursor)?; - let divided = cursor.check(is_hyphen).ok_or_else(|| Error::AbruptEnd)?; + let divided = cursor + .check(is_hyphen) + .ok_or_else(TemporalError::abrupt_end)?; if divided { cursor.advance(); @@ -167,11 +168,7 @@ fn parse_date(cursor: &mut IsoCursor) -> ParseResult { if cursor.check_or(false, is_hyphen) { if !divided { - return Err(LexError::syntax( - "Invalid date separator", - Position::new(1, cursor.pos() + 1), - ) - .into()); + return Err(TemporalError::syntax().with_message("Invalid date separator")); } cursor.advance(); } @@ -182,8 +179,8 @@ fn parse_date(cursor: &mut IsoCursor) -> ParseResult { } /// Determines if the string can be parsed as a `DateSpecYearMonth`. -pub(crate) fn peek_year_month(cursor: &IsoCursor) -> ParseResult { - let mut ym_peek = if is_sign(cursor.peek().ok_or_else(|| Error::AbruptEnd)?) { +pub(crate) fn peek_year_month(cursor: &Cursor) -> TemporalResult { + let mut ym_peek = if is_sign(cursor.peek().ok_or_else(TemporalError::abrupt_end)?) { 7 } else { 4 @@ -192,7 +189,7 @@ pub(crate) fn peek_year_month(cursor: &IsoCursor) -> ParseResult { if cursor .peek_n(ym_peek) .map(is_hyphen) - .ok_or_else(|| Error::AbruptEnd)? + .ok_or_else(TemporalError::abrupt_end)? { ym_peek += 1; } @@ -207,7 +204,7 @@ pub(crate) fn peek_year_month(cursor: &IsoCursor) -> ParseResult { } /// Parses a `DateSpecYearMonth` -pub(crate) fn parse_year_month(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)> { +pub(crate) fn parse_year_month(cursor: &mut Cursor) -> TemporalResult<(i32, i32)> { let year = parse_date_year(cursor)?; if cursor.check_or(false, is_hyphen) { @@ -220,11 +217,11 @@ pub(crate) fn parse_year_month(cursor: &mut IsoCursor) -> ParseResult<(i32, i32) } /// Determines if the string can be parsed as a `DateSpecYearMonth`. -pub(crate) fn peek_month_day(cursor: &IsoCursor) -> ParseResult { +pub(crate) fn peek_month_day(cursor: &Cursor) -> TemporalResult { let mut md_peek = if cursor .peek_n(1) .map(is_hyphen) - .ok_or_else(|| Error::AbruptEnd)? + .ok_or_else(TemporalError::abrupt_end)? { 4 } else { @@ -234,7 +231,7 @@ pub(crate) fn peek_month_day(cursor: &IsoCursor) -> ParseResult { if cursor .peek_n(md_peek) .map(is_hyphen) - .ok_or_else(|| Error::AbruptEnd)? + .ok_or_else(TemporalError::abrupt_end)? { md_peek += 1; } @@ -249,21 +246,19 @@ pub(crate) fn peek_month_day(cursor: &IsoCursor) -> ParseResult { } /// Parses a `DateSpecMonthDay` -pub(crate) fn parse_month_day(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)> { - let dash_one = cursor.check(is_hyphen).ok_or_else(|| Error::AbruptEnd)?; +pub(crate) fn parse_month_day(cursor: &mut Cursor) -> TemporalResult<(i32, i32)> { + let dash_one = cursor + .check(is_hyphen) + .ok_or_else(TemporalError::abrupt_end)?; let dash_two = cursor .peek_n(1) .map(is_hyphen) - .ok_or_else(|| Error::AbruptEnd)?; + .ok_or_else(TemporalError::abrupt_end)?; if dash_two && dash_one { cursor.advance_n(2); } else if dash_two && !dash_one { - return Err(LexError::syntax( - "MonthDay requires two dashes", - Position::new(1, cursor.pos()), - ) - .into()); + return Err(TemporalError::syntax().with_message("MonthDay requires two dashes")); } let month = parse_date_month(cursor)?; @@ -278,8 +273,8 @@ pub(crate) fn parse_month_day(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)> // ==== Unit Parsers ==== -fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult { - if is_sign(cursor.peek().ok_or_else(|| Error::AbruptEnd)?) { +fn parse_date_year(cursor: &mut Cursor) -> TemporalResult { + if is_sign(cursor.peek().ok_or_else(TemporalError::abrupt_end)?) { let year_start = cursor.pos(); let sign = if cursor.check_or(false, |ch| ch == '+') { 1 @@ -290,12 +285,9 @@ fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult { cursor.advance(); for _ in 0..6 { - let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?; + let year_digit = cursor.peek().ok_or_else(TemporalError::abrupt_end)?; if !year_digit.is_ascii_digit() { - return Err(Error::lex(LexError::syntax( - "DateYear must contain digit", - Position::new(1, cursor.pos() + 1), - ))); + return Err(TemporalError::syntax().with_message("DateYear must contain digit")); } cursor.advance(); } @@ -303,16 +295,13 @@ fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult { let year_string = cursor.slice(year_start + 1, cursor.pos()); let year_value = year_string .parse::() - .map_err(|e| Error::general(e.to_string(), Position::new(1, year_start + 1)))?; + .map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; // 13.30.1 Static Semantics: Early Errors // // It is a Syntax Error if DateYear is "-000000" or "−000000" (U+2212 MINUS SIGN followed by 000000). if sign == -1 && year_value == 0 { - return Err(Error::lex(LexError::syntax( - "Cannot have negative 0 years.", - Position::new(1, year_start + 1), - ))); + return Err(TemporalError::syntax().with_message("Cannot have negative 0 years.")); } return Ok(sign * year_value); @@ -321,13 +310,9 @@ fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult { let year_start = cursor.pos(); for _ in 0..4 { - let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?; + let year_digit = cursor.peek().ok_or_else(TemporalError::abrupt_end)?; if !year_digit.is_ascii_digit() { - return Err(LexError::syntax( - "DateYear must contain digit", - Position::new(1, cursor.pos() + 1), - ) - .into()); + return Err(TemporalError::syntax().with_message("DateYear must contain digit")); } cursor.advance(); } @@ -335,38 +320,30 @@ fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult { let year_string = cursor.slice(year_start, cursor.pos()); let year_value = year_string .parse::() - .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() + 1)))?; + .map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; Ok(year_value) } -fn parse_date_month(cursor: &mut IsoCursor) -> ParseResult { +fn parse_date_month(cursor: &mut Cursor) -> TemporalResult { let month_value = cursor .slice(cursor.pos(), cursor.pos() + 2) .parse::() - .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() + 1)))?; + .map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; if !(1..=12).contains(&month_value) { - return Err(LexError::syntax( - "DateMonth must be in a range of 1-12", - Position::new(1, cursor.pos() + 1), - ) - .into()); + return Err(TemporalError::syntax().with_message("DateMonth must be in a range of 1-12")); } cursor.advance_n(2); Ok(month_value) } -fn parse_date_day(cursor: &mut IsoCursor) -> ParseResult { +fn parse_date_day(cursor: &mut Cursor) -> TemporalResult { let day_value = cursor .slice(cursor.pos(), cursor.pos() + 2) .parse::() - .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?; + .map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; if !(1..=31).contains(&day_value) { - return Err(LexError::syntax( - "DateDay must be in a range of 1-31", - Position::new(1, cursor.pos() + 1), - ) - .into()); + return Err(TemporalError::syntax().with_message("DateDay must be in a range of 1-31")); } cursor.advance_n(2); Ok(day_value) diff --git a/boa_parser/src/temporal/duration.rs b/boa_temporal/src/parser/duration.rs similarity index 66% rename from boa_parser/src/temporal/duration.rs rename to boa_temporal/src/parser/duration.rs index e99dd87bc2..1bc4785321 100644 --- a/boa_parser/src/temporal/duration.rs +++ b/boa_temporal/src/parser/duration.rs @@ -1,16 +1,14 @@ -use boa_ast::Position; - use crate::{ - error::{Error, ParseResult}, - temporal::{ + parser::{ grammar::{ is_day_designator, is_decimal_separator, is_duration_designator, is_hour_designator, is_minute_designator, is_month_designator, is_second_designator, is_sign, is_time_designator, is_week_designator, is_year_designator, }, time::parse_fraction, - IsoCursor, + Cursor, }, + TemporalError, TemporalResult, }; /// A ISO8601 `DurationRecord` Parse Node. @@ -54,8 +52,11 @@ pub(crate) struct TimeDuration { pub(crate) fseconds: f64, } -pub(crate) fn parse_duration(cursor: &mut IsoCursor) -> ParseResult { - let sign = if cursor.check(is_sign).ok_or_else(|| Error::AbruptEnd)? { +pub(crate) fn parse_duration(cursor: &mut Cursor) -> TemporalResult { + let sign = if cursor + .check(is_sign) + .ok_or_else(TemporalError::abrupt_end)? + { let sign = cursor.check_or(false, |ch| ch == '+'); cursor.advance(); sign @@ -65,12 +66,11 @@ pub(crate) fn parse_duration(cursor: &mut IsoCursor) -> ParseResult ParseResult ParseResult { +pub(crate) fn parse_date_duration(cursor: &mut Cursor) -> TemporalResult { let mut date = DateDuration::default(); let mut previous_unit = DateUnit::None; @@ -125,52 +122,46 @@ pub(crate) fn parse_date_duration(cursor: &mut IsoCursor) -> ParseResult() - .map_err(|err| { - Error::general(err.to_string(), Position::new(digit_start, cursor.pos())) - })?; + .map_err(|err| TemporalError::syntax().with_message(err.to_string()))?; match cursor.peek() { Some(ch) if is_year_designator(ch) => { if previous_unit > DateUnit::Year { - return Err(Error::general( - "Not a valid DateDuration order", - Position::new(1, cursor.pos()), - )); + return Err( + TemporalError::syntax().with_message("Not a valid DateDuration order") + ); } date.years = value; previous_unit = DateUnit::Year; } Some(ch) if is_month_designator(ch) => { if previous_unit > DateUnit::Month { - return Err(Error::general( - "Not a valid DateDuration order", - Position::new(1, cursor.pos()), - )); + return Err( + TemporalError::syntax().with_message("Not a valid DateDuration order") + ); } date.months = value; previous_unit = DateUnit::Month; } Some(ch) if is_week_designator(ch) => { if previous_unit > DateUnit::Week { - return Err(Error::general( - "Not a valid DateDuration order", - Position::new(1, cursor.pos()), - )); + return Err( + TemporalError::syntax().with_message("Not a valid DateDuration order") + ); } date.weeks = value; previous_unit = DateUnit::Week; } Some(ch) if is_day_designator(ch) => { if previous_unit > DateUnit::Day { - return Err(Error::general( - "Not a valid DateDuration order", - Position::new(1, cursor.pos()), - )); + return Err( + TemporalError::syntax().with_message("Not a valid DateDuration order") + ); } date.days = value; previous_unit = DateUnit::Day; } - Some(_) | None => return Err(Error::AbruptEnd), + Some(_) | None => return Err(TemporalError::abrupt_end()), } cursor.advance(); @@ -187,14 +178,13 @@ enum TimeUnit { Second, } -pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult { +pub(crate) fn parse_time_duration(cursor: &mut Cursor) -> TemporalResult { let mut time = TimeDuration::default(); if !cursor.check_or(false, |ch| ch.is_ascii()) { - return Err(Error::general( - "No time values provided after TimeDesignator.", - Position::new(1, cursor.pos()), - )); + return Err( + TemporalError::syntax().with_message("No time values provided after TimeDesignator.") + ); } let mut previous_unit = TimeUnit::None; @@ -209,9 +199,7 @@ pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult() - .map_err(|err| { - Error::general(err.to_string(), Position::new(digit_start, cursor.pos())) - })?; + .map_err(|err| TemporalError::syntax().with_message(err.to_string()))?; let fraction = if cursor.check_or(false, is_decimal_separator) { fraction_present = true; @@ -223,10 +211,9 @@ pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult { if previous_unit > TimeUnit::Hour { - return Err(Error::general( - "Not a valid DateDuration order", - Position::new(1, cursor.pos()), - )); + return Err( + TemporalError::syntax().with_message("Not a valid DateDuration order") + ); } time.hours = value; time.fhours = fraction; @@ -234,10 +221,9 @@ pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult { if previous_unit > TimeUnit::Minute { - return Err(Error::general( - "Not a valid DateDuration order", - Position::new(1, cursor.pos()), - )); + return Err( + TemporalError::syntax().with_message("Not a valid DateDuration order") + ); } time.minutes = value; time.fminutes = fraction; @@ -245,26 +231,23 @@ pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult { if previous_unit > TimeUnit::Second { - return Err(Error::general( - "Not a valid DateDuration order", - Position::new(1, cursor.pos()), - )); + return Err( + TemporalError::syntax().with_message("Not a valid DateDuration order") + ); } time.seconds = value; time.fseconds = fraction; previous_unit = TimeUnit::Second; } - Some(_) | None => return Err(Error::AbruptEnd), + Some(_) | None => return Err(TemporalError::abrupt_end()), } cursor.advance(); if fraction_present { if cursor.check_or(false, |ch| ch.is_ascii_digit()) { - return Err(Error::general( - "Invalid TimeDuration continuation after FractionPart.", - Position::new(1, cursor.pos()), - )); + return Err(TemporalError::syntax() + .with_message("Invalid TimeDuration continuation after FractionPart.")); } break; diff --git a/boa_parser/src/temporal/grammar.rs b/boa_temporal/src/parser/grammar.rs similarity index 100% rename from boa_parser/src/temporal/grammar.rs rename to boa_temporal/src/parser/grammar.rs diff --git a/boa_parser/src/temporal/mod.rs b/boa_temporal/src/parser/mod.rs similarity index 63% rename from boa_parser/src/temporal/mod.rs rename to boa_temporal/src/parser/mod.rs index c9fc55d448..1dece1be82 100644 --- a/boa_parser/src/temporal/mod.rs +++ b/boa_temporal/src/parser/mod.rs @@ -1,25 +1,40 @@ //! Implementation of Iso8601 grammar lexing/parsing -use crate::error::ParseResult; +use crate::TemporalResult; + +use date_time::DateRecord; +use nodes::{IsoDate, IsoDateTime, IsoTime, TimeZone}; +use time::TimeSpec; mod annotations; -mod date_time; -mod duration; +pub(crate) mod date_time; +pub(crate) mod duration; mod grammar; +mod nodes; mod time; -mod time_zone; - -use boa_ast::temporal::{IsoDate, IsoDateTime, IsoDuration, IsoTime, TimeZone}; +pub(crate) mod time_zone; -use date_time::DateRecord; -use time::TimeSpec; +use self::date_time::DateTimeFlags; -#[cfg(feature = "experimental")] #[cfg(test)] mod tests; // TODO: optimize where possible. +/// A utility function for parsing a `DateTime` string +pub(crate) fn parse_date_time(target: &str) -> TemporalResult { + date_time::parse_annotated_date_time(DateTimeFlags::empty(), &mut Cursor::new(target)) +} + +/// A utility function for parsing an `Instant` string +#[allow(unused)] +pub(crate) fn parse_instant(target: &str) -> TemporalResult { + date_time::parse_annotated_date_time( + DateTimeFlags::UTC_REQ | DateTimeFlags::TIME_REQ, + &mut Cursor::new(target), + ) +} + /// An `IsoParseRecord` is an intermediary record returned by ISO parsing functions. /// /// `IsoParseRecord` is converted into the ISO AST Nodes. @@ -35,40 +50,7 @@ pub(crate) struct IsoParseRecord { pub(crate) calendar: Option, } -/// Parse a [`TemporalDateTimeString`][proposal]. -/// -/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalDateTimeString -#[derive(Debug, Clone, Copy)] -pub struct TemporalDateTimeString; - -impl TemporalDateTimeString { - /// Parses a targeted string as a `DateTime`. - /// - /// # Errors - /// - /// The parse will error if the provided target is not valid - /// Iso8601 grammar. - pub fn parse(zoned: bool, cursor: &mut IsoCursor) -> ParseResult { - let parse_record = date_time::parse_annotated_date_time(zoned, false, false, cursor)?; - - let date = IsoDate { - year: parse_record.date.year, - month: parse_record.date.month, - day: parse_record.date.day, - calendar: parse_record.calendar, - }; - - let time = parse_record.time.map_or_else(IsoTime::default, |time| { - IsoTime::from_components(time.hour, time.minute, time.second, time.fraction) - }); - - Ok(IsoDateTime { - date, - time, - tz: parse_record.tz, - }) - } -} +// TODO: Phase out the below and integrate parsing with Temporal components. /// Parse a [`TemporalTimeZoneString`][proposal]. /// @@ -83,7 +65,7 @@ impl TemporalTimeZoneString { /// /// The parse will error if the provided target is not valid /// Iso8601 grammar. - pub fn parse(cursor: &mut IsoCursor) -> ParseResult { + pub fn parse(cursor: &mut Cursor) -> TemporalResult { time_zone::parse_time_zone(cursor) } } @@ -101,7 +83,8 @@ impl TemporalYearMonthString { /// /// The parse will error if the provided target is not valid /// Iso8601 grammar. - pub fn parse(cursor: &mut IsoCursor) -> ParseResult { + pub fn parse(cursor: &mut Cursor) -> TemporalResult { + // TODO: Remove peek in favor of AnnotatedDateTime flag. if date_time::peek_year_month(cursor)? { let ym = date_time::parse_year_month(cursor)?; @@ -120,7 +103,7 @@ impl TemporalYearMonthString { }); } - let parse_record = date_time::parse_annotated_date_time(false, false, false, cursor)?; + let parse_record = date_time::parse_annotated_date_time(DateTimeFlags::empty(), cursor)?; Ok(IsoDate { year: parse_record.date.year, @@ -144,7 +127,8 @@ impl TemporalMonthDayString { /// /// The parse will error if the provided target is not valid /// Iso8601 grammar. - pub fn parse(cursor: &mut IsoCursor) -> ParseResult { + pub fn parse(cursor: &mut Cursor) -> TemporalResult { + // TODO: Remove peek in favor of AnnotatedDateTime flag. if date_time::peek_month_day(cursor)? { let md = date_time::parse_month_day(cursor)?; @@ -163,7 +147,7 @@ impl TemporalMonthDayString { }); } - let parse_record = date_time::parse_annotated_date_time(false, false, false, cursor)?; + let parse_record = date_time::parse_annotated_date_time(DateTimeFlags::empty(), cursor)?; Ok(IsoDate { year: parse_record.date.year, @@ -187,8 +171,11 @@ impl TemporalInstantString { /// /// The parse will error if the provided target is not valid /// Iso8601 grammar. - pub fn parse(cursor: &mut IsoCursor) -> ParseResult { - let parse_record = date_time::parse_annotated_date_time(false, true, true, cursor)?; + pub fn parse(cursor: &mut Cursor) -> TemporalResult { + let parse_record = date_time::parse_annotated_date_time( + DateTimeFlags::UTC_REQ | DateTimeFlags::TIME_REQ, + cursor, + )?; let date = IsoDate { year: parse_record.date.year, @@ -209,74 +196,16 @@ impl TemporalInstantString { } } -// TODO: implement TemporalTimeString. - -/// Parser for a [`TemporalDurationString`][proposal]. -/// -/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalDurationString -#[derive(Debug, Clone, Copy)] -pub struct TemporalDurationString; - -impl TemporalDurationString { - /// Parses a targeted string as a `Duration`. - /// - /// # Errors - /// - /// The parse will error if the provided target is not valid - /// Iso8601 grammar. - pub fn parse(cursor: &mut IsoCursor) -> ParseResult { - let parse_record = duration::parse_duration(cursor)?; - - let minutes = if parse_record.time.fhours > 0.0 { - parse_record.time.fhours * 60.0 - } else { - f64::from(parse_record.time.minutes) - }; - - let seconds = if parse_record.time.fminutes > 0.0 { - parse_record.time.fminutes * 60.0 - } else if parse_record.time.seconds > 0 { - f64::from(parse_record.time.seconds) - } else { - minutes.rem_euclid(1.0) * 60.0 - }; - - let milliseconds = if parse_record.time.fseconds > 0.0 { - parse_record.time.fseconds * 1000.0 - } else { - seconds.rem_euclid(1.0) * 1000.0 - }; - - let micro = milliseconds.rem_euclid(1.0) * 1000.0; - let nano = micro.rem_euclid(1.0) * 1000.0; - - let sign = if parse_record.sign { 1 } else { -1 }; - - Ok(IsoDuration { - years: parse_record.date.years * sign, - months: parse_record.date.months * sign, - weeks: parse_record.date.weeks * sign, - days: parse_record.date.days * sign, - hours: parse_record.time.hours * sign, - minutes: minutes.floor() * f64::from(sign), - seconds: seconds.floor() * f64::from(sign), - milliseconds: milliseconds.floor() * f64::from(sign), - microseconds: micro.floor() * f64::from(sign), - nanoseconds: nano.floor() * f64::from(sign), - }) - } -} - // ==== Mini cursor implementation for Iso8601 targets ==== -/// `IsoCursor` is a small cursor implementation for parsing Iso8601 grammar. +/// `Cursor` is a small cursor implementation for parsing Iso8601 grammar. #[derive(Debug)] -pub struct IsoCursor { +pub struct Cursor { pos: u32, source: Vec, } -impl IsoCursor { +impl Cursor { /// Create a new cursor from a source `String` value. #[must_use] pub fn new(source: &str) -> Self { diff --git a/boa_ast/src/temporal/mod.rs b/boa_temporal/src/parser/nodes.rs similarity index 76% rename from boa_ast/src/temporal/mod.rs rename to boa_temporal/src/parser/nodes.rs index a5bc0172de..184ab750ec 100644 --- a/boa_ast/src/temporal/mod.rs +++ b/boa_temporal/src/parser/nodes.rs @@ -1,5 +1,7 @@ //! AST nodes for Temporal's implementation of ISO8601 grammar. +// TODO: Slowly remove the below nodes in favor of Temporal components. + /// An ISO Date Node consisting of non-validated date fields and calendar value. #[derive(Default, Debug)] pub struct IsoDate { @@ -37,9 +39,9 @@ impl IsoTime { pub fn from_components(hour: u8, minute: u8, second: u8, fraction: f64) -> Self { // Note: Precision on nanoseconds drifts, so opting for round over floor or ceil for now. // e.g. 0.329402834 becomes 329.402833.999 - let millisecond = fraction * 1000.0; - let micros = millisecond.rem_euclid(1.0) * 1000.0; - let nanos = micros.rem_euclid(1.0) * 1000.0; + let millisecond = fraction * 1000f64; + let micros = millisecond.rem_euclid(1f64) * 1000f64; + let nanos = micros.rem_euclid(1f64) * 1000f64; Self { hour, @@ -86,28 +88,3 @@ pub struct UTCOffset { /// Any sub second components of the `UTCOffset` pub fraction: f64, } - -/// An `IsoDuration` Node output by the ISO parser. -#[derive(Debug, Default, Clone, Copy)] -pub struct IsoDuration { - /// Years value. - pub years: i32, - /// Months value. - pub months: i32, - /// Weeks value. - pub weeks: i32, - /// Days value. - pub days: i32, - /// Hours value. - pub hours: i32, - /// Minutes value. - pub minutes: f64, - /// Seconds value. - pub seconds: f64, - /// Milliseconds value. - pub milliseconds: f64, - /// Microseconds value. - pub microseconds: f64, - /// Nanoseconds value. - pub nanoseconds: f64, -} diff --git a/boa_parser/src/temporal/tests.rs b/boa_temporal/src/parser/tests.rs similarity index 52% rename from boa_parser/src/temporal/tests.rs rename to boa_temporal/src/parser/tests.rs index 20d91e1eda..b1348e04bd 100644 --- a/boa_parser/src/temporal/tests.rs +++ b/boa_temporal/src/parser/tests.rs @@ -1,6 +1,12 @@ -use super::{ - IsoCursor, TemporalDateTimeString, TemporalDurationString, TemporalInstantString, - TemporalMonthDayString, TemporalYearMonthString, +use std::str::FromStr; + +use crate::{ + datetime::DateTime, + duration::Duration, + parser::{ + parse_date_time, Cursor, TemporalInstantString, TemporalMonthDayString, + TemporalYearMonthString, + }, }; #[test] @@ -8,17 +14,19 @@ fn temporal_parser_basic() { let basic = "20201108"; let basic_separated = "2020-11-08"; - let basic_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic)).unwrap(); + let basic_result = basic.parse::().unwrap(); - let sep_result = - TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic_separated)).unwrap(); + let sep_result = basic_separated.parse::().unwrap(); - assert_eq!(basic_result.date.year, 2020); - assert_eq!(basic_result.date.month, 11); - assert_eq!(basic_result.date.day, 8); - assert_eq!(basic_result.date.year, sep_result.date.year); - assert_eq!(basic_result.date.month, sep_result.date.month); - assert_eq!(basic_result.date.day, sep_result.date.day); + assert_eq!(basic_result.iso_date().year(), 2020); + assert_eq!(basic_result.iso_date().month(), 11); + assert_eq!(basic_result.iso_date().day(), 8); + assert_eq!(basic_result.iso_date().year(), sep_result.iso_date().year()); + assert_eq!( + basic_result.iso_date().month(), + sep_result.iso_date().month() + ); + assert_eq!(basic_result.iso_date().day(), sep_result.iso_date().day()); } #[test] @@ -28,9 +36,9 @@ fn temporal_date_time_max() { let date_time = "+002020-11-08T12:28:32.329402834[!America/Argentina/ComodRivadavia][!u-ca=iso8601]"; - let result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(date_time)).unwrap(); + let result = date_time.parse::().unwrap(); - let time_results = &result.time; + let time_results = result.iso_time(); assert_eq!(time_results.hour, 12); assert_eq!(time_results.minute, 28); @@ -38,17 +46,6 @@ fn temporal_date_time_max() { assert_eq!(time_results.millisecond, 329); assert_eq!(time_results.microsecond, 402); assert_eq!(time_results.nanosecond, 834); - - let tz = &result.tz.unwrap(); - - // OffsetSubMinute is Empty when TimeZoneIdentifier is present. - assert!(&tz.offset.is_none()); - - let tz_name = &tz.name.clone().unwrap(); - - assert_eq!(tz_name, "America/Argentina/ComodRivadavia"); - - assert_eq!(&result.date.calendar, &Some("iso8601".to_string())); } #[test] @@ -56,10 +53,10 @@ fn temporal_year_parsing() { let long = "+002020-11-08"; let bad_year = "-000000-11-08"; - let result_good = TemporalDateTimeString::parse(false, &mut IsoCursor::new(long)).unwrap(); - assert_eq!(result_good.date.year, 2020); + let result_good = long.parse::().unwrap(); + assert_eq!(result_good.iso_date().year(), 2020); - let err_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(bad_year)); + let err_result = bad_year.parse::(); assert!(err_result.is_err()); } @@ -68,19 +65,19 @@ fn temporal_annotated_date_time() { let basic = "2020-11-08[America/Argentina/ComodRivadavia][u-ca=iso8601][foo=bar]"; let omitted = "+0020201108[u-ca=iso8601][f-1a2b=a0sa-2l4s]"; - let result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic)).unwrap(); + let result = parse_date_time(basic).unwrap(); let tz = &result.tz.unwrap().name.unwrap(); assert_eq!(tz, "America/Argentina/ComodRivadavia"); - assert_eq!(&result.date.calendar, &Some("iso8601".to_string())); + assert_eq!(&result.calendar, &Some("iso8601".to_string())); - let omit_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(omitted)).unwrap(); + let omit_result = parse_date_time(omitted).unwrap(); assert!(&omit_result.tz.is_none()); - assert_eq!(&omit_result.date.calendar, &Some("iso8601".to_string())); + assert_eq!(&omit_result.calendar, &Some("iso8601".to_string())); } #[test] @@ -93,7 +90,7 @@ fn temporal_year_month() { ]; for ym in possible_year_months { - let result = TemporalYearMonthString::parse(&mut IsoCursor::new(ym)).unwrap(); + let result = TemporalYearMonthString::parse(&mut Cursor::new(ym)).unwrap(); assert_eq!(result.year, 2020); assert_eq!(result.month, 11); @@ -109,7 +106,7 @@ fn temporal_month_day() { let possible_month_day = ["11-07", "1107[+04:00]", "--11-07", "--1107[+04:00]"]; for md in possible_month_day { - let result = TemporalMonthDayString::parse(&mut IsoCursor::new(md)).unwrap(); + let result = TemporalMonthDayString::parse(&mut Cursor::new(md)).unwrap(); assert_eq!(result.month, 11); assert_eq!(result.day, 7); @@ -125,7 +122,7 @@ fn temporal_invalid_annotations() { ]; for invalid in invalid_annotations { - let err_result = TemporalMonthDayString::parse(&mut IsoCursor::new(invalid)); + let err_result = TemporalMonthDayString::parse(&mut Cursor::new(invalid)); assert!(err_result.is_err()); } } @@ -139,13 +136,14 @@ fn temporal_valid_instant_strings() { ]; for test in instants { - let result = TemporalInstantString::parse(&mut IsoCursor::new(test)); + let result = TemporalInstantString::parse(&mut Cursor::new(test)); assert!(result.is_ok()); } } #[test] #[allow(clippy::cast_possible_truncation)] +#[allow(clippy::float_cmp)] fn temporal_duration_parsing() { let durations = [ "p1y1m1dt1h1m1s", @@ -155,23 +153,21 @@ fn temporal_duration_parsing() { ]; for dur in durations { - let ok_result = TemporalDurationString::parse(&mut IsoCursor::new(dur)); + let ok_result = Duration::from_str(dur); assert!(ok_result.is_ok()); } - let sub = durations[2]; - let sub_second = TemporalDurationString::parse(&mut IsoCursor::new(sub)).unwrap(); + let sub_second = durations[2].parse::().unwrap(); - assert_eq!(sub_second.milliseconds, -123.0); - assert_eq!(sub_second.microseconds, -456.0); - assert_eq!(sub_second.nanoseconds, -789.0); + assert_eq!(sub_second.time().milliseconds(), -123.0); + assert_eq!(sub_second.time().microseconds(), -456.0); + assert_eq!(sub_second.time().nanoseconds(), -789.0); - let dur = durations[3]; - let test_result = TemporalDurationString::parse(&mut IsoCursor::new(dur)).unwrap(); + let test_result = durations[3].parse::().unwrap(); - assert_eq!(test_result.years, -1); - assert_eq!(test_result.weeks, -3); - assert_eq!(test_result.minutes, -30.0); + assert_eq!(test_result.date().years(), -1f64); + assert_eq!(test_result.date().weeks(), -3f64); + assert_eq!(test_result.time().minutes(), -30.0); } #[test] @@ -184,7 +180,7 @@ fn temporal_invalid_durations() { ]; for test in invalids { - let err = TemporalDurationString::parse(&mut IsoCursor::new(test)); + let err = test.parse::(); assert!(err.is_err()); } } diff --git a/boa_parser/src/temporal/time.rs b/boa_temporal/src/parser/time.rs similarity index 67% rename from boa_parser/src/temporal/time.rs rename to boa_temporal/src/parser/time.rs index f8b8075f64..a1f2cec09d 100644 --- a/boa_parser/src/temporal/time.rs +++ b/boa_temporal/src/parser/time.rs @@ -2,12 +2,9 @@ use super::{ grammar::{is_decimal_separator, is_time_separator}, - IsoCursor, -}; -use crate::{ - error::{Error, ParseResult}, - lexer::Error as LexError, + Cursor, }; +use crate::{TemporalError, TemporalResult}; /// Parsed Time info #[derive(Debug, Default, Clone, Copy)] @@ -22,10 +19,8 @@ pub(crate) struct TimeSpec { pub(crate) fraction: f64, } -use boa_ast::Position; - /// Parse `TimeSpec` -pub(crate) fn parse_time_spec(cursor: &mut IsoCursor) -> ParseResult { +pub(crate) fn parse_time_spec(cursor: &mut Cursor) -> TemporalResult { let hour = parse_hour(cursor)?; let mut separator = false; @@ -50,9 +45,7 @@ pub(crate) fn parse_time_spec(cursor: &mut IsoCursor) -> ParseResult { if separator && is_time_separator { cursor.advance(); } else if is_time_separator { - return Err( - LexError::syntax("Invalid TimeSeparator", Position::new(1, cursor.pos())).into(), - ); + return Err(TemporalError::syntax().with_message("Invalid TimeSeparator")); } } else { return Ok(TimeSpec { @@ -79,17 +72,13 @@ pub(crate) fn parse_time_spec(cursor: &mut IsoCursor) -> ParseResult { }) } -pub(crate) fn parse_hour(cursor: &mut IsoCursor) -> ParseResult { +pub(crate) fn parse_hour(cursor: &mut Cursor) -> TemporalResult { let hour_value = cursor .slice(cursor.pos(), cursor.pos() + 2) .parse::() - .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?; + .map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; if !(0..=23).contains(&hour_value) { - return Err(LexError::syntax( - "Hour must be in a range of 0-23", - Position::new(1, cursor.pos() + 1), - ) - .into()); + return Err(TemporalError::syntax().with_message("Hour must be in a range of 0-23")); } cursor.advance_n(2); Ok(hour_value) @@ -97,19 +86,15 @@ pub(crate) fn parse_hour(cursor: &mut IsoCursor) -> ParseResult { // NOTE: `TimeSecond` is a 60 inclusive `MinuteSecond`. /// Parse `MinuteSecond` -pub(crate) fn parse_minute_second(cursor: &mut IsoCursor, inclusive: bool) -> ParseResult { +pub(crate) fn parse_minute_second(cursor: &mut Cursor, inclusive: bool) -> TemporalResult { let min_sec_value = cursor .slice(cursor.pos(), cursor.pos() + 2) .parse::() - .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?; + .map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; let valid_range = if inclusive { 0..=60 } else { 0..=59 }; if !valid_range.contains(&min_sec_value) { - return Err(LexError::syntax( - "MinuteSecond must be in a range of 0-59", - Position::new(1, cursor.pos() + 1), - ) - .into()); + return Err(TemporalError::syntax().with_message("MinuteSecond must be in a range of 0-59")); } cursor.advance_n(2); @@ -120,27 +105,26 @@ pub(crate) fn parse_minute_second(cursor: &mut IsoCursor, inclusive: bool) -> Pa /// /// This is primarily used in ISO8601 to add percision past /// a second. -pub(crate) fn parse_fraction(cursor: &mut IsoCursor) -> ParseResult { +pub(crate) fn parse_fraction(cursor: &mut Cursor) -> TemporalResult { // Decimal is skipped by next call. let mut fraction_components = Vec::from(['.']); while let Some(ch) = cursor.next() { if !ch.is_ascii_digit() { if fraction_components.len() > 10 { - return Err(Error::general( - "Fraction exceeds 9 DecimalDigits", - Position::new(1, cursor.pos() - 1), - )); + return Err( + TemporalError::syntax().with_message("Fraction exceeds 9 DecimalDigits") + ); } let fraction_value = fraction_components .iter() .collect::() .parse::() - .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() - 1)))?; + .map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; return Ok(fraction_value); } fraction_components.push(ch); } - Err(Error::AbruptEnd) + Err(TemporalError::abrupt_end()) } diff --git a/boa_parser/src/temporal/time_zone.rs b/boa_temporal/src/parser/time_zone.rs similarity index 73% rename from boa_parser/src/temporal/time_zone.rs rename to boa_temporal/src/parser/time_zone.rs index 1905eb1f10..810a11290a 100644 --- a/boa_parser/src/temporal/time_zone.rs +++ b/boa_temporal/src/parser/time_zone.rs @@ -7,18 +7,11 @@ use super::{ is_decimal_separator, is_sign, is_time_separator, is_tz_char, is_tz_leading_char, is_tz_name_separator, is_utc_designator, }, + nodes::{TimeZone, UTCOffset}, time::{parse_fraction, parse_hour, parse_minute_second}, - IsoCursor, -}; -use crate::{ - error::{Error, ParseResult}, - lexer::Error as LexError, -}; - -use boa_ast::{ - temporal::{TimeZone, UTCOffset}, - Position, + Cursor, }; +use crate::{TemporalError, TemporalResult}; /// A `TimeZoneAnnotation`. #[derive(Debug, Clone)] @@ -33,14 +26,14 @@ pub(crate) struct TimeZoneAnnotation { // ==== Time Zone Annotation Parsing ==== pub(crate) fn parse_ambiguous_tz_annotation( - cursor: &mut IsoCursor, -) -> ParseResult> { + cursor: &mut Cursor, +) -> TemporalResult> { // Peek position + 1 to check for critical flag. let mut current_peek = 1; let critical = cursor .peek_n(current_peek) .map(is_critical_flag) - .ok_or_else(|| Error::AbruptEnd)?; + .ok_or_else(TemporalError::abrupt_end)?; // Advance cursor if critical flag present. if critical { @@ -49,7 +42,7 @@ pub(crate) fn parse_ambiguous_tz_annotation( let leading_char = cursor .peek_n(current_peek) - .ok_or_else(|| Error::AbruptEnd)?; + .ok_or_else(TemporalError::abrupt_end)?; if is_tz_leading_char(leading_char) || is_sign(leading_char) { // Ambigious start values when lowercase alpha that is shared between `TzLeadingChar` and `KeyLeadingChar`. @@ -64,16 +57,12 @@ pub(crate) fn parse_ambiguous_tz_annotation( { return Ok(None); } else if is_annotation_close(ch) { - return Err(LexError::syntax( - "Invalid Annotation", - Position::new(1, peek_pos + 1), - ) - .into()); + return Err(TemporalError::syntax().with_message("Invalid Annotation")); } peek_pos += 1; } - return Err(Error::AbruptEnd); + return Err(TemporalError::abrupt_end()); } let tz = parse_tz_annotation(cursor)?; return Ok(Some(tz)); @@ -83,16 +72,13 @@ pub(crate) fn parse_ambiguous_tz_annotation( return Ok(None); }; - Err(Error::lex(LexError::syntax( - "Unexpected character in ambiguous annotation.", - Position::new(1, cursor.pos() + 1), - ))) + Err(TemporalError::syntax().with_message("Unexpected character in ambiguous annotation.")) } -fn parse_tz_annotation(cursor: &mut IsoCursor) -> ParseResult { +fn parse_tz_annotation(cursor: &mut Cursor) -> TemporalResult { debug_assert!(is_annotation_open(cursor.peek().expect("annotation start"))); - let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?; + let potential_critical = cursor.next().ok_or_else(TemporalError::abrupt_end)?; let critical = is_critical_flag(potential_critical); if critical { @@ -102,11 +88,7 @@ fn parse_tz_annotation(cursor: &mut IsoCursor) -> ParseResult ParseResult ParseResult { +pub(crate) fn parse_time_zone(cursor: &mut Cursor) -> TemporalResult { let is_iana = cursor .check(is_tz_leading_char) - .ok_or_else(|| Error::AbruptEnd)?; + .ok_or_else(TemporalError::abrupt_end)?; let is_offset = cursor.check_or(false, is_sign); if is_iana { @@ -133,24 +115,17 @@ pub(crate) fn parse_time_zone(cursor: &mut IsoCursor) -> ParseResult { }); } - Err(LexError::syntax( - "Invalid leading character for a TimeZoneIdentifier", - Position::new(1, cursor.pos() + 1), - ) - .into()) + Err(TemporalError::syntax().with_message("Invalid leading character for a TimeZoneIdentifier")) } /// Parse a `TimeZoneIANAName` Parse Node -fn parse_tz_iana_name(cursor: &mut IsoCursor) -> ParseResult { +fn parse_tz_iana_name(cursor: &mut Cursor) -> TemporalResult { let tz_name_start = cursor.pos(); while let Some(potential_value_char) = cursor.next() { if is_tz_name_separator(potential_value_char) { if !cursor.peek_n(1).map_or(false, is_tz_char) { - return Err(LexError::syntax( - "Missing TimeZoneIANANameComponent after '/'", - Position::new(1, cursor.pos() + 2), - ) - .into()); + return Err(TemporalError::syntax() + .with_message("Missing TimeZoneIANANameComponent after '/'")); } continue; } @@ -164,13 +139,13 @@ fn parse_tz_iana_name(cursor: &mut IsoCursor) -> ParseResult { } } - Err(Error::AbruptEnd) + Err(TemporalError::abrupt_end()) } // ==== Utc Offset Parsing ==== /// Parse a full precision `UtcOffset` -pub(crate) fn parse_date_time_utc(cursor: &mut IsoCursor) -> ParseResult { +pub(crate) fn parse_date_time_utc(cursor: &mut Cursor) -> TemporalResult { if cursor.check_or(false, is_utc_designator) { cursor.advance(); return Ok(TimeZone { @@ -185,11 +160,7 @@ pub(crate) fn parse_date_time_utc(cursor: &mut IsoCursor) -> ParseResult ParseResult ParseResult { +pub(crate) fn parse_utc_offset_minute_precision(cursor: &mut Cursor) -> TemporalResult { let sign = if let Some(ch) = cursor.next() { if ch == '+' { 1_i8 @@ -228,14 +199,14 @@ pub(crate) fn parse_utc_offset_minute_precision(cursor: &mut IsoCursor) -> Parse -1_i8 } } else { - return Err(Error::AbruptEnd); + return Err(TemporalError::abrupt_end()); }; let hour = parse_hour(cursor)?; // If at the end of the utc, then return. if cursor .check(|ch| !(ch.is_ascii_digit() || is_time_separator(ch))) - .ok_or_else(|| Error::AbruptEnd)? + .ok_or_else(TemporalError::abrupt_end)? { return Ok(UTCOffset { sign,