diff --git a/src/builtins/core/zoned_date_time.rs b/src/builtins/core/zoned_date_time.rs index ff4e32041..a5b1199d9 100644 --- a/src/builtins/core/zoned_date_time.rs +++ b/src/builtins/core/zoned_date_time.rs @@ -1301,17 +1301,20 @@ impl ZonedDateTime { // c. Let startNs be ? GetStartOfDay(timeZone, dateStart). // d. Assert: thisNs ≥ startNs. // e. Let endNs be ? GetStartOfDay(timeZone, dateEnd). - // f. Assert: thisNs < endNs. + // f. [Struck out in spec] Assert: thisNs < endNs. let start = self.time_zone.get_start_of_day(&iso_start.date, provider)?; let end = self.time_zone.get_start_of_day(&iso_end, provider)?; - if !(this_ns.0 >= start.ns.0 && this_ns.0 < end.ns.0) { + if this_ns.0 < start.ns.0 { return Err(TemporalError::range().with_enum(ErrorMessage::ZDTOutOfDayBounds)); } - // g. Let dayLengthNs be ℝ(endNs - startNs). - // h. Let dayProgressNs be TimeDurationFromEpochNanosecondsDifference(thisNs, startNs). + // g. Set thisNs to min(thisNs, endNs - 1). + let this_ns_val = this_ns.0.min(end.ns.0 - 1); + // h. Let dayLengthNs be ℝ(endNs - startNs). + // i. Let dayProgressNs be TimeDurationFromEpochNanosecondsDifference(thisNs, startNs). let day_len_ns = TimeDuration::from_nanosecond_difference(end.ns.0, start.ns.0)?; - let day_progress_ns = TimeDuration::from_nanosecond_difference(this_ns.0, start.ns.0)?; - // i. Let roundedDayNs be ! RoundTimeDurationToIncrement(dayProgressNs, dayLengthNs, roundingMode). + let day_progress_ns = + TimeDuration::from_nanosecond_difference(this_ns_val, start.ns.0)?; + // j. Let roundedDayNs be ! RoundTimeDurationToIncrement(dayProgressNs, dayLengthNs, roundingMode). let rounded = if let Some(increment) = NonZeroU128::new(day_len_ns.0.unsigned_abs()) { IncrementRounder::::from_signed_num(day_progress_ns.0, increment)? .round(resolved.rounding_mode) @@ -1326,7 +1329,7 @@ impl ZonedDateTime { end.offset }; - // j. Let epochNanoseconds be AddTimeDurationToEpochNanoseconds(roundedDayNs, startNs). + // k. Let epochNanoseconds be AddTimeDurationToEpochNanoseconds(roundedDayNs, startNs). let candidate = start.ns.0 + rounded; Instant::try_new(candidate)?; // 20. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar). diff --git a/src/builtins/core/zoned_date_time/tests.rs b/src/builtins/core/zoned_date_time/tests.rs index e56df724b..0efc54261 100644 --- a/src/builtins/core/zoned_date_time/tests.rs +++ b/src/builtins/core/zoned_date_time/tests.rs @@ -1278,3 +1278,104 @@ fn hours_in_day_dst_changes() { assert_eq!(spring.hours_in_day_with_provider(provider), Ok(23.0)); }) } + +// Case where midnight occurs twice (e.g., Antarctica/Casey on 2010-03-05). +// Upstream tests: https://github.com/tc39/test262/pull/5047 +#[test] +fn test_same_date_starts_twice() { + test_all_providers!(provider: { + let zdt1 = ZonedDateTime::from_utf8_with_provider( + b"2010-03-04T23:10:00+11:00[Antarctica/Casey]", + Disambiguation::Compatible, + OffsetDisambiguation::Use, + provider, + ); + if zdt1.is_err() { + std::println!("Antarctica/Casey not supported by provider, skipping test."); + return; + } + let zdt1 = zdt1.unwrap(); + + let zdt2 = ZonedDateTime::from_utf8_with_provider( + b"2010-03-05T00:45:00+11:00[Antarctica/Casey]", + Disambiguation::Compatible, + OffsetDisambiguation::Use, + provider, + ).unwrap(); + + let zdt3 = ZonedDateTime::from_utf8_with_provider( + b"2010-03-04T23:10:00+08:00[Antarctica/Casey]", + Disambiguation::Compatible, + OffsetDisambiguation::Use, + provider, + ).unwrap(); + + let zdt4 = ZonedDateTime::from_utf8_with_provider( + b"2010-03-05T00:45:00+08:00[Antarctica/Casey]", + Disambiguation::Compatible, + OffsetDisambiguation::Use, + provider, + ).unwrap(); + + let start_of_march4 = "2010-03-04T00:00:00+11:00[Antarctica/Casey]"; + let start_of_march5 = "2010-03-05T00:00:00+11:00[Antarctica/Casey]"; + let start_of_march6 = "2010-03-06T00:00:00+08:00[Antarctica/Casey]"; + + // Hours in day + assert_eq!(zdt1.hours_in_day_with_provider(provider).unwrap(), 24.0); + assert_eq!(zdt2.hours_in_day_with_provider(provider).unwrap(), 27.0); + assert_eq!(zdt3.hours_in_day_with_provider(provider).unwrap(), 24.0); + assert_eq!(zdt4.hours_in_day_with_provider(provider).unwrap(), 27.0); + + // Start of day + assert_eq!(zdt1.start_of_day_with_provider(provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march4); + assert_eq!(zdt2.start_of_day_with_provider(provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5); + assert_eq!(zdt3.start_of_day_with_provider(provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march4); + assert_eq!(zdt4.start_of_day_with_provider(provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5); + + // Rounding down + for rounding_mode in [RoundingMode::Floor, RoundingMode::Trunc] { + let options = RoundingOptions { + smallest_unit: Some(Unit::Day), + rounding_mode: Some(rounding_mode), + ..Default::default() + }; + assert_eq!(zdt1.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march4); + assert_eq!(zdt2.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5); + assert_eq!(zdt3.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march4); + assert_eq!(zdt4.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5); + } + + // Rounding to nearest + for rounding_mode in [ + RoundingMode::HalfCeil, + RoundingMode::HalfEven, + RoundingMode::HalfExpand, + RoundingMode::HalfFloor, + RoundingMode::HalfTrunc, + ] { + let options = RoundingOptions { + smallest_unit: Some(Unit::Day), + rounding_mode: Some(rounding_mode), + ..Default::default() + }; + assert_eq!(zdt1.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5); + assert_eq!(zdt2.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5); + assert_eq!(zdt3.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5); + assert_eq!(zdt4.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5); + } + + // Rounding up + for rounding_mode in [RoundingMode::Ceil, RoundingMode::Expand] { + let options = RoundingOptions { + smallest_unit: Some(Unit::Day), + rounding_mode: Some(rounding_mode), + ..Default::default() + }; + assert_eq!(zdt1.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5); + assert_eq!(zdt2.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march6); + assert_eq!(zdt3.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5); + assert_eq!(zdt4.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march6); + } + }) +}