Skip to content

Commit ced23bc

Browse files
authored
ZonedDateTime backward shift spec change (#725)
From tc39/proposal-temporal#3312 Spec change (https://ptomato.name/talks/tc39-2026-05/#7) <img width="915" height="327" alt="image" src="https://github.com/user-attachments/assets/cb788171-8305-4e6f-89db-004f2cb6864d" /> Fixes tc39/test262#5047
1 parent d475808 commit ced23bc

2 files changed

Lines changed: 111 additions & 7 deletions

File tree

src/builtins/core/zoned_date_time.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,17 +1301,20 @@ impl ZonedDateTime {
13011301
// c. Let startNs be ? GetStartOfDay(timeZone, dateStart).
13021302
// d. Assert: thisNs ≥ startNs.
13031303
// e. Let endNs be ? GetStartOfDay(timeZone, dateEnd).
1304-
// f. Assert: thisNs < endNs.
1304+
// f. [Struck out in spec] Assert: thisNs < endNs.
13051305
let start = self.time_zone.get_start_of_day(&iso_start.date, provider)?;
13061306
let end = self.time_zone.get_start_of_day(&iso_end, provider)?;
1307-
if !(this_ns.0 >= start.ns.0 && this_ns.0 < end.ns.0) {
1307+
if this_ns.0 < start.ns.0 {
13081308
return Err(TemporalError::range().with_enum(ErrorMessage::ZDTOutOfDayBounds));
13091309
}
1310-
// g. Let dayLengthNs be ℝ(endNs - startNs).
1311-
// h. Let dayProgressNs be TimeDurationFromEpochNanosecondsDifference(thisNs, startNs).
1310+
// g. Set thisNs to min(thisNs, endNs - 1).
1311+
let this_ns_val = this_ns.0.min(end.ns.0 - 1);
1312+
// h. Let dayLengthNs be ℝ(endNs - startNs).
1313+
// i. Let dayProgressNs be TimeDurationFromEpochNanosecondsDifference(thisNs, startNs).
13121314
let day_len_ns = TimeDuration::from_nanosecond_difference(end.ns.0, start.ns.0)?;
1313-
let day_progress_ns = TimeDuration::from_nanosecond_difference(this_ns.0, start.ns.0)?;
1314-
// i. Let roundedDayNs be ! RoundTimeDurationToIncrement(dayProgressNs, dayLengthNs, roundingMode).
1315+
let day_progress_ns =
1316+
TimeDuration::from_nanosecond_difference(this_ns_val, start.ns.0)?;
1317+
// j. Let roundedDayNs be ! RoundTimeDurationToIncrement(dayProgressNs, dayLengthNs, roundingMode).
13151318
let rounded = if let Some(increment) = NonZeroU128::new(day_len_ns.0.unsigned_abs()) {
13161319
IncrementRounder::<i128>::from_signed_num(day_progress_ns.0, increment)?
13171320
.round(resolved.rounding_mode)
@@ -1326,7 +1329,7 @@ impl ZonedDateTime {
13261329
end.offset
13271330
};
13281331

1329-
// j. Let epochNanoseconds be AddTimeDurationToEpochNanoseconds(roundedDayNs, startNs).
1332+
// k. Let epochNanoseconds be AddTimeDurationToEpochNanoseconds(roundedDayNs, startNs).
13301333
let candidate = start.ns.0 + rounded;
13311334
Instant::try_new(candidate)?;
13321335
// 20. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar).

src/builtins/core/zoned_date_time/tests.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,3 +1278,104 @@ fn hours_in_day_dst_changes() {
12781278
assert_eq!(spring.hours_in_day_with_provider(provider), Ok(23.0));
12791279
})
12801280
}
1281+
1282+
// Case where midnight occurs twice (e.g., Antarctica/Casey on 2010-03-05).
1283+
// Upstream tests: https://github.com/tc39/test262/pull/5047
1284+
#[test]
1285+
fn test_same_date_starts_twice() {
1286+
test_all_providers!(provider: {
1287+
let zdt1 = ZonedDateTime::from_utf8_with_provider(
1288+
b"2010-03-04T23:10:00+11:00[Antarctica/Casey]",
1289+
Disambiguation::Compatible,
1290+
OffsetDisambiguation::Use,
1291+
provider,
1292+
);
1293+
if zdt1.is_err() {
1294+
std::println!("Antarctica/Casey not supported by provider, skipping test.");
1295+
return;
1296+
}
1297+
let zdt1 = zdt1.unwrap();
1298+
1299+
let zdt2 = ZonedDateTime::from_utf8_with_provider(
1300+
b"2010-03-05T00:45:00+11:00[Antarctica/Casey]",
1301+
Disambiguation::Compatible,
1302+
OffsetDisambiguation::Use,
1303+
provider,
1304+
).unwrap();
1305+
1306+
let zdt3 = ZonedDateTime::from_utf8_with_provider(
1307+
b"2010-03-04T23:10:00+08:00[Antarctica/Casey]",
1308+
Disambiguation::Compatible,
1309+
OffsetDisambiguation::Use,
1310+
provider,
1311+
).unwrap();
1312+
1313+
let zdt4 = ZonedDateTime::from_utf8_with_provider(
1314+
b"2010-03-05T00:45:00+08:00[Antarctica/Casey]",
1315+
Disambiguation::Compatible,
1316+
OffsetDisambiguation::Use,
1317+
provider,
1318+
).unwrap();
1319+
1320+
let start_of_march4 = "2010-03-04T00:00:00+11:00[Antarctica/Casey]";
1321+
let start_of_march5 = "2010-03-05T00:00:00+11:00[Antarctica/Casey]";
1322+
let start_of_march6 = "2010-03-06T00:00:00+08:00[Antarctica/Casey]";
1323+
1324+
// Hours in day
1325+
assert_eq!(zdt1.hours_in_day_with_provider(provider).unwrap(), 24.0);
1326+
assert_eq!(zdt2.hours_in_day_with_provider(provider).unwrap(), 27.0);
1327+
assert_eq!(zdt3.hours_in_day_with_provider(provider).unwrap(), 24.0);
1328+
assert_eq!(zdt4.hours_in_day_with_provider(provider).unwrap(), 27.0);
1329+
1330+
// Start of day
1331+
assert_eq!(zdt1.start_of_day_with_provider(provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march4);
1332+
assert_eq!(zdt2.start_of_day_with_provider(provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5);
1333+
assert_eq!(zdt3.start_of_day_with_provider(provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march4);
1334+
assert_eq!(zdt4.start_of_day_with_provider(provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5);
1335+
1336+
// Rounding down
1337+
for rounding_mode in [RoundingMode::Floor, RoundingMode::Trunc] {
1338+
let options = RoundingOptions {
1339+
smallest_unit: Some(Unit::Day),
1340+
rounding_mode: Some(rounding_mode),
1341+
..Default::default()
1342+
};
1343+
assert_eq!(zdt1.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march4);
1344+
assert_eq!(zdt2.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5);
1345+
assert_eq!(zdt3.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march4);
1346+
assert_eq!(zdt4.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5);
1347+
}
1348+
1349+
// Rounding to nearest
1350+
for rounding_mode in [
1351+
RoundingMode::HalfCeil,
1352+
RoundingMode::HalfEven,
1353+
RoundingMode::HalfExpand,
1354+
RoundingMode::HalfFloor,
1355+
RoundingMode::HalfTrunc,
1356+
] {
1357+
let options = RoundingOptions {
1358+
smallest_unit: Some(Unit::Day),
1359+
rounding_mode: Some(rounding_mode),
1360+
..Default::default()
1361+
};
1362+
assert_eq!(zdt1.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5);
1363+
assert_eq!(zdt2.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5);
1364+
assert_eq!(zdt3.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5);
1365+
assert_eq!(zdt4.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5);
1366+
}
1367+
1368+
// Rounding up
1369+
for rounding_mode in [RoundingMode::Ceil, RoundingMode::Expand] {
1370+
let options = RoundingOptions {
1371+
smallest_unit: Some(Unit::Day),
1372+
rounding_mode: Some(rounding_mode),
1373+
..Default::default()
1374+
};
1375+
assert_eq!(zdt1.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5);
1376+
assert_eq!(zdt2.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march6);
1377+
assert_eq!(zdt3.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march5);
1378+
assert_eq!(zdt4.round_with_provider(options, provider).unwrap().to_string_with_provider(provider).unwrap(), start_of_march6);
1379+
}
1380+
})
1381+
}

0 commit comments

Comments
 (0)