timens/
timezone.rs

1use crate::{Date, OfDay, Span, Time};
2
3#[derive(Copy, Clone)]
4pub struct TzOffset {
5    pub utc_offset: i32,
6    pub dst_offset: i32,
7}
8
9#[derive(Clone)]
10pub struct TzInfo {
11    pub first: TzOffset,
12    pub rest: &'static [(i64, TzOffset)],
13}
14
15impl TzOffset {
16    pub const ZERO: TzOffset = TzOffset { utc_offset: 0, dst_offset: 0 };
17
18    pub fn total_offset_sec(&self) -> i32 {
19        self.utc_offset + self.dst_offset
20    }
21}
22
23#[derive(Clone, Debug, PartialEq, Eq)]
24#[allow(clippy::enum_variant_names)]
25pub enum TzError {
26    NoTimeInThisTz,
27    TwoTimesInThisTz(Time, Time),
28}
29
30impl std::fmt::Display for TzError {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        write!(f, "{self:?}")
33    }
34}
35
36impl std::error::Error for TzError {}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub enum TzParseError {
40    UnknownZone(String),
41}
42
43impl std::fmt::Display for TzParseError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "{self:?}")
46    }
47}
48
49impl std::error::Error for TzParseError {}
50
51impl TzInfo {
52    pub fn find(&self, time: Time) -> &TzOffset {
53        let sec = time.to_int_ns_since_epoch().div_euclid(Span::SEC.to_int_ns());
54        let index = self.rest.partition_point(|&(start_sec, _)| sec >= start_sec);
55        if index == 0 {
56            &self.first
57        } else {
58            &self.rest[index - 1].1
59        }
60    }
61
62    pub fn offset(&self, time: Time) -> Span {
63        let fixed_timespan = self.find(time);
64        Span::of_int_sec(fixed_timespan.total_offset_sec() as i64)
65    }
66
67    pub const GMT: TzInfo = TzInfo { first: TzOffset::ZERO, rest: &[] };
68
69    fn valid_time(&self, gmt_sec: i64, nanosecond: i64, next_i: usize) -> Option<Time> {
70        let (min_sec, tz_info) = if next_i == 0 {
71            (i64::MIN, self.first)
72        } else if next_i > self.rest.len() {
73            return None;
74        } else {
75            self.rest[next_i - 1]
76        };
77        let sec = gmt_sec - tz_info.total_offset_sec() as i64;
78        if sec >= min_sec && (self.rest.len() == next_i || sec < self.rest[next_i].0) {
79            Some(crate::Time::of_int_ns_since_epoch(sec * Span::SEC.to_int_ns() + nanosecond))
80        } else {
81            None
82        }
83    }
84
85    pub fn date_ofday_to_time(&self, date: Date, ofday: OfDay) -> Result<Time, TzError> {
86        let gmt_ns = (date - Date::UNIX_EPOCH) as i64 * Span::DAY.to_int_ns();
87        let gmt_ns = gmt_ns + ofday.to_ns_since_midnight();
88        let gmt_sec = gmt_ns.div_euclid(Span::SEC.to_int_ns());
89        let nanosecond = gmt_ns.rem_euclid(Span::SEC.to_int_ns());
90        let next_i = self.rest.partition_point(|&(start_sec, _)| gmt_sec >= start_sec);
91        if next_i == 0 {
92            let t1 = self.valid_time(gmt_sec, nanosecond, next_i);
93            let t2 = self.valid_time(gmt_sec, nanosecond, next_i + 1);
94            match (t1, t2) {
95                (None, None) => Err(TzError::NoTimeInThisTz),
96                (Some(v), None) | (None, Some(v)) => Ok(v),
97                (Some(v1), Some(v2)) => Err(TzError::TwoTimesInThisTz(v1, v2)),
98            }
99        } else {
100            let t0 = self.valid_time(gmt_sec, nanosecond, next_i - 1);
101            let t1 = self.valid_time(gmt_sec, nanosecond, next_i);
102            let t2 = self.valid_time(gmt_sec, nanosecond, next_i + 1);
103            match (t0, t1, t2) {
104                (None, None, None) => Err(TzError::NoTimeInThisTz),
105                (Some(v), None, None) | (None, Some(v), None) | (None, None, Some(v)) => Ok(v),
106                (Some(v1), Some(v2), _) | (Some(v1), _, Some(v2)) | (_, Some(v1), Some(v2)) => {
107                    Err(TzError::TwoTimesInThisTz(v1, v2))
108                }
109            }
110        }
111    }
112}