Skip to content

Commit fe975d7

Browse files
authored
Add NonZeroSign type to ensure proper sign handling (#652)
This adds a `NonZeroSign` type to `lib.rs` and makes appropriate adjustments for the new type. Open question is with a `Sign` and `NonZeroSign` type now in temporal_rs. Should we move them to their own `sign` module or keep them in the crate root?
1 parent 305b33f commit fe975d7

4 files changed

Lines changed: 136 additions & 56 deletions

File tree

src/builtins/core/duration/normalized.rs

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

2525
use super::{DateDuration, Duration, Sign};
@@ -409,7 +409,7 @@ impl InternalDurationRecord {
409409
/// `compute_and_adjust_nudge_window` in `temporal_rs` refers to step 1-12 of `NudgeToCalendarUnit`.
410410
fn compute_and_adjust_nudge_window(
411411
&self,
412-
sign: Sign,
412+
sign: NonZeroSign,
413413
origin_epoch_ns: EpochNanoseconds,
414414
dest_epoch_ns: i128,
415415
dt: &PlainDateTime,
@@ -430,47 +430,61 @@ impl InternalDurationRecord {
430430
// (implicitly used)
431431

432432
// 5. If sign is 1, then
433-
if sign != Sign::Negative {
434-
// a. If startEpochNs ≤ destEpochNs ≤ endEpochNs is false, then
435-
if !(nudge_window.start_epoch_ns <= dest_epoch_ns
436-
&& dest_epoch_ns <= nudge_window.end_epoch_ns)
437-
{
438-
// i. Set nudgeWindow to ? ComputeNudgeWindow(sign, duration, originEpochNs, isoDateTime, timeZone, calendar, increment, unit, true).
439-
nudge_window =
440-
self.compute_nudge_window(sign, origin_epoch_ns, dt, time_zone, options, true)?;
441-
// ii. Assert: nudgeWindow.[[StartEpochNs]] ≤ destEpochNs ≤ nudgeWindow.[[EndEpochNs]].
442-
temporal_assert!(
443-
nudge_window.start_epoch_ns <= dest_epoch_ns
444-
&& dest_epoch_ns <= nudge_window.end_epoch_ns
445-
);
446-
// iii. Set didExpandCalendarUnit to true.
447-
did_expand_calendar_unit = true;
433+
match sign {
434+
NonZeroSign::Positive => {
435+
// a. If startEpochNs ≤ destEpochNs ≤ endEpochNs is false, then
436+
if !(nudge_window.start_epoch_ns <= dest_epoch_ns
437+
&& dest_epoch_ns <= nudge_window.end_epoch_ns)
438+
{
439+
// i. Set nudgeWindow to ? ComputeNudgeWindow(sign, duration, originEpochNs, isoDateTime, timeZone, calendar, increment, unit, true).
440+
nudge_window = self.compute_nudge_window(
441+
sign,
442+
origin_epoch_ns,
443+
dt,
444+
time_zone,
445+
options,
446+
true,
447+
)?;
448+
// ii. Assert: nudgeWindow.[[StartEpochNs]] ≤ destEpochNs ≤ nudgeWindow.[[EndEpochNs]].
449+
temporal_assert!(
450+
nudge_window.start_epoch_ns <= dest_epoch_ns
451+
&& dest_epoch_ns <= nudge_window.end_epoch_ns
452+
);
453+
// iii. Set didExpandCalendarUnit to true.
454+
did_expand_calendar_unit = true;
455+
}
448456
}
449-
} else {
450-
// a. If endEpochNs ≤ destEpochNs ≤ startEpochNs is false, then
451-
if !(nudge_window.end_epoch_ns <= dest_epoch_ns
452-
&& dest_epoch_ns <= nudge_window.start_epoch_ns)
453-
{
454-
// i. Set nudgeWindow to ? ComputeNudgeWindow(sign, duration, originEpochNs, isoDateTime, timeZone, calendar, increment, unit, true).
455-
nudge_window =
456-
self.compute_nudge_window(sign, origin_epoch_ns, dt, time_zone, options, true)?;
457-
// ii. Assert: nudgeWindow.[[EndEpochNs]] ≤ destEpochNs ≤ nudgeWindow.[[StartEpochNs]].
458-
temporal_assert!(
459-
nudge_window.end_epoch_ns <= dest_epoch_ns
460-
&& dest_epoch_ns <= nudge_window.start_epoch_ns
461-
);
462-
// iii. Set didExpandCalendarUnit to true.
463-
did_expand_calendar_unit = true;
457+
NonZeroSign::Negative => {
458+
// a. If endEpochNs ≤ destEpochNs ≤ startEpochNs is false, then
459+
if !(nudge_window.end_epoch_ns <= dest_epoch_ns
460+
&& dest_epoch_ns <= nudge_window.start_epoch_ns)
461+
{
462+
// i. Set nudgeWindow to ? ComputeNudgeWindow(sign, duration, originEpochNs, isoDateTime, timeZone, calendar, increment, unit, true).
463+
nudge_window = self.compute_nudge_window(
464+
sign,
465+
origin_epoch_ns,
466+
dt,
467+
time_zone,
468+
options,
469+
true,
470+
)?;
471+
// ii. Assert: nudgeWindow.[[EndEpochNs]] ≤ destEpochNs ≤ nudgeWindow.[[StartEpochNs]].
472+
temporal_assert!(
473+
nudge_window.end_epoch_ns <= dest_epoch_ns
474+
&& dest_epoch_ns <= nudge_window.start_epoch_ns
475+
);
476+
// iii. Set didExpandCalendarUnit to true.
477+
did_expand_calendar_unit = true;
478+
}
464479
}
465480
}
466-
467481
Ok((nudge_window, did_expand_calendar_unit))
468482
}
469483

470484
/// <https://tc39.es/proposal-temporal/#sec-temporal-computenudgewindow>
471485
fn compute_nudge_window(
472486
&self,
473-
sign: Sign,
487+
sign: NonZeroSign,
474488
origin_epoch_ns: EpochNanoseconds,
475489
dt: &PlainDateTime,
476490
time_zone: Option<(&TimeZone, &(impl TimeZoneProvider + ?Sized))>, // ???
@@ -689,8 +703,8 @@ impl InternalDurationRecord {
689703
// 6. Assert: If sign is -1, r1 ≤ 0 and r1 > r2.
690704
// n.b. sign == 1 means nonnegative
691705
crate::temporal_assert!(
692-
(sign != Sign::Negative && r1 >= 0 && r1 < r2)
693-
|| (sign == Sign::Negative && r1 <= 0 && r1 > r2)
706+
(sign != NonZeroSign::Negative && r1 >= 0 && r1 < r2)
707+
|| (sign == NonZeroSign::Negative && r1 <= 0 && r1 > r2)
694708
);
695709

696710
let start_epoch_ns = if r1 == 0 {
@@ -749,7 +763,7 @@ impl InternalDurationRecord {
749763

750764
fn nudge_calendar_unit_total(
751765
&self,
752-
sign: Sign,
766+
sign: NonZeroSign,
753767
origin_epoch_ns: EpochNanoseconds,
754768
dest_epoch_ns: i128,
755769
dt: &PlainDateTime,
@@ -827,7 +841,7 @@ impl InternalDurationRecord {
827841
// TODO: Add unit tests specifically for nudge_calendar_unit if possible.
828842
fn nudge_calendar_unit(
829843
&self,
830-
sign: Sign,
844+
sign: NonZeroSign,
831845
origin_epoch_ns: EpochNanoseconds,
832846
dest_epoch_ns: i128,
833847
dt: &PlainDateTime,
@@ -912,7 +926,7 @@ impl InternalDurationRecord {
912926
// n.b. get_unsigned_round_mode takes is_positive, but it actually cares about nonnegative
913927
let unsigned_rounding_mode = options
914928
.rounding_mode
915-
.get_unsigned_round_mode(sign != Sign::Negative);
929+
.get_unsigned_round_mode(sign != NonZeroSign::Negative);
916930

917931
// NOTE (nekevss):
918932
//
@@ -966,7 +980,7 @@ impl InternalDurationRecord {
966980
#[inline]
967981
fn nudge_to_zoned_time(
968982
&self,
969-
sign: Sign,
983+
sign: NonZeroSign,
970984
dt: &PlainDateTime,
971985
time_zone: &TimeZone,
972986
options: ResolvedRoundingOptions,
@@ -1012,7 +1026,7 @@ impl InternalDurationRecord {
10121026
let beyond_day_span = rounded_time.checked_add(day_span.negate().0)?;
10131027
// 12. If TimeDurationSign(beyondDaySpan) ≠ -sign, then
10141028
let (expanded, day_delta, rounded_time, nudge_ns) =
1015-
if (beyond_day_span.sign() != sign.negate()) && sign != Sign::Zero {
1029+
if beyond_day_span.sign() != sign.negate() {
10161030
// a. Let didRoundBeyondDay be true.
10171031
// b. Let dayDelta be sign.
10181032
// c. Set roundedTimeDuration to ? RoundTimeDurationToIncrement(beyondDaySpan, increment × unitLength, roundingMode).
@@ -1127,7 +1141,7 @@ impl InternalDurationRecord {
11271141
#[allow(clippy::too_many_arguments)]
11281142
fn bubble_relative_duration(
11291143
&self,
1130-
sign: Sign,
1144+
sign: NonZeroSign,
11311145
nudged_epoch_ns: i128,
11321146
iso_date_time: &IsoDateTime,
11331147
time_zone: Option<(&TimeZone, &(impl TimeZoneProvider + ?Sized))>,
@@ -1277,7 +1291,7 @@ impl InternalDurationRecord {
12771291
|| (time_zone.is_some() && options.smallest_unit == Unit::Day);
12781292

12791293
// 4. If InternalDurationSign(duration) < 0, let sign be -1; else let sign be 1.
1280-
let sign = duration.sign();
1294+
let sign = duration.sign().to_nonzero_sign();
12811295

12821296
// 5. If irregularLengthUnit is true, then
12831297
let nudge_result = if irregular_length_unit {
@@ -1336,8 +1350,8 @@ impl InternalDurationRecord {
13361350
) -> TemporalResult<FiniteF64> {
13371351
// 1. If IsCalendarUnit(unit) is true, or timeZone is not unset and unit is day, then
13381352
if unit.is_calendar_unit() || (time_zone.is_some() && unit == Unit::Day) {
1339-
// a. Let sign be InternalDurationSign(duration).
1340-
let sign = self.sign();
1353+
// a. If InternalDurationSign(duration) < 0, let sign be -1; else let sign be 1
1354+
let sign = self.sign().to_nonzero_sign();
13411355
// b. Let record be ? NudgeToCalendarUnit(sign, duration, destEpochNs, isoDateTime, timeZone, calendar, 1, unit, trunc).
13421356
// c. Return record.[[Total]].
13431357
return self.nudge_calendar_unit_total(

src/builtins/core/duration/tests.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,3 +578,21 @@ fn rounding_window() {
578578
let result = d.round(options, Some(relative_to.into())).unwrap();
579579
assert_eq!(result.years(), 1, "months rounding should no-op");
580580
}
581+
582+
#[test]
583+
#[cfg(feature = "compiled_data")]
584+
fn zero_duration() {
585+
use crate::{TimeZone, ZonedDateTime};
586+
587+
let zero = Duration::new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0).unwrap();
588+
let relative_to = ZonedDateTime::try_new_iso(0, TimeZone::utc()).unwrap();
589+
590+
let options = RoundingOptions {
591+
smallest_unit: Some(Unit::Hour),
592+
largest_unit: Some(Unit::Day),
593+
..Default::default()
594+
};
595+
let result = zero.round(options, Some(relative_to.into())).unwrap();
596+
597+
assert_eq!(result, Duration::default(), "Duration's must be zero");
598+
}

src/builtins/core/zoned_date_time.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! This module contains the core implementation of the `ZonedDateTime`
22
//! builtin type.
33
4-
use crate::provider::EpochNanosecondsAndOffset;
4+
use crate::{provider::EpochNanosecondsAndOffset, NonZeroSign};
55
use alloc::string::String;
66
use core::{cmp::Ordering, num::NonZeroU128};
77
use tinystr::TinyAsciiStr;
@@ -459,18 +459,18 @@ impl ZonedDateTime {
459459
return InternalDurationRecord::new(Default::default(), time_duration);
460460
}
461461
// 5. If ns2 - ns1 < 0, let sign be -1; else let sign be 1.
462-
let sign = if other.epoch_nanoseconds().as_i128() - self.epoch_nanoseconds().as_i128() < 0 {
463-
Sign::Negative
464-
} else {
465-
Sign::Positive
466-
};
462+
let diff = other.epoch_nanoseconds().as_i128() - self.epoch_nanoseconds().as_i128();
463+
let sign = NonZeroSign::from(diff.signum() as i8);
467464
// 6. If sign = 1, let maxDayCorrection be 2; else let maxDayCorrection be 1.
468-
let max_correction = if sign == Sign::Positive { 2 } else { 1 };
465+
let max_correction = match sign {
466+
NonZeroSign::Positive => 2,
467+
NonZeroSign::Negative => 1,
468+
};
469469
// 7. Let dayCorrection be 0.
470470
// 8. Let timeDuration be DifferenceTime(startDateTime.[[Time]], endDateTime.[[Time]]).
471-
let time = start.time.diff(&end.time);
471+
let time_duration = start.time.diff(&end.time);
472472
// 9. If TimeDurationSign(timeDuration) = -sign, set dayCorrection to dayCorrection + 1.
473-
let mut day_correction = if time.sign() as i8 == -(sign as i8) {
473+
let mut day_correction = if time_duration.sign() == sign.negate() {
474474
1
475475
} else {
476476
0

src/lib.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,9 +431,57 @@ impl Sign {
431431
pub(crate) fn negate(&self) -> Sign {
432432
Sign::from(-(*self as i8))
433433
}
434+
435+
pub(crate) fn to_nonzero_sign(self) -> NonZeroSign {
436+
self.into()
437+
}
438+
}
439+
440+
impl PartialEq<NonZeroSign> for Sign {
441+
fn eq(&self, other: &NonZeroSign) -> bool {
442+
*self as i8 == *other as i8
443+
}
444+
}
445+
446+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
447+
pub(crate) enum NonZeroSign {
448+
Positive = 1,
449+
Negative = -1,
450+
}
451+
452+
impl NonZeroSign {
453+
pub(crate) const fn as_sign_multiplier(&self) -> i8 {
454+
*self as i8
455+
}
456+
457+
/// Negate the current sign
458+
pub(crate) fn negate(&self) -> NonZeroSign {
459+
Sign::from(-(*self as i8)).into()
460+
}
461+
}
462+
463+
impl From<Sign> for NonZeroSign {
464+
fn from(sign: Sign) -> Self {
465+
match sign {
466+
Sign::Positive | Sign::Zero => NonZeroSign::Positive,
467+
Sign::Negative => NonZeroSign::Negative,
468+
}
469+
}
470+
}
471+
472+
impl From<i8> for NonZeroSign {
473+
fn from(value: i8) -> Self {
474+
Sign::from(value).to_nonzero_sign()
475+
}
476+
}
477+
478+
impl PartialEq<Sign> for NonZeroSign {
479+
fn eq(&self, other: &Sign) -> bool {
480+
*self as i8 == *other as i8
481+
}
434482
}
435483

436-
// Relevant numeric constants
484+
// ==== Relevant numeric constants ====
437485

438486
/// Nanoseconds per day constant: 8.64e+13
439487
#[doc(hidden)]

0 commit comments

Comments
 (0)