openpine_vm/time/
timezone.rs1use 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#[derive(Debug, PartialEq, Eq)]
16pub enum TimeZone {
17 UtcOffset(UtcOffset),
19 Tz(&'static Tz),
21}
22
23#[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}