openpine_vm/time/
timezone.rs

1use std::{fmt, str::FromStr};
2
3use nom::{
4    IResult, Parser,
5    branch::alt,
6    bytes::complete::tag,
7    character::complete::satisfy,
8    combinator::{eof, map, map_res, recognize, value},
9    multi::fold_many_m_n,
10};
11use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
12use time_tz::{OffsetDateTimeExt, PrimitiveDateTimeExt, TimeZone as _, Tz};
13
14/// A parsed timezone representation.
15#[derive(Debug, PartialEq, Eq)]
16pub enum TimeZone {
17    /// Time zone specified as an explicit UTC offset.
18    UtcOffset(UtcOffset),
19    /// Time zone specified by IANA name.
20    Tz(&'static Tz),
21}
22
23/// Error returned when parsing a [`TimeZone`] fails.
24#[derive(Debug, thiserror::Error)]
25#[error("failed to parse timezone")]
26#[non_exhaustive]
27pub struct ParseTimeZoneError;
28
29impl FromStr for TimeZone {
30    type Err = ParseTimeZoneError;
31
32    fn from_str(s: &str) -> Result<Self, Self::Err> {
33        fn number<'a>(
34            length: usize,
35        ) -> impl Parser<&'a str, Output = i32, Error = nom::error::Error<&'a str>> {
36            map(
37                recognize(fold_many_m_n(
38                    length,
39                    length,
40                    satisfy(|c| c.is_ascii_digit()),
41                    || (),
42                    |_, _| (),
43                )),
44                |s: &str| s.parse::<i32>().unwrap(),
45            )
46        }
47
48        fn utc_offset(input: &str) -> IResult<&str, UtcOffset> {
49            let input = input.trim();
50            let prefix = alt((tag("UTC"), tag("GMT")));
51            let sign = alt((value(1, tag("+")), value(-1, tag("-"))));
52            let n1 = map(number(1), |h: i32| h * 3600);
53            let n2 = map((number(2), tag(":"), number(2)), |(hours, _, minutes)| {
54                hours * 3600 + minutes * 60
55            });
56            let n3 = map(number(4), |n| {
57                let hours = n / 100;
58                let minutes = n % 100;
59                hours * 3600 + minutes * 60
60            });
61            map_res(
62                (prefix, sign, alt((n3, n2, n1)), eof),
63                |(_, sign, offset, _)| UtcOffset::from_whole_seconds(sign * offset),
64            )
65            .parse_complete(input)
66        }
67
68        if let Ok((_, offset)) = utc_offset(s) {
69            Ok(TimeZone::UtcOffset(offset))
70        } else if let Some(tz) = time_tz::timezones::get_by_name(s) {
71            Ok(TimeZone::Tz(tz))
72        } else {
73            Err(ParseTimeZoneError)
74        }
75    }
76}
77
78impl fmt::Display for TimeZone {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            TimeZone::UtcOffset(offset) => {
82                let minutes = offset.whole_minutes();
83                let (sign, abs_minutes) = if minutes < 0 {
84                    ('-', -minutes)
85                } else {
86                    ('+', minutes)
87                };
88                let hours = abs_minutes / 60;
89                let minutes = abs_minutes % 60;
90                if minutes == 0 {
91                    write!(f, "UTC{}{:02}", sign, hours)
92                } else {
93                    write!(f, "UTC{}{:02}:{:02}", sign, hours, minutes)
94                }
95            }
96            TimeZone::Tz(tz) => write!(f, "{}", tz.name()),
97        }
98    }
99}
100
101impl TimeZone {
102    #[inline]
103    pub(crate) fn with(&self, time: OffsetDateTime) -> OffsetDateTime {
104        match self {
105            TimeZone::UtcOffset(utc_offset) => time.to_offset(*utc_offset),
106            TimeZone::Tz(tz) => time.to_timezone(*tz),
107        }
108    }
109
110    #[inline]
111    pub(crate) fn assume(&self, time: PrimitiveDateTime) -> Option<OffsetDateTime> {
112        match self {
113            TimeZone::UtcOffset(utc_offset) => Some(time.assume_offset(*utc_offset)),
114            TimeZone::Tz(tz) => time.assume_timezone(*tz).take_first(),
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_parse_timezone() {
125        let tz: TimeZone = "UTC+5".parse().unwrap();
126        assert_eq!(
127            tz,
128            TimeZone::UtcOffset(UtcOffset::from_whole_seconds(5 * 3600).unwrap())
129        );
130
131        let tz: TimeZone = "UTC-05:30".parse().unwrap();
132        assert_eq!(
133            tz,
134            TimeZone::UtcOffset(UtcOffset::from_whole_seconds(-(5 * 3600 + 30 * 60)).unwrap())
135        );
136
137        let tz: TimeZone = "UTC-0530".parse().unwrap();
138        assert_eq!(
139            tz,
140            TimeZone::UtcOffset(UtcOffset::from_whole_seconds(-(5 * 3600 + 30 * 60)).unwrap())
141        );
142
143        let tz: TimeZone = "GMT+3".parse().unwrap();
144        assert_eq!(
145            tz,
146            TimeZone::UtcOffset(UtcOffset::from_whole_seconds(3 * 3600).unwrap())
147        );
148
149        let tz: TimeZone = "America/New_York".parse().unwrap();
150        assert_eq!(tz, TimeZone::Tz(time_tz::timezones::db::america::NEW_YORK));
151    }
152
153    #[test]
154    fn test_display() {
155        let tz = TimeZone::UtcOffset(UtcOffset::from_whole_seconds(5 * 3600).unwrap());
156        assert_eq!(tz.to_string(), "UTC+05");
157
158        let tz = TimeZone::UtcOffset(UtcOffset::from_whole_seconds(-(5 * 3600 + 30 * 60)).unwrap());
159        assert_eq!(tz.to_string(), "UTC-05:30");
160
161        let tz = TimeZone::Tz(time_tz::timezones::db::america::NEW_YORK);
162        assert_eq!(tz.to_string(), "America/New_York");
163    }
164}