1use std::{fmt, str::FromStr};
2
3use navi_types::Tz;
4use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
5use time_tz::{OffsetDateTimeExt, PrimitiveDateTimeExt};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TimeZone {
10 UtcOffset(UtcOffset),
12 Named(Tz),
14}
15
16#[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}