vrl 0.32.0

Vector Remap Language
Documentation
use crate::compiler::function::EnumVariant;
use crate::compiler::prelude::*;
use regex::Regex;
use rust_decimal::{Decimal, prelude::ToPrimitive};
use std::{collections::HashMap, str::FromStr, sync::LazyLock};

static UNIT_ENUM: &[EnumVariant] = &[
    EnumVariant {
        value: "ns",
        description: "Nanoseconds (1 billion nanoseconds in a second)",
    },
    EnumVariant {
        value: "us",
        description: "Microseconds (1 million microseconds in a second)",
    },
    EnumVariant {
        value: "µs",
        description: "Microseconds (1 million microseconds in a second)",
    },
    EnumVariant {
        value: "ms",
        description: "Milliseconds (1 thousand microseconds in a second)",
    },
    EnumVariant {
        value: "cs",
        description: "Centiseconds (100 centiseconds in a second)",
    },
    EnumVariant {
        value: "ds",
        description: "Deciseconds (10 deciseconds in a second)",
    },
    EnumVariant {
        value: "s",
        description: "Seconds",
    },
    EnumVariant {
        value: "m",
        description: "Minutes (60 seconds in a minute)",
    },
    EnumVariant {
        value: "h",
        description: "Hours (60 minutes in an hour)",
    },
    EnumVariant {
        value: "d",
        description: "Days (24 hours in a day)",
    },
];

static PARAMETERS: &[Parameter] = &[
    Parameter::required("value", kind::BYTES, "The string of the duration."),
    Parameter::required("unit", kind::BYTES, "The output units for the duration.")
        .enum_variants(UNIT_ENUM),
];

fn parse_duration(bytes: &Value, unit: &Value) -> Resolved {
    let value = bytes.try_bytes_utf8_lossy()?;
    let mut value = &value[..];
    let conversion_factor = {
        let string = unit.try_bytes_utf8_lossy()?;

        UNITS
            .get(string.as_ref())
            .ok_or(format!("unknown unit format: '{string}'"))?
    };
    let mut num = 0.0;
    while !value.is_empty() {
        let captures = RE
            .captures(value)
            .ok_or(format!("unable to parse duration: '{value}'"))?;
        let capture_match = captures.get(0).unwrap();

        let value_decimal = Decimal::from_str(&captures["value"])
            .map_err(|error| format!("unable to parse number: {error}"))?;
        let unit = UNITS
            .get(&captures["unit"])
            .ok_or(format!("unknown duration unit: '{}'", &captures["unit"]))?;
        let number = value_decimal
            .checked_mul(*unit)
            .and_then(|v| v.checked_div(*conversion_factor))
            .ok_or(format!("unable to convert duration: '{value}'"))?;
        let number = number
            .to_f64()
            .ok_or(format!("unable to format duration: '{number}'"))?;
        num += number;
        value = &value[capture_match.end()..];
    }
    Ok(Value::from_f64_or_zero(num))
}

static RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        r"(?ix)                        # i: case-insensitive, x: ignore whitespace + comments
            (?P<value>[0-9]*\.?[0-9]+) # value: integer or float
            \s?                        # optional space between value and unit
            (?P<unit>[µa-z]{1,2})      # unit: one or two letters",
    )
    .unwrap()
});

static UNITS: LazyLock<HashMap<String, Decimal>> = LazyLock::new(|| {
    vec![
        ("ns", Decimal::new(1, 9)),
        ("us", Decimal::new(1, 6)),
        ("µs", Decimal::new(1, 6)),
        ("ms", Decimal::new(1, 3)),
        ("cs", Decimal::new(1, 2)),
        ("ds", Decimal::new(1, 1)),
        ("s", Decimal::new(1, 0)),
        ("m", Decimal::new(60, 0)),
        ("h", Decimal::new(3_600, 0)),
        ("d", Decimal::new(86_400, 0)),
        ("w", Decimal::new(604_800, 0)),
    ]
    .into_iter()
    .map(|(k, v)| (k.to_owned(), v))
    .collect()
});

#[derive(Clone, Copy, Debug)]
pub struct ParseDuration;

impl Function for ParseDuration {
    fn identifier(&self) -> &'static str {
        "parse_duration"
    }

    fn usage(&self) -> &'static str {
        "Parses the `value` into a human-readable duration format specified by `unit`."
    }

    fn category(&self) -> &'static str {
        Category::Parse.as_ref()
    }

    fn internal_failure_reasons(&self) -> &'static [&'static str] {
        &["`value` is not a properly formatted duration."]
    }

    fn return_kind(&self) -> u16 {
        kind::FLOAT
    }

    fn examples(&self) -> &'static [Example] {
        &[
            example! {
                title: "Parse duration (milliseconds)",
                source: r#"parse_duration!("1005ms", unit: "s")"#,
                result: Ok("1.005"),
            },
            example! {
                title: "Parse multiple durations (seconds & milliseconds)",
                source: r#"parse_duration!("1s 1ms", unit: "ms")"#,
                result: Ok("1001.0"),
            },
        ]
    }

    fn compile(
        &self,
        _state: &state::TypeState,
        _ctx: &mut FunctionCompileContext,
        arguments: ArgumentList,
    ) -> Compiled {
        let value = arguments.required("value");
        let unit = arguments.required("unit");

        Ok(ParseDurationFn { value, unit }.as_expr())
    }

    fn parameters(&self) -> &'static [Parameter] {
        PARAMETERS
    }
}

#[derive(Debug, Clone)]
struct ParseDurationFn {
    value: Box<dyn Expression>,
    unit: Box<dyn Expression>,
}

impl FunctionExpression for ParseDurationFn {
    fn resolve(&self, ctx: &mut Context) -> Resolved {
        let bytes = self.value.resolve(ctx)?;
        let unit = self.unit.resolve(ctx)?;

        parse_duration(&bytes, &unit)
    }

    fn type_def(&self, _: &state::TypeState) -> TypeDef {
        TypeDef::float().fallible()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::value;

    test_function![
        parse_duration => ParseDuration;

        s_m {
            args: func_args![value: "30s",
                             unit: "m"],
            want: Ok(value!(0.5)),
            tdef: TypeDef::float().fallible(),
        }

        ms_ms {
            args: func_args![value: "100ms",
                             unit: "ms"],
            want: Ok(100.0),
            tdef: TypeDef::float().fallible(),
        }

        ms_s {
            args: func_args![value: "1005ms",
                             unit: "s"],
            want: Ok(1.005),
            tdef: TypeDef::float().fallible(),
        }

        ns_ms {
            args: func_args![value: "100ns",
                             unit: "ms"],
            want: Ok(0.0001),
            tdef: TypeDef::float().fallible(),
        }

        us_ms {
            args: func_args![value: "100µs",
                             unit: "ms"],
            want: Ok(0.1),
            tdef: TypeDef::float().fallible(),
        }

        d_s {
            args: func_args![value: "1d",
                             unit: "s"],
            want: Ok(86400.0),
            tdef: TypeDef::float().fallible(),
        }

        ds_s {
            args: func_args![value: "1d1s",
                             unit: "s"],
            want: Ok(86401.0),
            tdef: TypeDef::float().fallible(),
        }

        s_space_ms_ms {
            args: func_args![value: "1s 1ms",
                             unit: "ms"],
            want: Ok(1001.0),
            tdef: TypeDef::float().fallible(),
        }

        ms_space_us_ms {
            args: func_args![value: "1ms1 µs",
                             unit: "ms"],
            want: Ok(1.001),
            tdef: TypeDef::float().fallible(),
        }

        s_space_m_ms_order_agnostic {
            args: func_args![value: "1s1m",
                             unit: "ms"],
            want: Ok(61000.0),
            tdef: TypeDef::float().fallible(),
        }

        s_ns {
            args: func_args![value: "1 s",
                             unit: "ns"],
            want: Ok(1_000_000_000.0),
            tdef: TypeDef::float().fallible(),
        }

        us_space_ms {
            args: func_args![value: "1 µs",
                             unit: "ms"],
            want: Ok(0.001),
            tdef: TypeDef::float().fallible(),
        }

        w_ns {
            args: func_args![value: "1w",
                             unit: "ns"],
            want: Ok(604_800_000_000_000.0),
            tdef: TypeDef::float().fallible(),
        }

        w_d {
            args: func_args![value: "1.1w",
                             unit: "d"],
            want: Ok(7.7),
            tdef: TypeDef::float().fallible(),
        }

        d_w {
            args: func_args![value: "8d",
                             unit: "w"],
            want: Ok(1.142_857_142_857_142_8),
            tdef: TypeDef::float().fallible(),
        }

        d_w_2 {
            args: func_args![value: "30d",
                             unit: "w"],
            want: Ok(4.285_714_285_714_286),
            tdef: TypeDef::float().fallible(),
        }

        d_s_2 {
            args: func_args![value: "8d",
                             unit: "s"],
            want: Ok(691_200.0),
            tdef: TypeDef::float().fallible(),
        }

        decimal_s_ms {
            args: func_args![value: "12.3s",
                             unit: "ms"],
            want: Ok(12300.0),
            tdef: TypeDef::float().fallible(),
        }

        decimal_s_ms_2 {
            args: func_args![value: "123.0s",
                             unit: "ms"],
            want: Ok(123_000.0),
            tdef: TypeDef::float().fallible(),
        }

        decimal_h_s_ms {
            args: func_args![value: "1h12.3s",
                             unit: "ms"],
            want: Ok(3_612_300.0),
            tdef: TypeDef::float().fallible(),
        }

        decimal_d_s_s {
            args: func_args![value: "1.1d12.3s",
                             unit: "s"],
            want: Ok(95052.3),
            tdef: TypeDef::float().fallible(),
        }

        error_invalid {
            args: func_args![value: "foo",
                             unit: "ms"],
            want: Err("unable to parse duration: 'foo'"),
            tdef: TypeDef::float().fallible(),
        }

        error_ns {
            args: func_args![value: "1",
                             unit: "ns"],
            want: Err("unable to parse duration: '1'"),
            tdef: TypeDef::float().fallible(),
        }
        error_overflow {
            args: func_args![value: "1234567890123456789012345d",
                             unit: "s"],
            want: Err("unable to convert duration: '1234567890123456789012345d'"),
            tdef: TypeDef::float().fallible(),
        }

        s_w {
            args: func_args![value: "1s",
                             unit: "w"],
            want: Ok(0.000_001_653_439_153_439_153_5),
            tdef: TypeDef::float().fallible(),
        }

        error_failed_2nd_unit {
            args: func_args![value: "1d foo",
                             unit: "s"],
            want: Err("unable to parse duration: ' foo'"),
            tdef: TypeDef::float().fallible(),
        }
    ];
}