Skip to main content

common_time/
timezone.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
27/// System timezone in `frontend`/`standalone`,
28/// config by option `default_timezone` in toml,
29/// default value is `UTC` when `default_timezone` is not set.
30static DEFAULT_TIMEZONE: OnceCell<Timezone> = OnceCell::new();
31
32// Set the System timezone by `tz_str`
33pub 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)]
43/// If the `tz=Some(timezone)`, return `timezone` directly,
44/// or return current system timezone.
45pub 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)]
50/// If the `tz = Some("") || None || Some(Invalid timezone)`, return system timezone,
51/// or return parsed `tz` as timezone.
52pub 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    /// Compute timezone from given offset hours and minutes
67    /// Return `Err` if given offset exceeds scope
68    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    /// Parse timezone offset string and return None if given offset exceeds
84    /// scope.
85    ///
86    /// String examples are available as described in
87    /// <https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html>
88    ///
89    /// - `SYSTEM`
90    /// - Offset to UTC: `+08:00` , `-11:30`
91    /// - Named zones: `Asia/Shanghai`, `Europe/Berlin`
92    pub fn from_tz_string(tz_string: &str) -> Result<Self> {
93        // Use system timezone
94        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    /// A named zone merely sitting at +00:00 today is not UTC: it may have been
112    /// elsewhere at the timestamp being converted.
113    pub fn is_utc(&self) -> bool {
114        match self {
115            Self::Offset(offset) => offset.local_minus_utc() == 0,
116            Self::Named(tz) => matches!(tz, Tz::UTC),
117        }
118    }
119
120    /// Returns the number of seconds to add to convert from UTC to the local time.
121    pub fn local_minus_utc(&self) -> i64 {
122        match self {
123            Self::Offset(offset) => offset.local_minus_utc().into(),
124            Self::Named(tz) => {
125                let datetime = chrono::DateTime::from_timestamp(0, 0)
126                    .map(|x| x.naive_utc())
127                    .expect("invalid timestamp");
128                let datetime = tz.from_utc_datetime(&datetime);
129                let utc_offset = datetime.offset().base_utc_offset();
130                let dst_offset = datetime.offset().dst_offset();
131                let total_offset = utc_offset + dst_offset;
132                total_offset.num_seconds()
133            }
134        }
135    }
136}
137
138impl Display for Timezone {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            Self::Named(tz) => write!(f, "{}", tz.name()),
142            Self::Offset(offset) => write!(f, "{}", offset),
143        }
144    }
145}
146
147#[inline]
148/// Return current system config timezone, default config is UTC
149pub fn system_timezone_name() -> String {
150    format!("{}", get_timezone(None))
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_local_minus_utc() {
159        assert_eq!(
160            28800,
161            Timezone::from_tz_string("+8:00").unwrap().local_minus_utc()
162        );
163        assert_eq!(
164            28800,
165            Timezone::from_tz_string("Asia/Shanghai")
166                .unwrap()
167                .local_minus_utc()
168        );
169        assert_eq!(
170            -14400,
171            Timezone::from_tz_string("America/Aruba")
172                .unwrap()
173                .local_minus_utc()
174        );
175
176        assert_eq!(
177            -36000,
178            Timezone::from_tz_string("HST").unwrap().local_minus_utc()
179        );
180    }
181
182    #[test]
183    fn test_from_tz_string() {
184        unsafe {
185            std::env::remove_var("TZ");
186        }
187        assert_eq!(
188            Timezone::Named(Tz::UTC),
189            Timezone::from_tz_string("SYSTEM").unwrap()
190        );
191
192        let utc_plus_8 = Timezone::Offset(FixedOffset::east_opt(3600 * 8).unwrap());
193        assert_eq!(utc_plus_8, Timezone::from_tz_string("+8:00").unwrap());
194        assert_eq!(utc_plus_8, Timezone::from_tz_string("+08:00").unwrap());
195        assert_eq!(utc_plus_8, Timezone::from_tz_string("08:00").unwrap());
196
197        let utc_minus_8 = Timezone::Offset(FixedOffset::west_opt(3600 * 8).unwrap());
198        assert_eq!(utc_minus_8, Timezone::from_tz_string("-08:00").unwrap());
199        assert_eq!(utc_minus_8, Timezone::from_tz_string("-8:00").unwrap());
200
201        let utc_minus_8_5 = Timezone::Offset(FixedOffset::west_opt(3600 * 8 + 60 * 30).unwrap());
202        assert_eq!(utc_minus_8_5, Timezone::from_tz_string("-8:30").unwrap());
203
204        let utc_plus_max = Timezone::Offset(FixedOffset::east_opt(3600 * 14).unwrap());
205        assert_eq!(utc_plus_max, Timezone::from_tz_string("14:00").unwrap());
206
207        let utc_minus_max = Timezone::Offset(FixedOffset::west_opt(3600 * 13 + 60 * 59).unwrap());
208        assert_eq!(utc_minus_max, Timezone::from_tz_string("-13:59").unwrap());
209
210        assert_eq!(
211            Timezone::Named(Tz::Asia__Shanghai),
212            Timezone::from_tz_string("Asia/Shanghai").unwrap()
213        );
214        assert_eq!(
215            Timezone::Named(Tz::Asia__Shanghai),
216            Timezone::from_tz_string("Asia/ShangHai").unwrap()
217        );
218        assert_eq!(
219            Timezone::Named(Tz::UTC),
220            Timezone::from_tz_string("UTC").unwrap()
221        );
222
223        assert!(Timezone::from_tz_string("WORLD_PEACE").is_err());
224        assert!(Timezone::from_tz_string("A0:01").is_err());
225        assert!(Timezone::from_tz_string("20:0A").is_err());
226        assert!(Timezone::from_tz_string(":::::").is_err());
227        assert!(Timezone::from_tz_string("Asia/London").is_err());
228        assert!(Timezone::from_tz_string("Unknown").is_err());
229    }
230
231    #[test]
232    fn test_timezone_to_string() {
233        assert_eq!("UTC", Timezone::Named(Tz::UTC).to_string());
234        assert_eq!(
235            "+01:00",
236            Timezone::from_tz_string("01:00").unwrap().to_string()
237        );
238        assert_eq!(
239            "Asia/Shanghai",
240            Timezone::from_tz_string("Asia/Shanghai")
241                .unwrap()
242                .to_string()
243        );
244    }
245}