vrl/stdlib/
parse_timestamp.rs

1use crate::compiler::TimeZone;
2use crate::compiler::conversion::Conversion;
3use crate::compiler::prelude::*;
4
5fn parse_timestamp(
6    value: Value,
7    format: &Value,
8    timezone: Option<Value>,
9    ctx: &Context,
10) -> Resolved {
11    match value {
12        Value::Bytes(v) => {
13            let format = format.try_bytes_utf8_lossy()?;
14
15            let timezone_bytes = timezone.map(VrlValueConvert::try_bytes).transpose()?;
16            let timezone = timezone_bytes.as_ref().map(|b| String::from_utf8_lossy(b));
17            let timezone = timezone
18                .as_deref()
19                .map(|timezone| {
20                    TimeZone::parse(timezone).ok_or(format!("unable to parse timezone: {timezone}"))
21                })
22                .transpose()?
23                .unwrap_or(*ctx.timezone());
24
25            Conversion::timestamp(&format, timezone)
26                .convert(v)
27                .map_err(|e| e.to_string().into())
28        }
29        Value::Timestamp(_) => Ok(value),
30        _ => Err(format!("unable to convert {} value to timestamp", value.kind_str()).into()),
31    }
32}
33
34#[derive(Clone, Copy, Debug)]
35pub struct ParseTimestamp;
36
37impl Function for ParseTimestamp {
38    fn identifier(&self) -> &'static str {
39        "parse_timestamp"
40    }
41
42    fn usage(&self) -> &'static str {
43        "Parses the `value` in [strptime](https://docs.rs/chrono/latest/chrono/format/strftime/index.html#specifiers) `format`."
44    }
45
46    fn category(&self) -> &'static str {
47        Category::Parse.as_ref()
48    }
49
50    fn internal_failure_reasons(&self) -> &'static [&'static str] {
51        &[
52            "`value` fails to parse using the provided `format`.",
53            "`value` fails to parse using the provided `timezone`.",
54        ]
55    }
56
57    fn return_kind(&self) -> u16 {
58        kind::TIMESTAMP
59    }
60
61    fn examples(&self) -> &'static [Example] {
62        &[
63            example! {
64                title: "Parse timestamp",
65                source: r#"parse_timestamp!("10-Oct-2020 16:00+00:00", format: "%v %R %:z")"#,
66                result: Ok("t'2020-10-10T16:00:00Z'"),
67            },
68            example! {
69                title: "Parse timestamp with timezone",
70                source: r#"parse_timestamp!("16/10/2019 12:00:00", format: "%d/%m/%Y %H:%M:%S", timezone: "Asia/Taipei")"#,
71                result: Ok("t'2019-10-16T04:00:00Z'"),
72            },
73        ]
74    }
75
76    fn compile(
77        &self,
78        _state: &state::TypeState,
79        _ctx: &mut FunctionCompileContext,
80        arguments: ArgumentList,
81    ) -> Compiled {
82        let value = arguments.required("value");
83        let format = arguments.required("format");
84        let timezone = arguments.optional("timezone");
85
86        Ok(ParseTimestampFn {
87            value,
88            format,
89            timezone,
90        }
91        .as_expr())
92    }
93
94    fn parameters(&self) -> &'static [Parameter] {
95        const PARAMETERS: &[Parameter] = &[
96            Parameter::required("value", kind::BYTES | kind::TIMESTAMP, "The text of the timestamp."),
97            Parameter::required("format", kind::BYTES, "The [strptime](https://docs.rs/chrono/latest/chrono/format/strftime/index.html#specifiers) format."),
98            Parameter::optional("timezone", kind::BYTES, "The [TZ database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) format. By default, this function parses the timestamp by global [`timezone` option](/docs/reference/configuration//global-options#timezone).
99This argument overwrites the setting and is useful for parsing timestamps without a specified timezone, such as `16/10/2019 12:00:00`."),
100        ];
101        PARAMETERS
102    }
103}
104
105#[derive(Debug, Clone)]
106struct ParseTimestampFn {
107    value: Box<dyn Expression>,
108    format: Box<dyn Expression>,
109    timezone: Option<Box<dyn Expression>>,
110}
111
112impl FunctionExpression for ParseTimestampFn {
113    fn resolve(&self, ctx: &mut Context) -> Resolved {
114        let value = self.value.resolve(ctx)?;
115        let format = self.format.resolve(ctx)?;
116        let tz = self
117            .timezone
118            .as_ref()
119            .map(|tz| tz.resolve(ctx))
120            .transpose()?;
121        parse_timestamp(value, &format, tz, ctx)
122    }
123
124    fn type_def(&self, _: &state::TypeState) -> TypeDef {
125        TypeDef::timestamp().fallible(/* always fallible because the format and the timezone need to be parsed at runtime */)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::value;
133    use chrono::{DateTime, Utc};
134
135    test_function![
136        parse_timestamp => ParseTimestamp;
137
138        parse_timestamp {
139            args: func_args![
140                value: DateTime::parse_from_rfc2822("Wed, 16 Oct 2019 12:00:00 +0000")
141                    .unwrap()
142                    .with_timezone(&Utc),
143                format:"%d/%m/%Y:%H:%M:%S %z"
144            ],
145            want: Ok(value!(
146                DateTime::parse_from_rfc2822("Wed, 16 Oct 2019 12:00:00 +0000")
147                    .unwrap()
148                    .with_timezone(&Utc)
149            )),
150            tdef: TypeDef::timestamp().fallible(),
151            tz: TimeZone::default(),
152        }
153
154        parse_text {
155            args: func_args![
156                value: "16/10/2019:12:00:00 +0000",
157                format: "%d/%m/%Y:%H:%M:%S %z"
158            ],
159            want: Ok(value!(
160                DateTime::parse_from_rfc2822("Wed, 16 Oct 2019 12:00:00 +0000")
161                    .unwrap()
162                    .with_timezone(&Utc)
163            )),
164            tdef: TypeDef::timestamp().fallible(),
165            tz: TimeZone::default(),
166        }
167
168        parse_text_with_tz {
169            args: func_args![
170                value: "16/10/2019:12:00:00",
171                format: "%d/%m/%Y:%H:%M:%S"
172            ],
173            want: Ok(value!(
174                DateTime::parse_from_rfc2822("Wed, 16 Oct 2019 10:00:00 +0000")
175                    .unwrap()
176                    .with_timezone(&Utc)
177            )),
178            tdef: TypeDef::timestamp().fallible(),
179            tz: TimeZone::Named(chrono_tz::Europe::Paris),
180        }
181
182        // test without Daylight Saving Time (DST)
183        parse_text_with_timezone_args_no_dst {
184            args: func_args![
185                value: "31/12/2019:12:00:00",
186                format: "%d/%m/%Y:%H:%M:%S",
187                timezone: "Europe/Paris"
188            ],
189            want: Ok(value!(
190                DateTime::parse_from_rfc2822("Tue, 31 Dec 2019 11:00:00 +0000")
191                    .unwrap()
192                    .with_timezone(&Utc)
193            )),
194            tdef: TypeDef::timestamp().fallible(),
195            tz: TimeZone::default(),
196        }
197
198        parse_text_with_favor_timezone_args_than_tz_no_dst {
199            args: func_args![
200                value: "31/12/2019:12:00:00",
201                format: "%d/%m/%Y:%H:%M:%S",
202                timezone: "Europe/Paris"
203            ],
204            want: Ok(value!(
205                DateTime::parse_from_rfc2822("Tue, 31 Dec 2019 11:00:00 +0000")
206                    .unwrap()
207                    .with_timezone(&Utc)
208            )),
209            tdef: TypeDef::timestamp().fallible(),
210            tz: TimeZone::Named(chrono_tz::Europe::London),
211        }
212
213        err_timezone_args {
214            args: func_args![
215                value: "16/10/2019:12:00:00",
216                format: "%d/%m/%Y:%H:%M:%S",
217                timezone: "Europe/Pariss"
218            ],
219            want: Err("unable to parse timezone: Europe/Pariss"),
220            tdef: TypeDef::timestamp().fallible(),
221            tz: TimeZone::default(),
222        }
223
224        err_value_null {
225            args: func_args![
226                value: value!(null),
227                format: "%d/%m/%Y:%H:%M:%S",
228            ],
229            want: Err("unable to convert null value to timestamp"),
230            tdef: TypeDef::timestamp().fallible(),
231            tz: TimeZone::default(),
232        }
233    ];
234}