Skip to content

Commit c36a6cf

Browse files
committed
reverse
1 parent 36bc1c4 commit c36a6cf

File tree

4 files changed

+182
-59
lines changed

4 files changed

+182
-59
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

utils/zoneinfo64/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ icu_time = { workspace = true }
2222
potential_utf = { workspace = true }
2323
resb = { workspace = true }
2424
serde = { workspace = true, features = ["derive"] }
25+
calendrical_calculations = { workspace = true }
2526

2627
chrono = { version = "0.4", optional = true }
2728

2829
[dev-dependencies]
2930
chrono-tz = { git = "https://github.com/robertbastian/chrono-tz", branch = "fix" }
3031

3132
[features]
32-
chrono = ["dep:chrono"]
33+
chrono = ["dep:chrono"]

utils/zoneinfo64/src/chrono_impls.rs

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
// called LICENSE at the top level of the ICU4X source tree
33
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
44

5-
use crate::{Transition, Zone};
5+
use crate::{Offset, PossibleOffset, Zone};
6+
use chrono::{
7+
Datelike, FixedOffset, MappedLocalTime, NaiveDate, NaiveDateTime, TimeZone, Timelike,
8+
};
69
use icu_time::zone::UtcOffset;
710

811
#[derive(Clone, Copy, Debug)]
9-
pub struct ChronoOffset<'a>(Transition, Zone<'a>);
12+
pub struct ChronoOffset<'a>(Offset, Zone<'a>);
1013

1114
impl core::ops::Deref for ChronoOffset<'_> {
1215
type Target = UtcOffset;
@@ -22,43 +25,61 @@ impl ChronoOffset<'_> {
2225
}
2326
}
2427

25-
#[cfg(feature = "chrono")]
2628
impl<'a> chrono::Offset for ChronoOffset<'a> {
27-
fn fix(&self) -> chrono::FixedOffset {
28-
chrono::FixedOffset::east_opt(self.0.offset.to_seconds()).unwrap()
29+
fn fix(&self) -> FixedOffset {
30+
FixedOffset::east_opt(self.0.offset.to_seconds()).unwrap()
2931
}
3032
}
3133

32-
#[cfg(feature = "chrono")]
33-
impl<'a> chrono::TimeZone for Zone<'a> {
34+
impl<'a> TimeZone for Zone<'a> {
3435
type Offset = ChronoOffset<'a>;
3536

3637
fn from_offset(offset: &Self::Offset) -> Self {
3738
offset.1
3839
}
3940

40-
fn offset_from_local_date(
41-
&self,
42-
_local: &chrono::NaiveDate,
43-
) -> chrono::MappedLocalTime<Self::Offset> {
44-
todo!()
41+
fn offset_from_local_date(&self, local: &NaiveDate) -> MappedLocalTime<Self::Offset> {
42+
match self.for_date_time(
43+
local.year(),
44+
local.month() as u8,
45+
local.day() as u8,
46+
0,
47+
0,
48+
0,
49+
) {
50+
PossibleOffset::None => chrono::MappedLocalTime::None,
51+
PossibleOffset::Single(o) => chrono::MappedLocalTime::Single(ChronoOffset(o, *self)),
52+
PossibleOffset::Ambiguous(a, b) => {
53+
MappedLocalTime::Ambiguous(ChronoOffset(a, *self), ChronoOffset(b, *self))
54+
}
55+
}
4556
}
4657

47-
fn offset_from_local_datetime(
48-
&self,
49-
_local: &chrono::NaiveDateTime,
50-
) -> chrono::MappedLocalTime<Self::Offset> {
51-
todo!()
58+
fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> MappedLocalTime<Self::Offset> {
59+
match self.for_date_time(
60+
local.year(),
61+
local.month() as u8,
62+
local.day() as u8,
63+
local.hour() as u8,
64+
local.minute() as u8,
65+
local.second() as u8,
66+
) {
67+
PossibleOffset::None => chrono::MappedLocalTime::None,
68+
PossibleOffset::Single(o) => chrono::MappedLocalTime::Single(ChronoOffset(o, *self)),
69+
PossibleOffset::Ambiguous(a, b) => {
70+
MappedLocalTime::Ambiguous(ChronoOffset(a, *self), ChronoOffset(b, *self))
71+
}
72+
}
5273
}
5374

54-
fn offset_from_utc_date(&self, utc: &chrono::NaiveDate) -> Self::Offset {
75+
fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset {
5576
ChronoOffset(
56-
self.previous_transition(utc.and_time(chrono::NaiveTime::MIN).and_utc().timestamp()),
77+
self.for_timestamp(utc.and_time(chrono::NaiveTime::MIN).and_utc().timestamp()),
5778
*self,
5879
)
5980
}
6081

61-
fn offset_from_utc_datetime(&self, utc: &chrono::NaiveDateTime) -> Self::Offset {
62-
ChronoOffset(self.previous_transition(utc.and_utc().timestamp()), *self)
82+
fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
83+
ChronoOffset(self.for_timestamp(utc.and_utc().timestamp()), *self)
6384
}
6485
}

utils/zoneinfo64/src/lib.rs

Lines changed: 137 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
77
use std::{fmt::Debug, marker::PhantomData};
88

9+
use calendrical_calculations::rata_die::RataDie;
910
use potential_utf::PotentialUtf16;
1011
use resb::binary::BinaryDeserializerError;
1112
use serde::Deserialize;
@@ -57,25 +58,25 @@ impl Debug for TzZoneData<'_> {
5758
}
5859

5960
write!(f, "transitions/offsets: [")?;
60-
write!(f, "{:?}, ", self.type_offsets[0])?;
61+
let (std, rule) = self.type_offsets[0];
62+
write!(f, "{:?}, ", (std as f64 / 3600.0, rule as f64 / 3600.0))?;
6163
let mut i = 0;
62-
for &(lo, hi) in self.trans_pre32 {
63-
dbg_timestamp(f, (lo as i64) << 32 | hi as i64)?;
64-
write!(f, "{:?}, ", self.type_offsets[self.type_map[i] as usize])?;
64+
for &(hi, lo) in self.trans_pre32 {
65+
dbg_timestamp(f, ((hi as u32 as u64) << 32 | (lo as u32 as u64)) as i64)?;
66+
let (std, rule) = self.type_offsets[self.type_map[i] as usize];
67+
write!(f, "{:?}, ", (std as f64 / 3600.0, rule as f64 / 3600.0))?;
6568
i += 1;
6669
}
6770
for &t in self.trans {
6871
dbg_timestamp(f, t as i64)?;
6972
let (std, rule) = self.type_offsets[self.type_map[i] as usize];
70-
let std = std as f64 / 3600.0;
71-
let rule = rule as f64 / 3600.0;
72-
write!(f, "{:?}, ", (std, rule))?;
73+
write!(f, "{:?}, ", (std as f64 / 3600.0, rule as f64 / 3600.0))?;
7374
i += 1;
7475
}
75-
76-
for &(lo, hi) in self.trans_post32 {
77-
dbg_timestamp(f, (lo as i64) << 32 | hi as i64)?;
78-
write!(f, "{:?}, ", self.type_offsets[self.type_map[i] as usize])?;
76+
for &(hi, lo) in self.trans_post32 {
77+
dbg_timestamp(f, ((hi as u32 as u64) << 32 | (lo as u32 as u64)) as i64)?;
78+
let (std, rule) = self.type_offsets[self.type_map[i] as usize];
79+
write!(f, "{:?}, ", (std as f64 / 3600.0, rule as f64 / 3600.0))?;
7980
i += 1;
8081
}
8182
write!(f, "], ")?;
@@ -423,16 +424,23 @@ struct Rule<'a> {
423424
inner: &'a TzRule,
424425
}
425426

426-
#[derive(Debug, Clone, Copy)]
427-
pub struct Transition {
428-
pub transition: i64,
427+
#[derive(Debug, Clone, Copy, PartialEq)]
428+
pub struct Offset {
429+
pub since: i64,
429430
pub offset: UtcOffset,
430431
pub rule_applies: bool,
431432
}
432433

434+
pub enum PossibleOffset {
435+
Single(Offset),
436+
Ambiguous(Offset, Offset),
437+
None,
438+
}
439+
433440
impl Zone<'_> {
434-
fn previous_transition(&self, seconds_since_epoch: i64) -> Transition {
435-
let idx = if seconds_since_epoch < i32::MIN as i64 {
441+
// Returns the index of the previous transition. As this can be -1, it returns an isize.
442+
fn offset_idx(&self, seconds_since_epoch: i64) -> isize {
443+
if seconds_since_epoch < i32::MIN as i64 {
436444
self.simple
437445
.trans_pre32
438446
.binary_search(&(
@@ -464,14 +472,16 @@ impl Zone<'_> {
464472
.map(|i| i as isize)
465473
// binary_search returns the index of the next (higher) transition, so we subtract one
466474
.unwrap_or_else(|i| i as isize - 1)
467-
};
475+
}
476+
}
468477

478+
fn offset_at(&self, idx: isize, seconds_since_epoch: i64) -> Offset {
469479
// before first transition don't use `type_map`, just the first entry in `type_offsets`
470-
if idx == -1 {
480+
if idx < 0 || self.simple.type_map.is_empty() {
471481
#[expect(clippy::unwrap_used)] // type_offsets non-empty by invariant
472482
let &(standard, rule_additional) = self.simple.type_offsets.first().unwrap();
473-
return Transition {
474-
transition: i64::MIN,
483+
return Offset {
484+
since: i64::MIN,
475485
offset: UtcOffset::from_seconds_unchecked(standard + rule_additional),
476486
rule_applies: rule_additional > 0,
477487
};
@@ -480,13 +490,13 @@ impl Zone<'_> {
480490
let idx = idx as usize;
481491

482492
// after the last transition, respect the rule
483-
if idx == self.simple.type_map.len() - 1 {
493+
if idx >= self.simple.type_map.len() - 1 {
484494
if let Some(rule) = self.final_rule.as_ref() {
485495
let (additional_offset_seconds, transition) = rule
486496
.inner
487497
.additional_offset_since(seconds_since_epoch, rule.start_year);
488-
return Transition {
489-
transition,
498+
return Offset {
499+
since: transition,
490500
rule_applies: additional_offset_seconds != 0,
491501
offset: UtcOffset::from_seconds_unchecked(
492502
rule.standard_offset_seconds + additional_offset_seconds,
@@ -495,29 +505,102 @@ impl Zone<'_> {
495505
}
496506
}
497507

508+
let idx = core::cmp::min(idx, self.simple.type_map.len() - 1);
509+
498510
#[expect(clippy::indexing_slicing)]
499511
// type_map has length sum(trans*), and type_map values are validated to be valid indices in type_offsets
500512
let (standard, rule_additional) =
501513
self.simple.type_offsets[self.simple.type_map[idx] as usize];
502514

503515
#[expect(clippy::indexing_slicing)] // by guards or invariant
504-
let transition = if idx < self.simple.trans_pre32.len() {
505-
let (lo, hi) = self.simple.trans_pre32[idx];
506-
(lo as i64) << 32 | hi as i64
516+
let since = if idx < self.simple.trans_pre32.len() {
517+
let (hi, lo) = self.simple.trans_pre32[idx];
518+
((hi as u32 as u64) << 32 | (lo as u32 as u64)) as i64
507519
} else if idx - self.simple.trans_pre32.len() < self.simple.trans.len() {
508520
self.simple.trans[idx - self.simple.trans_pre32.len()] as i64
509521
} else {
510-
let (lo, hi) = self.simple.trans_post32
522+
let (hi, lo) = self.simple.trans_post32
511523
[idx - self.simple.trans_pre32.len() - self.simple.trans.len()];
512-
(lo as i64) << 32 | hi as i64
524+
((hi as u32 as u64) << 32 | (lo as u32 as u64)) as i64
513525
};
514526

515-
Transition {
516-
transition,
527+
Offset {
528+
since,
517529
offset: UtcOffset::from_seconds_unchecked(standard + rule_additional),
518530
rule_applies: rule_additional > 0,
519531
}
520532
}
533+
534+
pub fn for_date_time(
535+
&self,
536+
year: i32,
537+
month: u8,
538+
day: u8,
539+
hour: u8,
540+
minute: u8,
541+
second: u8,
542+
) -> PossibleOffset {
543+
const EPOCH: RataDie = calendrical_calculations::iso::const_fixed_from_iso(1970, 1, 1);
544+
let seconds_since_local_epoch =
545+
(((calendrical_calculations::iso::fixed_from_iso(year, month, day) - EPOCH) * 24
546+
+ hour as i64)
547+
* 60
548+
+ minute as i64)
549+
* 60
550+
+ second as i64;
551+
552+
// Pretend date time is UTC to get a candidate
553+
let idx = self.offset_idx(seconds_since_local_epoch);
554+
555+
let candidate = self.offset_at(idx, seconds_since_local_epoch);
556+
let before_candidate = self.offset_at(idx - 1, seconds_since_local_epoch);
557+
let after_candidate = self.offset_at(idx + 1, seconds_since_local_epoch);
558+
559+
let before_candidate_local_until = candidate
560+
.since
561+
.saturating_add(before_candidate.offset.to_seconds() as i64);
562+
563+
let candidate_local_since = candidate
564+
.since
565+
.saturating_add(candidate.offset.to_seconds() as i64);
566+
let candidate_local_until = after_candidate
567+
.since
568+
.saturating_add(candidate.offset.to_seconds() as i64);
569+
570+
let after_candidate_local_since = after_candidate
571+
.since
572+
.saturating_add(after_candidate.offset.to_seconds() as i64);
573+
574+
if seconds_since_local_epoch < before_candidate_local_until
575+
&& seconds_since_local_epoch >= candidate_local_since
576+
&& before_candidate != candidate
577+
{
578+
return PossibleOffset::Ambiguous(before_candidate, candidate);
579+
}
580+
581+
if seconds_since_local_epoch < candidate_local_until
582+
&& seconds_since_local_epoch >= after_candidate_local_since
583+
&& candidate != after_candidate
584+
{
585+
return PossibleOffset::Ambiguous(candidate, after_candidate);
586+
}
587+
588+
if seconds_since_local_epoch < before_candidate_local_until {
589+
return PossibleOffset::Single(before_candidate);
590+
}
591+
if seconds_since_local_epoch < candidate_local_until {
592+
return PossibleOffset::Single(candidate);
593+
}
594+
if seconds_since_local_epoch >= after_candidate_local_since {
595+
return PossibleOffset::Single(after_candidate);
596+
}
597+
598+
PossibleOffset::None
599+
}
600+
601+
pub fn for_timestamp(&self, seconds_since_epoch: i64) -> Offset {
602+
self.offset_at(self.offset_idx(seconds_since_epoch), seconds_since_epoch)
603+
}
521604
}
522605

523606
#[test]
@@ -546,7 +629,13 @@ fn test() {
546629

547630
// TODO
548631
let max_working_timestamp = if zoneinfo64.final_rule.is_some() {
549-
zoneinfo64.simple.trans.last().copied().unwrap_or(i32::MAX) as i64
632+
zoneinfo64
633+
.simple
634+
.trans
635+
.len()
636+
.checked_sub(2)
637+
.map(|i| zoneinfo64.simple.trans[i])
638+
.unwrap_or(i32::MAX) as i64
550639
} else {
551640
FUTURE
552641
};
@@ -555,15 +644,26 @@ fn test() {
555644
let utc_datetime = chrono::DateTime::from_timestamp(seconds_since_epoch, 0)
556645
.unwrap()
557646
.naive_utc();
558-
let zoneinfo64_date = zoneinfo64.offset_from_utc_datetime(&utc_datetime);
559-
let chrono_date = chrono.offset_from_utc_datetime(&utc_datetime);
560647

648+
let zoneinfo64_date = zoneinfo64.from_utc_datetime(&utc_datetime);
649+
let chrono_date = chrono.from_utc_datetime(&utc_datetime);
561650
assert_eq!(
562-
zoneinfo64_date.fix(),
563-
chrono_date.fix(),
651+
zoneinfo64_date.offset().fix(),
652+
chrono_date.offset().fix(),
564653
"{seconds_since_epoch}, {iana:?}",
565654
);
566655

656+
let local_datetime = chrono_date.naive_local();
657+
assert_eq!(
658+
zoneinfo64
659+
.offset_from_local_datetime(&local_datetime)
660+
.map(|o| o.fix()),
661+
chrono
662+
.offset_from_local_datetime(&local_datetime)
663+
.map(|o| o.fix()),
664+
"{seconds_since_epoch}, {zoneinfo64:?} {local_datetime}",
665+
);
666+
567667
// Rearguard / vanguard diffs
568668
if [
569669
"Africa/Casablanca",
@@ -580,8 +680,8 @@ fn test() {
580680
}
581681

582682
assert_eq!(
583-
zoneinfo64_date.rule_applies(),
584-
!chrono_date.dst_offset().is_zero(),
683+
zoneinfo64_date.offset().rule_applies(),
684+
!chrono_date.offset().dst_offset().is_zero(),
585685
"{seconds_since_epoch}, {iana:?}",
586686
);
587687
}

0 commit comments

Comments
 (0)