1use std::fmt::Display;
16use std::str::FromStr;
17
18use chrono::{FixedOffset, TimeZone};
19use chrono_tz::{OffsetComponents, Tz};
20use once_cell::sync::OnceCell;
21use snafu::{OptionExt, ResultExt};
22
23use crate::error::{
24 InvalidTimezoneOffsetSnafu, ParseOffsetStrSnafu, ParseTimezoneNameSnafu, Result,
25};
26use crate::util::find_tz_from_env;
27
28static DEFAULT_TIMEZONE: OnceCell<Timezone> = OnceCell::new();
32
33pub fn set_default_timezone(tz_str: Option<&str>) -> Result<()> {
35 let tz = match tz_str {
36 None | Some("") => Timezone::Named(Tz::UTC),
37 Some(tz) => Timezone::from_tz_string(tz)?,
38 };
39 DEFAULT_TIMEZONE.get_or_init(|| tz);
40 Ok(())
41}
42
43#[inline(always)]
44pub fn get_timezone(tz: Option<&Timezone>) -> &Timezone {
47 tz.unwrap_or_else(|| DEFAULT_TIMEZONE.get().unwrap_or(&Timezone::Named(Tz::UTC)))
48}
49
50#[inline(always)]
51pub fn parse_timezone(tz: Option<&str>) -> Timezone {
54 match tz {
55 None | Some("") => Timezone::Named(Tz::UTC),
56 Some(tz) => Timezone::from_tz_string(tz).unwrap_or(Timezone::Named(Tz::UTC)),
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum Timezone {
62 Offset(FixedOffset),
63 Named(Tz),
64}
65
66impl Timezone {
67 pub fn hours_mins_opt(offset_hours: i32, offset_mins: u32) -> Result<Self> {
70 let offset_secs = if offset_hours > 0 {
71 offset_hours * 3600 + offset_mins as i32 * 60
72 } else {
73 offset_hours * 3600 - offset_mins as i32 * 60
74 };
75
76 FixedOffset::east_opt(offset_secs)
77 .map(Self::Offset)
78 .context(InvalidTimezoneOffsetSnafu {
79 hours: offset_hours,
80 minutes: offset_mins,
81 })
82 }
83
84 pub fn from_tz_string(tz_string: &str) -> Result<Self> {
94 if tz_string.eq_ignore_ascii_case("SYSTEM") {
96 Ok(Timezone::Named(find_tz_from_env().unwrap_or(Tz::UTC)))
97 } else if let Some((hrs, mins)) = tz_string.split_once(':') {
98 let hrs = hrs
99 .parse::<i32>()
100 .context(ParseOffsetStrSnafu { raw: tz_string })?;
101 let mins = mins
102 .parse::<u32>()
103 .context(ParseOffsetStrSnafu { raw: tz_string })?;
104 Self::hours_mins_opt(hrs, mins)
105 } else if let Ok(tz) = Tz::from_str(tz_string) {
106 Ok(Self::Named(tz))
107 } else {
108 ParseTimezoneNameSnafu { raw: tz_string }.fail()
109 }
110 }
111
112 pub fn local_minus_utc(&self) -> i64 {
114 match self {
115 Self::Offset(offset) => offset.local_minus_utc().into(),
116 Self::Named(tz) => {
117 let datetime = chrono::DateTime::from_timestamp(0, 0)
118 .map(|x| x.naive_utc())
119 .expect("invalid timestamp");
120 let datetime = tz.from_utc_datetime(&datetime);
121 let utc_offset = datetime.offset().base_utc_offset();
122 let dst_offset = datetime.offset().dst_offset();
123 let total_offset = utc_offset + dst_offset;
124 total_offset.num_seconds()
125 }
126 }
127 }
128}
129
130impl Display for Timezone {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 match self {
133 Self::Named(tz) => write!(f, "{}", tz.name()),
134 Self::Offset(offset) => write!(f, "{}", offset),
135 }
136 }
137}
138
139#[inline]
140pub fn system_timezone_name() -> String {
142 format!("{}", get_timezone(None))
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_local_minus_utc() {
151 assert_eq!(
152 28800,
153 Timezone::from_tz_string("+8:00").unwrap().local_minus_utc()
154 );
155 assert_eq!(
156 28800,
157 Timezone::from_tz_string("Asia/Shanghai")
158 .unwrap()
159 .local_minus_utc()
160 );
161 assert_eq!(
162 -14400,
163 Timezone::from_tz_string("America/Aruba")
164 .unwrap()
165 .local_minus_utc()
166 );
167
168 assert_eq!(
169 -36000,
170 Timezone::from_tz_string("HST").unwrap().local_minus_utc()
171 );
172 }
173
174 #[test]
175 fn test_from_tz_string() {
176 assert_eq!(
177 Timezone::Named(Tz::UTC),
178 Timezone::from_tz_string("SYSTEM").unwrap()
179 );
180
181 let utc_plus_8 = Timezone::Offset(FixedOffset::east_opt(3600 * 8).unwrap());
182 assert_eq!(utc_plus_8, Timezone::from_tz_string("+8:00").unwrap());
183 assert_eq!(utc_plus_8, Timezone::from_tz_string("+08:00").unwrap());
184 assert_eq!(utc_plus_8, Timezone::from_tz_string("08:00").unwrap());
185
186 let utc_minus_8 = Timezone::Offset(FixedOffset::west_opt(3600 * 8).unwrap());
187 assert_eq!(utc_minus_8, Timezone::from_tz_string("-08:00").unwrap());
188 assert_eq!(utc_minus_8, Timezone::from_tz_string("-8:00").unwrap());
189
190 let utc_minus_8_5 = Timezone::Offset(FixedOffset::west_opt(3600 * 8 + 60 * 30).unwrap());
191 assert_eq!(utc_minus_8_5, Timezone::from_tz_string("-8:30").unwrap());
192
193 let utc_plus_max = Timezone::Offset(FixedOffset::east_opt(3600 * 14).unwrap());
194 assert_eq!(utc_plus_max, Timezone::from_tz_string("14:00").unwrap());
195
196 let utc_minus_max = Timezone::Offset(FixedOffset::west_opt(3600 * 13 + 60 * 59).unwrap());
197 assert_eq!(utc_minus_max, Timezone::from_tz_string("-13:59").unwrap());
198
199 assert_eq!(
200 Timezone::Named(Tz::Asia__Shanghai),
201 Timezone::from_tz_string("Asia/Shanghai").unwrap()
202 );
203 assert_eq!(
204 Timezone::Named(Tz::UTC),
205 Timezone::from_tz_string("UTC").unwrap()
206 );
207
208 assert!(Timezone::from_tz_string("WORLD_PEACE").is_err());
209 assert!(Timezone::from_tz_string("A0:01").is_err());
210 assert!(Timezone::from_tz_string("20:0A").is_err());
211 assert!(Timezone::from_tz_string(":::::").is_err());
212 assert!(Timezone::from_tz_string("Asia/London").is_err());
213 assert!(Timezone::from_tz_string("Unknown").is_err());
214 }
215
216 #[test]
217 fn test_timezone_to_string() {
218 assert_eq!("UTC", Timezone::Named(Tz::UTC).to_string());
219 assert_eq!(
220 "+01:00",
221 Timezone::from_tz_string("01:00").unwrap().to_string()
222 );
223 assert_eq!(
224 "Asia/Shanghai",
225 Timezone::from_tz_string("Asia/Shanghai")
226 .unwrap()
227 .to_string()
228 );
229 }
230}