Skip to content
12 changes: 11 additions & 1 deletion src/builtins/compiled/zoneddatetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::ZonedDateTime;
use crate::{
options::{
ArithmeticOverflow, DifferenceSettings, Disambiguation, DisplayCalendar, DisplayOffset,
DisplayTimeZone, OffsetDisambiguation, ToStringRoundingOptions,
DisplayTimeZone, OffsetDisambiguation, RoundingOptions, ToStringRoundingOptions,
},
Duration, MonthCode, PlainDate, PlainDateTime, PlainTime, TemporalError, TemporalResult,
};
Expand Down Expand Up @@ -396,6 +396,16 @@ impl ZonedDateTime {
self.to_plain_datetime_with_provider(&*provider)
}

/// Rounds this [`ZonedDateTime`] to the nearest value according to the given rounding options.
///
/// Enable with the `compiled_data` feature flag.
pub fn round(&self, options: RoundingOptions) -> TemporalResult<Self> {
let provider = TZ_PROVIDER
.lock()
.map_err(|_| TemporalError::general("Unable to acquire lock"))?;
self.round_with_provider(options, &*provider)
}

/// Returns a RFC9557 (IXDTF) string with the provided options.
///
/// Enable with the `compiled_data` feature flag.
Expand Down
174 changes: 171 additions & 3 deletions src/builtins/core/zoneddatetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use crate::{
options::{
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, Disambiguation,
DisplayCalendar, DisplayOffset, DisplayTimeZone, OffsetDisambiguation,
ResolvedRoundingOptions, RoundingIncrement, RoundingMode, ToStringRoundingOptions, Unit,
UnitGroup,
ResolvedRoundingOptions, RoundingIncrement, RoundingMode, RoundingOptions,
ToStringRoundingOptions, Unit, UnitGroup,
},
parsers::{self, FormattableOffset, FormattableTime, IxdtfStringBuilder, Precision},
partial::{PartialDate, PartialTime},
Expand Down Expand Up @@ -1023,6 +1023,113 @@ impl ZonedDateTime {
)
}

/// 6.3.39 Temporal.ZonedDateTime.prototype.round
pub fn round_with_provider(
&self,
options: RoundingOptions,
provider: &impl TimeZoneProvider,
) -> TemporalResult<Self> {
// 1. Let zonedDateTime be the this value.
// 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]).
// 3. If roundTo is undefined, then
// a. Throw a TypeError exception.
// 4. If roundTo is a String, then
// a. Let paramString be roundTo.
// b. Set roundTo to OrdinaryObjectCreate(null).
// c. Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit", paramString).
// 5. Else,
// a. Set roundTo to ? GetOptionsObject(roundTo).
// 6. NOTE: The following steps read options and perform independent validation in alphabetical order (GetRoundingIncrementOption reads "roundingIncrement" and GetRoundingModeOption reads "roundingMode").
// 7. Let roundingIncrement be ? GetRoundingIncrementOption(roundTo).
// 8. Let roundingMode be ? GetRoundingModeOption(roundTo, half-expand).
// 9. Let smallestUnit be ? GetTemporalUnitValuedOption(roundTo, "smallestUnit", time, required, « day »).
// 10. If smallestUnit is day, then
// a. Let maximum be 1.
// b. Let inclusive be true.
// 11. Else,
// a. Let maximum be MaximumTemporalDurationRoundingIncrement(smallestUnit).
// b. Assert: maximum is not unset.
// c. Let inclusive be false.
let resolved = ResolvedRoundingOptions::from_datetime_options(options)?;
// 12. Perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, inclusive).
// 13. If maximum is not unset, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false).
// 13. If smallestUnit is nanosecond and roundingIncrement = 1, then
if resolved.smallest_unit == Unit::Nanosecond
&& resolved.increment == RoundingIncrement::ONE
{
// a. Return ! CreateTemporalZonedDateTime(zonedDateTime.[[EpochNanoseconds]], zonedDateTime.[[TimeZone]], zonedDateTime.[[Calendar]]).
return Ok(self.clone());
}
// 14. Let thisNs be zonedDateTime.[[EpochNanoseconds]].
let this_ns = self.epoch_nanoseconds();
// 15. Let timeZone be zonedDateTime.[[TimeZone]].
// 16. Let calendar be zonedDateTime.[[Calendar]].
// 17. Let isoDateTime be GetISODateTimeFor(timeZone, thisNs).
// 18. If smallestUnit is day, then
if resolved.smallest_unit == Unit::Day {
// a. Let dateStart be isoDateTime.[[ISODate]].
let iso_start = self.tz.get_iso_datetime_for(&self.instant, provider)?;
// b. Let dateEnd be BalanceISODate(dateStart.[[Year]], dateStart.[[Month]], dateStart.[[Day]] + 1).
let iso_end = IsoDate::balance(
iso_start.date.year,
iso_start.date.month.into(),
i32::from(iso_start.date.day + 1),
);
// c. Let startNs be ? GetStartOfDay(timeZone, dateStart).
// d. Assert: thisNs ≥ startNs.
// e. Let endNs be ? GetStartOfDay(timeZone, dateEnd).
// f. Assert: thisNs < endNs.
let start_ns = self.tz.get_start_of_day(&iso_start.date, provider)?;
let end_ns = self.tz.get_start_of_day(&iso_end, provider)?;
if !(this_ns.0 >= start_ns.0 && this_ns.0 < end_ns.0) {
return Err(TemporalError::range()
.with_message("ZonedDateTime is outside the expected day bounds"));
}
// g. Let dayLengthNs be ℝ(endNs - startNs).
// h. Let dayProgressNs be TimeDurationFromEpochNanosecondsDifference(thisNs, startNs).
let day_len_ns =
NormalizedTimeDuration::from_nanosecond_difference(end_ns.0, start_ns.0)?;
let day_progress_ns =
NormalizedTimeDuration::from_nanosecond_difference(this_ns.0, start_ns.0)?;
// i. Let roundedDayNs be ! RoundTimeDurationToIncrement(dayProgressNs, dayLengthNs, roundingMode).
let rounded = if let Some(increment) = NonZeroU128::new(day_len_ns.0.unsigned_abs()) {
IncrementRounder::<i128>::from_signed_num(day_progress_ns.0, increment)?
.round(resolved.rounding_mode)
} else {
0 // Zero-length day: round to start of day
};

// j. Let epochNanoseconds be AddTimeDurationToEpochNanoseconds(roundedDayNs, startNs).
let candidate = start_ns.0 + rounded;
Instant::try_new(candidate)?;
// 20. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar).
ZonedDateTime::try_new(candidate, self.calendar.clone(), self.tz.clone())
} else {
// 19. Else,
// a. Let roundResult be RoundISODateTime(isoDateTime, roundingIncrement, smallestUnit, roundingMode).
// b. Let offsetNanoseconds be GetOffsetNanosecondsFor(timeZone, thisNs).
// c. Let epochNanoseconds be ? InterpretISODateTimeOffset(roundResult.[[ISODate]], roundResult.[[Time]], option, offsetNanoseconds, timeZone, compatible, prefer, match-exactly).
// 20. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar).
let iso_dt = self.tz.get_iso_datetime_for(&self.instant, provider)?;
let rounded_dt = iso_dt.round(resolved)?;
let offset_ns = self.tz.get_offset_nanos_for(this_ns.as_i128(), provider)?;

let epoch_ns = interpret_isodatetime_offset(
rounded_dt.date,
Some(rounded_dt.time),
false,
Some(offset_ns as i64),
&self.tz,
Disambiguation::Compatible,
OffsetDisambiguation::Prefer,
true,
provider,
)?;

ZonedDateTime::try_new(epoch_ns.0, self.calendar.clone(), self.tz.clone())
}
}

/// Creates an IXDTF (RFC 9557) date/time string for the provided `ZonedDateTime` according
/// to the provided display options.
pub fn to_ixdtf_string_with_provider(
Expand Down Expand Up @@ -1138,6 +1245,7 @@ impl ZonedDateTime {
}
}

/// InterpretISODateTimeOffset
#[allow(clippy::too_many_arguments)]
pub(crate) fn interpret_isodatetime_offset(
date: IsoDate,
Expand Down Expand Up @@ -1275,7 +1383,8 @@ mod tests {
use super::ZonedDateTime;
use crate::{
options::{
ArithmeticOverflow, DifferenceSettings, Disambiguation, OffsetDisambiguation, Unit,
ArithmeticOverflow, DifferenceSettings, Disambiguation, OffsetDisambiguation,
RoundingIncrement, RoundingMode, RoundingOptions, Unit,
},
partial::{PartialDate, PartialTime, PartialZonedDateTime},
time::EpochNanoseconds,
Expand Down Expand Up @@ -1333,6 +1442,65 @@ mod tests {
assert_eq!(zdt_plus_eleven.second_with_provider(provider).unwrap(), 12);
}

#[test]
// https://tc39.es/proposal-temporal/docs/zoneddatetime.html#round
fn round_with_provider_test() {
let provider = &FsTzdbProvider::default();
let dt = "1995-12-07T03:24:30.000003500-08:00[America/Los_Angeles]";
let zdt = ZonedDateTime::from_str_with_provider(
dt,
Disambiguation::default(),
OffsetDisambiguation::Use,
provider,
)
.unwrap();

let result = zdt
.round_with_provider(
RoundingOptions {
smallest_unit: Some(Unit::Hour),
..Default::default()
},
provider,
)
.unwrap();
assert_eq!(
result.to_string_with_provider(provider).unwrap(),
"1995-12-07T03:00:00-08:00[America/Los_Angeles]"
);

let result = zdt
.round_with_provider(
RoundingOptions {
smallest_unit: Some(Unit::Minute),
increment: Some((RoundingIncrement::try_new(30)).unwrap()),
..Default::default()
},
provider,
)
.unwrap();
assert_eq!(
result.to_string_with_provider(provider).unwrap(),
"1995-12-07T03:30:00-08:00[America/Los_Angeles]"
);

let result = zdt
.round_with_provider(
RoundingOptions {
smallest_unit: Some(Unit::Minute),
increment: Some((RoundingIncrement::try_new(30)).unwrap()),
rounding_mode: Some(RoundingMode::Floor),
..Default::default()
},
provider,
)
.unwrap();
assert_eq!(
result.to_string_with_provider(provider).unwrap(),
"1995-12-07T03:00:00-08:00[America/Los_Angeles]"
);
}

#[test]
fn zdt_from_partial() {
let provider = &FsTzdbProvider::default();
Expand Down