vrl/stdlib/
parse_duration.rs

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}