Skip to content

Commit 07be388

Browse files
lockelssebastianjacmatt
authored andcommitted
Initial implementation of Duration.prototype.total (boa-dev#241)
Fixes boa-dev#139 Tests adapted from sample code in https://tc39.es/proposal-temporal/docs/duration.html under total now pass :) I have a question in duration.rs about return types! There's also another question in normalized.rs regarding some code i'd like to refactor that makes this PR quite a bit larger than it needs to be
1 parent cf0870f commit 07be388

File tree

7 files changed

+348
-13
lines changed

7 files changed

+348
-13
lines changed

src/builtins/compiled/duration.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::{
22
builtins::TZ_PROVIDER,
33
options::{RelativeTo, RoundingOptions, TemporalUnit},
4+
primitive::FiniteF64,
45
Duration, TemporalError, TemporalResult,
56
};
67

@@ -44,7 +45,7 @@ impl Duration {
4445
&self,
4546
unit: TemporalUnit,
4647
relative_to: Option<RelativeTo>,
47-
) -> TemporalResult<i64> {
48+
) -> TemporalResult<FiniteF64> {
4849
let provider = TZ_PROVIDER
4950
.lock()
5051
.map_err(|_| TemporalError::general("Unable to acquire lock"))?;

src/builtins/compiled/duration/tests.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,3 +685,118 @@ fn test_duration_compare() {
685685
)
686686
}
687687
}
688+
689+
#[test]
690+
fn test_duration_total() {
691+
let d1 = Duration::from_partial_duration(PartialDuration {
692+
hours: Some(FiniteF64::from(130)),
693+
minutes: Some(FiniteF64::from(20)),
694+
..Default::default()
695+
})
696+
.unwrap();
697+
assert_eq!(d1.total(TemporalUnit::Second, None).unwrap(), 469200.0);
698+
699+
// How many 24-hour days is 123456789 seconds?
700+
let d2 = Duration::from_str("PT123456789S").unwrap();
701+
assert_eq!(
702+
d2.total(TemporalUnit::Day, None).unwrap(),
703+
1428.8980208333332
704+
);
705+
706+
// Find totals in months, with and without taking DST into account
707+
let d3 = Duration::from_partial_duration(PartialDuration {
708+
hours: Some(FiniteF64::from(2756)),
709+
..Default::default()
710+
})
711+
.unwrap();
712+
let relative_to = ZonedDateTime::from_str(
713+
"2020-01-01T00:00+01:00[Europe/Rome]",
714+
Default::default(),
715+
OffsetDisambiguation::Reject,
716+
)
717+
.unwrap();
718+
assert_eq!(
719+
d3.total(
720+
TemporalUnit::Month,
721+
Some(RelativeTo::ZonedDateTime(relative_to))
722+
)
723+
.unwrap(),
724+
3.7958333333333334
725+
);
726+
assert_eq!(
727+
d3.total(
728+
TemporalUnit::Month,
729+
Some(RelativeTo::PlainDate(
730+
PlainDate::new(2020, 1, 1, Calendar::default()).unwrap()
731+
))
732+
)
733+
.unwrap(),
734+
3.7944444444444443
735+
);
736+
}
737+
738+
// balance-subseconds.js
739+
#[test]
740+
fn balance_subseconds() {
741+
// Test positive
742+
let pos = Duration::from_partial_duration(PartialDuration {
743+
milliseconds: Some(FiniteF64::from(999)),
744+
microseconds: Some(FiniteF64::from(999999)),
745+
nanoseconds: Some(FiniteF64::from(999999999)),
746+
..Default::default()
747+
})
748+
.unwrap();
749+
assert_eq!(pos.total(TemporalUnit::Second, None).unwrap(), 2.998998999);
750+
751+
// Test negative
752+
let neg = Duration::from_partial_duration(PartialDuration {
753+
milliseconds: Some(FiniteF64::from(-999)),
754+
microseconds: Some(FiniteF64::from(-999999)),
755+
nanoseconds: Some(FiniteF64::from(-999999999)),
756+
..Default::default()
757+
})
758+
.unwrap();
759+
assert_eq!(neg.total(TemporalUnit::Second, None).unwrap(), -2.998998999);
760+
}
761+
762+
// balances-days-up-to-both-years-and-months.js
763+
#[test]
764+
fn balance_days_up_to_both_years_and_months() {
765+
// Test positive
766+
let two_years = Duration::from_partial_duration(PartialDuration {
767+
months: Some(FiniteF64::from(11)),
768+
days: Some(FiniteF64::from(396)),
769+
..Default::default()
770+
})
771+
.unwrap();
772+
773+
let relative_to = PlainDate::new(2017, 1, 1, Calendar::default()).unwrap();
774+
775+
assert_eq!(
776+
two_years
777+
.total(
778+
TemporalUnit::Year,
779+
Some(RelativeTo::PlainDate(relative_to.clone()))
780+
)
781+
.unwrap(),
782+
2.0
783+
);
784+
785+
// Test negative
786+
let two_years_negative = Duration::from_partial_duration(PartialDuration {
787+
months: Some(FiniteF64::from(-11)),
788+
days: Some(FiniteF64::from(-396)),
789+
..Default::default()
790+
})
791+
.unwrap();
792+
793+
assert_eq!(
794+
two_years_negative
795+
.total(
796+
TemporalUnit::Year,
797+
Some(RelativeTo::PlainDate(relative_to.clone()))
798+
)
799+
.unwrap(),
800+
-2.0
801+
);
802+
}

src/builtins/core/datetime.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::{
1212
ResolvedRoundingOptions, RoundingOptions, TemporalUnit, ToStringRoundingOptions, UnitGroup,
1313
},
1414
parsers::{parse_date_time, IxdtfStringBuilder},
15+
primitive::FiniteF64,
1516
provider::NeverProvider,
1617
temporal_assert, MonthCode, TemporalError, TemporalResult, TemporalUnwrap, TimeZone,
1718
};
@@ -202,6 +203,38 @@ impl PlainDateTime {
202203
options,
203204
)
204205
}
206+
207+
// 5.5.14 DifferencePlainDateTimeWithTotal ( isoDateTime1, isoDateTime2, calendar, unit )
208+
pub(crate) fn diff_dt_with_total(
209+
&self,
210+
other: &Self,
211+
unit: TemporalUnit,
212+
) -> TemporalResult<FiniteF64> {
213+
// 1. If CompareISODateTime(isoDateTime1, isoDateTime2) = 0, then
214+
// a. Return 0.
215+
if matches!(self.iso.cmp(&other.iso), Ordering::Equal) {
216+
return FiniteF64::try_from(0.0);
217+
}
218+
// 2. If ISODateTimeWithinLimits(isoDateTime1) is false or ISODateTimeWithinLimits(isoDateTime2) is false, throw a RangeError exception.
219+
if !self.iso.is_within_limits() || !other.iso.is_within_limits() {
220+
return Err(TemporalError::range().with_message("DateTime is not within valid limits."));
221+
}
222+
// 3. Let diff be DifferenceISODateTime(isoDateTime1, isoDateTime2, calendar, unit).
223+
let diff = self.iso.diff(&other.iso, &self.calendar, unit)?;
224+
// 4. If unit is nanosecond, return diff.[[Time]].
225+
if unit == TemporalUnit::Nanosecond {
226+
return FiniteF64::try_from(diff.normalized_time_duration().0);
227+
}
228+
// 5. Let destEpochNs be GetUTCEpochNanoseconds(isoDateTime2).
229+
let dest_epoch_ns = other.iso.as_nanoseconds()?;
230+
// 6. Return ? TotalRelativeDuration(diff, destEpochNs, isoDateTime1, unset, calendar, unit).
231+
diff.total_relative_duration(
232+
dest_epoch_ns.0,
233+
self,
234+
Option::<(&TimeZone, &NeverProvider)>::None,
235+
unit,
236+
)
237+
}
205238
}
206239

207240
// ==== Public PlainDateTime API ====

src/builtins/core/duration.rs

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -686,11 +686,79 @@ impl Duration {
686686
/// Returns the total of the `Duration`
687687
pub fn total_with_provider(
688688
&self,
689-
_unit: TemporalUnit,
690-
_relative_to: Option<RelativeTo>,
691-
_provider: &impl TimeZoneProvider,
692-
) -> TemporalResult<i64> {
693-
Err(TemporalError::general("Not yet implemented"))
689+
unit: TemporalUnit,
690+
relative_to: Option<RelativeTo>,
691+
provider: &impl TimeZoneProvider,
692+
// Review question what is the return type of duration.prototye.total?
693+
) -> TemporalResult<FiniteF64> {
694+
match relative_to {
695+
// 11. If zonedRelativeTo is not undefined, then
696+
Some(RelativeTo::ZonedDateTime(zoned_datetime)) => {
697+
// a. Let internalDuration be ToInternalDurationRecord(duration).
698+
// b. Let timeZone be zonedRelativeTo.[[TimeZone]].
699+
// c. Let calendar be zonedRelativeTo.[[Calendar]].
700+
// d. Let relativeEpochNs be zonedRelativeTo.[[EpochNanoseconds]].
701+
// e. Let targetEpochNs be ? AddZonedDateTime(relativeEpochNs, timeZone, calendar, internalDuration, constrain).
702+
let target_epcoh_ns =
703+
zoned_datetime.add_as_instant(self, ArithmeticOverflow::Constrain, provider)?;
704+
// f. Let total be ? DifferenceZonedDateTimeWithTotal(relativeEpochNs, targetEpochNs, timeZone, calendar, unit).
705+
let total = zoned_datetime.diff_with_total(
706+
&ZonedDateTime::new_unchecked(
707+
target_epcoh_ns,
708+
zoned_datetime.calendar().clone(),
709+
zoned_datetime.timezone().clone(),
710+
),
711+
unit,
712+
provider,
713+
)?;
714+
Ok(total)
715+
}
716+
// 12. Else if plainRelativeTo is not undefined, then
717+
Some(RelativeTo::PlainDate(plain_date)) => {
718+
// a. Let internalDuration be ToInternalDurationRecordWith24HourDays(duration).
719+
// b. Let targetTime be AddTime(MidnightTimeRecord(), internalDuration.[[Time]]).
720+
let (balanced_days, time) =
721+
PlainTime::default().add_normalized_time_duration(self.time.to_normalized());
722+
// c. Let calendar be plainRelativeTo.[[Calendar]].
723+
// d. Let dateDuration be ! AdjustDateDurationRecord(internalDuration.[[Date]], targetTime.[[Days]]).
724+
let date_duration = DateDuration::new(
725+
self.years(),
726+
self.months(),
727+
self.weeks(),
728+
self.days().checked_add(&FiniteF64::from(balanced_days))?,
729+
)?;
730+
// e. Let targetDate be ? CalendarDateAdd(calendar, plainRelativeTo.[[ISODate]], dateDuration, constrain).
731+
let target_date = plain_date.calendar().date_add(
732+
&plain_date.iso,
733+
&Duration::from(date_duration),
734+
ArithmeticOverflow::Constrain,
735+
)?;
736+
// f. Let isoDateTime be CombineISODateAndTimeRecord(plainRelativeTo.[[ISODate]], MidnightTimeRecord()).
737+
let iso_date_time = IsoDateTime::new_unchecked(plain_date.iso, IsoTime::default());
738+
// g. Let targetDateTime be CombineISODateAndTimeRecord(targetDate, targetTime).
739+
let target_date_time = IsoDateTime::new_unchecked(target_date.iso, time.iso);
740+
// h. Let total be ? DifferencePlainDateTimeWithTotal(isoDateTime, targetDateTime, calendar, unit).
741+
let plain_dt =
742+
PlainDateTime::new_unchecked(iso_date_time, plain_date.calendar().clone());
743+
let total = plain_dt.diff_dt_with_total(
744+
&PlainDateTime::new_unchecked(target_date_time, plain_date.calendar().clone()),
745+
unit,
746+
)?;
747+
Ok(total)
748+
}
749+
None => {
750+
// a. Let largestUnit be DefaultTemporalLargestUnit(duration).
751+
let largest_unit = self.default_largest_unit();
752+
// b. If IsCalendarUnit(largestUnit) is true, or IsCalendarUnit(unit) is true, throw a RangeError exception.
753+
if largest_unit.is_calendar_unit() || unit.is_calendar_unit() {
754+
return Err(TemporalError::range());
755+
}
756+
// c. Let internalDuration be ToInternalDurationRecordWith24HourDays(duration).
757+
// d. Let total be TotalTimeDuration(internalDuration.[[Time]], unit).
758+
let total = self.time.to_normalized().total(unit)?;
759+
Ok(total)
760+
}
761+
}
694762
}
695763

696764
/// Returns the `Duration` as a formatted string

0 commit comments

Comments
 (0)