Skip to content

Commit 9a445cb

Browse files
committed
Update to new ComputeNudgeWindow spec text
1 parent dcd5688 commit 9a445cb

File tree

3 files changed

+227
-38
lines changed

3 files changed

+227
-38
lines changed

src/builtins/core/duration/normalized.rs

Lines changed: 164 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ use crate::{
1818
primitive::{DoubleDouble, FiniteF64},
1919
provider::TimeZoneProvider,
2020
rounding::IncrementRounder,
21-
Calendar, TemporalError, TemporalResult, TemporalUnwrap, NS_PER_DAY, NS_PER_DAY_NONZERO,
21+
temporal_assert, Calendar, TemporalError, TemporalResult, TemporalUnwrap, NS_PER_DAY,
22+
NS_PER_DAY_NONZERO,
2223
};
2324

2425
use super::{DateDuration, Duration, Sign};
@@ -395,18 +396,26 @@ struct NudgeRecord {
395396
expanded: bool,
396397
}
397398

399+
struct NudgeWindow {
400+
r1: i128,
401+
r2: i128,
402+
start_epoch_ns: EpochNanoseconds,
403+
end_epoch_ns: EpochNanoseconds,
404+
start_duration: DateDuration,
405+
end_duration: DateDuration,
406+
}
407+
398408
impl InternalDurationRecord {
399-
// TODO: Add assertion into impl.
400-
// TODO: Add unit tests specifically for nudge_calendar_unit if possible.
401-
fn nudge_calendar_unit(
409+
/// <https://tc39.es/proposal-temporal/#sec-temporal-computenudgewindow>
410+
fn compute_nudge_window(
402411
&self,
403412
sign: Sign,
404413
origin_epoch_ns: EpochNanoseconds,
405-
dest_epoch_ns: i128,
406414
dt: &PlainDateTime,
407415
time_zone: Option<(&TimeZone, &(impl TimeZoneProvider + ?Sized))>, // ???
408416
options: ResolvedRoundingOptions,
409-
) -> TemporalResult<NudgeRecord> {
417+
additional_shift: bool,
418+
) -> TemporalResult<NudgeWindow> {
410419
// NOTE: r2 may never be used...need to test.
411420
let (r1, r2, start_duration, end_duration) = match options.smallest_unit {
412421
// 1. If unit is "year", then
@@ -417,11 +426,18 @@ impl InternalDurationRecord {
417426
options.increment.as_extended_increment(),
418427
)?
419428
.round(RoundingMode::Trunc);
420-
// b. Let r1 be years.
421-
let r1 = years;
422-
// c. Let r2 be years + increment × sign.
423-
let r2 = years
424-
+ i128::from(options.increment.get()) * i128::from(sign.as_sign_multiplier());
429+
let increment_x_sign =
430+
i128::from(options.increment.get()) * i128::from(sign.as_sign_multiplier());
431+
// b. If additionalShift is false, then
432+
let r1 = if !additional_shift {
433+
// i. Let r1 be years.
434+
years
435+
} else {
436+
// i. Let r1 be years + increment × sign.
437+
years + increment_x_sign
438+
};
439+
// c. Let r2 be r1 + increment × sign.
440+
let r2 = r1 + increment_x_sign;
425441
// d. Let startDuration be ? CreateNormalizedDurationRecord(r1, 0, 0, 0, ZeroTimeDuration()).
426442
// e. Let endDuration be ? CreateNormalizedDurationRecord(r2, 0, 0, 0, ZeroTimeDuration()).
427443
(
@@ -449,11 +465,18 @@ impl InternalDurationRecord {
449465
options.increment.as_extended_increment(),
450466
)?
451467
.round(RoundingMode::Trunc);
452-
// b. Let r1 be months.
453-
let r1 = months;
454-
// c. Let r2 be months + increment × sign.
455-
let r2 = months
456-
+ i128::from(options.increment.get()) * i128::from(sign.as_sign_multiplier());
468+
let increment_x_sign =
469+
i128::from(options.increment.get()) * i128::from(sign.as_sign_multiplier());
470+
// b. If additionalShift is false, then
471+
let r1 = if !additional_shift {
472+
// i. Let r1 be months.
473+
months
474+
} else {
475+
// i. Let r1 be months + increment × sign.
476+
months + increment_x_sign
477+
};
478+
// c. Let r2 be r1 + increment × sign.
479+
let r2 = r1 + increment_x_sign;
457480
// d. Let startDuration be ? CreateNormalizedDurationRecord(duration.[[Years]], r1, 0, 0, ZeroTimeDuration()).
458481
// e. Let endDuration be ? CreateNormalizedDurationRecord(duration.[[Years]], r2, 0, 0, ZeroTimeDuration()).
459482
(
@@ -602,6 +625,14 @@ impl InternalDurationRecord {
602625
}
603626
};
604627

628+
// 5. Assert: If sign is 1, r1 ≥ 0 and r1 < r2.
629+
// 6. Assert: If sign is -1, r1 ≤ 0 and r1 > r2.
630+
// n.b. sign == 1 means nonnegative
631+
crate::temporal_assert!(
632+
(sign != Sign::Negative && r1 >= 0 && r1 < r2)
633+
|| (sign == Sign::Negative && r1 < 0 && r1 > r2)
634+
);
635+
605636
let start_epoch_ns = if r1 == 0 {
606637
origin_epoch_ns
607638
} else {
@@ -646,36 +677,132 @@ impl InternalDurationRecord {
646677
end.as_nanoseconds()
647678
};
648679

649-
// TODO: look into handling asserts
650-
// 13. If sign is 1, then
651-
// a. Assert: startEpochNs ≤ destEpochNs ≤ endEpochNs.
652-
// 14. Else,
653-
// a. Assert: endEpochNs ≤ destEpochNs ≤ startEpochNs.
654-
// 15. Assert: startEpochNs ≠ endEpochNs.
680+
return Ok(NudgeWindow {
681+
r1,
682+
r2,
683+
start_epoch_ns,
684+
end_epoch_ns,
685+
start_duration,
686+
end_duration,
687+
});
688+
}
689+
// TODO: Add assertion into impl.
690+
// TODO: Add unit tests specifically for nudge_calendar_unit if possible.
691+
fn nudge_calendar_unit(
692+
&self,
693+
sign: Sign,
694+
origin_epoch_ns: EpochNanoseconds,
695+
dest_epoch_ns: i128,
696+
dt: &PlainDateTime,
697+
time_zone: Option<(&TimeZone, &(impl TimeZoneProvider + ?Sized))>, // ???
698+
options: ResolvedRoundingOptions,
699+
) -> TemporalResult<NudgeRecord> {
700+
let dest_epoch_ns = EpochNanoseconds(dest_epoch_ns);
701+
702+
// 1. Let didExpandCalendarUnit be false.
703+
let mut did_expand_calendar_unit = false;
704+
705+
// 2. Let nudgeWindow be ? ComputeNudgeWindow(sign, duration, originEpochNs, isoDateTime, timeZone, calendar, increment, unit, false).
706+
let mut nudge_window =
707+
self.compute_nudge_window(sign, origin_epoch_ns, dt, time_zone, options, false)?;
708+
709+
// 3. Let startEpochNs be nudgeWindow.[[StartEpochNs]].
710+
// 4. Let endEpochNs be nudgeWindow.[[EndEpochNs]].
711+
// (implicitly used)
712+
713+
// 5. If sign is 1, then
714+
if sign != Sign::Negative {
715+
// a. If startEpochNs ≤ destEpochNs ≤ endEpochNs is false, then
716+
if !(nudge_window.start_epoch_ns <= dest_epoch_ns
717+
&& dest_epoch_ns <= nudge_window.end_epoch_ns)
718+
{
719+
// i. Set nudgeWindow to ? ComputeNudgeWindow(sign, duration, originEpochNs, isoDateTime, timeZone, calendar, increment, unit, true).
720+
nudge_window =
721+
self.compute_nudge_window(sign, origin_epoch_ns, dt, time_zone, options, true)?;
722+
// ii. Assert: nudgeWindow.[[StartEpochNs]] ≤ destEpochNs ≤ nudgeWindow.[[EndEpochNs]].
723+
temporal_assert!(
724+
nudge_window.start_epoch_ns <= dest_epoch_ns
725+
&& dest_epoch_ns <= nudge_window.end_epoch_ns
726+
);
727+
// iii. Set didExpandCalendarUnit to true.
728+
did_expand_calendar_unit = true;
729+
}
730+
} else {
731+
// a. If endEpochNs ≤ destEpochNs ≤ startEpochNs is false, then
732+
if !(nudge_window.end_epoch_ns <= dest_epoch_ns
733+
&& dest_epoch_ns <= nudge_window.start_epoch_ns)
734+
{
735+
// i. Set nudgeWindow to ? ComputeNudgeWindow(sign, duration, originEpochNs, isoDateTime, timeZone, calendar, increment, unit, true).
736+
nudge_window =
737+
self.compute_nudge_window(sign, origin_epoch_ns, dt, time_zone, options, true)?;
738+
// ii. Assert: nudgeWindow.[[EndEpochNs]] ≤ destEpochNs ≤ nudgeWindow.[[StartEpochNs]].
739+
temporal_assert!(
740+
nudge_window.end_epoch_ns <= dest_epoch_ns
741+
&& dest_epoch_ns <= nudge_window.start_epoch_ns
742+
);
743+
// iii. Set didExpandCalendarUnit to true.
744+
did_expand_calendar_unit = true;
745+
}
746+
}
747+
748+
// 7. Let r1 be nudgeWindow.[[R1]].
749+
// 8. Let r2 be nudgeWindow.[[R2]].
750+
// 9. Set startEpochNs to nudgeWindow.[[StartEpochNs]].
751+
// 10. Set endEpochNs to nudgeWindow.[[StartEpochNs]].
752+
// 11. Let startDuration be nudgeWindow.[[StartDuration]].
753+
// 12. Let endDuration be nudgeWindow.[[EndDuration]].
754+
755+
let NudgeWindow {
756+
r1,
757+
r2,
758+
start_epoch_ns,
759+
end_epoch_ns,
760+
start_duration,
761+
end_duration,
762+
} = nudge_window;
763+
764+
// 13. Assert: startEpochNs ≠ endEpochNs.
765+
temporal_assert!(start_epoch_ns != end_epoch_ns);
655766

656767
// TODO: Don't use f64 below ...
657768
// NOTE(nekevss): Step 12..13 could be problematic...need tests
658769
// and verify, or completely change the approach involved.
659770
// TODO(nekevss): Validate that the `f64` casts here are valid in all scenarios
660-
// 16. Let progress be (destEpochNs - startEpochNs) / (endEpochNs - startEpochNs).
661-
// 17. Let total be r1 + progress × increment × sign.
662-
let progress =
663-
(dest_epoch_ns - start_epoch_ns.0) as f64 / (end_epoch_ns.0 - start_epoch_ns.0) as f64;
771+
// 14. Let progress be (destEpochNs - startEpochNs) / (endEpochNs - startEpochNs).
772+
// 15. Let total be r1 + progress × increment × sign.
773+
let progress = (dest_epoch_ns.0 - start_epoch_ns.0) as f64
774+
/ (end_epoch_ns.0 - start_epoch_ns.0) as f64;
664775
let total = r1 as f64
665776
+ progress * options.increment.get() as f64 * f64::from(sign.as_sign_multiplier());
666777

667-
// 14. NOTE: The above two steps cannot be implemented directly using floating-point arithmetic.
778+
// 16. NOTE: The above two steps cannot be implemented directly using floating-point arithmetic.
668779
// This division can be implemented as if constructing Normalized Time Duration Records for the denominator
669780
// and numerator of total and performing one division operation with a floating-point result.
670-
// 15. Let roundedUnit be ApplyUnsignedRoundingMode(total, r1, r2, unsignedRoundingMode).
671-
let rounded_unit =
672-
IncrementRounder::from_signed_num(total, options.increment.as_extended_increment())?
673-
.round(options.rounding_mode);
674-
675-
// 16. If roundedUnit - total < 0, let roundedSign be -1; else let roundedSign be 1.
676-
// 19. Return Duration Nudge Result Record { [[Duration]]: resultDuration, [[Total]]: total, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didExpandCalendarUnit }.
677-
// 17. If roundedSign = sign, then
678-
if rounded_unit == r2 {
781+
// 17. Assert: 0 ≤ progress ≤ 1.
782+
temporal_assert!(0. <= progress && progress <= 1.);
783+
// 18. If sign < 0, let isNegative be negative; else let isNegative be positive.
784+
// (used implicitly)
785+
786+
// 19. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative).
787+
// n.b. get_unsigned_round_mode takes is_positive, but it actually cares about nonnegative
788+
let unsigned_rounding_mode = options
789+
.rounding_mode
790+
.get_unsigned_round_mode(sign != Sign::Negative);
791+
792+
// 20. If progress = 1, then
793+
let rounded_unit = if progress == 1. {
794+
// a. Let roundedUnit be abs(r2).
795+
r2.abs()
796+
} else {
797+
// a. Assert: abs(r1) ≤ abs(total) < abs(r2).
798+
temporal_assert!(r1.abs() as f64 <= total.abs() && total.abs() < r2.abs() as f64);
799+
// b. Let roundedUnit be ApplyUnsignedRoundingMode(abs(total), abs(r1), abs(r2), unsignedRoundingMode).
800+
// TODO: what happens to r2 here?
801+
unsigned_rounding_mode.apply(total.abs(), r1.abs(), r2.abs())
802+
};
803+
804+
// 22. If roundedUnit is abs(r2), then
805+
if rounded_unit == r2.abs() {
679806
// a. Let didExpandCalendarUnit be true.
680807
// b. Let resultDuration be endDuration.
681808
// c. Let nudgedEpochNs be endEpochNs.
@@ -687,14 +814,13 @@ impl InternalDurationRecord {
687814
})
688815
// 18. Else,
689816
} else {
690-
// a. Let didExpandCalendarUnit be false.
691817
// b. Let resultDuration be startDuration.
692818
// c. Let nudgedEpochNs be startEpochNs.
693819
Ok(NudgeRecord {
694820
normalized: InternalDurationRecord::new(start_duration, TimeDuration::default())?,
695821
total: Some(FiniteF64::try_from(total)?),
696822
nudge_epoch_ns: start_epoch_ns.0,
697-
expanded: false,
823+
expanded: did_expand_calendar_unit,
698824
})
699825
}
700826
}

src/builtins/core/duration/tests.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::{
55
parsers::Precision,
66
partial::PartialDuration,
77
provider::NeverProvider,
8+
Calendar, PlainDate,
89
};
910

1011
use super::Duration;
@@ -452,3 +453,24 @@ fn total_full_numeric_precision() {
452453
let d = Duration::new(0, 0, 0, 0, 0, 0, 0, MAX_SAFE_INTEGER + 1, 1999, 0).unwrap();
453454
assert_eq!(d.total(Unit::Millisecond, None).unwrap(), 9007199254740994.);
454455
}
456+
457+
/// Test for https://github.com/tc39/proposal-temporal/pull/3172/
458+
///
459+
/// test262: built-ins/Temporal/Duration/prototype/total/rounding-window
460+
#[test]
461+
#[cfg(feature = "compiled_data")]
462+
fn test_nudge_relative_date_total() {
463+
let d = Duration::new(1, 0, 0, 0, 1, 0, 0, 0, 0, 0).unwrap();
464+
let relative = PlainDate::new(2020, 2, 29, Calendar::ISO).unwrap();
465+
assert_eq!(
466+
d.total(Unit::Year, Some(relative.into())).unwrap(),
467+
1.0001141552511414
468+
);
469+
470+
let d = Duration::new(0, 1, 0, 0, 10, 0, 0, 0, 0, 0).unwrap();
471+
let relative = PlainDate::new(2020, 1, 31, Calendar::ISO).unwrap();
472+
assert_eq!(
473+
d.total(Unit::Month, Some(relative.into())).unwrap(),
474+
1.0134408602150538
475+
);
476+
}

src/options.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,47 @@ impl RoundingMode {
847847
}
848848
}
849849

850+
impl UnsignedRoundingMode {
851+
/// <https://tc39.es/proposal-temporal/#sec-applyunsignedroundingmode>
852+
pub(crate) fn apply(self, x: f64, r1: i128, r2: i128) -> i128 {
853+
// 1. If x = r1, return r1.
854+
if r1 as f64 == x {
855+
return r1;
856+
}
857+
// 4. If unsignedRoundingMode is zero, return r1.
858+
if self == UnsignedRoundingMode::Zero {
859+
return r1;
860+
} else if self == UnsignedRoundingMode::Infinity {
861+
return r2;
862+
}
863+
// 6. Let d1 be x – r1.
864+
// 7. Let d2 be r2 – x.
865+
let d1 = x - r1 as f64;
866+
let d2 = r2 as f64 - x;
867+
if d1 < d2 {
868+
return r1;
869+
} else if d1 > d2 {
870+
return r2;
871+
}
872+
match self {
873+
UnsignedRoundingMode::HalfZero => return r1,
874+
UnsignedRoundingMode::HalfInfinity => return r2,
875+
// HalfEven
876+
_ => {
877+
// 14. Let cardinality be (r1 / (r2 – r1)) modulo 2.
878+
let diff = r2 - r1;
879+
let cardinality = (r1 as f64 / diff as f64).rem_euclid(2.);
880+
// 15. If cardinality = 0, return r1.
881+
if cardinality == 0. {
882+
return r1;
883+
} else {
884+
return r2;
885+
}
886+
}
887+
}
888+
}
889+
}
890+
850891
impl FromStr for RoundingMode {
851892
type Err = TemporalError;
852893

0 commit comments

Comments
 (0)