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;
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
28/// System timezone in `frontend`/`standalone`,
29/// config by option `default_timezone` in toml,
30/// default value is `UTC` when `default_timezone` is not set.
31static DEFAULT_TIMEZONE: OnceCell<Timezone> = OnceCell::new();
32
33// Set the System timezone by `tz_str`
34pub 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)]
44/// If the `tz=Some(timezone)`, return `timezone` directly,
45/// or return current system timezone.
46pub 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)]
51/// If the `tz = Some("") || None || Some(Invalid timezone)`, return system timezone,
52/// or return parsed `tz` as timezone.
53pub 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    /// Compute timezone from given offset hours and minutes
68    /// Return `Err` if given offset exceeds scope
69    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    /// Parse timezone offset string and return None if given offset exceeds
85    /// scope.
86    ///
87    /// String examples are available as described in
88    /// <https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html>
89    ///
90    /// - `SYSTEM`
91    /// - Offset to UTC: `+08:00` , `-11:30`
92    /// - Named zones: `Asia/Shanghai`, `Europe/Berlin`
93    pub fn from_tz_string(tz_string: &str) -> Result<Self> {
94        // Use system timezone
95        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    /// Returns the number of seconds to add to convert from UTC to the local time.
113    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]
140/// Return current system config timezone, default config is UTC
141pub 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}