Skip to content

Commit ae0828f

Browse files
sebastianjacmattnekevss
authored andcommitted
Impl round_with_provider for ZonedDateTIme (#278)
fixes #150 Implementation for round method in ZonedDateTime
1 parent b462cd2 commit ae0828f

2 files changed

Lines changed: 182 additions & 4 deletions

File tree

src/builtins/compiled/zoneddatetime.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::ZonedDateTime;
44
use crate::{
55
options::{
66
ArithmeticOverflow, DifferenceSettings, Disambiguation, DisplayCalendar, DisplayOffset,
7-
DisplayTimeZone, OffsetDisambiguation, ToStringRoundingOptions,
7+
DisplayTimeZone, OffsetDisambiguation, RoundingOptions, ToStringRoundingOptions,
88
},
99
Duration, MonthCode, PlainDate, PlainDateTime, PlainTime, TemporalError, TemporalResult,
1010
};
@@ -396,6 +396,16 @@ impl ZonedDateTime {
396396
self.to_plain_datetime_with_provider(&*provider)
397397
}
398398

399+
/// Rounds this [`ZonedDateTime`] to the nearest value according to the given rounding options.
400+
///
401+
/// Enable with the `compiled_data` feature flag.
402+
pub fn round(&self, options: RoundingOptions) -> TemporalResult<Self> {
403+
let provider = TZ_PROVIDER
404+
.lock()
405+
.map_err(|_| TemporalError::general("Unable to acquire lock"))?;
406+
self.round_with_provider(options, &*provider)
407+
}
408+
399409
/// Returns a RFC9557 (IXDTF) string with the provided options.
400410
///
401411
/// Enable with the `compiled_data` feature flag.

src/builtins/core/zoneddatetime.rs

Lines changed: 171 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use crate::{
1717
options::{
1818
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, Disambiguation,
1919
DisplayCalendar, DisplayOffset, DisplayTimeZone, OffsetDisambiguation,
20-
ResolvedRoundingOptions, RoundingIncrement, RoundingMode, ToStringRoundingOptions, Unit,
21-
UnitGroup,
20+
ResolvedRoundingOptions, RoundingIncrement, RoundingMode, RoundingOptions,
21+
ToStringRoundingOptions, Unit, UnitGroup,
2222
},
2323
parsers::{self, FormattableOffset, FormattableTime, IxdtfStringBuilder, Precision},
2424
partial::{PartialDate, PartialTime},
@@ -1023,6 +1023,113 @@ impl ZonedDateTime {
10231023
)
10241024
}
10251025

1026+
/// 6.3.39 Temporal.ZonedDateTime.prototype.round
1027+
pub fn round_with_provider(
1028+
&self,
1029+
options: RoundingOptions,
1030+
provider: &impl TimeZoneProvider,
1031+
) -> TemporalResult<Self> {
1032+
// 1. Let zonedDateTime be the this value.
1033+
// 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]).
1034+
// 3. If roundTo is undefined, then
1035+
// a. Throw a TypeError exception.
1036+
// 4. If roundTo is a String, then
1037+
// a. Let paramString be roundTo.
1038+
// b. Set roundTo to OrdinaryObjectCreate(null).
1039+
// c. Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit", paramString).
1040+
// 5. Else,
1041+
// a. Set roundTo to ? GetOptionsObject(roundTo).
1042+
// 6. NOTE: The following steps read options and perform independent validation in alphabetical order (GetRoundingIncrementOption reads "roundingIncrement" and GetRoundingModeOption reads "roundingMode").
1043+
// 7. Let roundingIncrement be ? GetRoundingIncrementOption(roundTo).
1044+
// 8. Let roundingMode be ? GetRoundingModeOption(roundTo, half-expand).
1045+
// 9. Let smallestUnit be ? GetTemporalUnitValuedOption(roundTo, "smallestUnit", time, required, « day »).
1046+
// 10. If smallestUnit is day, then
1047+
// a. Let maximum be 1.
1048+
// b. Let inclusive be true.
1049+
// 11. Else,
1050+
// a. Let maximum be MaximumTemporalDurationRoundingIncrement(smallestUnit).
1051+
// b. Assert: maximum is not unset.
1052+
// c. Let inclusive be false.
1053+
let resolved = ResolvedRoundingOptions::from_datetime_options(options)?;
1054+
// 12. Perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, inclusive).
1055+
// 13. If maximum is not unset, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false).
1056+
// 13. If smallestUnit is nanosecond and roundingIncrement = 1, then
1057+
if resolved.smallest_unit == Unit::Nanosecond
1058+
&& resolved.increment == RoundingIncrement::ONE
1059+
{
1060+
// a. Return ! CreateTemporalZonedDateTime(zonedDateTime.[[EpochNanoseconds]], zonedDateTime.[[TimeZone]], zonedDateTime.[[Calendar]]).
1061+
return Ok(self.clone());
1062+
}
1063+
// 14. Let thisNs be zonedDateTime.[[EpochNanoseconds]].
1064+
let this_ns = self.epoch_nanoseconds();
1065+
// 15. Let timeZone be zonedDateTime.[[TimeZone]].
1066+
// 16. Let calendar be zonedDateTime.[[Calendar]].
1067+
// 17. Let isoDateTime be GetISODateTimeFor(timeZone, thisNs).
1068+
// 18. If smallestUnit is day, then
1069+
if resolved.smallest_unit == Unit::Day {
1070+
// a. Let dateStart be isoDateTime.[[ISODate]].
1071+
let iso_start = self.tz.get_iso_datetime_for(&self.instant, provider)?;
1072+
// b. Let dateEnd be BalanceISODate(dateStart.[[Year]], dateStart.[[Month]], dateStart.[[Day]] + 1).
1073+
let iso_end = IsoDate::balance(
1074+
iso_start.date.year,
1075+
iso_start.date.month.into(),
1076+
i32::from(iso_start.date.day + 1),
1077+
);
1078+
// c. Let startNs be ? GetStartOfDay(timeZone, dateStart).
1079+
// d. Assert: thisNs ≥ startNs.
1080+
// e. Let endNs be ? GetStartOfDay(timeZone, dateEnd).
1081+
// f. Assert: thisNs < endNs.
1082+
let start_ns = self.tz.get_start_of_day(&iso_start.date, provider)?;
1083+
let end_ns = self.tz.get_start_of_day(&iso_end, provider)?;
1084+
if !(this_ns.0 >= start_ns.0 && this_ns.0 < end_ns.0) {
1085+
return Err(TemporalError::range()
1086+
.with_message("ZonedDateTime is outside the expected day bounds"));
1087+
}
1088+
// g. Let dayLengthNs be ℝ(endNs - startNs).
1089+
// h. Let dayProgressNs be TimeDurationFromEpochNanosecondsDifference(thisNs, startNs).
1090+
let day_len_ns =
1091+
NormalizedTimeDuration::from_nanosecond_difference(end_ns.0, start_ns.0)?;
1092+
let day_progress_ns =
1093+
NormalizedTimeDuration::from_nanosecond_difference(this_ns.0, start_ns.0)?;
1094+
// i. Let roundedDayNs be ! RoundTimeDurationToIncrement(dayProgressNs, dayLengthNs, roundingMode).
1095+
let rounded = if let Some(increment) = NonZeroU128::new(day_len_ns.0.unsigned_abs()) {
1096+
IncrementRounder::<i128>::from_signed_num(day_progress_ns.0, increment)?
1097+
.round(resolved.rounding_mode)
1098+
} else {
1099+
0 // Zero-length day: round to start of day
1100+
};
1101+
1102+
// j. Let epochNanoseconds be AddTimeDurationToEpochNanoseconds(roundedDayNs, startNs).
1103+
let candidate = start_ns.0 + rounded;
1104+
Instant::try_new(candidate)?;
1105+
// 20. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar).
1106+
ZonedDateTime::try_new(candidate, self.calendar.clone(), self.tz.clone())
1107+
} else {
1108+
// 19. Else,
1109+
// a. Let roundResult be RoundISODateTime(isoDateTime, roundingIncrement, smallestUnit, roundingMode).
1110+
// b. Let offsetNanoseconds be GetOffsetNanosecondsFor(timeZone, thisNs).
1111+
// c. Let epochNanoseconds be ? InterpretISODateTimeOffset(roundResult.[[ISODate]], roundResult.[[Time]], option, offsetNanoseconds, timeZone, compatible, prefer, match-exactly).
1112+
// 20. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar).
1113+
let iso_dt = self.tz.get_iso_datetime_for(&self.instant, provider)?;
1114+
let rounded_dt = iso_dt.round(resolved)?;
1115+
let offset_ns = self.tz.get_offset_nanos_for(this_ns.as_i128(), provider)?;
1116+
1117+
let epoch_ns = interpret_isodatetime_offset(
1118+
rounded_dt.date,
1119+
Some(rounded_dt.time),
1120+
false,
1121+
Some(offset_ns as i64),
1122+
&self.tz,
1123+
Disambiguation::Compatible,
1124+
OffsetDisambiguation::Prefer,
1125+
true,
1126+
provider,
1127+
)?;
1128+
1129+
ZonedDateTime::try_new(epoch_ns.0, self.calendar.clone(), self.tz.clone())
1130+
}
1131+
}
1132+
10261133
/// Creates an IXDTF (RFC 9557) date/time string for the provided `ZonedDateTime` according
10271134
/// to the provided display options.
10281135
pub fn to_ixdtf_string_with_provider(
@@ -1138,6 +1245,7 @@ impl ZonedDateTime {
11381245
}
11391246
}
11401247

1248+
/// InterpretISODateTimeOffset
11411249
#[allow(clippy::too_many_arguments)]
11421250
pub(crate) fn interpret_isodatetime_offset(
11431251
date: IsoDate,
@@ -1275,7 +1383,8 @@ mod tests {
12751383
use super::ZonedDateTime;
12761384
use crate::{
12771385
options::{
1278-
ArithmeticOverflow, DifferenceSettings, Disambiguation, OffsetDisambiguation, Unit,
1386+
ArithmeticOverflow, DifferenceSettings, Disambiguation, OffsetDisambiguation,
1387+
RoundingIncrement, RoundingMode, RoundingOptions, Unit,
12791388
},
12801389
partial::{PartialDate, PartialTime, PartialZonedDateTime},
12811390
time::EpochNanoseconds,
@@ -1333,6 +1442,65 @@ mod tests {
13331442
assert_eq!(zdt_plus_eleven.second_with_provider(provider).unwrap(), 12);
13341443
}
13351444

1445+
#[test]
1446+
// https://tc39.es/proposal-temporal/docs/zoneddatetime.html#round
1447+
fn round_with_provider_test() {
1448+
let provider = &FsTzdbProvider::default();
1449+
let dt = "1995-12-07T03:24:30.000003500-08:00[America/Los_Angeles]";
1450+
let zdt = ZonedDateTime::from_str_with_provider(
1451+
dt,
1452+
Disambiguation::default(),
1453+
OffsetDisambiguation::Use,
1454+
provider,
1455+
)
1456+
.unwrap();
1457+
1458+
let result = zdt
1459+
.round_with_provider(
1460+
RoundingOptions {
1461+
smallest_unit: Some(Unit::Hour),
1462+
..Default::default()
1463+
},
1464+
provider,
1465+
)
1466+
.unwrap();
1467+
assert_eq!(
1468+
result.to_string_with_provider(provider).unwrap(),
1469+
"1995-12-07T03:00:00-08:00[America/Los_Angeles]"
1470+
);
1471+
1472+
let result = zdt
1473+
.round_with_provider(
1474+
RoundingOptions {
1475+
smallest_unit: Some(Unit::Minute),
1476+
increment: Some((RoundingIncrement::try_new(30)).unwrap()),
1477+
..Default::default()
1478+
},
1479+
provider,
1480+
)
1481+
.unwrap();
1482+
assert_eq!(
1483+
result.to_string_with_provider(provider).unwrap(),
1484+
"1995-12-07T03:30:00-08:00[America/Los_Angeles]"
1485+
);
1486+
1487+
let result = zdt
1488+
.round_with_provider(
1489+
RoundingOptions {
1490+
smallest_unit: Some(Unit::Minute),
1491+
increment: Some((RoundingIncrement::try_new(30)).unwrap()),
1492+
rounding_mode: Some(RoundingMode::Floor),
1493+
..Default::default()
1494+
},
1495+
provider,
1496+
)
1497+
.unwrap();
1498+
assert_eq!(
1499+
result.to_string_with_provider(provider).unwrap(),
1500+
"1995-12-07T03:00:00-08:00[America/Los_Angeles]"
1501+
);
1502+
}
1503+
13361504
#[test]
13371505
fn zdt_from_partial() {
13381506
let provider = &FsTzdbProvider::default();

0 commit comments

Comments
 (0)