vrl 0.32.0

Vector Remap Language
Documentation
use crate::compiler::function::EnumVariant;
use crate::compiler::prelude::*;
use std::str::FromStr;
use std::sync::LazyLock;

static DEFAULT_UNIT: LazyLock<Value> = LazyLock::new(|| Value::Bytes(Bytes::from("seconds")));

static UNIT_ENUM: &[EnumVariant] = &[
    EnumVariant {
        value: "seconds",
        description: "Express Unix time in seconds",
    },
    EnumVariant {
        value: "milliseconds",
        description: "Express Unix time in milliseconds",
    },
    EnumVariant {
        value: "nanoseconds",
        description: "Express Unix time in nanoseconds",
    },
];

static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
    vec![
        Parameter::required(
            "value",
            kind::TIMESTAMP,
            "The timestamp to convert into a Unix timestamp.",
        ),
        Parameter::optional("unit", kind::BYTES, "The time unit.")
            .default(&DEFAULT_UNIT)
            .enum_variants(UNIT_ENUM),
    ]
});

fn to_unix_timestamp(value: Value, unit: Unit) -> Resolved {
    let ts = value.try_timestamp()?;
    let time = match unit {
        Unit::Seconds => ts.timestamp(),
        Unit::Milliseconds => ts.timestamp_millis(),
        Unit::Microseconds => ts.timestamp_micros(),
        Unit::Nanoseconds => match ts.timestamp_nanos_opt() {
            None => return Err(ValueError::OutOfRange(Kind::timestamp()).into()),
            Some(nanos) => nanos,
        },
    };
    Ok(time.into())
}

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

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

    fn usage(&self) -> &'static str {
        indoc! {"
            Converts the `value` timestamp into a [Unix timestamp](https://en.wikipedia.org/wiki/Unix_time).

            Returns the number of seconds since the Unix epoch by default. To return the number in milliseconds or nanoseconds, set the `unit` argument to `milliseconds` or `nanoseconds`.
        "}
    }

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

    fn internal_failure_reasons(&self) -> &'static [&'static str] {
        &[
            "`value` cannot be represented in nanoseconds. Result is too large or too small for a 64 bit integer.",
        ]
    }

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

    fn examples(&self) -> &'static [Example] {
        &[
            example! {
                title: "Convert to a Unix timestamp (seconds)",
                source: "to_unix_timestamp(t'2021-01-01T00:00:00+00:00')",
                result: Ok("1609459200"),
            },
            example! {
                title: "Convert to a Unix timestamp (milliseconds)",
                source: r#"to_unix_timestamp(t'2021-01-01T00:00:00Z', unit: "milliseconds")"#,
                result: Ok("1609459200000"),
            },
            example! {
                title: "Convert to a Unix timestamp (microseconds)",
                source: r#"to_unix_timestamp(t'2021-01-01T00:00:00Z', unit: "microseconds")"#,
                result: Ok("1609459200000000"),
            },
            example! {
                title: "Convert to a Unix timestamp (nanoseconds)",
                source: r#"to_unix_timestamp(t'2021-01-01T00:00:00Z', unit: "nanoseconds")"#,
                result: Ok("1609459200000000000"),
            },
        ]
    }

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

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

        let unit = Unit::from_str(
            &arguments
                .optional_enum("unit", Unit::all_value().as_slice(), state)?
                .unwrap_or_else(|| DEFAULT_UNIT.clone())
                .try_bytes_utf8_lossy()
                .expect("unit not bytes"),
        )
        .expect("validated enum");

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

#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
enum Unit {
    #[default]
    Seconds,
    Milliseconds,
    Microseconds,
    Nanoseconds,
}

impl Unit {
    fn all_value() -> Vec<Value> {
        use Unit::{Microseconds, Milliseconds, Nanoseconds, Seconds};

        vec![Seconds, Milliseconds, Microseconds, Nanoseconds]
            .into_iter()
            .map(|u| u.as_str().into())
            .collect::<Vec<_>>()
    }

    const fn as_str(self) -> &'static str {
        use Unit::{Microseconds, Milliseconds, Nanoseconds, Seconds};

        match self {
            Seconds => "seconds",
            Milliseconds => "milliseconds",
            Microseconds => "microseconds",
            Nanoseconds => "nanoseconds",
        }
    }
}

impl FromStr for Unit {
    type Err = &'static str;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        use Unit::{Microseconds, Milliseconds, Nanoseconds, Seconds};

        match s {
            "seconds" => Ok(Seconds),
            "milliseconds" => Ok(Milliseconds),
            "microseconds" => Ok(Microseconds),
            "nanoseconds" => Ok(Nanoseconds),
            _ => Err("unit not recognized"),
        }
    }
}

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

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

        to_unix_timestamp(value, unit)
    }

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

#[cfg(test)]
mod test {
    use chrono::{TimeZone, Utc};

    use super::*;

    test_function![
        to_unix_timestamp => ToUnixTimestamp;

        seconds {
            args: func_args![value: Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(),
                             unit: "seconds"
            ],
            want: Ok(1_609_459_200_i64),
            tdef: TypeDef::integer().infallible(),
        }

        milliseconds {
            args: func_args![value: Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(),
                             unit: "milliseconds"
            ],
            want: Ok(1_609_459_200_000_i64),
            tdef: TypeDef::integer().infallible(),
        }

        microseconds {
            args: func_args![value: Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(),
                             unit: "microseconds"
            ],
            want: Ok(1_609_459_200_000_000_i64),
            tdef: TypeDef::integer().infallible(),
        }

        nanoseconds {
             args: func_args![value: Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(),
                              unit: "nanoseconds"
             ],
             want: Ok(1_609_459_200_000_000_000_i64),
             tdef: TypeDef::integer().infallible(),
         }
         out_of_range {
             args: func_args![value: Utc.with_ymd_and_hms(0, 1, 1, 0, 0, 0).unwrap(),
                              unit: "nanoseconds"
             ],
             want: Err("can't convert out of range timestamp"),
             tdef: TypeDef::integer().infallible(),
         }
    ];
}