diff --git a/Cargo.lock b/Cargo.lock index ee8eb316d..f71d9bfc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -554,9 +554,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "ixdtf" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8289f7f711a1a51f80e2e368355d023042ca55d8d554fd5e953f01464c15842d" +checksum = "c64f97f99b995d752759955a8c14a822b2e009ce99c952a4957c2ec94989ba3e" dependencies = [ "displaydoc", ] diff --git a/Cargo.toml b/Cargo.toml index d56ee5e7f..12eefffb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ icu_calendar = { version = "2.0.2", default-features = false } icu_locale = "2.0.0" rustc-hash = "2.1.0" num-traits = { version = "0.2.19", default-features = false } -ixdtf = "0.5.0" +ixdtf = "0.6.0" iana-time-zone = "0.1.63" log = "0.4.27" tzif = "0.4.0" diff --git a/src/builtins/core/duration.rs b/src/builtins/core/duration.rs index 7a088a373..f1506382b 100644 --- a/src/builtins/core/duration.rs +++ b/src/builtins/core/duration.rs @@ -15,7 +15,7 @@ use crate::{ use alloc::format; use alloc::string::String; use core::{cmp::Ordering, str::FromStr}; -use ixdtf::parsers::{records::TimeDurationRecord, IsoDurationParser}; +use ixdtf::{encoding::Utf8, parsers::IsoDurationParser, records::TimeDurationRecord}; use normalized::NormalizedDurationRecord; use self::normalized::NormalizedTimeDuration; @@ -418,7 +418,7 @@ impl Duration { // Converts a UTF-8 encoded string into a `Duration`. pub fn from_utf8(s: &[u8]) -> TemporalResult { - let parse_record = IsoDurationParser::from_utf8(s) + let parse_record = IsoDurationParser::::from_utf8(s) .parse() .map_err(|e| TemporalError::range().with_message(format!("{e}")))?; diff --git a/src/builtins/core/instant.rs b/src/builtins/core/instant.rs index 6fe5efaad..4aa4be88f 100644 --- a/src/builtins/core/instant.rs +++ b/src/builtins/core/instant.rs @@ -19,7 +19,7 @@ use crate::{ Calendar, TemporalError, TemporalResult, TemporalUnwrap, TimeZone, }; -use ixdtf::parsers::records::UtcOffsetRecordOrZ; +use ixdtf::records::UtcOffsetRecordOrZ; use num_traits::Euclid; use writeable::Writeable; diff --git a/src/builtins/core/timezone.rs b/src/builtins/core/timezone.rs index 367f2a457..880689a8b 100644 --- a/src/builtins/core/timezone.rs +++ b/src/builtins/core/timezone.rs @@ -3,8 +3,12 @@ use alloc::string::{String, ToString}; use alloc::{vec, vec::Vec}; -use ixdtf::parsers::records::{MinutePrecisionOffset, TimeZoneRecord, UtcOffsetRecord}; -use ixdtf::parsers::TimeZoneParser; +use core::str::from_utf8; +use ixdtf::encoding::Utf8; +use ixdtf::{ + parsers::TimeZoneParser, + records::{MinutePrecisionOffset, TimeZoneRecord, UtcOffsetRecord}, +}; use num_traits::ToPrimitive; use crate::builtins::core::duration::DateDuration; @@ -88,11 +92,12 @@ pub enum TimeZone { impl TimeZone { // Create a `TimeZone` from an ixdtf `TimeZoneRecord`. #[inline] - pub(crate) fn from_time_zone_record(record: TimeZoneRecord) -> TemporalResult { + pub(crate) fn from_time_zone_record(record: TimeZoneRecord) -> TemporalResult { let timezone = match record { - TimeZoneRecord::Name(s) => { - TimeZone::IanaIdentifier(String::from_utf8_lossy(s).into_owned()) - } + TimeZoneRecord::Name(s) => TimeZone::IanaIdentifier( + String::from_utf8(s.to_vec()) + .map_err(|e| TemporalError::range().with_message(e.to_string()))?, + ), TimeZoneRecord::Offset(offset_record) => { let offset = UtcOffset::from_ixdtf_record(offset_record); TimeZone::UtcOffset(offset) @@ -109,7 +114,19 @@ impl TimeZone { if identifier == "Z" { return Ok(TimeZone::UtcOffset(UtcOffset(0))); } - parse_identifier(identifier) + parse_identifier(identifier).map(|tz| match tz { + TimeZoneRecord::Name(items) => Ok(TimeZone::IanaIdentifier( + from_utf8(items) + .or(Err( + TemporalError::range().with_message("Invalid TimeZone Identifier") + ))? + .to_string(), + )), + TimeZoneRecord::Offset(minute_precision_offset) => Ok(TimeZone::UtcOffset( + UtcOffset::from_ixdtf_record(minute_precision_offset), + )), + _ => Err(TemporalError::range().with_message("Invalid TimeZone Identifier")), + })? } pub fn try_from_str(src: &str) -> TemporalResult { diff --git a/src/builtins/core/zoneddatetime.rs b/src/builtins/core/zoneddatetime.rs index 919994b97..8a7be98a0 100644 --- a/src/builtins/core/zoneddatetime.rs +++ b/src/builtins/core/zoneddatetime.rs @@ -3,7 +3,7 @@ use alloc::string::String; use core::{cmp::Ordering, num::NonZeroU128}; -use ixdtf::parsers::records::UtcOffsetRecordOrZ; +use ixdtf::records::UtcOffsetRecordOrZ; use tinystr::TinyAsciiStr; use crate::{ diff --git a/src/iso.rs b/src/iso.rs index 292826fb9..fdda011f3 100644 --- a/src/iso.rs +++ b/src/iso.rs @@ -23,7 +23,7 @@ //! An `IsoDateTime` has the internal slots of both an `IsoDate` and `IsoTime`. use core::num::NonZeroU128; -use ixdtf::parsers::records::TimeRecord; +use ixdtf::records::TimeRecord; use crate::{ builtins::core::{ diff --git a/src/options/relative_to.rs b/src/options/relative_to.rs index 27d8043b4..475f4b451 100644 --- a/src/options/relative_to.rs +++ b/src/options/relative_to.rs @@ -8,7 +8,7 @@ use crate::parsers::parse_date_time; use crate::provider::TimeZoneProvider; use crate::{TemporalResult, TemporalUnwrap}; -use ixdtf::parsers::records::UtcOffsetRecordOrZ; +use ixdtf::records::UtcOffsetRecordOrZ; // ==== RelativeTo Object ==== diff --git a/src/parsers.rs b/src/parsers.rs index db26fc1fb..affb9b945 100644 --- a/src/parsers.rs +++ b/src/parsers.rs @@ -6,9 +6,10 @@ use crate::{ Sign, TemporalError, TemporalResult, }; use alloc::format; -use ixdtf::parsers::{ +use ixdtf::{ + encoding::Utf8, + parsers::IxdtfParser, records::{Annotation, DateRecord, IxdtfParseRecord, TimeRecord, UtcOffsetRecordOrZ}, - IxdtfParser, }; use writeable::{impl_display_with_writeable, LengthHint, Writeable}; @@ -658,19 +659,19 @@ enum ParseVariant { } #[inline] -fn parse_ixdtf(source: &[u8], variant: ParseVariant) -> TemporalResult { +fn parse_ixdtf(source: &[u8], variant: ParseVariant) -> TemporalResult> { fn cast_handler<'a>( - _: &mut IxdtfParser<'a>, - handler: impl FnMut(Annotation<'a>) -> Option>, - ) -> impl FnMut(Annotation<'a>) -> Option> { + _: &mut IxdtfParser<'a, Utf8>, + handler: impl FnMut(Annotation<'a, Utf8>) -> Option>, + ) -> impl FnMut(Annotation<'a, Utf8>) -> Option> { handler } - let mut first_calendar: Option = None; + let mut first_calendar: Option> = None; let mut critical_duplicate_calendar = false; let mut parser = IxdtfParser::from_utf8(source); - let handler = cast_handler(&mut parser, |annotation: Annotation<'_>| { + let handler = cast_handler(&mut parser, |annotation: Annotation| { if annotation.key == "u-ca".as_bytes() { match first_calendar { Some(ref cal) => { @@ -716,7 +717,7 @@ fn parse_ixdtf(source: &[u8], variant: ParseVariant) -> TemporalResult TemporalResult { +pub(crate) fn parse_date_time(source: &[u8]) -> TemporalResult> { let record = parse_ixdtf(source, ParseVariant::DateTime)?; if record.offset == Some(UtcOffsetRecordOrZ::Z) { @@ -728,7 +729,7 @@ pub(crate) fn parse_date_time(source: &[u8]) -> TemporalResult } #[inline] -pub(crate) fn parse_zoned_date_time(source: &str) -> TemporalResult { +pub(crate) fn parse_zoned_date_time(source: &str) -> TemporalResult> { let record = parse_ixdtf(source.as_bytes(), ParseVariant::DateTime)?; // TODO: Support rejecting subminute precision in time zone annootations @@ -769,7 +770,7 @@ pub(crate) fn parse_instant(source: &[u8]) -> TemporalResult TemporalResult { +fn check_offset(record: IxdtfParseRecord) -> TemporalResult> { if record.offset == Some(UtcOffsetRecordOrZ::Z) { return Err(TemporalError::range() .with_message("UTC designator is not valid for plain date/time parsing.")); @@ -779,7 +780,7 @@ fn check_offset(record: IxdtfParseRecord) -> TemporalResult { /// A utility function for parsing a `YearMonth` string #[inline] -pub(crate) fn parse_year_month(source: &[u8]) -> TemporalResult { +pub(crate) fn parse_year_month(source: &[u8]) -> TemporalResult> { let ym_record = parse_ixdtf(source, ParseVariant::YearMonth); let Err(ref e) = ym_record else { @@ -796,7 +797,7 @@ pub(crate) fn parse_year_month(source: &[u8]) -> TemporalResult TemporalResult { +pub(crate) fn parse_month_day(source: &[u8]) -> TemporalResult> { let md_record = parse_ixdtf(source, ParseVariant::MonthDay); let Err(ref e) = md_record else { return md_record.and_then(check_offset); @@ -812,7 +813,7 @@ pub(crate) fn parse_month_day(source: &[u8]) -> TemporalResult } // Ensures that an IxdtfParseRecord was parsed with [~Zoned][+TimeRequired] -fn check_time_record(record: IxdtfParseRecord) -> TemporalResult { +fn check_time_record(record: IxdtfParseRecord) -> TemporalResult { // Handle [~Zoned] let record = check_offset(record)?; // Handle [+TimeRequired] diff --git a/src/parsers/timezone.rs b/src/parsers/timezone.rs index fcf3c0057..3a61e7552 100644 --- a/src/parsers/timezone.rs +++ b/src/parsers/timezone.rs @@ -1,8 +1,7 @@ -use alloc::borrow::ToOwned; -use core::{iter::Peekable, str::Chars}; -use ixdtf::parsers::{ - records::{UtcOffsetRecord, UtcOffsetRecordOrZ}, - IxdtfParser, +use ixdtf::{ + encoding::Utf8, + parsers::{IxdtfParser, TimeZoneParser}, + records::{TimeZoneRecord, UtcOffsetRecord, UtcOffsetRecordOrZ}, }; use crate::{builtins::timezone::UtcOffset, TemporalError, TemporalResult, TimeZone}; @@ -52,171 +51,10 @@ pub(crate) fn parse_allowed_timezone_formats(s: &str) -> Option { None } -// TODO: Update `ixdtf` to expose parse_time_zone_record #[inline] -pub(crate) fn parse_identifier(source: &str) -> TemporalResult { - let mut cursor = source.chars().peekable(); - if let Some(offset) = parse_offset(&mut cursor)? { - return Ok(TimeZone::UtcOffset(UtcOffset(offset))); - } else if parse_iana_component(&mut cursor) { - return Ok(TimeZone::IanaIdentifier(source.to_owned())); - } - Err(TemporalError::range().with_message("Invalid TimeZone Identifier")) -} - -#[inline] -pub(crate) fn parse_offset(chars: &mut Peekable>) -> TemporalResult> { - if chars.peek().is_none() || !chars.peek().is_some_and(is_ascii_sign) { - return Ok(None); - } - - let sign = chars.next().map_or(1, |c| if c == '+' { 1 } else { -1 }); - // First offset portion - let hours = parse_digit_pair(chars)?; - - if !(0..24).contains(&hours) { - return Err(TemporalError::range().with_message("Invalid offset hour value.")); - } - - let sep = chars.peek().is_some_and(|ch| *ch == ':'); - if sep { - let _ = chars.next(); - } - - let digit_peek = chars.peek().map(|ch| ch.is_ascii_digit()); - - let minutes = match digit_peek { - Some(true) => parse_digit_pair(chars)?, - Some(false) => return Err(non_ascii_digit()), - None => 0, - }; - - if !(0..60).contains(&minutes) { - return Err(TemporalError::range().with_message("Invalid offset hour value.")); - } - - let result = Some((hours * 60 + minutes) * sign); - - // We continue parsing for correctness, but we only care about - // minute precision - - let next_peek = chars.peek(); - match next_peek { - Some(&':') if sep => _ = chars.next(), - Some(&':') => { - return Err(TemporalError::range().with_message("offset separators do not align.")) - } - Some(_) => _ = parse_digit_pair(chars), - None => return Ok(result), - } - - let potential_fraction = chars.next(); - match potential_fraction { - Some(ch) if ch == '.' || ch == ',' => { - if !chars.peek().is_some_and(|ch| ch.is_ascii_digit()) { - return Err( - TemporalError::range().with_message("fraction separator must have digit after") - ); - } - } - Some(_) => return Err(TemporalError::range().with_message("Invalid offset")), - None => return Ok(result), - } - - for _ in 0..9 { - let digit_or_end = chars.next().map(|ch| ch.is_ascii_digit()); - match digit_or_end { - Some(true) => {} - Some(false) => { - return Err(TemporalError::range().with_message("Not a valid fractional second")) - } - None => break, - } - } - - if chars.peek().is_some() { - return Err(TemporalError::range().with_message("Invalid offset")); - } - - Ok(result) -} - -fn parse_digit_pair(chars: &mut Peekable>) -> TemporalResult { - let valid = chars - .peek() - .map_or(Err(abrupt_end()), |ch| Ok(ch.is_ascii_digit()))?; - let first = if valid { - chars.next().expect("validated.") - } else { - return Err(non_ascii_digit()); - }; - let valid = chars - .peek() - .map_or(Err(abrupt_end()), |ch| Ok(ch.is_ascii_digit()))?; - let second = if valid { - chars.next().expect("validated.") - } else { - return Err(non_ascii_digit()); - }; - - let tens = (first.to_digit(10).expect("validated") * 10) as i16; - let ones = second.to_digit(10).expect("validated") as i16; - - let result = tens + ones; - - if !(0..=59).contains(&result) { - return Err( - TemporalError::range().with_message("digit pair not in a valid range of [0..59]") - ); - } - - Ok(result) -} - -fn parse_iana_component(chars: &mut Peekable>) -> bool { - // Confirm leading Tz char - if !chars.peek().is_some_and(is_tz_leading_char) { - return false; - } - chars.next(); - - // Move and check that chars are an expected tz char - while chars.peek().is_some_and(is_tz_char) { - chars.next(); - } - - // Check for sub component and parse - if chars.peek().is_some_and(is_slash) { - chars.next(); - return parse_iana_component(chars); - } - - // Confirm full source text has been parsed. - chars.peek().is_none() -} - -// NOTE: Spec calls for throwing a RangeError when parse node is a list of errors for timezone. - -fn abrupt_end() -> TemporalError { - TemporalError::range().with_message("Abrupt end while parsing offset string") -} - -fn non_ascii_digit() -> TemporalError { - TemporalError::range().with_message("Non ascii digit found while parsing offset string") -} - -fn is_ascii_sign(ch: &char) -> bool { - *ch == '+' || *ch == '-' -} - -fn is_slash(ch: &char) -> bool { - *ch == '/' -} - -fn is_tz_leading_char(ch: &char) -> bool { - ch.is_alphabetic() || *ch == '.' || *ch == '_' -} - -fn is_tz_char(ch: &char) -> bool { - is_tz_leading_char(ch) || ch.is_ascii_digit() || *ch == '+' || *ch == '-' +pub(crate) fn parse_identifier(source: &str) -> TemporalResult> { + let mut parser = TimeZoneParser::from_str(source); + parser.parse_identifier().or(Err( + TemporalError::range().with_message("Invalid TimeZone Identifier") + )) }