1use crate::compiler::function::EnumVariant;
2use crate::compiler::prelude::*;
3use regex::Regex;
4use rust_decimal::{Decimal, prelude::ToPrimitive};
5use std::{collections::HashMap, str::FromStr, sync::LazyLock};
6
7static UNIT_ENUM: &[EnumVariant] = &[
8 EnumVariant {
9 value: "ns",
10 description: "Nanoseconds (1 billion nanoseconds in a second)",
11 },
12 EnumVariant {
13 value: "us",
14 description: "Microseconds (1 million microseconds in a second)",
15 },
16 EnumVariant {
17 value: "µs",
18 description: "Microseconds (1 million microseconds in a second)",
19 },
20 EnumVariant {
21 value: "ms",
22 description: "Milliseconds (1 thousand microseconds in a second)",
23 },
24 EnumVariant {
25 value: "cs",
26 description: "Centiseconds (100 centiseconds in a second)",
27 },
28 EnumVariant {
29 value: "ds",
30 description: "Deciseconds (10 deciseconds in a second)",
31 },
32 EnumVariant {
33 value: "s",
34 description: "Seconds",
35 },
36 EnumVariant {
37 value: "m",
38 description: "Minutes (60 seconds in a minute)",
39 },
40 EnumVariant {
41 value: "h",
42 description: "Hours (60 minutes in an hour)",
43 },
44 EnumVariant {
45 value: "d",
46 description: "Days (24 hours in a day)",
47 },
48];
49
50static PARAMETERS: &[Parameter] = &[
51 Parameter::required("value", kind::BYTES, "The string of the duration."),
52 Parameter::required("unit", kind::BYTES, "The output units for the duration.")
53 .enum_variants(UNIT_ENUM),
54];
55
56fn parse_duration(bytes: &Value, unit: &Value) -> Resolved {
57 let value = bytes.try_bytes_utf8_lossy()?;
58 let mut value = &value[..];
59 let conversion_factor = {
60 let string = unit.try_bytes_utf8_lossy()?;
61
62 UNITS
63 .get(string.as_ref())
64 .ok_or(format!("unknown unit format: '{string}'"))?
65 };
66 let mut num = 0.0;
67 while !value.is_empty() {
68 let captures = RE
69 .captures(value)
70 .ok_or(format!("unable to parse duration: '{value}'"))?;
71 let capture_match = captures.get(0).unwrap();
72
73 let value_decimal = Decimal::from_str(&captures["value"])
74 .map_err(|error| format!("unable to parse number: {error}"))?;
75 let unit = UNITS
76 .get(&captures["unit"])
77 .ok_or(format!("unknown duration unit: '{}'", &captures["unit"]))?;
78 let number = value_decimal
79 .checked_mul(*unit)
80 .and_then(|v| v.checked_div(*conversion_factor))
81 .ok_or(format!("unable to convert duration: '{value}'"))?;
82 let number = number
83 .to_f64()
84 .ok_or(format!("unable to format duration: '{number}'"))?;
85 num += number;
86 value = &value[capture_match.end()..];
87 }
88 Ok(Value::from_f64_or_zero(num))
89}
90
91static RE: LazyLock<Regex> = LazyLock::new(|| {
92 Regex::new(
93 r"(?ix) # i: case-insensitive, x: ignore whitespace + comments
94 (?P<value>[0-9]*\.?[0-9]+) # value: integer or float
95 \s? # optional space between value and unit
96 (?P<unit>[µa-z]{1,2}) # unit: one or two letters",
97 )
98 .unwrap()
99});
100
101static UNITS: LazyLock<HashMap<String, Decimal>> = LazyLock::new(|| {
102 vec![
103 ("ns", Decimal::new(1, 9)),
104 ("us", Decimal::new(1, 6)),
105 ("µs", Decimal::new(1, 6)),
106 ("ms", Decimal::new(1, 3)),
107 ("cs", Decimal::new(1, 2)),
108 ("ds", Decimal::new(1, 1)),
109 ("s", Decimal::new(1, 0)),
110 ("m", Decimal::new(60, 0)),
111 ("h", Decimal::new(3_600, 0)),
112 ("d", Decimal::new(86_400, 0)),
113 ("w", Decimal::new(604_800, 0)),
114 ]
115 .into_iter()
116 .map(|(k, v)| (k.to_owned(), v))
117 .collect()
118});
119
120#[derive(Clone, Copy, Debug)]
121pub struct ParseDuration;
122
123impl Function for ParseDuration {
124 fn identifier(&self) -> &'static str {
125 "parse_duration"
126 }
127
128 fn usage(&self) -> &'static str {
129 "Parses the `value` into a human-readable duration format specified by `unit`."
130 }
131
132 fn category(&self) -> &'static str {
133 Category::Parse.as_ref()
134 }
135
136 fn internal_failure_reasons(&self) -> &'static [&'static str] {
137 &["`value` is not a properly formatted duration."]
138 }
139
140 fn return_kind(&self) -> u16 {
141 kind::FLOAT
142 }
143
144 fn examples(&self) -> &'static [Example] {
145 &[
146 example! {
147 title: "Parse duration (milliseconds)",
148 source: r#"parse_duration!("1005ms", unit: "s")"#,
149 result: Ok("1.005"),
150 },
151 example! {
152 title: "Parse multiple durations (seconds & milliseconds)",
153 source: r#"parse_duration!("1s 1ms", unit: "ms")"#,
154 result: Ok("1001.0"),
155 },
156 ]
157 }
158
159 fn compile(
160 &self,
161 _state: &state::TypeState,
162 _ctx: &mut FunctionCompileContext,
163 arguments: ArgumentList,
164 ) -> Compiled {
165 let value = arguments.required("value");
166 let unit = arguments.required("unit");
167
168 Ok(ParseDurationFn { value, unit }.as_expr())
169 }
170
171 fn parameters(&self) -> &'static [Parameter] {
172 PARAMETERS
173 }
174}
175
176#[derive(Debug, Clone)]
177struct ParseDurationFn {
178 value: Box<dyn Expression>,
179 unit: Box<dyn Expression>,
180}
181
182impl FunctionExpression for ParseDurationFn {
183 fn resolve(&self, ctx: &mut Context) -> Resolved {
184 let bytes = self.value.resolve(ctx)?;
185 let unit = self.unit.resolve(ctx)?;
186
187 parse_duration(&bytes, &unit)
188 }
189
190 fn type_def(&self, _: &state::TypeState) -> TypeDef {
191 TypeDef::float().fallible()
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::value;
199
200 test_function![
201 parse_duration => ParseDuration;
202
203 s_m {
204 args: func_args![value: "30s",
205 unit: "m"],
206 want: Ok(value!(0.5)),
207 tdef: TypeDef::float().fallible(),
208 }
209
210 ms_ms {
211 args: func_args![value: "100ms",
212 unit: "ms"],
213 want: Ok(100.0),
214 tdef: TypeDef::float().fallible(),
215 }
216
217 ms_s {
218 args: func_args![value: "1005ms",
219 unit: "s"],
220 want: Ok(1.005),
221 tdef: TypeDef::float().fallible(),
222 }
223
224 ns_ms {
225 args: func_args![value: "100ns",
226 unit: "ms"],
227 want: Ok(0.0001),
228 tdef: TypeDef::float().fallible(),
229 }
230
231 us_ms {
232 args: func_args![value: "100µs",
233 unit: "ms"],
234 want: Ok(0.1),
235 tdef: TypeDef::float().fallible(),
236 }
237
238 d_s {
239 args: func_args![value: "1d",
240 unit: "s"],
241 want: Ok(86400.0),
242 tdef: TypeDef::float().fallible(),
243 }
244
245 ds_s {
246 args: func_args![value: "1d1s",
247 unit: "s"],
248 want: Ok(86401.0),
249 tdef: TypeDef::float().fallible(),
250 }
251
252 s_space_ms_ms {
253 args: func_args![value: "1s 1ms",
254 unit: "ms"],
255 want: Ok(1001.0),
256 tdef: TypeDef::float().fallible(),
257 }
258
259 ms_space_us_ms {
260 args: func_args![value: "1ms1 µs",
261 unit: "ms"],
262 want: Ok(1.001),
263 tdef: TypeDef::float().fallible(),
264 }
265
266 s_space_m_ms_order_agnostic {
267 args: func_args![value: "1s1m",
268 unit: "ms"],
269 want: Ok(61000.0),
270 tdef: TypeDef::float().fallible(),
271 }
272
273 s_ns {
274 args: func_args![value: "1 s",
275 unit: "ns"],
276 want: Ok(1_000_000_000.0),
277 tdef: TypeDef::float().fallible(),
278 }
279
280 us_space_ms {
281 args: func_args![value: "1 µs",
282 unit: "ms"],
283 want: Ok(0.001),
284 tdef: TypeDef::float().fallible(),
285 }
286
287 w_ns {
288 args: func_args![value: "1w",
289 unit: "ns"],
290 want: Ok(604_800_000_000_000.0),
291 tdef: TypeDef::float().fallible(),
292 }
293
294 w_d {
295 args: func_args![value: "1.1w",
296 unit: "d"],
297 want: Ok(7.7),
298 tdef: TypeDef::float().fallible(),
299 }
300
301 d_w {
302 args: func_args![value: "8d",
303 unit: "w"],
304 want: Ok(1.142_857_142_857_142_8),
305 tdef: TypeDef::float().fallible(),
306 }
307
308 d_w_2 {
309 args: func_args![value: "30d",
310 unit: "w"],
311 want: Ok(4.285_714_285_714_286),
312 tdef: TypeDef::float().fallible(),
313 }
314
315 d_s_2 {
316 args: func_args![value: "8d",
317 unit: "s"],
318 want: Ok(691_200.0),
319 tdef: TypeDef::float().fallible(),
320 }
321
322 decimal_s_ms {
323 args: func_args![value: "12.3s",
324 unit: "ms"],
325 want: Ok(12300.0),
326 tdef: TypeDef::float().fallible(),
327 }
328
329 decimal_s_ms_2 {
330 args: func_args![value: "123.0s",
331 unit: "ms"],
332 want: Ok(123_000.0),
333 tdef: TypeDef::float().fallible(),
334 }
335
336 decimal_h_s_ms {
337 args: func_args![value: "1h12.3s",
338 unit: "ms"],
339 want: Ok(3_612_300.0),
340 tdef: TypeDef::float().fallible(),
341 }
342
343 decimal_d_s_s {
344 args: func_args![value: "1.1d12.3s",
345 unit: "s"],
346 want: Ok(95052.3),
347 tdef: TypeDef::float().fallible(),
348 }
349
350 error_invalid {
351 args: func_args![value: "foo",
352 unit: "ms"],
353 want: Err("unable to parse duration: 'foo'"),
354 tdef: TypeDef::float().fallible(),
355 }
356
357 error_ns {
358 args: func_args![value: "1",
359 unit: "ns"],
360 want: Err("unable to parse duration: '1'"),
361 tdef: TypeDef::float().fallible(),
362 }
363 error_overflow {
364 args: func_args![value: "1234567890123456789012345d",
365 unit: "s"],
366 want: Err("unable to convert duration: '1234567890123456789012345d'"),
367 tdef: TypeDef::float().fallible(),
368 }
369
370 s_w {
371 args: func_args![value: "1s",
372 unit: "w"],
373 want: Ok(0.000_001_653_439_153_439_153_5),
374 tdef: TypeDef::float().fallible(),
375 }
376
377 error_failed_2nd_unit {
378 args: func_args![value: "1d foo",
379 unit: "s"],
380 want: Err("unable to parse duration: ' foo'"),
381 tdef: TypeDef::float().fallible(),
382 }
383 ];
384}