Skip to main content

navi_vm\time/
timezone.rs

1use std::{fmt, str::FromStr};
2
3use navi_types::Tz;
4use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
5use time_tz::{OffsetDateTimeExt, PrimitiveDateTimeExt};
6
7/// A parsed timezone representation.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TimeZone {
10    /// Time zone specified as an explicit UTC offset.
11    UtcOffset(UtcOffset),
12    /// Time zone specified by one of the supported market IANA names.
13    Named(Tz),
14}
15
16/// Error returned when parsing a [`TimeZone`] fails.
17#[derive(Debug, thiserror::Error)]
18#[error("failed to parse timezone")]
19#[non_exhaustive]
20pub struct ParseTimeZoneError;
21
22impl FromStr for TimeZone {
23    type Err = ParseTimeZoneError;
24
25    fn from_str(s: &str) -> Result<Self, Self::Err> {
26        if let Some(offset) = parse_utc_offset(s) {
27            Ok(TimeZone::UtcOffset(offset))
28        } else if let Ok(tz) = s.parse::<Tz>() {
29            Ok(TimeZone::Named(tz))
30        } else {
31            Err(ParseTimeZoneError)
32        }
33    }
34}
35
36fn parse_utc_offset(s: &str) -> Option<UtcOffset> {
37    let s = s.trim();
38    let rest = s.strip_prefix("UTC").or_else(|| s.strip_prefix("GMT"))?;
39    let (sign, rest) = if let Some(r) = rest.strip_prefix('+') {
40        (1i32, r)
41    } else if let Some(r) = rest.strip_prefix('-') {
42        (-1i32, r)
43    } else {
44        return None;
45    };
46    let seconds = match rest.len() {
47        4 if rest.bytes().all(|b| b.is_ascii_digit()) => {
48            let h: i32 = rest[..2].parse().ok()?;
49            let m: i32 = rest[2..].parse().ok()?;
50            h * 3600 + m * 60
51        }
52        5 if rest.as_bytes().get(2) == Some(&b':') => {
53            let h: i32 = rest[..2].parse().ok()?;
54            let m: i32 = rest[3..].parse().ok()?;
55            h * 3600 + m * 60
56        }
57        1 if rest.as_bytes()[0].is_ascii_digit() => {
58            let h: i32 = rest.parse().ok()?;
59            h * 3600
60        }
61        _ => return None,
62    };
63    UtcOffset::from_whole_seconds(sign * seconds).ok()
64}
65
66impl fmt::Display for TimeZone {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            TimeZone::UtcOffset(offset) => {
70                let minutes = offset.whole_minutes();
71                let (sign, abs_minutes) = if minutes < 0 {
72                    ('-', -minutes)
73                } else {
74                    ('+', minutes)
75                };
76                let hours = abs_minutes / 60;
77                let minutes = abs_minutes % 60;
78                if minutes == 0 {
79                    write!(f, "UTC{}{:02}", sign, hours)
80                } else {
81                    write!(f, "UTC{}{:02}:{:02}", sign, hours, minutes)
82                }
83            }
84            TimeZone::Named(tz) => write!(f, "{tz}"),
85        }
86    }
87}
88
89impl TimeZone {
90    #[inline]
91    pub(crate) fn with(&self, time: OffsetDateTime) -> OffsetDateTime {
92        match self {
93            TimeZone::UtcOffset(utc_offset) => time.to_offset(*utc_offset),
94            TimeZone::Named(tz) => time.to_timezone(tz),
95        }
96    }
97
98    #[inline]
99    pub(crate) fn assume(&self, time: PrimitiveDateTime) -> Option<OffsetDateTime> {
100        match self {
101            TimeZone::UtcOffset(utc_offset) => Some(time.assume_offset(*utc_offset)),
102            TimeZone::Named(tz) => time.assume_timezone(tz).take_first(),
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_parse_timezone() {
113        let tz: TimeZone = "UTC+5".parse().unwrap();
114        assert_eq!(
115            tz,
116            TimeZone::UtcOffset(UtcOffset::from_whole_seconds(5 * 3600).unwrap())
117        );
118
119        let tz: TimeZone = "UTC-05:30".parse().unwrap();
120        assert_eq!(
121            tz,
122            TimeZone::UtcOffset(UtcOffset::from_whole_seconds(-(5 * 3600 + 30 * 60)).unwrap())
123        );
124
125        let tz: TimeZone = "UTC-0530".parse().unwrap();
126        assert_eq!(
127            tz,
128            TimeZone::UtcOffset(UtcOffset::from_whole_seconds(-(5 * 3600 + 30 * 60)).unwrap())
129        );
130
131        let tz: TimeZone = "GMT+3".parse().unwrap();
132        assert_eq!(
133            tz,
134            TimeZone::UtcOffset(UtcOffset::from_whole_seconds(3 * 3600).unwrap())
135        );
136
137        let tz: TimeZone = "America/New_York".parse().unwrap();
138        assert_eq!(tz, TimeZone::Named(Tz::NewYork));
139    }
140
141    #[test]
142    fn test_display() {
143        let tz = TimeZone::UtcOffset(UtcOffset::from_whole_seconds(5 * 3600).unwrap());
144        assert_eq!(tz.to_string(), "UTC+05");
145
146        let tz = TimeZone::UtcOffset(UtcOffset::from_whole_seconds(-(5 * 3600 + 30 * 60)).unwrap());
147        assert_eq!(tz.to_string(), "UTC-05:30");
148
149        let tz = TimeZone::Named(Tz::NewYork);
150        assert_eq!(tz.to_string(), "America/New_York");
151    }
152}