cedar_policy_core/extensions/
datetime.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! This module contains the Cedar 'datetime' extension.
18use std::{fmt::Display, sync::Arc};
19
20use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeDelta};
21use miette::Diagnostic;
22use smol_str::SmolStr;
23use thiserror::Error;
24
25use crate::{
26    ast::{
27        CallStyle, Extension, ExtensionFunction, ExtensionOutputValue, ExtensionValue, Literal,
28        Name, RepresentableExtensionValue, RestrictedExpr, Type, Value, ValueKind,
29    },
30    entities::SchemaType,
31    evaluator::{self, EvaluationError},
32};
33
34const DATETIME_EXTENSION_NAME: &str = "datetime";
35
36// PANIC SAFETY The `Name`s and `Regex` here are valid
37#[allow(clippy::expect_used, clippy::unwrap_used)]
38mod constants {
39    use regex::Regex;
40    use std::sync::LazyLock;
41
42    use crate::{ast::Name, extensions::datetime::DATETIME_EXTENSION_NAME};
43
44    pub static DATETIME_CONSTRUCTOR_NAME: LazyLock<Name> = LazyLock::new(|| {
45        Name::parse_unqualified_name(DATETIME_EXTENSION_NAME).expect("should be a valid identifier")
46    });
47    pub static DURATION_CONSTRUCTOR_NAME: LazyLock<Name> = LazyLock::new(|| {
48        Name::parse_unqualified_name("duration").expect("should be a valid identifier")
49    });
50    pub static OFFSET_METHOD_NAME: LazyLock<Name> = LazyLock::new(|| {
51        Name::parse_unqualified_name("offset").expect("should be a valid identifier")
52    });
53    pub static DURATION_SINCE_NAME: LazyLock<Name> = LazyLock::new(|| {
54        Name::parse_unqualified_name("durationSince").expect("should be a valid identifier")
55    });
56    pub static TO_DATE_NAME: LazyLock<Name> = LazyLock::new(|| {
57        Name::parse_unqualified_name("toDate").expect("should be a valid identifier")
58    });
59    pub static TO_TIME_NAME: LazyLock<Name> = LazyLock::new(|| {
60        Name::parse_unqualified_name("toTime").expect("should be a valid identifier")
61    });
62    pub static TO_MILLISECONDS_NAME: LazyLock<Name> = LazyLock::new(|| {
63        Name::parse_unqualified_name("toMilliseconds").expect("should be a valid identifier")
64    });
65    pub static TO_SECONDS_NAME: LazyLock<Name> = LazyLock::new(|| {
66        Name::parse_unqualified_name("toSeconds").expect("should be a valid identifier")
67    });
68    pub static TO_MINUTES_NAME: LazyLock<Name> = LazyLock::new(|| {
69        Name::parse_unqualified_name("toMinutes").expect("should be a valid identifier")
70    });
71    pub static TO_HOURS_NAME: LazyLock<Name> = LazyLock::new(|| {
72        Name::parse_unqualified_name("toHours").expect("should be a valid identifier")
73    });
74    pub static TO_DAYS_NAME: LazyLock<Name> = LazyLock::new(|| {
75        Name::parse_unqualified_name("toDays").expect("should be a valid identifier")
76    });
77
78    // Global regex, initialized at first use
79    // PANIC SAFETY: These are valid `Regex`
80    pub static DURATION_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
81        Regex::new(r"^-?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$").unwrap()
82    });
83    pub static DATE_PATTERN: LazyLock<Regex> =
84        LazyLock::new(|| Regex::new(r"^([0-9]{4})-([0-9]{2})-([0-9]{2})").unwrap());
85    pub static HMS_PATTERN: LazyLock<Regex> =
86        LazyLock::new(|| Regex::new(r"^T([0-9]{2}):([0-9]{2}):([0-9]{2})").unwrap());
87    pub static MS_AND_OFFSET_PATTERN: LazyLock<Regex> =
88        LazyLock::new(|| Regex::new(r"^(\.([0-9]{3}))?(Z|((\+|-)([0-9]{2})([0-9]{2})))$").unwrap());
89}
90
91// The `datetime` type, represented internally as an `i64`.
92#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
93struct DateTime {
94    // The number of non-leap milliseconds from the Unix epoch
95    epoch: i64,
96}
97
98fn extension_err(
99    msg: String,
100    extension_name: &crate::ast::Name,
101    advice: Option<String>,
102) -> evaluator::EvaluationError {
103    evaluator::EvaluationError::failed_extension_function_application(
104        extension_name.clone(),
105        msg,
106        None, // source loc will be added by the evaluator
107        advice,
108    )
109}
110
111fn construct_from_str<Ext>(
112    arg: &Value,
113    constructor_name: Name,
114    constructor: impl Fn(&str) -> Result<Ext, EvaluationError>,
115) -> evaluator::Result<ExtensionOutputValue>
116where
117    Ext: ExtensionValue + std::cmp::Ord + 'static + std::clone::Clone,
118{
119    let s = arg.get_as_string()?;
120    let ext_value: Ext = constructor(s)?;
121    let arg_source_loc = arg.source_loc();
122    let e = RepresentableExtensionValue::new(
123        Arc::new(ext_value),
124        constructor_name,
125        vec![arg.clone().into()],
126    );
127    Ok(Value {
128        value: ValueKind::ExtensionValue(Arc::new(e)),
129        loc: arg_source_loc.cloned(), // follow the same convention as the `decimal` extension
130    }
131    .into())
132}
133
134/// Cedar function that constructs a `datetime` Cedar type from a
135/// Cedar string
136fn datetime_from_str(arg: &Value) -> evaluator::Result<ExtensionOutputValue> {
137    construct_from_str(arg, constants::DATETIME_CONSTRUCTOR_NAME.clone(), |s| {
138        parse_datetime(s).map(DateTime::from).map_err(|err| {
139            extension_err(
140                err.to_string(),
141                &constants::DATETIME_CONSTRUCTOR_NAME,
142                err.help().map(|v| v.to_string()),
143            )
144        })
145    })
146}
147
148fn as_ext<'a, Ext>(v: &'a Value, type_name: &'a Name) -> Result<&'a Ext, evaluator::EvaluationError>
149where
150    Ext: ExtensionValue + std::cmp::Ord + 'static,
151{
152    match &v.value {
153        ValueKind::ExtensionValue(ev) if ev.typename() == *type_name => {
154            // PANIC SAFETY Conditional above performs a typecheck
155            #[allow(clippy::expect_used)]
156            let ext = ev
157                .value()
158                .as_any()
159                .downcast_ref::<Ext>()
160                .expect("already typechecked, so this downcast should succeed");
161            Ok(ext)
162        }
163        ValueKind::Lit(Literal::String(_)) => {
164            Err(evaluator::EvaluationError::type_error_with_advice_single(
165                Type::Extension {
166                    name: type_name.to_owned(),
167                },
168                v,
169                format!("maybe you forgot to apply the `{type_name}` constructor?"),
170            ))
171        }
172        _ => Err(evaluator::EvaluationError::type_error_single(
173            Type::Extension {
174                name: type_name.to_owned(),
175            },
176            v,
177        )),
178    }
179}
180
181/// Check that `v` is a datetime type and, if it is, return the wrapped value
182fn as_datetime(v: &Value) -> Result<DateTime, evaluator::EvaluationError> {
183    as_ext(v, &constants::DATETIME_CONSTRUCTOR_NAME).copied()
184}
185
186/// Check that `v` is a duration type and, if it is, return the wrapped value
187fn as_duration(v: &Value) -> Result<Duration, evaluator::EvaluationError> {
188    as_ext(v, &constants::DURATION_CONSTRUCTOR_NAME).copied()
189}
190
191fn offset(datetime: &Value, duration: &Value) -> evaluator::Result<ExtensionOutputValue> {
192    let datetime = as_datetime(datetime)?;
193    let duration = as_duration(duration)?;
194    let ret = datetime.offset(duration).ok_or_else(|| {
195        extension_err(
196            format!(
197                "overflows when adding an offset: {}+({})",
198                RestrictedExpr::from(datetime),
199                duration
200            ),
201            &constants::OFFSET_METHOD_NAME,
202            None,
203        )
204    })?;
205    Ok(Value {
206        value: ValueKind::ExtensionValue(Arc::new(ret.into())),
207        loc: None,
208    }
209    .into())
210}
211
212fn duration_since(lhs: &Value, rhs: &Value) -> evaluator::Result<ExtensionOutputValue> {
213    let lhs = as_datetime(lhs)?;
214    let rhs = as_datetime(rhs)?;
215    let ret = lhs.duration_since(rhs).ok_or_else(|| {
216        extension_err(
217            format!(
218                "overflows when computing the duration between {} and {}",
219                RestrictedExpr::from(lhs),
220                RestrictedExpr::from(rhs)
221            ),
222            &constants::DURATION_SINCE_NAME,
223            None,
224        )
225    })?;
226    Ok(Value {
227        value: ValueKind::ExtensionValue(Arc::new(ret.into())),
228        loc: None,
229    }
230    .into())
231}
232
233fn to_date(value: &Value) -> evaluator::Result<ExtensionOutputValue> {
234    let d = as_datetime(value)?;
235    let ret = d.to_date().ok_or_else(|| {
236        extension_err(
237            format!(
238                "overflows when computing the date of {}",
239                RestrictedExpr::from(d),
240            ),
241            &constants::TO_DATE_NAME,
242            None,
243        )
244    })?;
245    Ok(Value {
246        value: ValueKind::ExtensionValue(Arc::new(ret.into())),
247        loc: None,
248    }
249    .into())
250}
251
252fn to_time(value: &Value) -> evaluator::Result<ExtensionOutputValue> {
253    let d = as_datetime(value)?;
254    let ret = d.to_time();
255    Ok(Value {
256        value: ValueKind::ExtensionValue(Arc::new(ret.into())),
257        loc: None,
258    }
259    .into())
260}
261
262impl ExtensionValue for DateTime {
263    fn typename(&self) -> crate::ast::Name {
264        constants::DATETIME_CONSTRUCTOR_NAME.to_owned()
265    }
266    fn supports_operator_overloading(&self) -> bool {
267        true
268    }
269}
270
271impl DateTime {
272    const DAY_IN_MILLISECONDS: i64 = 1000 * 3600 * 24;
273    const UNIX_EPOCH_STR: &'static str = "1970-01-01";
274
275    fn offset(self, duration: Duration) -> Option<Self> {
276        self.epoch
277            .checked_add(duration.ms)
278            .map(|epoch| Self { epoch })
279    }
280
281    fn duration_since(self, other: DateTime) -> Option<Duration> {
282        self.epoch
283            .checked_sub(other.epoch)
284            .map(|ms| Duration { ms })
285    }
286
287    // essentially `self.epoch.div_floor(Self::DAY_IN_MILLISECONDS) * Self::DAY_IN_MILLISECONDS`
288    // but `div_floor` is only available on nightly
289    fn to_date(self) -> Option<Self> {
290        if self.epoch.is_negative() {
291            if self.epoch % Self::DAY_IN_MILLISECONDS == 0 {
292                Some(self.epoch)
293            } else {
294                (self.epoch / Self::DAY_IN_MILLISECONDS - 1).checked_mul(Self::DAY_IN_MILLISECONDS)
295            }
296        } else {
297            Some((self.epoch / Self::DAY_IN_MILLISECONDS) * Self::DAY_IN_MILLISECONDS)
298        }
299        .map(|epoch| Self { epoch })
300    }
301
302    fn to_time(self) -> Duration {
303        Duration {
304            ms: if self.epoch.is_negative() {
305                let rem = self.epoch % Self::DAY_IN_MILLISECONDS;
306                if rem == 0 {
307                    rem
308                } else {
309                    rem + Self::DAY_IN_MILLISECONDS
310                }
311            } else {
312                self.epoch % Self::DAY_IN_MILLISECONDS
313            },
314        }
315    }
316
317    fn as_ext_func_call(self) -> (Name, Vec<RestrictedExpr>) {
318        (
319            constants::OFFSET_METHOD_NAME.clone(),
320            vec![
321                RestrictedExpr::call_extension_fn(
322                    constants::DATETIME_CONSTRUCTOR_NAME.clone(),
323                    vec![Value::from(DateTime::UNIX_EPOCH_STR).into()],
324                ),
325                Duration { ms: self.epoch }.into(),
326            ],
327        )
328    }
329}
330
331impl From<DateTime> for RestrictedExpr {
332    fn from(value: DateTime) -> Self {
333        let (func, args) = value.as_ext_func_call();
334        Self::call_extension_fn(func, args)
335    }
336}
337
338impl From<DateTime> for RepresentableExtensionValue {
339    fn from(value: DateTime) -> Self {
340        let (func, args) = value.as_ext_func_call();
341        Self {
342            func,
343            args,
344            value: Arc::new(value),
345        }
346    }
347}
348
349impl From<NaiveDateTime> for DateTime {
350    fn from(value: NaiveDateTime) -> Self {
351        let delta = chrono::DateTime::from_naive_utc_and_offset(value, chrono::Utc)
352            - chrono::DateTime::UNIX_EPOCH;
353        Self {
354            epoch: delta.num_milliseconds(),
355        }
356    }
357}
358
359// The `duration` type
360#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
361struct Duration {
362    // The number of milliseconds
363    ms: i64,
364}
365
366impl Display for Duration {
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        write!(f, "{}ms", self.ms)
369    }
370}
371
372impl ExtensionValue for Duration {
373    fn typename(&self) -> crate::ast::Name {
374        constants::DURATION_CONSTRUCTOR_NAME.to_owned()
375    }
376    fn supports_operator_overloading(&self) -> bool {
377        true
378    }
379}
380
381impl From<Duration> for RestrictedExpr {
382    fn from(value: Duration) -> Self {
383        let (func, args) = value.as_ext_func_call();
384        RestrictedExpr::call_extension_fn(func, args)
385    }
386}
387
388impl From<Duration> for RepresentableExtensionValue {
389    fn from(value: Duration) -> Self {
390        let (func, args) = value.as_ext_func_call();
391        Self {
392            func,
393            args,
394            value: Arc::new(value),
395        }
396    }
397}
398
399/// Cedar function that constructs a `duration` Cedar type from a
400/// Cedar string
401fn duration_from_str(arg: &Value) -> evaluator::Result<ExtensionOutputValue> {
402    construct_from_str(arg, constants::DURATION_CONSTRUCTOR_NAME.clone(), |s| {
403        parse_duration(s).map_err(|err| {
404            extension_err(
405                err.to_string(),
406                &constants::DURATION_CONSTRUCTOR_NAME,
407                err.help().map(|v| v.to_string()),
408            )
409        })
410    })
411}
412
413impl Duration {
414    fn to_milliseconds(self) -> i64 {
415        self.ms
416    }
417
418    fn to_seconds(self) -> i64 {
419        self.to_milliseconds() / 1000
420    }
421
422    fn to_minutes(self) -> i64 {
423        self.to_seconds() / 60
424    }
425
426    fn to_hours(self) -> i64 {
427        self.to_minutes() / 60
428    }
429
430    fn to_days(self) -> i64 {
431        self.to_hours() / 24
432    }
433
434    fn as_ext_func_call(self) -> (Name, Vec<RestrictedExpr>) {
435        (
436            constants::DURATION_CONSTRUCTOR_NAME.clone(),
437            vec![Value::from(self.to_string()).into()],
438        )
439    }
440}
441
442fn duration_method(
443    value: &Value,
444    internal_func: impl Fn(Duration) -> i64,
445) -> evaluator::Result<ExtensionOutputValue> {
446    let d = as_duration(value)?;
447    Ok(Value::from(internal_func(d)).into())
448}
449
450#[derive(Debug, Clone, Error, Diagnostic)]
451enum DurationParseError {
452    #[error("invalid duration pattern")]
453    #[help("A valid duration string is a concatenated sequence of quantity-unit pairs with an optional `-` at the beginning")]
454    InvalidPattern,
455    #[error("Duration overflows internal representation")]
456    #[help("A duration in milliseconds must be representable by a signed 64 bit integer")]
457    Overflow,
458}
459
460fn parse_duration(s: &str) -> Result<Duration, DurationParseError> {
461    if s.is_empty() || s == "-" {
462        return Err(DurationParseError::InvalidPattern);
463    }
464    let captures = constants::DURATION_PATTERN
465        .captures(s)
466        .ok_or(DurationParseError::InvalidPattern)?;
467    let get_number = |idx| {
468        captures
469            .get(idx)
470            .map_or(Some(0), |m| m.as_str().parse().ok())
471            .ok_or(DurationParseError::Overflow)
472    };
473    let d: u64 = get_number(2)?;
474    let h: u64 = get_number(4)?;
475    let m: u64 = get_number(6)?;
476    let sec: u64 = get_number(8)?;
477    let ms: u64 = get_number(10)?;
478    let checked_op = |x, y: u64, mul| {
479        (if s.starts_with('-') {
480            i64::checked_sub
481        } else {
482            i64::checked_add
483        })(
484            x,
485            i64::checked_mul(y.try_into().map_err(|_| DurationParseError::Overflow)?, mul)
486                .ok_or(DurationParseError::Overflow)?,
487        )
488        .ok_or(DurationParseError::Overflow)
489    };
490    let mut ms = if s.starts_with('-') {
491        i64::try_from(-i128::from(ms)).map_err(|_| DurationParseError::Overflow)?
492    } else {
493        i64::try_from(ms).map_err(|_| DurationParseError::Overflow)?
494    };
495    ms = checked_op(ms, sec, 1000)?;
496    ms = checked_op(ms, m, 1000 * 60)?;
497    ms = checked_op(ms, h, 1000 * 60 * 60)?;
498    ms = checked_op(ms, d, 1000 * 60 * 60 * 24)?;
499    Ok(Duration { ms })
500}
501
502#[derive(Debug, Clone, Error, Diagnostic)]
503enum DateTimeParseError {
504    #[error("invalid date pattern")]
505    #[help("A valid datetime string should start with YYYY-MM-DD")]
506    InvalidDatePattern,
507    #[error("invalid date: {0}")]
508    InvalidDate(SmolStr),
509    #[error("invalid hour/minute/second pattern")]
510    #[help("A valid datetime string should have HH:MM:SS after the date")]
511    InvalidHMSPattern,
512    #[error("invalid hour/minute/second: {0}")]
513    InvalidHMS(SmolStr),
514    #[error("invalid millisecond and/or offset pattern")]
515    #[help("A valid datetime should end with Z|.SSSZ|(+|-)hhmm|.SSS(+|-)hhmm")]
516    InvalidMSOffsetPattern,
517    #[error("invalid offset range: {}{}", ._0.0, ._0.1)]
518    #[help("A valid offset hour range should be [0,24) and minute range should be [0, 60)")]
519    InvalidOffset((u32, u32)),
520}
521
522#[derive(Debug, Clone, PartialEq, Eq)]
523struct UTCOffset {
524    positive: bool,
525    hh: u32,
526    mm: u32,
527}
528
529impl UTCOffset {
530    const MAX_HH: u32 = 24;
531    const MAX_MM: u32 = 60;
532
533    fn to_seconds(&self) -> i64 {
534        let offset_in_seconds_unsigned = i64::from(self.hh * 3600 + self.mm * 60);
535        if self.positive {
536            offset_in_seconds_unsigned
537        } else {
538            -offset_in_seconds_unsigned
539        }
540    }
541
542    fn is_valid(&self) -> bool {
543        self.hh < Self::MAX_HH && self.mm < Self::MAX_MM
544    }
545}
546
547impl PartialOrd for UTCOffset {
548    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
549        Some(self.cmp(other))
550    }
551}
552
553impl Ord for UTCOffset {
554    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
555        self.to_seconds().cmp(&other.to_seconds())
556    }
557}
558
559fn parse_datetime(s: &str) -> Result<NaiveDateTime, DateTimeParseError> {
560    // Get date first
561    let (date_str, [year, month, day]) = constants::DATE_PATTERN
562        .captures(s)
563        .ok_or(DateTimeParseError::InvalidDatePattern)?
564        .extract();
565
566    // It's a closure because we want to perform syntactical check first and
567    // hence delay semantic check
568    // Both checks are from left to right
569    // PANIC SAFETY: `year`, `month`, and `day` should be all valid given the limit on the number of digits.
570    #[allow(clippy::unwrap_used)]
571    let date = || {
572        NaiveDate::from_ymd_opt(
573            year.parse().unwrap(),
574            month.parse().unwrap(),
575            day.parse().unwrap(),
576        )
577        .ok_or_else(|| DateTimeParseError::InvalidDate(date_str.into()))
578    };
579
580    // A complete match; simply return
581    if date_str.len() == s.len() {
582        // PANIC SAFETY: `0`s should be all valid given the limit on the number of digits.
583        #[allow(clippy::unwrap_used)]
584        return Ok(NaiveDateTime::new(
585            date()?,
586            NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
587        ));
588    }
589
590    // Get hour, minute, and second
591    let s = &s[date_str.len()..];
592
593    let (hms_str, [h, m, sec]) = constants::HMS_PATTERN
594        .captures(s)
595        .ok_or(DateTimeParseError::InvalidHMSPattern)?
596        .extract();
597    // PANIC SAFETY: `h`, `m`, and `sec` should be all valid given the limit on the number of digits.
598    #[allow(clippy::unwrap_used)]
599    let (h, m, sec): (u32, u32, u32) =
600        (h.parse().unwrap(), m.parse().unwrap(), sec.parse().unwrap());
601
602    // Get millisecond and offset
603    let s = &s[hms_str.len()..];
604    let captures = constants::MS_AND_OFFSET_PATTERN
605        .captures(s)
606        .ok_or(DateTimeParseError::InvalidMSOffsetPattern)?;
607    let ms: u32 = if captures.get(1).is_some() {
608        // PANIC SAFETY: should be valid given the limit on the number of digits.
609        #[allow(clippy::unwrap_used)]
610        captures[2].parse().unwrap()
611    } else {
612        0
613    };
614
615    let date = date()?;
616    let time = NaiveTime::from_hms_milli_opt(h, m, sec, ms)
617        .ok_or_else(|| DateTimeParseError::InvalidHMS(hms_str[1..].into()))?;
618    let offset: Result<TimeDelta, DateTimeParseError> = if captures.get(4).is_some() {
619        let positive = &captures[5] == "+";
620        // PANIC SAFETY: should be valid given the limit on the number of digits.
621        #[allow(clippy::unwrap_used)]
622        let (offset_hour, offset_min): (u32, u32) =
623            (captures[6].parse().unwrap(), captures[7].parse().unwrap());
624        let offset = UTCOffset {
625            positive,
626            hh: offset_hour,
627            mm: offset_min,
628        };
629        if offset.is_valid() {
630            let offset_in_secs = offset.to_seconds();
631            // PANIC SAFETY: should be valid because the limit on the values of offsets.
632            #[allow(clippy::unwrap_used)]
633            Ok(TimeDelta::new(-offset_in_secs, 0).unwrap())
634        } else {
635            Err(DateTimeParseError::InvalidOffset((offset_hour, offset_min)))
636        }
637    } else {
638        Ok(TimeDelta::default())
639    };
640    Ok(NaiveDateTime::new(date, time) + offset?)
641}
642
643/// Construct the extension
644pub fn extension() -> Extension {
645    let datetime_type = SchemaType::Extension {
646        name: constants::DATETIME_CONSTRUCTOR_NAME.to_owned(),
647    };
648    let duration_type = SchemaType::Extension {
649        name: constants::DURATION_CONSTRUCTOR_NAME.to_owned(),
650    };
651    Extension::new(
652        constants::DATETIME_CONSTRUCTOR_NAME.clone(),
653        vec![
654            ExtensionFunction::unary(
655                constants::DATETIME_CONSTRUCTOR_NAME.clone(),
656                CallStyle::FunctionStyle,
657                Box::new(datetime_from_str),
658                datetime_type.clone(),
659                SchemaType::String,
660            ),
661            ExtensionFunction::unary(
662                constants::DURATION_CONSTRUCTOR_NAME.clone(),
663                CallStyle::FunctionStyle,
664                Box::new(duration_from_str),
665                duration_type.clone(),
666                SchemaType::String,
667            ),
668            ExtensionFunction::binary(
669                constants::OFFSET_METHOD_NAME.clone(),
670                CallStyle::MethodStyle,
671                Box::new(offset),
672                datetime_type.clone(),
673                (datetime_type.clone(), duration_type.clone()),
674            ),
675            ExtensionFunction::binary(
676                constants::DURATION_SINCE_NAME.clone(),
677                CallStyle::MethodStyle,
678                Box::new(duration_since),
679                duration_type.clone(),
680                (datetime_type.clone(), duration_type.clone()),
681            ),
682            ExtensionFunction::unary(
683                constants::TO_DATE_NAME.clone(),
684                CallStyle::MethodStyle,
685                Box::new(to_date),
686                datetime_type.clone(),
687                datetime_type.clone(),
688            ),
689            ExtensionFunction::unary(
690                constants::TO_TIME_NAME.clone(),
691                CallStyle::MethodStyle,
692                Box::new(to_time),
693                duration_type.clone(),
694                datetime_type,
695            ),
696            ExtensionFunction::unary(
697                constants::TO_MILLISECONDS_NAME.clone(),
698                CallStyle::MethodStyle,
699                Box::new(|value| duration_method(value, Duration::to_milliseconds)),
700                SchemaType::Long,
701                duration_type.clone(),
702            ),
703            ExtensionFunction::unary(
704                constants::TO_SECONDS_NAME.clone(),
705                CallStyle::MethodStyle,
706                Box::new(|value| duration_method(value, Duration::to_seconds)),
707                SchemaType::Long,
708                duration_type.clone(),
709            ),
710            ExtensionFunction::unary(
711                constants::TO_MINUTES_NAME.clone(),
712                CallStyle::MethodStyle,
713                Box::new(|value| duration_method(value, Duration::to_minutes)),
714                SchemaType::Long,
715                duration_type.clone(),
716            ),
717            ExtensionFunction::unary(
718                constants::TO_HOURS_NAME.clone(),
719                CallStyle::MethodStyle,
720                Box::new(|value| duration_method(value, Duration::to_hours)),
721                SchemaType::Long,
722                duration_type.clone(),
723            ),
724            ExtensionFunction::unary(
725                constants::TO_DAYS_NAME.clone(),
726                CallStyle::MethodStyle,
727                Box::new(|value| duration_method(value, Duration::to_days)),
728                SchemaType::Long,
729                duration_type,
730            ),
731        ],
732        [
733            constants::DATETIME_CONSTRUCTOR_NAME.clone(),
734            constants::DURATION_CONSTRUCTOR_NAME.clone(),
735        ],
736    )
737}
738
739#[cfg(test)]
740#[allow(clippy::cognitive_complexity)]
741mod tests {
742    use std::{str::FromStr, sync::Arc};
743
744    use chrono::NaiveDateTime;
745    use cool_asserts::assert_matches;
746    use nonempty::nonempty;
747
748    use crate::{
749        ast::{Eid, EntityUID, EntityUIDEntry, Expr, Request, Type, Value, ValueKind},
750        entities::Entities,
751        evaluator::{EvaluationError, Evaluator},
752        extensions::{
753            datetime::{
754                constants::{
755                    DURATION_CONSTRUCTOR_NAME, TO_DATE_NAME, TO_DAYS_NAME, TO_HOURS_NAME,
756                    TO_MILLISECONDS_NAME, TO_MINUTES_NAME, TO_SECONDS_NAME, TO_TIME_NAME,
757                },
758                parse_datetime, parse_duration, DateTimeParseError, Duration,
759            },
760            Extensions,
761        },
762        parser::parse_expr,
763    };
764
765    use super::{constants::DATETIME_CONSTRUCTOR_NAME, DateTime};
766
767    #[test]
768    fn test_parse_pos() {
769        let s = "2024-10-15";
770        assert_eq!(
771            parse_datetime(s).unwrap(),
772            NaiveDateTime::from_str("2024-10-15T00:00:00").unwrap()
773        );
774        let s = "2024-10-15T11:38:02Z";
775        assert_eq!(
776            parse_datetime(s).unwrap(),
777            NaiveDateTime::from_str("2024-10-15T11:38:02").unwrap()
778        );
779        let s = "2024-10-15T11:38:02.101Z";
780        assert_eq!(
781            parse_datetime(s).unwrap(),
782            NaiveDateTime::from_str("2024-10-15T11:38:02.101").unwrap()
783        );
784        let s = "2024-10-15T11:38:02.101+1134";
785        assert_eq!(
786            parse_datetime(s).unwrap(),
787            NaiveDateTime::from_str("2024-10-15T00:04:02.101").unwrap()
788        );
789        let s = "2024-10-15T11:38:02.101-1134";
790        assert_eq!(
791            parse_datetime(s).unwrap(),
792            NaiveDateTime::from_str("2024-10-15T23:12:02.101").unwrap()
793        );
794        let s = "2024-10-15T11:38:02+1134";
795        assert_eq!(
796            parse_datetime(s).unwrap(),
797            NaiveDateTime::from_str("2024-10-15T00:04:02").unwrap()
798        );
799        let s = "2024-10-15T11:38:02-1134";
800        assert_eq!(
801            parse_datetime(s).unwrap(),
802            NaiveDateTime::from_str("2024-10-15T23:12:02").unwrap()
803        );
804        let s = "2024-10-15T23:59:00+2359";
805        assert_eq!(
806            parse_datetime(s).unwrap(),
807            NaiveDateTime::from_str("2024-10-15T00:00:00").unwrap()
808        );
809        let s = "2024-10-15T00:00:00-2359";
810        assert_eq!(
811            parse_datetime(s).unwrap(),
812            NaiveDateTime::from_str("2024-10-15T23:59:00").unwrap()
813        );
814    }
815
816    #[test]
817    fn test_parse_neg() {
818        for s in [
819            "",
820            "a",
821            "-",
822            "-1",
823            "11-12-13",
824            "1111-1x-20",
825            "2024-10-15Z",
826            "2024-10-15T11:38:02ZZ",
827        ] {
828            assert!(parse_datetime(s).is_err());
829        }
830
831        // invalid dates
832        assert_matches!(
833            parse_datetime("0000-0a-01"),
834            Err(DateTimeParseError::InvalidDatePattern)
835        );
836        assert_matches!(
837            parse_datetime("10000-01-01"),
838            Err(DateTimeParseError::InvalidDatePattern)
839        );
840        assert_matches!(
841            parse_datetime("10000-01-01T00:00:00Z"),
842            Err(DateTimeParseError::InvalidDatePattern)
843        );
844        assert_matches!(parse_datetime("2024-00-01"), Err(DateTimeParseError::InvalidDate(s)) if s == "2024-00-01");
845        assert_matches!(parse_datetime("2024-01-00"), Err(DateTimeParseError::InvalidDate(s)) if s == "2024-01-00");
846        assert_matches!(parse_datetime("2024-02-30"), Err(DateTimeParseError::InvalidDate(s)) if s == "2024-02-30");
847        assert_matches!(parse_datetime("2025-02-29"), Err(DateTimeParseError::InvalidDate(s)) if s == "2025-02-29");
848        assert_matches!(parse_datetime("2024-20-01"), Err(DateTimeParseError::InvalidDate(s)) if s == "2024-20-01");
849        assert_matches!(parse_datetime("2024-01-32"), Err(DateTimeParseError::InvalidDate(s)) if s == "2024-01-32");
850        assert_matches!(parse_datetime("2024-01-99"), Err(DateTimeParseError::InvalidDate(s)) if s == "2024-01-99");
851        assert_matches!(parse_datetime("2024-04-31"), Err(DateTimeParseError::InvalidDate(s)) if s == "2024-04-31");
852
853        // invalid hms
854        assert_matches!(
855            parse_datetime("2024-01-01T"),
856            Err(DateTimeParseError::InvalidHMSPattern)
857        );
858        assert_matches!(
859            parse_datetime("2024-01-01Ta"),
860            Err(DateTimeParseError::InvalidHMSPattern)
861        );
862        assert_matches!(
863            parse_datetime("2024-01-01T01:"),
864            Err(DateTimeParseError::InvalidHMSPattern)
865        );
866        assert_matches!(
867            parse_datetime("2024-01-01T01:02"),
868            Err(DateTimeParseError::InvalidHMSPattern)
869        );
870        assert_matches!(
871            parse_datetime("2024-01-01T01:02:0b"),
872            Err(DateTimeParseError::InvalidHMSPattern)
873        );
874        assert_matches!(
875            parse_datetime("2024-01-01T01::02:03"),
876            Err(DateTimeParseError::InvalidHMSPattern)
877        );
878        assert_matches!(
879            parse_datetime("2024-01-01T01::02::03"),
880            Err(DateTimeParseError::InvalidHMSPattern)
881        );
882        assert_matches!(parse_datetime("2024-01-01T31:02:03Z"), Err(DateTimeParseError::InvalidHMS(s)) if s == "31:02:03");
883        assert_matches!(parse_datetime("2024-01-01T01:60:03Z"), Err(DateTimeParseError::InvalidHMS(s)) if s == "01:60:03");
884        // we disallow `60`th second (i.e., potentially a leap second) because
885        // we can't check if it's a true leap second or not, though we can
886        // handle any computation if it's deemed to be a valid leap second
887        // Note that `2016-12-31T23:59:60Z` is the latest leap second as of
888        // writing
889        assert_matches!(parse_datetime("2016-12-31T23:59:60Z"), Err(DateTimeParseError::InvalidHMS(s)) if s == "23:59:60");
890        assert_matches!(parse_datetime("2016-12-31T23:59:61Z"), Err(DateTimeParseError::InvalidHMS(s)) if s == "23:59:61");
891
892        assert_matches!(
893            parse_datetime("2024-01-01T00:00:00"),
894            Err(DateTimeParseError::InvalidMSOffsetPattern)
895        );
896        assert_matches!(
897            parse_datetime("2024-01-01T00:00:00T"),
898            Err(DateTimeParseError::InvalidMSOffsetPattern)
899        );
900        assert_matches!(
901            parse_datetime("2024-01-01T00:00:00ZZ"),
902            Err(DateTimeParseError::InvalidMSOffsetPattern)
903        );
904        assert_matches!(
905            parse_datetime("2024-01-01T00:00:00x001Z"),
906            Err(DateTimeParseError::InvalidMSOffsetPattern)
907        );
908        assert_matches!(
909            parse_datetime("2024-01-01T00:00:00.001ZZ"),
910            Err(DateTimeParseError::InvalidMSOffsetPattern)
911        );
912        assert_matches!(
913            parse_datetime("2024-01-01T00:00:00➕0000"),
914            Err(DateTimeParseError::InvalidMSOffsetPattern)
915        );
916        assert_matches!(
917            parse_datetime("2024-01-01T00:00:00➖0000"),
918            Err(DateTimeParseError::InvalidMSOffsetPattern)
919        );
920        assert_matches!(
921            parse_datetime("2024-01-01T00:00:00.0001Z"),
922            Err(DateTimeParseError::InvalidMSOffsetPattern)
923        );
924        assert_matches!(
925            parse_datetime("2024-01-01T00:00:00.001➖0000"),
926            Err(DateTimeParseError::InvalidMSOffsetPattern)
927        );
928        assert_matches!(
929            parse_datetime("2024-01-01T00:00:00.001➕0000"),
930            Err(DateTimeParseError::InvalidMSOffsetPattern)
931        );
932        assert_matches!(
933            parse_datetime("2024-01-01T00:00:00.001+00000"),
934            Err(DateTimeParseError::InvalidMSOffsetPattern)
935        );
936        assert_matches!(
937            parse_datetime("2024-01-01T00:00:00.001-00000"),
938            Err(DateTimeParseError::InvalidMSOffsetPattern)
939        );
940        assert_matches!(
941            parse_datetime("2016-12-31T00:00:00+1160"),
942            Err(DateTimeParseError::InvalidOffset((11, 60)))
943        );
944        assert_matches!(
945            parse_datetime("2016-12-31T00:00:00+1199"),
946            Err(DateTimeParseError::InvalidOffset((11, 99)))
947        );
948        assert_matches!(
949            parse_datetime("2016-12-31T00:00:00+2400"),
950            Err(DateTimeParseError::InvalidOffset((24, 0)))
951        );
952    }
953
954    #[track_caller]
955    fn milliseconds_to_duration(ms: i128) -> String {
956        let sign = if ms < 0 { "-" } else { "" };
957        let mut ms = ms.abs();
958        let milliseconds = ms % 1000;
959        ms /= 1000;
960        let seconds = ms % 60;
961        ms /= 60;
962        let minutes = ms % 60;
963        ms /= 60;
964        let hours = ms % 24;
965        ms /= 24;
966        let days = ms;
967        format!("{sign}{days}d{hours}h{minutes}m{seconds}s{milliseconds}ms")
968    }
969
970    #[test]
971    fn parse_duration_pos() {
972        assert_eq!(parse_duration("1h").unwrap(), Duration { ms: 3600 * 1000 });
973        assert_eq!(
974            parse_duration("-10h").unwrap(),
975            Duration {
976                ms: -3600 * 10 * 1000
977            }
978        );
979        assert_eq!(
980            parse_duration("5d3ms").unwrap(),
981            Duration {
982                ms: 3600 * 24 * 5 * 1000 + 3
983            }
984        );
985        assert_eq!(
986            parse_duration("-3h5m").unwrap(),
987            Duration {
988                ms: -3600 * 3 * 1000 - 300 * 1000
989            }
990        );
991        assert!(parse_duration(&milliseconds_to_duration(i64::MAX.into())).is_ok());
992        assert!(parse_duration(&milliseconds_to_duration(i64::MIN.into())).is_ok());
993    }
994
995    #[test]
996    fn parse_duration_neg() {
997        for s in [
998            "", "a", "-", "-1", "➖1ms", "11dd", "00000mm", "-d", "-h", "-1hh", "-h2d", "-ms",
999            // incorrect ordering
1000            "1ms1s", "1ms1m", "1ms1h", "0ms1d", "1s1m", "1s1h", "0s0d", "0m0h", "0m0d", "1h1d",
1001            "1ms1m1d",
1002        ] {
1003            assert!(parse_duration(s).is_err());
1004        }
1005        assert!(parse_duration(&milliseconds_to_duration(i128::from(i64::MAX) + 1)).is_err());
1006        assert!(parse_duration(&milliseconds_to_duration(i128::from(i64::MIN) - 1)).is_err());
1007    }
1008
1009    #[test]
1010    fn test_offset() {
1011        let unix_epoch = DateTime { epoch: 0 };
1012        let date_time_max = unix_epoch
1013            .offset(parse_duration(&milliseconds_to_duration(i64::MAX.into())).unwrap())
1014            .expect("valid datetime");
1015        let date_time_min = unix_epoch
1016            .offset(parse_duration(&milliseconds_to_duration(i64::MIN.into())).unwrap())
1017            .expect("valid datetime");
1018        assert!(date_time_max
1019            .offset(parse_duration("1ms").unwrap())
1020            .is_none());
1021        assert_eq!(
1022            date_time_max.offset(parse_duration("-1ms").unwrap()),
1023            Some(
1024                unix_epoch
1025                    .offset(
1026                        parse_duration(&milliseconds_to_duration(i128::from(i64::MAX) - 1))
1027                            .unwrap()
1028                    )
1029                    .expect("valid datetime")
1030            )
1031        );
1032        assert!(date_time_min
1033            .offset(parse_duration("-1ms").unwrap())
1034            .is_none());
1035        assert_eq!(
1036            date_time_min.offset(parse_duration("1ms").unwrap()),
1037            Some(
1038                unix_epoch
1039                    .offset(
1040                        parse_duration(&milliseconds_to_duration(i128::from(i64::MIN) + 1))
1041                            .unwrap()
1042                    )
1043                    .expect("valid datetime")
1044            )
1045        );
1046        assert_eq!(
1047            unix_epoch.offset(parse_duration("1d").unwrap()),
1048            Some(parse_datetime("1970-01-02").unwrap().into())
1049        );
1050        assert_eq!(
1051            unix_epoch.offset(parse_duration("-1d").unwrap()),
1052            Some(parse_datetime("1969-12-31").unwrap().into())
1053        );
1054    }
1055
1056    #[test]
1057    fn test_duration_since() {
1058        let unix_epoch = DateTime { epoch: 0 };
1059        let today: DateTime = parse_datetime("2024-10-24").unwrap().into();
1060        assert_eq!(
1061            today.duration_since(unix_epoch),
1062            Some(parse_duration("20020d").unwrap())
1063        );
1064        let yesterday: DateTime = parse_datetime("2024-10-23").unwrap().into();
1065        assert_eq!(
1066            yesterday.duration_since(today),
1067            Some(parse_duration("-1d").unwrap())
1068        );
1069        assert_eq!(
1070            today.duration_since(yesterday),
1071            Some(parse_duration("1d").unwrap())
1072        );
1073
1074        let date_time_min = unix_epoch
1075            .offset(parse_duration(&milliseconds_to_duration(i64::MIN.into())).unwrap())
1076            .expect("valid datetime");
1077        assert!(today.duration_since(date_time_min).is_none());
1078    }
1079
1080    #[test]
1081    fn test_to_date() {
1082        let unix_epoch = DateTime { epoch: 0 };
1083        let today: DateTime = parse_datetime("2024-10-24").unwrap().into();
1084        assert_eq!(
1085            today.duration_since(unix_epoch),
1086            Some(parse_duration("20020d").unwrap())
1087        );
1088        let yesterday: DateTime = parse_datetime("2024-10-23").unwrap().into();
1089        assert_eq!(
1090            yesterday.duration_since(today),
1091            Some(parse_duration("-1d").unwrap())
1092        );
1093        let some_day_before_unix_epoch: DateTime = parse_datetime("1900-01-01").unwrap().into();
1094
1095        let max_day_offset = parse_duration("23h59m59s999ms").unwrap();
1096        let min_day_offset = parse_duration("-23h59m59s999ms").unwrap();
1097
1098        for d in [today, yesterday, unix_epoch, some_day_before_unix_epoch] {
1099            assert_eq!(d.to_date().expect("should not overflow"), d);
1100            assert_eq!(
1101                d.offset(max_day_offset)
1102                    .unwrap()
1103                    .to_date()
1104                    .expect("should not overflow"),
1105                d
1106            );
1107            assert_eq!(
1108                d.offset(min_day_offset)
1109                    .unwrap()
1110                    .to_date()
1111                    .expect("should not overflow"),
1112                d.offset(parse_duration("-1d").unwrap()).unwrap()
1113            );
1114        }
1115
1116        assert!(unix_epoch
1117            .offset(Duration { ms: i64::MIN })
1118            .expect("should be able to construct")
1119            .to_date()
1120            .is_none());
1121    }
1122
1123    #[test]
1124    fn test_to_time() {
1125        let unix_epoch = DateTime { epoch: 0 };
1126        let today: DateTime = parse_datetime("2024-10-24").unwrap().into();
1127        assert_eq!(
1128            today.duration_since(unix_epoch),
1129            Some(parse_duration("20020d").unwrap())
1130        );
1131        let yesterday: DateTime = parse_datetime("2024-10-23").unwrap().into();
1132        assert_eq!(
1133            yesterday.duration_since(today),
1134            Some(parse_duration("-1d").unwrap())
1135        );
1136        let some_day_before_unix_epoch: DateTime = parse_datetime("1900-01-01").unwrap().into();
1137
1138        let max_day_offset = parse_duration("23h59m59s999ms").unwrap();
1139        let min_day_offset = parse_duration("-23h59m59s999ms").unwrap();
1140
1141        for d in [today, yesterday, unix_epoch, some_day_before_unix_epoch] {
1142            assert_eq!(d.offset(max_day_offset).unwrap().to_time(), max_day_offset);
1143            assert_eq!(
1144                d.offset(min_day_offset).unwrap().to_time(),
1145                parse_duration("1ms").unwrap(),
1146            );
1147        }
1148    }
1149
1150    #[test]
1151    fn test_predicates() {
1152        let unix_epoch = DateTime { epoch: 0 };
1153        let today: DateTime = parse_datetime("2024-10-24").unwrap().into();
1154        let yesterday: DateTime = parse_datetime("2024-10-23").unwrap().into();
1155        let some_day_before_unix_epoch: DateTime = parse_datetime("1900-01-01").unwrap().into();
1156        assert!(unix_epoch <= unix_epoch);
1157        assert!(today == today);
1158        assert!(today != yesterday);
1159        assert!(unix_epoch < today);
1160        assert!(today > yesterday);
1161        assert!(some_day_before_unix_epoch <= unix_epoch);
1162        assert!(today >= some_day_before_unix_epoch);
1163        assert!(yesterday >= some_day_before_unix_epoch);
1164    }
1165
1166    #[test]
1167    fn test_duration_methods() {
1168        let day_offset = parse_duration("10d23h59m58s999ms").unwrap();
1169        let day_offset_neg = parse_duration("-10d23h59m58s999ms").unwrap();
1170        for o in [day_offset, day_offset_neg] {
1171            assert_eq!(o.to_days().abs(), 10);
1172            assert_eq!(o.to_hours().abs(), 10 * 24 + 23);
1173            assert_eq!(o.to_minutes().abs(), (10 * 24 + 23) * 60 + 59);
1174            assert_eq!(o.to_seconds().abs(), ((10 * 24 + 23) * 60 + 59) * 60 + 58);
1175            assert_eq!(
1176                o.to_milliseconds().abs(),
1177                (((10 * 24 + 23) * 60 + 59) * 60 + 58) * 1000 + 999
1178            );
1179        }
1180    }
1181
1182    fn dummy_entity() -> EntityUIDEntry {
1183        EntityUIDEntry::Known {
1184            euid: Arc::new(EntityUID::from_components(
1185                "A".parse().unwrap(),
1186                Eid::new(""),
1187                None,
1188            )),
1189            loc: None,
1190        }
1191    }
1192
1193    #[test]
1194    fn test_interpretation_datetime() {
1195        let dummy_entity = dummy_entity();
1196        let entities = Entities::default();
1197        let eval = Evaluator::new(
1198            Request::new_unchecked(
1199                dummy_entity.clone(),
1200                dummy_entity.clone(),
1201                dummy_entity,
1202                None,
1203            ),
1204            &entities,
1205            Extensions::all_available(),
1206        );
1207
1208        assert_matches!(
1209            eval.interpret_inline_policy(&Expr::call_extension_fn(
1210                DATETIME_CONSTRUCTOR_NAME.clone(),
1211                vec![Value::from("2024-10-28").into()]
1212            )),
1213            Ok(Value {
1214                value: ValueKind::ExtensionValue(ext),
1215                ..
1216            }) => {
1217                assert!(ext.value().equals_extvalue(&DateTime {epoch: 1730073600000}));
1218            }
1219        );
1220
1221        assert_matches!(
1222            eval.interpret_inline_policy(&Expr::call_extension_fn(
1223                DATETIME_CONSTRUCTOR_NAME.clone(),
1224                vec![Value::from("2024-10-28T01:22:33.456Z").into()]
1225            )),
1226            Ok(Value {
1227                value: ValueKind::ExtensionValue(ext),
1228                ..
1229            }) => {
1230                assert!(ext.value().equals_extvalue(&DateTime {epoch: 1730078553456}));
1231            }
1232        );
1233
1234        assert_matches!(
1235            eval.interpret_inline_policy(&Expr::call_extension_fn(
1236                DATETIME_CONSTRUCTOR_NAME.clone(),
1237                vec![Value::from("2024-10-28T10:12:13.456-0700").into()]
1238            )),
1239            Ok(Value {
1240                value: ValueKind::ExtensionValue(ext),
1241                ..
1242            }) => {
1243                assert!(ext.value().equals_extvalue(&DateTime {epoch: 1730135533456}));
1244            }
1245        );
1246
1247        assert_matches!(
1248            eval.interpret_inline_policy(&Expr::call_extension_fn(
1249                DATETIME_CONSTRUCTOR_NAME.clone(),
1250                vec![Value::from("22024-30-28T10:12:13.456-0700").into()]
1251            )),
1252            Err(EvaluationError::FailedExtensionFunctionExecution(err)) => {
1253                assert_eq!(err.extension_name, *DATETIME_CONSTRUCTOR_NAME);
1254                assert_eq!(err.msg, "invalid date pattern".to_owned());
1255                // TODO: figure out why it's none given the help annotations
1256                assert_eq!(err.advice, None);
1257            }
1258        );
1259
1260        // offset the offset component in a datetime specification
1261        assert_eq!(
1262            eval.interpret_inline_policy(
1263                &parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700").offset(duration("-7h"))"#)
1264                    .unwrap()
1265            )
1266            .unwrap(),
1267            eval.interpret_inline_policy(
1268                &parse_expr(r#"datetime("2024-10-28T10:12:13.456Z")"#).unwrap()
1269            )
1270            .unwrap()
1271        );
1272        assert_eq!(
1273            eval.interpret_inline_policy(
1274                &parse_expr(r#"datetime("2024-10-28T10:12:13.456+0700").offset(duration("7h"))"#)
1275                    .unwrap()
1276            )
1277            .unwrap(),
1278            eval.interpret_inline_policy(
1279                &parse_expr(r#"datetime("2024-10-28T10:12:13.456Z")"#).unwrap()
1280            )
1281            .unwrap()
1282        );
1283
1284        assert_matches!(
1285            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456+0700").offset("7h")"#).unwrap()),
1286            Err(EvaluationError::TypeError(err)) => {
1287                assert_eq!(err.expected, nonempty![Type::Extension { name: DURATION_CONSTRUCTOR_NAME.clone() }]);
1288                assert_eq!(err.actual, Type::String);
1289                assert_eq!(err.advice, Some("maybe you forgot to apply the `duration` constructor?".to_owned()));
1290            }
1291        );
1292
1293        // .durationSince
1294        assert_eq!(
1295            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456+0700").durationSince(datetime("2024-10-28T10:12:13.456Z"))"#).unwrap()).unwrap(),
1296            eval.interpret_inline_policy(&parse_expr(r#"duration("-7h")"#).unwrap()).unwrap()
1297        );
1298
1299        assert_eq!(
1300            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700").durationSince(datetime("2024-10-28T10:12:13.456Z"))"#).unwrap()).unwrap(),
1301            eval.interpret_inline_policy(&parse_expr(r#"duration("7h")"#).unwrap()).unwrap()
1302        );
1303
1304        assert_matches!(
1305            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456+0700").durationSince("7h")"#).unwrap()),
1306            Err(EvaluationError::TypeError(err)) => {
1307                assert_eq!(err.expected, nonempty![Type::Extension { name: DATETIME_CONSTRUCTOR_NAME.clone() }]);
1308                assert_eq!(err.actual, Type::String);
1309                assert_eq!(err.advice, Some("maybe you forgot to apply the `datetime` constructor?".to_owned()));
1310            }
1311        );
1312
1313        // .toDate
1314        assert_eq!(
1315            eval.interpret_inline_policy(
1316                &parse_expr(r#"datetime("2024-10-28T10:12:13.456+0700").toDate()"#).unwrap()
1317            )
1318            .unwrap(),
1319            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28")"#).unwrap())
1320                .unwrap()
1321        );
1322
1323        assert_eq!(
1324            eval.interpret_inline_policy(
1325                &parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700").toDate()"#).unwrap()
1326            )
1327            .unwrap(),
1328            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28")"#).unwrap())
1329                .unwrap()
1330        );
1331
1332        assert_matches!(
1333            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700").toDate(1)"#).unwrap()),
1334            Err(EvaluationError::WrongNumArguments(err)) => {
1335                assert_eq!(err.function_name, *TO_DATE_NAME);
1336                assert_eq!(err.actual, 2);
1337                assert_eq!(err.expected, 1);
1338            }
1339        );
1340
1341        // .toTime
1342        assert_eq!(
1343            eval.interpret_inline_policy(
1344                &parse_expr(r#"datetime("2024-10-28T10:12:13.456+0700").toTime()"#).unwrap()
1345            )
1346            .unwrap(),
1347            eval.interpret_inline_policy(&parse_expr(r#"duration("3h12m13s456ms")"#).unwrap())
1348                .unwrap()
1349        );
1350
1351        assert_eq!(
1352            eval.interpret_inline_policy(
1353                &parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700").toTime()"#).unwrap()
1354            )
1355            .unwrap(),
1356            eval.interpret_inline_policy(&parse_expr(r#"duration("17h12m13s456ms")"#).unwrap())
1357                .unwrap()
1358        );
1359
1360        assert_matches!(
1361            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700").toTime(1)"#).unwrap()),
1362            Err(EvaluationError::WrongNumArguments(err)) => {
1363                assert_eq!(err.function_name, *TO_TIME_NAME);
1364                assert_eq!(err.actual, 2);
1365                assert_eq!(err.expected, 1);
1366            }
1367        );
1368
1369        // comparisons
1370        assert_eq!(
1371            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700") > datetime("2024-10-28T10:12:13.456Z")"#).unwrap()).unwrap(),
1372            Value::from(true),
1373        );
1374        assert_eq!(
1375            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700") >= datetime("2024-10-28T10:12:13.456Z")"#).unwrap()).unwrap(),
1376            Value::from(true),
1377        );
1378        assert_eq!(
1379            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700") != datetime("2024-10-28T10:12:13.456Z")"#).unwrap()).unwrap(),
1380            Value::from(true),
1381        );
1382        assert_eq!(
1383            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700") == datetime("2024-10-28T17:12:13.456Z")"#).unwrap()).unwrap(),
1384            Value::from(true),
1385        );
1386        assert_eq!(
1387            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700") < datetime("2024-10-28T17:12:13.456-0800")"#).unwrap()).unwrap(),
1388            Value::from(true),
1389        );
1390        assert_eq!(
1391            eval.interpret_inline_policy(&parse_expr(r#"datetime("2024-10-28T10:12:13.456-0700") <= datetime("2024-10-28T17:12:13.456-0800")"#).unwrap()).unwrap(),
1392            Value::from(true),
1393        );
1394    }
1395
1396    #[test]
1397    fn test_interpretation_duration() {
1398        let dummy_entity = dummy_entity();
1399        let entities = Entities::default();
1400        let eval = Evaluator::new(
1401            Request::new_unchecked(
1402                dummy_entity.clone(),
1403                dummy_entity.clone(),
1404                dummy_entity,
1405                None,
1406            ),
1407            &entities,
1408            Extensions::all_available(),
1409        );
1410
1411        assert_matches!(
1412            eval.interpret_inline_policy(&Expr::call_extension_fn(
1413                DURATION_CONSTRUCTOR_NAME.clone(),
1414                vec![Value::from("1d2h3m4s50ms").into()]
1415            )),
1416            Ok(Value {
1417                value: ValueKind::ExtensionValue(ext),
1418                ..
1419            }) => {
1420                assert!(ext.value().equals_extvalue(&Duration {ms: 93784050}));
1421            }
1422        );
1423
1424        assert_matches!(
1425            eval.interpret_inline_policy(&Expr::call_extension_fn(
1426                DURATION_CONSTRUCTOR_NAME.clone(),
1427                vec![Value::from("1dd2h3m4s50ms").into()]
1428            )),
1429            Err(EvaluationError::FailedExtensionFunctionExecution(err)) => {
1430                assert_eq!(err.extension_name, *DURATION_CONSTRUCTOR_NAME);
1431                assert_eq!(err.msg, "invalid duration pattern".to_owned());
1432                // TODO: figure out why it's none given the help annotations
1433                assert_eq!(err.advice, None);
1434            }
1435        );
1436
1437        // .toMilliseconds
1438        assert_eq!(
1439            eval.interpret_inline_policy(
1440                &parse_expr(r#"duration("1d2h3m4s50ms").toMilliseconds()"#).unwrap()
1441            )
1442            .unwrap(),
1443            Value::from(93784050)
1444        );
1445
1446        assert_eq!(
1447            eval.interpret_inline_policy(
1448                &parse_expr(r#"duration("-1d2h3m4s50ms").toMilliseconds()"#).unwrap()
1449            )
1450            .unwrap(),
1451            Value::from(-93784050)
1452        );
1453
1454        assert_matches!(
1455            eval.interpret_inline_policy(&parse_expr(r#"duration("-1d2h3m4s50ms").toMilliseconds(1)"#).unwrap()),
1456            Err(EvaluationError::WrongNumArguments(err)) => {
1457                assert_eq!(err.function_name, *TO_MILLISECONDS_NAME);
1458                assert_eq!(err.actual, 2);
1459                assert_eq!(err.expected, 1);
1460            }
1461        );
1462
1463        // .toSeconds
1464        assert_eq!(
1465            eval.interpret_inline_policy(
1466                &parse_expr(r#"duration("1d2h3m4s50ms").toSeconds()"#).unwrap()
1467            )
1468            .unwrap(),
1469            Value::from(93784)
1470        );
1471
1472        assert_eq!(
1473            eval.interpret_inline_policy(
1474                &parse_expr(r#"duration("-1d2h3m4s50ms").toSeconds()"#).unwrap()
1475            )
1476            .unwrap(),
1477            Value::from(-93784)
1478        );
1479
1480        assert_matches!(
1481            eval.interpret_inline_policy(&parse_expr(r#"duration("-1d2h3m4s50ms").toSeconds(1)"#).unwrap()),
1482            Err(EvaluationError::WrongNumArguments(err)) => {
1483                assert_eq!(err.function_name, *TO_SECONDS_NAME);
1484                assert_eq!(err.actual, 2);
1485                assert_eq!(err.expected, 1);
1486            }
1487        );
1488
1489        // .toMinutes
1490        assert_eq!(
1491            eval.interpret_inline_policy(
1492                &parse_expr(r#"duration("1d2h3m4s50ms").toMinutes()"#).unwrap()
1493            )
1494            .unwrap(),
1495            Value::from(1563)
1496        );
1497
1498        assert_eq!(
1499            eval.interpret_inline_policy(
1500                &parse_expr(r#"duration("-1d2h3m4s50ms").toMinutes()"#).unwrap()
1501            )
1502            .unwrap(),
1503            Value::from(-1563)
1504        );
1505
1506        assert_matches!(
1507            eval.interpret_inline_policy(&parse_expr(r#"duration("-1d2h3m4s50ms").toMinutes(1)"#).unwrap()),
1508            Err(EvaluationError::WrongNumArguments(err)) => {
1509                assert_eq!(err.function_name, *TO_MINUTES_NAME);
1510                assert_eq!(err.actual, 2);
1511                assert_eq!(err.expected, 1);
1512            }
1513        );
1514
1515        // .toHours
1516        assert_eq!(
1517            eval.interpret_inline_policy(
1518                &parse_expr(r#"duration("1d2h3m4s50ms").toHours()"#).unwrap()
1519            )
1520            .unwrap(),
1521            Value::from(26)
1522        );
1523
1524        assert_eq!(
1525            eval.interpret_inline_policy(
1526                &parse_expr(r#"duration("-1d2h3m4s50ms").toHours()"#).unwrap()
1527            )
1528            .unwrap(),
1529            Value::from(-26)
1530        );
1531
1532        assert_matches!(
1533            eval.interpret_inline_policy(&parse_expr(r#"duration("-1d2h3m4s50ms").toHours(1)"#).unwrap()),
1534            Err(EvaluationError::WrongNumArguments(err)) => {
1535                assert_eq!(err.function_name, *TO_HOURS_NAME);
1536                assert_eq!(err.actual, 2);
1537                assert_eq!(err.expected, 1);
1538            }
1539        );
1540
1541        // .toDays
1542        assert_eq!(
1543            eval.interpret_inline_policy(
1544                &parse_expr(r#"duration("1d2h3m4s50ms").toDays()"#).unwrap()
1545            )
1546            .unwrap(),
1547            Value::from(1)
1548        );
1549
1550        assert_eq!(
1551            eval.interpret_inline_policy(
1552                &parse_expr(r#"duration("-1d2h3m4s50ms").toDays()"#).unwrap()
1553            )
1554            .unwrap(),
1555            Value::from(-1)
1556        );
1557
1558        assert_matches!(
1559            eval.interpret_inline_policy(&parse_expr(r#"duration("-1d2h3m4s50ms").toDays(1)"#).unwrap()),
1560            Err(EvaluationError::WrongNumArguments(err)) => {
1561                assert_eq!(err.function_name, *TO_DAYS_NAME);
1562                assert_eq!(err.actual, 2);
1563                assert_eq!(err.expected, 1);
1564            }
1565        );
1566
1567        // Python's datetime does this but is -2h shorter than 1h?
1568        assert_eq!(
1569            eval.interpret_inline_policy(
1570                &parse_expr(r#"duration("-2h") < duration("1h")"#).unwrap()
1571            )
1572            .unwrap(),
1573            Value::from(true),
1574        );
1575        assert_eq!(
1576            eval.interpret_inline_policy(
1577                &parse_expr(r#"duration("-2h") <= duration("1h")"#).unwrap()
1578            )
1579            .unwrap(),
1580            Value::from(true),
1581        );
1582        assert_eq!(
1583            eval.interpret_inline_policy(
1584                &parse_expr(r#"duration("-2h") != duration("1h")"#).unwrap()
1585            )
1586            .unwrap(),
1587            Value::from(true),
1588        );
1589        assert_eq!(
1590            eval.interpret_inline_policy(
1591                &parse_expr(r#"duration("-3d") == duration("-72h")"#).unwrap()
1592            )
1593            .unwrap(),
1594            Value::from(true),
1595        );
1596        assert_eq!(
1597            eval.interpret_inline_policy(
1598                &parse_expr(r#"duration("2h") > duration("1h")"#).unwrap()
1599            )
1600            .unwrap(),
1601            Value::from(true),
1602        );
1603        assert_eq!(
1604            eval.interpret_inline_policy(
1605                &parse_expr(r#"duration("2h") >= duration("1h")"#).unwrap()
1606            )
1607            .unwrap(),
1608            Value::from(true),
1609        );
1610    }
1611}