From b08c479f6702de3d02af1d4e692792b4379fc912 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 17 Feb 2026 19:53:24 -0600 Subject: [PATCH 1/2] Fix incorrect decimal amount bug --- src/builtins/core/instant.rs | 16 +++++++-- src/builtins/mod.rs | 64 ++++++++++++++++++++++++++++++++++++ src/parsed_intermediates.rs | 31 +++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/builtins/core/instant.rs b/src/builtins/core/instant.rs index 247078fec..db482553e 100644 --- a/src/builtins/core/instant.rs +++ b/src/builtins/core/instant.rs @@ -303,7 +303,13 @@ impl Instant { UtcOffsetRecordOrZ::Offset(offset) => { let ns = offset .fraction() - .and_then(|x| x.to_nanoseconds()) + .map(|x| { + x.to_nanoseconds().ok_or( + TemporalError::range() + .with_enum(ErrorMessage::FractionalTimeMoreThanNineDigits), + ) + }) + .transpose()? .unwrap_or(0); (offset.hour() as i64 * NANOSECONDS_PER_HOUR + i64::from(offset.minute()) * NANOSECONDS_PER_MINUTE @@ -317,7 +323,13 @@ impl Instant { let time_nanoseconds = ixdtf_record .time .fraction - .and_then(|x| x.to_nanoseconds()) + .map(|x| { + x.to_nanoseconds().ok_or( + TemporalError::range() + .with_enum(ErrorMessage::FractionalTimeMoreThanNineDigits), + ) + }) + .transpose()? .unwrap_or(0); let (millisecond, rem) = time_nanoseconds.div_rem_euclid(&1_000_000); let (microsecond, nanosecond) = rem.div_rem_euclid(&1_000); diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index 97434baf2..a6cc1911b 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -19,3 +19,67 @@ pub static TZ_PROVIDER: LazyLock = #[cfg(all(test, feature = "compiled_data"))] pub(crate) static FS_TZ_PROVIDER: LazyLock = LazyLock::new(FsTzdbProvider::default); + +#[cfg(test)] +mod tests { + use timezone_provider::tzif::CompiledTzdbProvider; + + use crate::ZonedDateTime; + + use super::{Instant, PlainDate, PlainDateTime}; + #[test] + fn builtins_from_str_10_digit_fractions() { + // Failure case with 10 digits + let test = "2020-01-01T00:00:00.1234567890Z"; + let result = Instant::from_utf8(test.as_bytes()); + assert!(result.is_err(), "Instant fraction should be invalid"); + let result = PlainDate::from_utf8(test.as_bytes()); + assert!(result.is_err(), "PlainDate fraction should be invalid"); + let result = PlainDateTime::from_utf8(test.as_bytes()); + assert!(result.is_err(), "PlainDateTime fraction should be invalid"); + } + + #[test] + fn instant_based_10_digit_offset() { + let provider = CompiledTzdbProvider::default(); + let test = "2020-01-01T00:00:00.123456789+02:30:00.1234567890[UTC]"; + let result = Instant::from_utf8(test.as_bytes()); + assert!(result.is_err(), "Instant should be invalid"); + let result = ZonedDateTime::from_utf8_with_provider( + test.as_bytes(), + crate::options::Disambiguation::Compatible, + crate::options::OffsetDisambiguation::Use, + &provider, + ); + assert!(result.is_err(), "ZonedDateTime should be invalid"); + } + + #[test] + fn instant_based_9_digit_offset() { + let provider = CompiledTzdbProvider::default(); + let test = "2020-01-01T00:00:00.123456789+02:30:00.123456789[UTC]"; + let result = Instant::from_utf8(test.as_bytes()); + assert!(result.is_ok(), "Instant should be valid"); + let result = ZonedDateTime::from_utf8_with_provider( + test.as_bytes(), + crate::options::Disambiguation::Compatible, + crate::options::OffsetDisambiguation::Use, + &provider, + ); + std::println!("{result:?}"); + assert!(result.is_ok(), "ZonedDateTime should be valid"); + } + + #[test] + fn builtin_from_str_9_digit_fractions() { + // Success case with 9 digits + let test = "2020-01-01T00:00:00.123456789Z"; + let result = Instant::from_utf8(test.as_bytes()); + assert!(result.is_ok(), "Instant fraction should be valid"); + let test = "2020-01-01T00:00:00.123456789"; + let result = PlainDate::from_utf8(test.as_bytes()); + assert!(result.is_ok(), "PlainDate fraction should be valid"); + let result = PlainDateTime::from_utf8(test.as_bytes()); + assert!(result.is_ok(), "PlainDateTime fraction should be valid"); + } +} diff --git a/src/parsed_intermediates.rs b/src/parsed_intermediates.rs index 0bab4d8ab..6b1c46f0d 100644 --- a/src/parsed_intermediates.rs +++ b/src/parsed_intermediates.rs @@ -48,6 +48,16 @@ impl ParsedDate { // Assertion: PlainDate must exist on a DateTime parse. let record = parse_record.date.temporal_unwrap()?; + // TODO: Potentially, remove this check in favor of type guarantee of a + // Temporal parser. + // NOTE: The check here is to confirm that the time component is + // valid. + let _time = parse_record + .time + .map(IsoTime::from_time_record) + .transpose()? + .unwrap_or_default(); + Ok(Self { record, calendar }) } /// Converts a UTF-8 encoded YearMonth string into a `ParsedDate`. @@ -59,8 +69,19 @@ impl ParsedDate { // Assertion: PlainDate must exist on a DateTime parse. let record = parse_record.date.temporal_unwrap()?; + // TODO: Potentially, remove this check in favor of type guarantee of a + // Temporal parser. + // NOTE: The check here is to confirm that the time component is + // valid. + let _time = parse_record + .time + .map(IsoTime::from_time_record) + .transpose()? + .unwrap_or_default(); + Ok(Self { record, calendar }) } + /// Converts a UTF-8 encoded MonthDay string into a `ParsedDate`. pub fn month_day_from_utf8(s: &[u8]) -> TemporalResult { let parse_record = parsers::parse_month_day(s)?; @@ -70,6 +91,16 @@ impl ParsedDate { // Assertion: PlainDate must exist on a DateTime parse. let record = parse_record.date.temporal_unwrap()?; + // TODO: Potentially, remove this check in favor of type guarantee of a + // Temporal parser. + // NOTE: The check here is to confirm that the time component is + // valid. + let _time = parse_record + .time + .map(IsoTime::from_time_record) + .transpose()? + .unwrap_or_default(); + Ok(Self { record, calendar }) } } From df001519f0401ecbff26ac32a6ecfd7adce452c6 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 17 Feb 2026 20:02:51 -0600 Subject: [PATCH 2/2] Move zdt tests to zdt test module --- src/builtins/core/zoned_date_time/tests.rs | 28 ++++++++++++++++++++++ src/builtins/mod.rs | 21 ---------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/builtins/core/zoned_date_time/tests.rs b/src/builtins/core/zoned_date_time/tests.rs index c89a4650b..e56df724b 100644 --- a/src/builtins/core/zoned_date_time/tests.rs +++ b/src/builtins/core/zoned_date_time/tests.rs @@ -1175,6 +1175,34 @@ fn test_same_date_reverse_wallclock() { }) } +#[test] +fn test_invalid_fractional_offset_digits() { + test_all_providers!(provider: { + let test = "2020-01-01T00:00:00.123456789+02:30:00.1234567890[UTC]"; + let result = ZonedDateTime::from_utf8_with_provider( + test.as_bytes(), + crate::options::Disambiguation::Compatible, + crate::options::OffsetDisambiguation::Use, + &provider, + ); + assert!(result.is_err(), "ZonedDateTime should be invalid"); + }) +} + +#[test] +fn test_valid_fractional_offset_digits() { + test_all_providers!(provider: { + let test = "2020-01-01T00:00:00.123456789+02:30:00.123456789[UTC]"; + let result = ZonedDateTime::from_utf8_with_provider( + test.as_bytes(), + crate::options::Disambiguation::Compatible, + crate::options::OffsetDisambiguation::Use, + &provider, + ); + assert!(result.is_ok(), "ZonedDateTime should be valid"); + }) +} + #[test] fn test_relativeto_back_transition() { // intl402/Temporal/Duration/prototype/round/relativeto-dst-back-transition diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index a6cc1911b..7ffa9a6fe 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -22,10 +22,6 @@ pub(crate) static FS_TZ_PROVIDER: LazyLock = LazyLock::new(FsTzd #[cfg(test)] mod tests { - use timezone_provider::tzif::CompiledTzdbProvider; - - use crate::ZonedDateTime; - use super::{Instant, PlainDate, PlainDateTime}; #[test] fn builtins_from_str_10_digit_fractions() { @@ -41,33 +37,16 @@ mod tests { #[test] fn instant_based_10_digit_offset() { - let provider = CompiledTzdbProvider::default(); let test = "2020-01-01T00:00:00.123456789+02:30:00.1234567890[UTC]"; let result = Instant::from_utf8(test.as_bytes()); assert!(result.is_err(), "Instant should be invalid"); - let result = ZonedDateTime::from_utf8_with_provider( - test.as_bytes(), - crate::options::Disambiguation::Compatible, - crate::options::OffsetDisambiguation::Use, - &provider, - ); - assert!(result.is_err(), "ZonedDateTime should be invalid"); } #[test] fn instant_based_9_digit_offset() { - let provider = CompiledTzdbProvider::default(); let test = "2020-01-01T00:00:00.123456789+02:30:00.123456789[UTC]"; let result = Instant::from_utf8(test.as_bytes()); assert!(result.is_ok(), "Instant should be valid"); - let result = ZonedDateTime::from_utf8_with_provider( - test.as_bytes(), - crate::options::Disambiguation::Compatible, - crate::options::OffsetDisambiguation::Use, - &provider, - ); - std::println!("{result:?}"); - assert!(result.is_ok(), "ZonedDateTime should be valid"); } #[test]