Skip to content

Commit 04cdeb3

Browse files
authored
Fix edge case for disambiguating ZonedDateTimes on DSTs skipping midnight (#156)
Fixes part of the test [intl402/Temporal/ZonedDateTime/from/dst-skipped-cross-midnight.js](https://github.com/tc39/test262/blob/d9b10790bc4bb5b3e1aa895f11cbd2d31a5ec743/test/intl402/Temporal/ZonedDateTime/from/dst-skipped-cross-midnight.js). This doesn't fix correctly resolving the start of the day for days that don't start at 00:00, since that'll require more advanced logic.
1 parent bd96e91 commit 04cdeb3

File tree

3 files changed

+42
-13
lines changed

3 files changed

+42
-13
lines changed

src/components/timezone.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use core::{iter::Peekable, str::Chars};
77

88
use num_traits::ToPrimitive;
99

10+
use crate::components::duration::DateDuration;
11+
use crate::Calendar;
1012
use crate::{
1113
components::{duration::normalized::NormalizedTimeDuration, EpochNanoseconds, Instant},
1214
iso::{IsoDate, IsoDateTime, IsoTime},
@@ -23,6 +25,8 @@ use std::sync::{LazyLock, Mutex};
2325
pub static TZ_PROVIDER: LazyLock<Mutex<FsTzdbProvider>> =
2426
LazyLock::new(|| Mutex::new(FsTzdbProvider::default()));
2527

28+
const NS_IN_HOUR: i128 = 60 * 60 * 1000 * 1000 * 1000;
29+
2630
// NOTE: It may be a good idea to eventually move this into it's
2731
// own individual crate rather than having it tied directly into `temporal_rs`
2832
pub trait TzProvider {
@@ -31,7 +35,7 @@ pub trait TzProvider {
3135
fn get_named_tz_epoch_nanoseconds(
3236
&self,
3337
identifier: &str,
34-
iso_datetime: IsoDateTime,
38+
local_datetime: IsoDateTime,
3539
) -> TemporalResult<Vec<EpochNanoseconds>>;
3640

3741
fn get_named_tz_offset_nanoseconds(
@@ -258,13 +262,22 @@ impl TimeZone {
258262
// which CompareISODateTime(before, isoDateTime) = -1 and !
259263
// GetPossibleEpochNanoseconds(timeZone, before) is not
260264
// empty.
261-
let mut before = iso;
262-
before.time.hour -= 3;
265+
let before = iso.add_date_duration(
266+
Calendar::default(),
267+
&DateDuration::default(),
268+
NormalizedTimeDuration(-3 * NS_IN_HOUR),
269+
None,
270+
)?;
271+
263272
// 7. Let after be the earliest possible ISO Date-Time Record
264273
// for which CompareISODateTime(after, isoDateTime) = 1 and !
265274
// GetPossibleEpochNanoseconds(timeZone, after) is not empty.
266-
let mut after = iso;
267-
after.time.hour += 3;
275+
let after = iso.add_date_duration(
276+
Calendar::default(),
277+
&DateDuration::default(),
278+
NormalizedTimeDuration(3 * NS_IN_HOUR),
279+
None,
280+
)?;
268281

269282
// 8. Let beforePossible be !
270283
// GetPossibleEpochNanoseconds(timeZone, before).

src/components/zoneddatetime.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use crate::{
2929
use crate::components::timezone::TZ_PROVIDER;
3030

3131
/// A struct representing a partial `ZonedDateTime`.
32+
#[derive(Debug, Default, Clone, PartialEq)]
3233
pub struct PartialZonedDateTime {
3334
/// The `PartialDate` portion of a `PartialZonedDateTime`
3435
pub date: PartialDate,
@@ -1117,7 +1118,7 @@ pub(crate) fn interpret_isodatetime_offset(
11171118
// i. Let roundedCandidateNanoseconds be RoundNumberToIncrement(candidateOffset, 60 × 10**9, half-expand).
11181119
let rounded_candidate = IncrementRounder::from_potentially_negative_parts(
11191120
candidate_offset,
1120-
unsafe { NonZeroU128::new_unchecked(60_000_000_000) },
1121+
const { NonZeroU128::new(60_000_000_000).expect("cannot be zero") },
11211122
)?
11221123
.round(TemporalRoundingMode::HalfExpand);
11231124
// ii. If roundedCandidateNanoseconds = offsetNanoseconds, then
@@ -1173,8 +1174,7 @@ pub(crate) fn nanoseconds_to_formattable_offset_minutes(
11731174
Ok((sign, hour as u8, minute as u8))
11741175
}
11751176

1176-
#[cfg(feature = "tzdb")]
1177-
#[cfg(test)]
1177+
#[cfg(all(test, feature = "tzdb"))]
11781178
mod tests {
11791179
use crate::{
11801180
options::{Disambiguation, OffsetDisambiguation},
@@ -1344,4 +1344,19 @@ mod tests {
13441344
);
13451345
assert!(result.is_ok());
13461346
}
1347+
1348+
#[test]
1349+
// https://github.com/tc39/test262/blob/d9b10790bc4bb5b3e1aa895f11cbd2d31a5ec743/test/intl402/Temporal/ZonedDateTime/from/dst-skipped-cross-midnight.js
1350+
fn dst_skipped_cross_midnight() {
1351+
let provider = &FsTzdbProvider::default();
1352+
let midnight_disambiguated = ZonedDateTime::from_str_with_provider(
1353+
"1919-03-31T00[America/Toronto]",
1354+
Disambiguation::Compatible,
1355+
OffsetDisambiguation::Reject,
1356+
provider,
1357+
)
1358+
.unwrap();
1359+
1360+
assert_eq!(midnight_disambiguated.epoch_milliseconds(), -1601751600000);
1361+
}
13471362
}

src/tzdb.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ impl From<LocalTimeTypeRecord> for LocalTimeRecord {
9292

9393
// TODO: Workshop record name?
9494
/// The `LocalTimeRecord` result represents the result of searching for a
95-
/// a for a time zone transition without the offset seconds applied to the
95+
/// time zone transition without the offset seconds applied to the
9696
/// epoch seconds.
9797
///
9898
/// As a result of the search, it is possible for the resulting search to be either
@@ -295,7 +295,8 @@ impl Tzif {
295295
match offset_range.contains(&current_diff.0) {
296296
true if next_record.is_dst => Ok(LocalTimeRecordResult::Empty),
297297
true => Ok((next_record, initial_record).into()),
298-
false => Ok(initial_record.into()),
298+
false if current_diff <= initial_record.utoff => Ok(initial_record.into()),
299+
false => Ok(next_record.into()),
299300
}
300301
}
301302
}
@@ -557,14 +558,14 @@ impl TzProvider for FsTzdbProvider {
557558
LocalTimeRecordResult::Empty => Vec::default(),
558559
LocalTimeRecordResult::Single(r) => {
559560
let epoch_ns =
560-
EpochNanoseconds::try_from(epoch_nanos.0 + seconds_to_nanoseconds(r.offset))?;
561+
EpochNanoseconds::try_from(epoch_nanos.0 - seconds_to_nanoseconds(r.offset))?;
561562
vec![epoch_ns]
562563
}
563564
LocalTimeRecordResult::Ambiguous { std, dst } => {
564565
let std_epoch_ns =
565-
EpochNanoseconds::try_from(epoch_nanos.0 + seconds_to_nanoseconds(std.offset))?;
566+
EpochNanoseconds::try_from(epoch_nanos.0 - seconds_to_nanoseconds(std.offset))?;
566567
let dst_epoch_ns =
567-
EpochNanoseconds::try_from(epoch_nanos.0 + seconds_to_nanoseconds(dst.offset))?;
568+
EpochNanoseconds::try_from(epoch_nanos.0 - seconds_to_nanoseconds(dst.offset))?;
568569
vec![std_epoch_ns, dst_epoch_ns]
569570
}
570571
};

0 commit comments

Comments
 (0)