simple_test/util/
duration.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 lazy_static::lazy_static;
16use regex::Regex;
17use std::fmt::Write;
18use std::time::Duration;
19
20lazy_static! {
21    static ref DURATION_RE: Regex = Regex::new(
22        r"(?x)
23^
24((?P<y>[0-9]+)y)?
25((?P<w>[0-9]+)w)?
26((?P<d>[0-9]+)d)?
27((?P<h>[0-9]+)h)?
28((?P<m>[0-9]+)m)?
29((?P<s>[0-9]+)s)?
30((?P<ms>[0-9]+)ms)?
31$",
32    )
33    .unwrap();
34}
35
36pub const MILLI_DURATION: Duration = Duration::from_millis(1);
37pub const SECOND_DURATION: Duration = Duration::from_secs(1);
38pub const MINUTE_DURATION: Duration = Duration::from_secs(60);
39pub const HOUR_DURATION: Duration = Duration::from_secs(60 * 60);
40pub const DAY_DURATION: Duration = Duration::from_secs(60 * 60 * 24);
41pub const WEEK_DURATION: Duration = Duration::from_secs(60 * 60 * 24 * 7);
42pub const YEAR_DURATION: Duration = Duration::from_secs(60 * 60 * 24 * 365);
43
44const ALL_CAPS: [(&str, Duration); 7] = [
45    ("y", YEAR_DURATION),
46    ("w", WEEK_DURATION),
47    ("d", DAY_DURATION),
48    ("h", HOUR_DURATION),
49    ("m", MINUTE_DURATION),
50    ("s", SECOND_DURATION),
51    ("ms", MILLI_DURATION),
52];
53
54/// parses a string into a Duration, assuming that a year
55/// always has 365d, a week always has 7d, and a day always has 24h.
56///
57/// # Examples
58///
59/// Basic usage:
60///
61/// ```
62/// use std::time::Duration;
63/// use promql_parser::util;
64///
65/// assert_eq!(util::parse_duration("1h").unwrap(), Duration::from_secs(3600));
66/// assert_eq!(util::parse_duration("4d").unwrap(), Duration::from_secs(3600 * 24 * 4));
67/// assert_eq!(util::parse_duration("4d1h").unwrap(), Duration::from_secs(3600 * 97));
68/// ```
69pub fn parse_duration(ds: &str) -> Result<Duration, String> {
70    if ds.is_empty() {
71        return Err("empty duration string".into());
72    }
73
74    if ds == "0" {
75        return Err("duration must be greater than 0".into());
76    }
77
78    if !DURATION_RE.is_match(ds) {
79        return Err(format!("not a valid duration string: {ds}"));
80    }
81
82    let caps = DURATION_RE.captures(ds).unwrap();
83    let dur = ALL_CAPS
84        .into_iter()
85        // map captured string to Option<Duration> iterator
86        // FIXME: None is ignored in closure. It is better to tell users which part is wrong.
87        .map(|(title, duration)| {
88            caps.name(title)
89                .and_then(|cap| cap.as_str().parse::<u32>().ok())
90                .and_then(|v| duration.checked_mul(v))
91        })
92        .try_fold(Duration::ZERO, |acc, x| {
93            acc.checked_add(x.unwrap_or(Duration::ZERO))
94                .ok_or_else(|| "duration overflowed".into())
95        });
96
97    if matches!(dur, Ok(d) if d == Duration::ZERO) {
98        Err("duration must be greater than 0".into())
99    } else {
100        dur
101    }
102}
103
104/// display Duration in Prometheus format
105pub fn display_duration(duration: &Duration) -> String {
106    if duration.is_zero() {
107        return "0s".into();
108    }
109    let mut ms = duration.as_millis();
110    let mut ss = String::new();
111
112    let mut f = |unit: &str, mult: u128, exact: bool| {
113        if exact && ms % mult != 0 {
114            return;
115        }
116
117        let v = ms / mult;
118        if v > 0 {
119            write!(ss, "{v}{unit}").unwrap();
120            ms -= v * mult
121        }
122    };
123
124    // Only format years and weeks if the remainder is zero, as it is often
125    // easier to read 90d than 12w6d.
126    f("y", 1000 * 60 * 60 * 24 * 365, true);
127    f("w", 1000 * 60 * 60 * 24 * 7, true);
128
129    f("d", 1000 * 60 * 60 * 24, false);
130    f("h", 1000 * 60 * 60, false);
131    f("m", 1000 * 60, false);
132    f("s", 1000, false);
133    f("ms", 1, false);
134
135    ss
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_re() {
144        // valid regex
145        let res = vec![
146            "1y", "2w", "3d", "4h", "5m", "6s", "7ms", "1y2w3d", "4h30m", "3600ms",
147        ];
148        for re in res {
149            assert!(DURATION_RE.is_match(re), "{} failed.", re)
150        }
151
152        // invalid regex
153        let res = vec!["1", "1y1m1d", "-1w", "1.5d", "d"];
154        for re in res {
155            assert!(!DURATION_RE.is_match(re), "{} failed.", re)
156        }
157    }
158
159    #[test]
160    fn test_valid_duration() {
161        let ds = vec![
162            ("324ms", Duration::from_millis(324)),
163            ("3s", Duration::from_secs(3)),
164            ("5m", MINUTE_DURATION * 5),
165            ("1h", HOUR_DURATION),
166            ("4d", DAY_DURATION * 4),
167            ("4d1h", DAY_DURATION * 4 + HOUR_DURATION),
168            ("14d", DAY_DURATION * 14),
169            ("3w", WEEK_DURATION * 3),
170            ("3w2d1h", WEEK_DURATION * 3 + HOUR_DURATION * 49),
171            ("10y", YEAR_DURATION * 10),
172        ];
173
174        for (s, expect) in ds {
175            let d = parse_duration(s);
176            assert!(d.is_ok());
177            assert_eq!(expect, d.unwrap(), "{} and {:?} not matched", s, expect);
178        }
179    }
180
181    // valid here but invalid in PromQL Go Version
182    #[test]
183    fn test_diff_with_promql() {
184        let ds = vec![
185            ("294y", YEAR_DURATION * 294),
186            ("200y10400w", YEAR_DURATION * 200 + WEEK_DURATION * 10400),
187            ("107675d", DAY_DURATION * 107675),
188            ("2584200h", HOUR_DURATION * 2584200),
189        ];
190
191        for (s, expect) in ds {
192            let d = parse_duration(s);
193            assert!(d.is_ok());
194            assert_eq!(expect, d.unwrap(), "{} and {:?} not matched", s, expect);
195        }
196    }
197
198    #[test]
199    fn test_invalid_duration() {
200        let ds = vec!["1", "1y1m1d", "-1w", "1.5d", "d", "", "0", "0w", "0s"];
201        for d in ds {
202            assert!(parse_duration(d).is_err(), "{} is invalid duration!", d);
203        }
204    }
205
206    #[test]
207    fn test_display_duration() {
208        let ds = vec![
209            (Duration::ZERO, "0s"),
210            (Duration::from_millis(324), "324ms"),
211            (Duration::from_secs(3), "3s"),
212            (MINUTE_DURATION * 5, "5m"),
213            (MINUTE_DURATION * 5 + MILLI_DURATION * 500, "5m500ms"),
214            (HOUR_DURATION, "1h"),
215            (DAY_DURATION * 4, "4d"),
216            (DAY_DURATION * 4 + HOUR_DURATION, "4d1h"),
217            (
218                DAY_DURATION * 4 + HOUR_DURATION * 2 + MINUTE_DURATION * 10,
219                "4d2h10m",
220            ),
221            (DAY_DURATION * 14, "2w"),
222            (WEEK_DURATION * 3, "3w"),
223            (WEEK_DURATION * 3 + HOUR_DURATION * 49, "23d1h"),
224            (YEAR_DURATION * 10, "10y"),
225        ];
226
227        for (d, expect) in ds {
228            let s = display_duration(&d);
229            assert_eq!(expect, s, "{} and {:?} not matched", s, expect);
230        }
231    }
232}