1use 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
54pub 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(|(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
104pub 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 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 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 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 #[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}