Skip to content

Commit 1566796

Browse files
sffcSebastianJMHenrikTennebekkMagnus-Fjeldstadlockels
authored
Temporal duration compare (#186)
Fixes #154 Wrote this along with @lockels, @Neelzee, @sebastianjacmatt, @Magnus-Fjeldstad, @HenrikTennebekk. Please see question regarding the return type of DateDurationDays. --------- Co-authored-by: SebastianJM <[email protected]> Co-authored-by: Henrik Tennebekk <[email protected]> Co-authored-by: Mafje8943 <[email protected]> Co-authored-by: Idris Elmi <[email protected]> Co-authored-by: lockels <[email protected]>
1 parent cba21e4 commit 1566796

File tree

7 files changed

+209
-8
lines changed

7 files changed

+209
-8
lines changed

src/builtins/compiled/duration.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use crate::{
44
Duration, TemporalError, TemporalResult,
55
};
66

7+
use core::cmp::Ordering;
8+
79
#[cfg(test)]
810
mod tests;
911

@@ -23,4 +25,20 @@ impl Duration {
2325
self.round_with_provider(options, relative_to, &*provider)
2426
.map(Into::into)
2527
}
28+
29+
/// Returns the ordering between two [`Duration`], takes an optional
30+
/// [`RelativeTo`]
31+
///
32+
/// Enable with the `compiled_data` feature flag.
33+
pub fn compare(
34+
&self,
35+
two: &Duration,
36+
relative_to: Option<RelativeTo>,
37+
) -> TemporalResult<Ordering> {
38+
let provider = TZ_PROVIDER
39+
.lock()
40+
.map_err(|_| TemporalError::general("Unable to acquire lock"))?;
41+
self.compare_with_provider(two, relative_to, &*provider)
42+
.map(Into::into)
43+
}
2644
}

src/builtins/compiled/duration/tests.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
use std::string::ToString;
2+
13
use crate::{
2-
options::{RelativeTo, RoundingIncrement, RoundingOptions, TemporalRoundingMode, TemporalUnit},
4+
options::{
5+
OffsetDisambiguation, RelativeTo, RoundingIncrement, RoundingOptions, TemporalRoundingMode,
6+
TemporalUnit,
7+
},
8+
partial::PartialDuration,
39
primitive::FiniteF64,
410
Calendar, DateDuration, PlainDate, TimeDuration, TimeZone, ZonedDateTime,
511
};
12+
613
use alloc::vec::Vec;
714
use core::str::FromStr;
815

@@ -629,3 +636,52 @@ fn round_relative_to_zoned_datetime() {
629636
assert_eq!(result.days(), 1.0);
630637
assert_eq!(result.hours(), 1.0);
631638
}
639+
640+
#[test]
641+
fn test_duration_compare() {
642+
// TODO(#199): fix this on Windows
643+
if cfg!(not(windows)) {
644+
let one = Duration::from_partial_duration(PartialDuration {
645+
hours: Some(FiniteF64::from(79)),
646+
minutes: Some(FiniteF64::from(10)),
647+
..Default::default()
648+
})
649+
.unwrap();
650+
let two = Duration::from_partial_duration(PartialDuration {
651+
days: Some(FiniteF64::from(3)),
652+
hours: Some(FiniteF64::from(7)),
653+
seconds: Some(FiniteF64::from(630)),
654+
..Default::default()
655+
})
656+
.unwrap();
657+
let three = Duration::from_partial_duration(PartialDuration {
658+
days: Some(FiniteF64::from(3)),
659+
hours: Some(FiniteF64::from(6)),
660+
minutes: Some(FiniteF64::from(50)),
661+
..Default::default()
662+
})
663+
.unwrap();
664+
665+
let mut arr = [&one, &two, &three];
666+
arr.sort_by(|a, b| Duration::compare(a, b, None).unwrap());
667+
assert_eq!(
668+
arr.map(ToString::to_string),
669+
[&three, &one, &two].map(ToString::to_string)
670+
);
671+
672+
// Sorting relative to a date, taking DST changes into account:
673+
let zdt = ZonedDateTime::from_str(
674+
"2020-11-01T00:00-07:00[America/Los_Angeles]",
675+
Default::default(),
676+
OffsetDisambiguation::Reject,
677+
)
678+
.unwrap();
679+
arr.sort_by(|a, b| {
680+
Duration::compare(a, b, Some(RelativeTo::ZonedDateTime(zdt.clone()))).unwrap()
681+
});
682+
assert_eq!(
683+
arr.map(ToString::to_string),
684+
[&one, &three, &two].map(ToString::to_string)
685+
)
686+
}
687+
}

src/builtins/core/duration.rs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use alloc::format;
1616
use alloc::string::String;
1717
use alloc::vec;
1818
use alloc::vec::Vec;
19-
use core::str::FromStr;
19+
use core::{cmp::Ordering, str::FromStr};
2020
use ixdtf::parsers::{
2121
records::{DateDurationRecord, DurationParseRecord, Sign as IxdtfSign, TimeDurationRecord},
2222
IsoDurationParser,
@@ -277,6 +277,63 @@ impl Duration {
277277
pub fn is_time_within_range(&self) -> bool {
278278
self.time.is_within_range()
279279
}
280+
281+
#[inline]
282+
pub fn compare_with_provider(
283+
&self,
284+
other: &Duration,
285+
relative_to: Option<RelativeTo>,
286+
provider: &impl TimeZoneProvider,
287+
) -> TemporalResult<Ordering> {
288+
if self.date == other.date && self.time == other.time {
289+
return Ok(Ordering::Equal);
290+
}
291+
// 8. Let largestUnit1 be DefaultTemporalLargestUnit(one).
292+
// 9. Let largestUnit2 be DefaultTemporalLargestUnit(two).
293+
let largest_unit_1 = self.default_largest_unit();
294+
let largest_unit_2 = other.default_largest_unit();
295+
// 10. Let duration1 be ToInternalDurationRecord(one).
296+
// 11. Let duration2 be ToInternalDurationRecord(two).
297+
// 12. If zonedRelativeTo is not undefined, and either TemporalUnitCategory(largestUnit1) or TemporalUnitCategory(largestUnit2) is date, then
298+
if let Some(RelativeTo::ZonedDateTime(zdt)) = relative_to.as_ref() {
299+
if largest_unit_1.is_date_unit() || largest_unit_2.is_date_unit() {
300+
// a. Let timeZone be zonedRelativeTo.[[TimeZone]].
301+
// b. Let calendar be zonedRelativeTo.[[Calendar]].
302+
// c. Let after1 be ? AddZonedDateTime(zonedRelativeTo.[[EpochNanoseconds]], timeZone, calendar, duration1, constrain).
303+
// d. Let after2 be ? AddZonedDateTime(zonedRelativeTo.[[EpochNanoseconds]], timeZone, calendar, duration2, constrain).
304+
let after1 = zdt.add_as_instant(self, ArithmeticOverflow::Constrain, provider)?;
305+
let after2 = zdt.add_as_instant(other, ArithmeticOverflow::Constrain, provider)?;
306+
// e. If after1 > after2, return 1𝔽.
307+
// f. If after1 < after2, return -1𝔽.
308+
// g. Return +0𝔽.
309+
return Ok(after1.cmp(&after2));
310+
}
311+
}
312+
// 13. If IsCalendarUnit(largestUnit1) is true or IsCalendarUnit(largestUnit2) is true, then
313+
let (days1, days2) =
314+
if largest_unit_1.is_calendar_unit() || largest_unit_2.is_calendar_unit() {
315+
// a. If plainRelativeTo is undefined, throw a RangeError exception.
316+
// b. Let days1 be ? DateDurationDays(duration1.[[Date]], plainRelativeTo).
317+
// c. Let days2 be ? DateDurationDays(duration2.[[Date]], plainRelativeTo).
318+
let Some(RelativeTo::PlainDate(pdt)) = relative_to.as_ref() else {
319+
return Err(TemporalError::range());
320+
};
321+
let days1 = self.date.days(pdt)?;
322+
let days2 = other.date.days(pdt)?;
323+
(days1, days2)
324+
} else {
325+
(
326+
self.date.days.as_integer_if_integral()?,
327+
other.date.days.as_integer_if_integral()?,
328+
)
329+
};
330+
// 15. Let timeDuration1 be ? Add24HourDaysToTimeDuration(duration1.[[Time]], days1).
331+
let time_duration_1 = self.time.to_normalized().add_days(days1)?;
332+
// 16. Let timeDuration2 be ? Add24HourDaysToTimeDuration(duration2.[[Time]], days2).
333+
let time_duration_2 = other.time.to_normalized().add_days(days2)?;
334+
// 17. Return 𝔽(CompareTimeDuration(timeDuration1, timeDuration2)).
335+
Ok(time_duration_1.cmp(&time_duration_2))
336+
}
280337
}
281338

282339
// ==== Public `Duration` Getters/Setters ====
@@ -323,7 +380,7 @@ impl Duration {
323380
self.date.weeks
324381
}
325382

326-
/// Returns the `weeks` field of duration.
383+
/// Returns the `days` field of duration.
327384
#[inline]
328385
#[must_use]
329386
pub const fn days(&self) -> FiniteF64 {
@@ -572,6 +629,7 @@ impl Duration {
572629
self.weeks(),
573630
self.days().checked_add(&FiniteF64::from(balanced_days))?,
574631
)?;
632+
// TODO: Should this be using AdjustDateDurationRecord?
575633

576634
// c. Let targetDate be ? AddDate(calendarRec, plainRelativeTo, dateDuration).
577635
let target_date = plain_date.add_date(&Duration::from(date_duration), None)?;

src/builtins/core/duration/date.rs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! Implementation of a `DateDuration`
22
3-
use crate::{primitive::FiniteF64, Sign, TemporalError, TemporalResult};
3+
use crate::{
4+
iso::iso_date_to_epoch_days, options::ArithmeticOverflow, primitive::FiniteF64, Duration,
5+
PlainDate, Sign, TemporalError, TemporalResult,
6+
};
47
use alloc::vec::Vec;
58

69
/// `DateDuration` represents the [date duration record][spec] of the `Duration.`
@@ -10,7 +13,7 @@ use alloc::vec::Vec;
1013
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-date-duration-records
1114
/// [field spec]: https://tc39.es/proposal-temporal/#sec-properties-of-temporal-duration-instances
1215
#[non_exhaustive]
13-
#[derive(Debug, Default, Clone, Copy)]
16+
#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
1417
pub struct DateDuration {
1518
/// `DateDuration`'s internal year value.
1619
pub years: FiniteF64,
@@ -105,4 +108,58 @@ impl DateDuration {
105108
pub fn sign(&self) -> Sign {
106109
super::duration_sign(&self.fields())
107110
}
111+
112+
/// DateDurationDays
113+
pub(crate) fn days(&self, relative_to: &PlainDate) -> TemporalResult<i64> {
114+
// 1. Let yearsMonthsWeeksDuration be ! AdjustDateDurationRecord(dateDuration, 0).
115+
let ymw_duration = self.adjust(FiniteF64(0.0), None, None)?;
116+
// 2. If DateDurationSign(yearsMonthsWeeksDuration) = 0, return dateDuration.[[Days]].
117+
if ymw_duration.sign() == Sign::Zero {
118+
return self.days.as_integer_if_integral();
119+
}
120+
// 3. Let later be ? CalendarDateAdd(plainRelativeTo.[[Calendar]], plainRelativeTo.[[ISODate]], yearsMonthsWeeksDuration, constrain).
121+
let later = relative_to.add(
122+
&Duration {
123+
date: *self,
124+
time: Default::default(),
125+
},
126+
Some(ArithmeticOverflow::Constrain),
127+
)?;
128+
// 4. Let epochDays1 be ISODateToEpochDays(plainRelativeTo.[[ISODate]].[[Year]], plainRelativeTo.[[ISODate]].[[Month]] - 1, plainRelativeTo.[[ISODate]].[[Day]]).
129+
let epoch_days_1 = iso_date_to_epoch_days(
130+
relative_to.iso_year(),
131+
i32::from(relative_to.iso_month()), // this function takes 1 based month number
132+
i32::from(relative_to.iso_day()),
133+
);
134+
// 5. Let epochDays2 be ISODateToEpochDays(later.[[Year]], later.[[Month]] - 1, later.[[Day]]).
135+
let epoch_days_2 = iso_date_to_epoch_days(
136+
later.iso_year(),
137+
i32::from(later.iso_month()), // this function takes 1 based month number
138+
i32::from(later.iso_day()),
139+
);
140+
// 6. Let yearsMonthsWeeksInDays be epochDays2 - epochDays1.
141+
let ymd_in_days = epoch_days_2 - epoch_days_1;
142+
// 7. Return dateDuration.[[Days]] + yearsMonthsWeeksInDays.
143+
Ok(self.days.as_integer_if_integral::<i64>()? + i64::from(ymd_in_days))
144+
}
145+
146+
/// AdjustDateDurationRecord
147+
pub(crate) fn adjust(
148+
&self,
149+
days: FiniteF64,
150+
weeks: Option<FiniteF64>,
151+
months: Option<FiniteF64>,
152+
) -> TemporalResult<Self> {
153+
// 1. If weeks is not present, set weeks to dateDuration.[[Weeks]].
154+
// 2. If months is not present, set months to dateDuration.[[Months]].
155+
// 3. Return ? CreateDateDurationRecord(dateDuration.[[Years]], months, weeks, days).
156+
let weeks = weeks.unwrap_or(self.weeks);
157+
let months = months.unwrap_or(self.months);
158+
Ok(Self {
159+
years: self.years,
160+
months,
161+
weeks,
162+
days,
163+
})
164+
}
108165
}

src/builtins/core/duration/normalized.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const NANOSECONDS_PER_HOUR: i128 = 60 * NANOSECONDS_PER_MINUTE;
3636
// nanoseconds.abs() <= MAX_TIME_DURATION
3737

3838
/// A Normalized `TimeDuration` that represents the current `TimeDuration` in nanoseconds.
39-
#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)]
39+
#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq, Ord)]
4040
pub(crate) struct NormalizedTimeDuration(pub(crate) i128);
4141

4242
impl NormalizedTimeDuration {
@@ -66,6 +66,7 @@ impl NormalizedTimeDuration {
6666

6767
// NOTE: `days: f64` should be an integer -> `i64`.
6868
/// Equivalent: 7.5.23 Add24HourDaysToNormalizedTimeDuration ( d, days )
69+
/// Add24HourDaysToTimeDuration??
6970
pub(crate) fn add_days(&self, days: i64) -> TemporalResult<Self> {
7071
let result = self.0 + i128::from(days) * i128::from(NS_PER_DAY);
7172
if result.abs() > MAX_TIME_DURATION {

src/iso.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,8 +927,9 @@ fn to_unchecked_epoch_nanoseconds(date: IsoDate, time: &IsoTime) -> i128 {
927927
// ==== `IsoDate` specific utiltiy functions ====
928928

929929
/// Returns the Epoch days based off the given year, month, and day.
930+
/// Note: Month should be 1 indexed
930931
#[inline]
931-
fn iso_date_to_epoch_days(year: i32, month: i32, day: i32) -> i32 {
932+
pub(crate) fn iso_date_to_epoch_days(year: i32, month: i32, day: i32) -> i32 {
932933
// 1. Let resolvedYear be year + floor(month / 12).
933934
let resolved_year = year + month.div_euclid(12);
934935
// 2. Let resolvedMonth be month modulo 12.

src/options.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,13 @@ impl TemporalUnit {
471471
matches!(self, Year | Month | Week)
472472
}
473473

474+
#[inline]
475+
#[must_use]
476+
pub fn is_date_unit(&self) -> bool {
477+
use TemporalUnit::{Day, Month, Week, Year};
478+
matches!(self, Day | Year | Month | Week)
479+
}
480+
474481
#[inline]
475482
#[must_use]
476483
pub fn is_time_unit(&self) -> bool {
@@ -644,9 +651,12 @@ impl fmt::Display for DurationOverflow {
644651
}
645652

646653
/// The disambiguation options for an instant.
647-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
654+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
648655
pub enum Disambiguation {
649656
/// Compatible option
657+
///
658+
/// This is the default according to GetTemporalDisambiguationOption
659+
#[default]
650660
Compatible,
651661
/// Earlier option
652662
Earlier,

0 commit comments

Comments
 (0)