Skip to content

Commit 71b78df

Browse files
Manishearthnekevss
andauthored
Updates for ZonedDateTime since / until issue (#619)
This updates the implementation for the tc39/proposal-temporal#3141 fix up for merge in tc39/proposal-temporal#3147. Ported from #530 The reverse_wallclock test still fails Co-authored-by: Kevin Ness <[email protected]>
1 parent c783303 commit 71b78df

File tree

6 files changed

+113
-29
lines changed

6 files changed

+113
-29
lines changed

src/builtins/core/duration/normalized.rs

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use core::{cmp, num::NonZeroU128, ops::Add};
44

55
use num_traits::AsPrimitive;
6+
use timezone_provider::epoch_nanoseconds::EpochNanoseconds;
67

78
use crate::{
89
builtins::core::{time_zone::TimeZone, PlainDate, PlainDateTime},
@@ -370,6 +371,7 @@ impl InternalDurationRecord {
370371
fn nudge_calendar_unit(
371372
&self,
372373
sign: Sign,
374+
origin_epoch_ns: EpochNanoseconds,
373375
dest_epoch_ns: i128,
374376
dt: &PlainDateTime,
375377
time_zone: Option<(&TimeZone, &(impl TimeZoneProvider + ?Sized))>, // ???
@@ -570,33 +572,48 @@ impl InternalDurationRecord {
570572
}
571573
};
572574

573-
// 7. Let start be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], startDuration, constrain).
574-
let start = dt
575-
.calendar()
576-
.date_add(&dt.iso.date, &start_duration, Overflow::Constrain)?;
575+
let start_epoch_ns = if r1 == 0 {
576+
origin_epoch_ns
577+
} else {
578+
// 7. Let start be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], startDuration, constrain).
579+
let start =
580+
dt.calendar()
581+
.date_add(&dt.iso.date, &start_duration, Overflow::Constrain)?;
582+
// 9. Let startDateTime be CombineISODateAndTimeRecord(start, isoDateTime.[[Time]]).
583+
let start_date_time = IsoDateTime::new_unchecked(start.iso, dt.iso.time);
584+
if let Some((time_zone, provider)) = time_zone {
585+
time_zone
586+
.get_epoch_nanoseconds_for(
587+
start_date_time,
588+
Disambiguation::Compatible,
589+
provider,
590+
)?
591+
.ns
592+
} else {
593+
start_date_time.as_nanoseconds()
594+
}
595+
};
596+
577597
// 8. Let end be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], endDuration, constrain).
578598
let end = dt
579599
.calendar()
580600
.date_add(&dt.iso.date, &end_duration, Overflow::Constrain)?;
581-
// 9. Let startDateTime be CombineISODateAndTimeRecord(start, isoDateTime.[[Time]]).
582-
let start = IsoDateTime::new_unchecked(start.iso, dt.iso.time);
601+
583602
// 10. Let endDateTime be CombineISODateAndTimeRecord(end, isoDateTime.[[Time]]).
584603
let end = IsoDateTime::new_unchecked(end.iso, dt.iso.time);
585604

586605
// 12. Else,
587-
let (start_epoch_ns, end_epoch_ns) = if let Some((time_zone, provider)) = time_zone {
606+
let end_epoch_ns = if let Some((time_zone, provider)) = time_zone {
588607
// a. Let startEpochNs be ? GetEpochNanosecondsFor(timeZone, startDateTime, compatible).
589608
// b. Let endEpochNs be ? GetEpochNanosecondsFor(timeZone, endDateTime, compatible).
590-
let start_epoch_ns =
591-
time_zone.get_epoch_nanoseconds_for(start, Disambiguation::Compatible, provider)?;
592-
let end_epoch_ns =
593-
time_zone.get_epoch_nanoseconds_for(end, Disambiguation::Compatible, provider)?;
594-
(start_epoch_ns.ns, end_epoch_ns.ns)
609+
time_zone
610+
.get_epoch_nanoseconds_for(end, Disambiguation::Compatible, provider)?
611+
.ns
595612
// 11. If timeZoneRec is unset, then
596613
} else {
597614
// a. Let startEpochNs be GetUTCEpochNanoseconds(start.[[Year]], start.[[Month]], start.[[Day]], start.[[Hour]], start.[[Minute]], start.[[Second]], start.[[Millisecond]], start.[[Microsecond]], start.[[Nanosecond]]).
598615
// b. Let endEpochNs be GetUTCEpochNanoseconds(end.[[Year]], end.[[Month]], end.[[Day]], end.[[Hour]], end.[[Minute]], end.[[Second]], end.[[Millisecond]], end.[[Microsecond]], end.[[Nanosecond]]).
599-
(start.as_nanoseconds(), end.as_nanoseconds())
616+
end.as_nanoseconds()
600617
};
601618

602619
// TODO: look into handling asserts
@@ -954,6 +971,7 @@ impl InternalDurationRecord {
954971
#[inline]
955972
pub(crate) fn round_relative_duration(
956973
&self,
974+
origin_epoch_ns: EpochNanoseconds,
957975
dest_epoch_ns: i128,
958976
dt: &PlainDateTime,
959977
time_zone: Option<(&TimeZone, &(impl TimeZoneProvider + ?Sized))>,
@@ -974,7 +992,14 @@ impl InternalDurationRecord {
974992
let nudge_result = if irregular_length_unit {
975993
// a. Let record be ? NudgeToCalendarUnit(sign, duration, destEpochNs, isoDateTime, timeZone, calendar, increment, smallestUnit, roundingMode).
976994
// b. Let nudgeResult be record.[[NudgeResult]].
977-
duration.nudge_calendar_unit(sign, dest_epoch_ns, dt, time_zone, options)?
995+
duration.nudge_calendar_unit(
996+
sign,
997+
origin_epoch_ns,
998+
dest_epoch_ns,
999+
dt,
1000+
time_zone,
1001+
options,
1002+
)?
9781003
} else if let Some((time_zone, time_zone_provider)) = time_zone {
9791004
// 6. Else if timeZone is not unset, then
9801005
// a. Let nudgeResult be ? NudgeToZonedTime(sign, duration, isoDateTime, timeZone, calendar, increment, smallestUnit, roundingMode).
@@ -1012,6 +1037,7 @@ impl InternalDurationRecord {
10121037
// 7.5.38 TotalRelativeDuration ( duration, destEpochNs, isoDateTime, timeZone, calendar, unit )
10131038
pub(crate) fn total_relative_duration(
10141039
&self,
1040+
origin_epoch_ns: EpochNanoseconds,
10151041
dest_epoch_ns: i128,
10161042
dt: &PlainDateTime,
10171043
time_zone: Option<(&TimeZone, &(impl TimeZoneProvider + ?Sized))>,
@@ -1024,6 +1050,7 @@ impl InternalDurationRecord {
10241050
// b. Let record be ? NudgeToCalendarUnit(sign, duration, destEpochNs, isoDateTime, timeZone, calendar, 1, unit, trunc).
10251051
let record = self.nudge_calendar_unit(
10261052
sign,
1053+
origin_epoch_ns,
10271054
dest_epoch_ns,
10281055
dt,
10291056
time_zone,

src/builtins/core/plain_date.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,15 +279,15 @@ impl PlainDate {
279279
resolved.smallest_unit == Unit::Day && resolved.increment.get() == 1;
280280
// 12. If roundingGranularityIsNoop is false, then
281281
if !rounding_granularity_is_noop {
282+
let iso_date_time = IsoDateTime::new_unchecked(self.iso, IsoTime::default());
283+
let origin_epoch_ns = iso_date_time.as_nanoseconds();
282284
// a. Let destEpochNs be GetUTCEpochNanoseconds(other.[[ISOYear]], other.[[ISOMonth]], other.[[ISODay]], 0, 0, 0, 0, 0, 0).
283285
let dest_epoch_ns = other.iso.as_nanoseconds();
284286
// b. Let dateTime be ISO Date-Time Record { [[Year]]: temporalDate.[[ISOYear]], [[Month]]: temporalDate.[[ISOMonth]], [[Day]]: temporalDate.[[ISODay]], [[Hour]]: 0, [[Minute]]: 0, [[Second]]: 0, [[Millisecond]]: 0, [[Microsecond]]: 0, [[Nanosecond]]: 0 }.
285-
let dt = PlainDateTime::new_unchecked(
286-
IsoDateTime::new_unchecked(self.iso, IsoTime::default()),
287-
self.calendar.clone(),
288-
);
287+
let dt = PlainDateTime::new_unchecked(iso_date_time, self.calendar.clone());
289288
// c. Set duration to ? RoundRelativeDuration(duration, destEpochNs, dateTime, calendarRec, unset, settings.[[LargestUnit]], settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[RoundingMode]]).
290289
duration = duration.round_relative_duration(
290+
origin_epoch_ns,
291291
dest_epoch_ns.0,
292292
&dt,
293293
Option::<(&TimeZone, &NeverProvider)>::None,

src/builtins/core/plain_date_time.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ impl PlainDateTime {
311311
let dest_epoch_ns = other.iso.as_nanoseconds();
312312
// 6. Return ? RoundRelativeDuration(diff, destEpochNs, isoDateTime1, unset, calendar, largestUnit, roundingIncrement, smallestUnit, roundingMode).
313313
diff.round_relative_duration(
314+
self.iso.as_nanoseconds(),
314315
dest_epoch_ns.0,
315316
self,
316317
Option::<(&TimeZone, &NeverProvider)>::None,
@@ -335,10 +336,12 @@ impl PlainDateTime {
335336
if unit == Unit::Nanosecond {
336337
return FiniteF64::try_from(diff.normalized_time_duration().0);
337338
}
339+
let origin_epoch_ns = self.iso.as_nanoseconds();
338340
// 5. Let destEpochNs be GetUTCEpochNanoseconds(isoDateTime2).
339341
let dest_epoch_ns = other.iso.as_nanoseconds();
340342
// 6. Return ? TotalRelativeDuration(diff, destEpochNs, isoDateTime1, unset, calendar, unit).
341343
diff.total_relative_duration(
344+
origin_epoch_ns,
342345
dest_epoch_ns.0,
343346
self,
344347
Option::<(&TimeZone, &NeverProvider)>::None,

src/builtins/core/plain_year_month.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ impl PlainYearMonth {
329329
let dest_epoch_ns = target_iso_date_time.as_nanoseconds();
330330
// d. Set duration to ? RoundRelativeDuration(duration, destEpochNs, isoDateTime, unset, calendar, resolved.[[LargestUnit]], resolved.[[RoundingIncrement]], resolved.[[SmallestUnit]], resolved.[[RoundingMode]]).
331331
duration = duration.round_relative_duration(
332+
iso_date_time.as_nanoseconds(),
332333
dest_epoch_ns.as_i128(),
333334
&PlainDateTime::new_unchecked(iso_date_time, self.calendar.clone()),
334335
Option::<(&TimeZone, &NeverProvider)>::None,

src/builtins/core/zoned_date_time.rs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ impl ZonedDateTime {
394394
let iso = self.get_iso_datetime();
395395
// 5. Return ? RoundRelativeDuration(difference, ns2, dateTime, timeZone, calendar, largestUnit, roundingIncrement, smallestUnit, roundingMode).
396396
diff.round_relative_duration(
397+
*self.epoch_nanoseconds(),
397398
other.epoch_nanoseconds().as_i128(),
398399
&PlainDateTime::new_unchecked(iso, self.calendar().clone()),
399400
Some((self.time_zone(), provider)),
@@ -425,6 +426,7 @@ impl ZonedDateTime {
425426
let iso = self.get_iso_datetime();
426427
// 4. Return ? TotalRelativeDuration(difference, ns2, dateTime, timeZone, calendar, unit).
427428
diff.total_relative_duration(
429+
*self.epoch_nanoseconds(),
428430
other.epoch_nanoseconds().as_i128(),
429431
&PlainDateTime::new_unchecked(iso, self.calendar().clone()),
430432
Some((self.time_zone(), provider)),
@@ -446,29 +448,39 @@ impl ZonedDateTime {
446448
let start = self.get_iso_datetime();
447449
// 3. Let endDateTime be GetISODateTimeFor(timeZone, ns2).
448450
let end = self.time_zone.get_iso_datetime_for(other, provider)?;
449-
// 4. If ns2 - ns1 < 0, let sign be -1; else let sign be 1.
451+
// 4. If CompareISODate(startDateTime.[[ISODate]], endDateTime.[[ISODate]]) = 0, then
452+
if start.date == end.date {
453+
// a. Let timeDuration be TimeDurationFromEpochNanosecondsDifference(ns2, ns1).
454+
let time_duration = TimeDuration::from_nanosecond_difference(
455+
other.epoch_nanoseconds().as_i128(),
456+
self.epoch_nanoseconds().as_i128(),
457+
)?;
458+
// b. Return CombineDateAndTimeDuration(ZeroDateDuration(), timeDuration).
459+
return InternalDurationRecord::new(Default::default(), time_duration);
460+
}
461+
// 5. If ns2 - ns1 < 0, let sign be -1; else let sign be 1.
450462
let sign = if other.epoch_nanoseconds().as_i128() - self.epoch_nanoseconds().as_i128() < 0 {
451463
Sign::Negative
452464
} else {
453465
Sign::Positive
454466
};
455-
// 5. If sign = 1, let maxDayCorrection be 2; else let maxDayCorrection be 1.
467+
// 6. If sign = 1, let maxDayCorrection be 2; else let maxDayCorrection be 1.
456468
let max_correction = if sign == Sign::Positive { 2 } else { 1 };
457-
// 6. Let dayCorrection be 0.
458-
// 7. Let timeDuration be DifferenceTime(startDateTime.[[Time]], endDateTime.[[Time]]).
469+
// 7. Let dayCorrection be 0.
470+
// 8. Let timeDuration be DifferenceTime(startDateTime.[[Time]], endDateTime.[[Time]]).
459471
let time = start.time.diff(&end.time);
460-
// 8. If TimeDurationSign(timeDuration) = -sign, set dayCorrection to dayCorrection + 1.
472+
// 9. If TimeDurationSign(timeDuration) = -sign, set dayCorrection to dayCorrection + 1.
461473
let mut day_correction = if time.sign() as i8 == -(sign as i8) {
462474
1
463475
} else {
464476
0
465477
};
466478

467-
// 9. Let success be false.
479+
// 10. Let success be false.
468480
let mut intermediate_dt = IsoDateTime::default();
469481
let mut time_duration = TimeDuration::default();
470482
let mut is_success = false;
471-
// 10. Repeat, while dayCorrection ≤ maxDayCorrection and success is false,
483+
// 11. Repeat, while dayCorrection ≤ maxDayCorrection and success is false,
472484
while day_correction <= max_correction && !is_success {
473485
// a. Let intermediateDate be BalanceISODate(endDateTime.[[ISODate]].[[Year]],
474486
// endDateTime.[[ISODate]].[[Month]], endDateTime.[[ISODate]].[[Day]] - dayCorrection × sign).
@@ -500,14 +512,15 @@ impl ZonedDateTime {
500512
// g. Set dayCorrection to dayCorrection + 1.
501513
day_correction += 1;
502514
}
503-
// 11. Assert: success is true.
504-
// 12. Let dateLargestUnit be LargerOfTwoUnits(largestUnit, day).
515+
// 12. Assert: success is true.
516+
// 13. Let dateLargestUnit be LargerOfTwoUnits(largestUnit, day).
505517
let date_largest = largest_unit.max(Unit::Day);
506-
// 13. Let dateDifference be CalendarDateUntil(calendar, startDateTime.[[ISODate]], intermediateDateTime.[[ISODate]], dateLargestUnit).
507-
// 14. Return CombineDateAndTimeDuration(dateDifference, timeDuration).
518+
// 14. Let dateDifference be CalendarDateUntil(calendar, startDateTime.[[ISODate]], intermediateDateTime.[[ISODate]], dateLargestUnit).
519+
// 15. Return CombineDateAndTimeDuration(dateDifference, timeDuration).
508520
let date_diff =
509521
self.calendar()
510522
.date_until(&start.date, &intermediate_dt.date, date_largest)?;
523+
511524
InternalDurationRecord::new(date_diff.date(), time_duration)
512525
}
513526

src/builtins/core/zoned_date_time/tests.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,3 +1146,43 @@ fn test_round_to_start_of_day() {
11461146
assert_eq!(rounded.get_iso_datetime(), known_rounded.get_iso_datetime());
11471147
})
11481148
}
1149+
1150+
#[test]
1151+
fn test_same_date_reverse_wallclock() {
1152+
// intl402/Temporal/ZonedDateTime/prototype/since/same-date-reverse-wallclock
1153+
test_all_providers!(provider: {
1154+
let later =
1155+
parse_zdt_with_reject("2025-11-02T01:00:00-08:00[America/Vancouver]", provider).unwrap();
1156+
let earlier =
1157+
parse_zdt_with_reject("2025-11-02T01:01:00-07:00[America/Vancouver]", provider).unwrap();
1158+
1159+
let diff = DifferenceSettings {
1160+
largest_unit: Some(Unit::Year),
1161+
smallest_unit: Some(Unit::Millisecond),
1162+
..Default::default()
1163+
};
1164+
let duration = later.since_with_provider(&earlier, diff, provider).unwrap();
1165+
assert_eq!(duration.minutes(), 59);
1166+
1167+
})
1168+
}
1169+
1170+
#[test]
1171+
fn test_relativeto_back_transition() {
1172+
// intl402/Temporal/Duration/prototype/round/relativeto-dst-back-transition
1173+
test_all_providers!(provider: {
1174+
let origin =
1175+
parse_zdt_with_reject("2025-11-02T01:00:00-08:00[America/Vancouver]", provider).unwrap();
1176+
let duration = Duration::new(0, 0, 0, 0, 11, 39, 0, 0, 0, 0).unwrap();
1177+
1178+
let opts = RoundingOptions {
1179+
largest_unit: Some(Unit::Day),
1180+
smallest_unit: Some(Unit::Day),
1181+
rounding_mode: Some(RoundingMode::HalfExpand),
1182+
..Default::default()
1183+
};
1184+
let rounded = duration.round_with_provider(opts, Some(origin.into()), provider).unwrap();
1185+
assert_eq!(rounded.days(), 0);
1186+
1187+
})
1188+
}

0 commit comments

Comments
 (0)