Skip to content

Commit 6fe1654

Browse files
authored
Update duration to unsigned fields + specification updates (#507)
This PR makes a large portion of updates from #366 without bringing in the `bnum` crate. The changes are primarily as follows: - Updates Duration to flat unsigned fields - Removes the previous TimeDuration - NormalizedDurationRecord -> InternalDurationRecord - NormalizedTimeDuration -> TimeDuration There are a handful of API changes linked to this change that are primarily related to moving addition variations that were using the old TimeDuration.
1 parent 77ef3a2 commit 6fe1654

33 files changed

+846
-1384
lines changed

src/builtins/compiled/duration/tests.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{
33
OffsetDisambiguation, RelativeTo, RoundingIncrement, RoundingMode, RoundingOptions, Unit,
44
},
55
partial::PartialDuration,
6-
Calendar, DateDuration, PlainDate, TimeDuration, TimeZone, ZonedDateTime,
6+
Calendar, DateDuration, PlainDate, TimeZone, ZonedDateTime,
77
};
88

99
use core::{num::NonZeroU32, str::FromStr};
@@ -373,7 +373,7 @@ fn basic_negative_expand_rounding() {
373373

374374
#[test]
375375
fn rounding_to_fractional_day_tests() {
376-
let twenty_five_hours = Duration::from(TimeDuration::new(25, 0, 0, 0, 0, 0).unwrap());
376+
let twenty_five_hours = Duration::from_hours(25);
377377
let options = RoundingOptions {
378378
largest_unit: Some(Unit::Day),
379379
smallest_unit: None,
@@ -485,7 +485,7 @@ fn basic_subtract_duration() {
485485
// days-24-hours-relative-to-zoned-date-time.js
486486
#[test]
487487
fn round_relative_to_zoned_datetime() {
488-
let duration = Duration::from(TimeDuration::new(25, 0, 0, 0, 0, 0).unwrap());
488+
let duration = Duration::from_hours(25);
489489
let zdt = ZonedDateTime::try_new(
490490
1_000_000_000_000_000_000,
491491
Calendar::default(),
@@ -609,7 +609,7 @@ fn balance_days_up_to_both_years_and_months() {
609609
);
610610
}
611611

612-
// relativeto-plaindate-add24hourdaystonormalizedtimeduration-out-of-range.js
612+
// relativeto-plaindate-add24hourdaystoTimeDuration-out-of-range.js
613613
#[test]
614614
fn add_normalized_time_duration_out_of_range() {
615615
let duration = Duration::from_partial_duration(PartialDuration {

src/builtins/core/date.rs

Lines changed: 22 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ use core::{cmp::Ordering, str::FromStr};
2323
use icu_calendar::AnyCalendarKind;
2424
use writeable::Writeable;
2525

26-
use super::{
27-
duration::{normalized::NormalizedDurationRecord, TimeDuration},
28-
PlainMonthDay, PlainYearMonth,
29-
};
26+
use super::{duration::normalized::InternalDurationRecord, PlainMonthDay, PlainYearMonth};
3027
use tinystr::TinyAsciiStr;
3128

3229
// TODO (potentially): Bump era up to TinyAsciiStr<18> to accomodate
@@ -185,53 +182,31 @@ impl PlainDate {
185182
Self { iso, calendar }
186183
}
187184

185+
// Updated: 2025-08-03
188186
/// Returns the date after adding the given duration to date.
189187
///
190-
/// Temporal Equivalent: 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ] ] )`
188+
/// 3.5.14 `AddDurationToDate`
189+
///
190+
/// More information:
191+
///
192+
/// - [AO specification](https://tc39.es/proposal-temporal/#sec-temporal-adddurationtodate)
191193
#[inline]
192-
pub(crate) fn add_date(
194+
pub(crate) fn add_duration_to_date(
193195
&self,
194196
duration: &Duration,
195197
overflow: Option<ArithmeticOverflow>,
196198
) -> TemporalResult<Self> {
197-
// 2. If options is not present, set options to undefined.
199+
// 3. If operation is subtract, set duration to CreateNegatedTemporalDuration(duration).
200+
// 4. Let dateDuration be ToDateDurationRecordWithoutTime(duration).
201+
// TODO: Look into why this is fallible, and make some adjustments
202+
let date_duration = duration.to_date_duration_record_without_time()?;
203+
// 5. Let resolvedOptions be ? GetOptionsObject(options).
204+
// 6. Let overflow be ? GetTemporalOverflowOption(resolvedOptions).
198205
let overflow = overflow.unwrap_or(ArithmeticOverflow::Constrain);
199-
// 3. If duration.[[Years]] ≠ 0, or duration.[[Months]] ≠ 0, or duration.[[Weeks]] ≠ 0, then
200-
if duration.date().years != 0 || duration.date().months != 0 || duration.date().weeks != 0 {
201-
// a. If dateAdd is not present, then
202-
// i. Set dateAdd to unused.
203-
// ii. If calendar is an Object, set dateAdd to ? GetMethod(calendar, "dateAdd").
204-
// b. Return ? CalendarDateAdd(calendar, plainDate, duration, options, dateAdd).
205-
return self
206-
.calendar()
207-
.date_add(&self.iso, duration.date(), overflow);
208-
}
209-
210-
// 4. Let overflow be ? ToTemporalOverflow(options).
211-
// 5. Let norm be NormalizeTimeDuration(duration.[[Hours]],
212-
// duration.[[Minutes]], duration.[[Seconds]],
213-
// duration.[[Milliseconds]], duration.[[Microseconds]],
214-
// duration.[[Nanoseconds]]).
215-
// 6. Let days be duration.[[Days]] + BalanceTimeDuration(norm,
216-
// "day").[[Days]].
217-
let days = duration
218-
.days()
219-
.checked_add(
220-
TimeDuration::from_normalized(duration.time().to_normalized(), Unit::Day)?.0,
221-
)
222-
.ok_or(TemporalError::range())?;
223-
224-
// 7. Let result be ? AddISODate(plainDate.[[ISOYear]], plainDate.[[ISOMonth]], plainDate.[[ISODay]], 0, 0, 0, days, overflow).
225-
let result = self
226-
.iso
227-
.add_date_duration(&DateDuration::new(0, 0, 0, days)?, overflow)?;
228-
229-
Self::try_new(
230-
result.year,
231-
result.month,
232-
result.day,
233-
self.calendar().clone(),
234-
)
206+
// 7. Let result be ? CalendarDateAdd(calendar, temporalDate.[[ISODate]], dateDuration, overflow).
207+
// 8. Return ! CreateTemporalDate(result, calendar).
208+
self.calendar()
209+
.date_add(&self.iso, &date_duration, overflow)
235210
}
236211

237212
/// Returns a duration representing the difference between the dates one and two.
@@ -298,7 +273,7 @@ impl PlainDate {
298273
let result = self.internal_diff_date(other, resolved.largest_unit)?;
299274

300275
// 10. Let duration be ! CreateNormalizedDurationRecord(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], ZeroTimeDuration()).
301-
let mut duration = NormalizedDurationRecord::from_date_duration(*result.date())?;
276+
let mut duration = InternalDurationRecord::from_date_duration(result.date())?;
302277
// 11. If settings.[[SmallestUnit]] is "day" and settings.[[RoundingIncrement]] = 1, let roundingGranularityIsNoop be true; else let roundingGranularityIsNoop be false.
303278
let rounding_granularity_is_noop =
304279
resolved.smallest_unit == Unit::Day && resolved.increment.get() == 1;
@@ -319,7 +294,7 @@ impl PlainDate {
319294
resolved,
320295
)?
321296
}
322-
let result = Duration::from_normalized(duration, Unit::Day)?;
297+
let result = Duration::from_internal(duration, Unit::Day)?;
323298
// 13. Return ! CreateTemporalDuration(sign × duration.[[Years]], sign × duration.[[Months]], sign × duration.[[Weeks]], sign × duration.[[Days]], 0, 0, 0, 0, 0, 0).
324299
match op {
325300
DifferenceOperation::Until => Ok(result),
@@ -520,7 +495,7 @@ impl PlainDate {
520495
duration: &Duration,
521496
overflow: Option<ArithmeticOverflow>,
522497
) -> TemporalResult<Self> {
523-
self.add_date(duration, overflow)
498+
self.add_duration_to_date(duration, overflow)
524499
}
525500

526501
#[inline]
@@ -530,7 +505,7 @@ impl PlainDate {
530505
duration: &Duration,
531506
overflow: Option<ArithmeticOverflow>,
532507
) -> TemporalResult<Self> {
533-
self.add_date(&duration.negated(), overflow)
508+
self.add_duration_to_date(&duration.negated(), overflow)
534509
}
535510

536511
#[inline]

src/builtins/core/datetime.rs

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! This module implements `DateTime` any directly related algorithms.
22
33
use super::{
4-
duration::normalized::{NormalizedDurationRecord, NormalizedTimeDuration},
5-
Duration, PartialTime, PlainDate, PlainTime, ZonedDateTime,
4+
duration::normalized::InternalDurationRecord, Duration, PartialTime, PlainDate, PlainTime,
5+
ZonedDateTime,
66
};
77
use crate::parsed_intermediates::ParsedDateTime;
88
use crate::{
@@ -19,7 +19,7 @@ use crate::{
1919
parsers::IxdtfStringBuilder,
2020
primitive::FiniteF64,
2121
provider::{NeverProvider, TimeZoneProvider},
22-
temporal_assert, MonthCode, TemporalError, TemporalResult, TimeZone,
22+
MonthCode, TemporalError, TemporalResult, TimeZone,
2323
};
2424
use alloc::string::String;
2525
use core::{cmp::Ordering, str::FromStr};
@@ -225,33 +225,25 @@ impl PlainDateTime {
225225
overflow: Option<ArithmeticOverflow>,
226226
) -> TemporalResult<Self> {
227227
// SKIP: 1, 2, 3, 4
228-
// 1. If operation is subtract, let sign be -1. Otherwise, let sign be 1.
229-
// 2. Let duration be ? ToTemporalDurationRecord(temporalDurationLike).
230-
// 3. Set options to ? GetOptionsObject(options).
231-
// 4. Let calendarRec be ? CreateCalendarMethodsRecord(dateTime.[[Calendar]], « date-add »).
232-
233-
// 5. Let norm be NormalizeTimeDuration(sign × duration.[[Hours]], sign × duration.[[Minutes]], sign × duration.[[Seconds]], sign × duration.[[Milliseconds]], sign × duration.[[Microseconds]], sign × duration.[[Nanoseconds]]).
234-
let norm = NormalizedTimeDuration::from_time_duration(duration.time());
235-
236-
// TODO: validate Constrain is default with all the recent changes.
237-
// 6. Let result be ? AddDateTime(dateTime.[[ISOYear]], dateTime.[[ISOMonth]], dateTime.[[ISODay]], dateTime.[[ISOHour]], dateTime.[[ISOMinute]], dateTime.[[ISOSecond]], dateTime.[[ISOMillisecond]], dateTime.[[ISOMicrosecond]], dateTime.[[ISONanosecond]], calendarRec, sign × duration.[[Years]], sign × duration.[[Months]], sign × duration.[[Weeks]], sign × duration.[[Days]], norm, options).
238-
let result =
239-
self.iso
240-
.add_date_duration(self.calendar().clone(), duration.date(), norm, overflow)?;
241-
242-
// 7. Assert: IsValidISODate(result.[[Year]], result.[[Month]], result.[[Day]]) is true.
243-
// 8. Assert: IsValidTime(result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]],
244-
// result.[[Microsecond]], result.[[Nanosecond]]) is true.
245-
temporal_assert!(
246-
result.is_within_limits(),
247-
"Assertion failed: the below datetime is not within valid limits:\n{:?}",
248-
result
249-
);
250-
251-
// 9. Return ? CreateTemporalDateTime(result.[[Year]], result.[[Month]], result.[[Day]], result.[[Hour]],
252-
// result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]],
253-
// result.[[Nanosecond]], dateTime.[[Calendar]]).
254-
Ok(Self::new_unchecked(result, self.calendar.clone()))
228+
// 5. Let internalDuration be ToInternalDurationRecordWith24HourDays(duration).
229+
let internal_duration = InternalDurationRecord::from_duration_with_24_hour_days(duration)?;
230+
// 6. Let timeResult be AddTime(dateTime.[[ISODateTime]].[[Time]], internalDuration.[[Time]]).
231+
let (days, time_result) = self
232+
.iso
233+
.time
234+
.add(internal_duration.normalized_time_duration());
235+
// 7. Let dateDuration be ? AdjustDateDurationRecord(internalDuration.[[Date]], timeResult.[[Days]]).
236+
let date_duration = internal_duration.date().adjust(days, None, None)?;
237+
// 8. Let addedDate be ? CalendarDateAdd(dateTime.[[Calendar]], dateTime.[[ISODateTime]].[[ISODate]], dateDuration, overflow).
238+
let added_date = self.calendar().date_add(
239+
&self.iso.date,
240+
&date_duration,
241+
overflow.unwrap_or(ArithmeticOverflow::Constrain),
242+
)?;
243+
// 9. Let result be CombineISODateAndTimeRecord(addedDate, timeResult).
244+
let result = IsoDateTime::new(added_date.iso, time_result)?;
245+
// 10. Return ? CreateTemporalDateTime(result, dateTime.[[Calendar]]).
246+
Ok(Self::new_unchecked(result, self.calendar().clone()))
255247
}
256248

257249
/// Difference two `DateTime`s together.
@@ -284,7 +276,7 @@ impl PlainDateTime {
284276
// Step 10-11.
285277
let norm_record = self.diff_dt_with_rounding(other, options)?;
286278

287-
let result = Duration::from_normalized(norm_record, options.largest_unit)?;
279+
let result = Duration::from_internal(norm_record, options.largest_unit)?;
288280

289281
// Step 12
290282
match op {
@@ -300,12 +292,12 @@ impl PlainDateTime {
300292
&self,
301293
other: &Self,
302294
options: ResolvedRoundingOptions,
303-
) -> TemporalResult<NormalizedDurationRecord> {
295+
) -> TemporalResult<InternalDurationRecord> {
304296
// 1. If CompareISODateTime(y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, d2, h2, min2, s2, ms2, mus2, ns2) = 0, then
305297
if matches!(self.iso.cmp(&other.iso), Ordering::Equal) {
306298
// a. Let durationRecord be CreateDurationRecord(0, 0, 0, 0, 0, 0, 0, 0, 0, 0).
307299
// b. Return the Record { [[DurationRecord]]: durationRecord, [[Total]]: 0 }.
308-
return Ok(NormalizedDurationRecord::default());
300+
return Ok(InternalDurationRecord::default());
309301
}
310302
// 2. If ISODateTimeWithinLimits(isoDateTime1) is false or ISODateTimeWithinLimits(isoDateTime2) is false, throw a RangeError exception.
311303
self.iso.check_validity()?;
@@ -1248,13 +1240,13 @@ mod tests {
12481240
PlainDateTime::try_new(2019, 10, 29, 10, 46, 38, 271, 986, 102, Calendar::default())
12491241
.unwrap();
12501242

1251-
let result = dt.subtract(&Duration::hour(12), None).unwrap();
1243+
let result = dt.subtract(&Duration::from_hours(12), None).unwrap();
12521244
assert_datetime(
12531245
result,
12541246
(2019, 10, tinystr!(4, "M10"), 28, 22, 46, 38, 271, 986, 102),
12551247
);
12561248

1257-
let result = dt.add(&Duration::hour(-12), None).unwrap();
1249+
let result = dt.add(&Duration::from_hours(-12), None).unwrap();
12581250
assert_datetime(
12591251
result,
12601252
(2019, 10, tinystr!(4, "M10"), 28, 22, 46, 38, 271, 986, 102),
@@ -1543,4 +1535,21 @@ mod tests {
15431535
"pads 4 decimal places to 9"
15441536
);
15451537
}
1538+
1539+
#[test]
1540+
fn datetime_add() {
1541+
use crate::{Duration, PlainDateTime};
1542+
use core::str::FromStr;
1543+
1544+
let dt = PlainDateTime::from_str("2024-01-15T12:00:00").unwrap();
1545+
1546+
let duration = Duration::from_str("P1M2DT3H4M").unwrap();
1547+
1548+
// Add duration
1549+
let later = dt.add(&duration, None).unwrap();
1550+
assert_eq!(later.month(), 2);
1551+
assert_eq!(later.day(), 17);
1552+
assert_eq!(later.hour(), 15);
1553+
assert_eq!(later.minute(), 4);
1554+
}
15461555
}

0 commit comments

Comments
 (0)