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