Skip to main content

zenoh_util/
time_range.rs

1//
2// Copyright (c) 2023 ZettaScale Technology
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7// which is available at https://www.apache.org/licenses/LICENSE-2.0.
8//
9// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10//
11// Contributors:
12//   ZettaScale Zenoh Team, <zenoh@zettascale.tech>
13//
14
15use std::{
16    convert::{TryFrom, TryInto},
17    fmt::Display,
18    ops::Add,
19    str::FromStr,
20    time::{Duration, SystemTime},
21};
22
23use humantime::{format_rfc3339, parse_rfc3339_weak};
24use zenoh_result::{bail, zerror, ZError};
25
26const U_TO_SECS: f64 = 0.000001;
27const MS_TO_SECS: f64 = 0.001;
28const M_TO_SECS: f64 = 60.0;
29const H_TO_SECS: f64 = M_TO_SECS * 60.0;
30const D_TO_SECS: f64 = H_TO_SECS * 24.0;
31const W_TO_SECS: f64 = D_TO_SECS * 7.0;
32
33/// The structural representation of the Zenoh Time DSL, which may adopt one of two syntax:
34/// - the "range" syntax: `<ldel: '[' | ']'><start: TimeExpr?>..<end: TimeExpr?><rdel: '[' | ']'>`
35/// - the "duration" syntax: `<ldel: '[' | ']'><start: TimeExpr>;<duration: Duration><rdel: '[' | ']'>`, which is
36///   equivalent to `<ldel><start>..<start+duration><rdel>`
37///
38/// Durations follow the `<duration: float><unit: "u", "ms", "s", "m", "h", "d", "w">` syntax.
39///
40/// Where [`TimeExpr`] itself may adopt one of two syntaxes:
41/// - the "instant" syntax, which must be a UTC [RFC3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp.
42/// - the "offset" syntax, which is written `now(<sign: '-'?><offset: Duration?>)`, and allows to specify a target instant as
43///   an offset applied to an instant of evaluation. These offset are resolved at the evaluation site.
44///
45/// In range syntax, omitting `<start>` and/or `<end>` implies that the range is unbounded in that direction.
46///
47/// Exclusive bounds are represented by their respective delimiters pointing towards the exterior.
48/// Interior bounds are represented by the opposite.
49///
50/// The comparison step for instants is the nanosecond, which makes exclusive and inclusive bounds extremely close.
51/// The `[<start>..<end>[` pattern may however be useful to guarantee that a same timestamp never appears twice when
52/// iteratively getting values for `[t0..t1[`, `[t1..t2[`, `[t2..t3[`...
53#[derive(Debug, Copy, Clone, PartialEq, Eq)]
54pub struct TimeRange<T = TimeExpr> {
55    pub start: TimeBound<T>,
56    pub end: TimeBound<T>,
57}
58
59impl TimeRange<TimeExpr> {
60    /// Resolves the offset bounds in the range using `now` as reference.
61    pub fn resolve_at(self, now: SystemTime) -> TimeRange<SystemTime> {
62        TimeRange {
63            start: self.start.resolve_at(now),
64            end: self.end.resolve_at(now),
65        }
66    }
67
68    /// Resolves the offset bounds in the range using [`SystemTime::now`] as reference.
69    pub fn resolve(self) -> TimeRange<SystemTime> {
70        self.resolve_at(SystemTime::now())
71    }
72
73    /// Returns `true` if the provided `instant` belongs to `self`.
74    ///
75    /// This method performs resolution with [`SystemTime::now`] if the bounds contain an "offset" time expression.
76    /// If you intend on performing this check multiple times, it may be wiser to resolve `self` first, and use
77    /// [`TimeRange::<SystemTime>::contains`] instead.
78    pub fn contains(&self, instant: SystemTime) -> bool {
79        let now = SystemTime::now();
80        match &self.start.resolve_at(now) {
81            TimeBound::Inclusive(t) if t > &instant => return false,
82            TimeBound::Exclusive(t) if t >= &instant => return false,
83            _ => {}
84        }
85        match &self.end.resolve_at(now) {
86            TimeBound::Inclusive(t) => t >= &instant,
87            TimeBound::Exclusive(t) => t > &instant,
88            _ => true,
89        }
90    }
91}
92
93impl TimeRange<SystemTime> {
94    /// Returns `true` if the provided `instant` belongs to `self`.
95    pub fn contains(&self, instant: SystemTime) -> bool {
96        match &self.start {
97            TimeBound::Inclusive(t) if *t > instant => return false,
98            TimeBound::Exclusive(t) if *t >= instant => return false,
99            _ => {}
100        }
101        match &self.end {
102            TimeBound::Inclusive(t) => *t >= instant,
103            TimeBound::Exclusive(t) => *t > instant,
104            _ => true,
105        }
106    }
107}
108
109impl From<TimeRange<SystemTime>> for TimeRange<TimeExpr> {
110    fn from(value: TimeRange<SystemTime>) -> Self {
111        TimeRange {
112            start: value.start.into(),
113            end: value.end.into(),
114        }
115    }
116}
117
118impl TryFrom<TimeRange<TimeExpr>> for TimeRange<SystemTime> {
119    type Error = ();
120    fn try_from(value: TimeRange<TimeExpr>) -> Result<Self, Self::Error> {
121        Ok(TimeRange {
122            start: value.start.try_into()?,
123            end: value.end.try_into()?,
124        })
125    }
126}
127
128impl Display for TimeRange<TimeExpr> {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        match &self.start {
131            TimeBound::Inclusive(t) => write!(f, "[{t}..")?,
132            TimeBound::Exclusive(t) => write!(f, "]{t}..")?,
133            TimeBound::Unbounded => f.write_str("[..")?,
134        }
135        match &self.end {
136            TimeBound::Inclusive(t) => write!(f, "{t}]"),
137            TimeBound::Exclusive(t) => write!(f, "{t}["),
138            TimeBound::Unbounded => f.write_str("]"),
139        }
140    }
141}
142
143impl Display for TimeRange<SystemTime> {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        match &self.start {
146            TimeBound::Inclusive(t) => write!(f, "[{}..", TimeExpr::Fixed(*t))?,
147            TimeBound::Exclusive(t) => write!(f, "]{}..", TimeExpr::Fixed(*t))?,
148            TimeBound::Unbounded => f.write_str("[..")?,
149        }
150        match &self.end {
151            TimeBound::Inclusive(t) => write!(f, "{}]", TimeExpr::Fixed(*t)),
152            TimeBound::Exclusive(t) => write!(f, "{}[", TimeExpr::Fixed(*t)),
153            TimeBound::Unbounded => f.write_str("]"),
154        }
155    }
156}
157
158impl FromStr for TimeRange<TimeExpr> {
159    type Err = ZError;
160    fn from_str(s: &str) -> Result<Self, Self::Err> {
161        // minimum str size is 4: "[..]"
162        let len = s.len();
163        if len < 4 {
164            bail!("Invalid TimeRange: {}", s);
165        }
166
167        let mut chars = s.chars();
168        let inclusive_start = match chars.next().unwrap() {
169            '[' => true,
170            ']' => false,
171            _ => bail!("Invalid TimeRange (must start with '[' or ']'): {}", s),
172        };
173        let inclusive_end = match chars.last().unwrap() {
174            ']' => true,
175            '[' => false,
176            _ => bail!("Invalid TimeRange (must end with '[' or ']'): {}", s),
177        };
178
179        let s = &s[1..len - 1];
180        if let Some((start, end)) = s.split_once("..") {
181            Ok(TimeRange {
182                start: parse_time_bound(start, inclusive_start)?,
183                end: parse_time_bound(end, inclusive_end)?,
184            })
185        } else if let Some((start, duration)) = s.split_once(';') {
186            let start_bound = parse_time_bound(start, inclusive_start)?;
187            let duration = parse_duration(duration)?;
188            let end_bound = match &start_bound {
189                TimeBound::Inclusive(time) | TimeBound::Exclusive(time) => {
190                    if inclusive_end {
191                        TimeBound::Inclusive(time + duration)
192                    } else {
193                        TimeBound::Exclusive(time + duration)
194                    }
195                }
196                TimeBound::Unbounded => bail!(
197                    r#"Invalid TimeRange (';' must contain a time and a duration)"): {}"#,
198                    s
199                ),
200            };
201            Ok(TimeRange {
202                start: start_bound,
203                end: end_bound,
204            })
205        } else {
206            bail!(
207                r#"Invalid TimeRange (must contain ".." or ";" as separator)"): {}"#,
208                s
209            )
210        }
211    }
212}
213
214#[derive(Debug, Copy, Clone, PartialEq, Eq)]
215pub enum TimeBound<T> {
216    Inclusive(T),
217    Exclusive(T),
218    Unbounded,
219}
220
221impl From<TimeBound<SystemTime>> for TimeBound<TimeExpr> {
222    fn from(value: TimeBound<SystemTime>) -> Self {
223        match value {
224            TimeBound::Inclusive(t) => TimeBound::Inclusive(t.into()),
225            TimeBound::Exclusive(t) => TimeBound::Exclusive(t.into()),
226            TimeBound::Unbounded => TimeBound::Unbounded,
227        }
228    }
229}
230
231impl TryFrom<TimeBound<TimeExpr>> for TimeBound<SystemTime> {
232    type Error = ();
233    fn try_from(value: TimeBound<TimeExpr>) -> Result<Self, Self::Error> {
234        Ok(match value {
235            TimeBound::Inclusive(t) => TimeBound::Inclusive(t.try_into()?),
236            TimeBound::Exclusive(t) => TimeBound::Exclusive(t.try_into()?),
237            TimeBound::Unbounded => TimeBound::Unbounded,
238        })
239    }
240}
241
242impl TimeBound<TimeExpr> {
243    /// Resolves `self` into a [`TimeBound<SystemTime>`], using `now` as a reference for offset expressions.
244    /// If `self` is time boundary that cannot be represented as `SystemTime` (which means it’s not inside
245    /// the bounds of the underlying data structure), then `TimeBound::Unbounded` is returned.
246    pub fn resolve_at(self, now: SystemTime) -> TimeBound<SystemTime> {
247        match self {
248            TimeBound::Inclusive(t) => match t.checked_resolve_at(now) {
249                Some(ts) => TimeBound::Inclusive(ts),
250                None => TimeBound::Unbounded,
251            },
252            TimeBound::Exclusive(t) => match t.checked_resolve_at(now) {
253                Some(ts) => TimeBound::Exclusive(ts),
254                None => TimeBound::Unbounded,
255            },
256            TimeBound::Unbounded => TimeBound::Unbounded,
257        }
258    }
259}
260
261#[derive(Debug, Copy, Clone, PartialEq)]
262pub enum TimeExpr {
263    Fixed(SystemTime),
264    Now { offset_secs: f64 },
265}
266
267impl From<SystemTime> for TimeExpr {
268    fn from(t: SystemTime) -> Self {
269        Self::Fixed(t)
270    }
271}
272
273impl TryFrom<TimeExpr> for SystemTime {
274    type Error = ();
275    fn try_from(value: TimeExpr) -> Result<Self, Self::Error> {
276        match value {
277            TimeExpr::Fixed(t) => Ok(t),
278            TimeExpr::Now { .. } => Err(()),
279        }
280    }
281}
282
283impl TimeExpr {
284    /// Resolves `self` into a [`SystemTime`], using `now` as a reference for offset expressions.
285    ///
286    ///# Panics
287    ///
288    /// This function may panic if the resulting point in time cannot be represented by the
289    /// underlying data structure. See [`TimeExpr::checked_resolve_at`] for a version without panic.
290    pub fn resolve_at(&self, now: SystemTime) -> SystemTime {
291        self.checked_resolve_at(now).unwrap()
292    }
293
294    /// Resolves `self` into a [`SystemTime`], using `now` as a reference for offset expressions.
295    /// If `self` is a `TimeExpr::Now{offset_secs}` and adding `offset_secs` to `now` results in a time
296    /// that would be outside the bounds of the underlying data structure, `None` is returned.
297    pub fn checked_resolve_at(&self, now: SystemTime) -> Option<SystemTime> {
298        match self {
299            TimeExpr::Fixed(t) => Some(*t),
300            TimeExpr::Now { offset_secs } => checked_duration_add(now, *offset_secs),
301        }
302    }
303    /// Adds `duration` to `self`, returning `None` if `self` is a `Fixed(SystemTime)` and adding the duration is not possible
304    /// because the result would be outside the bounds of the underlying data structure (see [`SystemTime::checked_add`]).
305    /// Otherwise returns `Some(time_expr)`.
306    pub fn checked_add(&self, duration: f64) -> Option<Self> {
307        match self {
308            Self::Fixed(time) => checked_duration_add(*time, duration).map(Self::Fixed),
309            Self::Now { offset_secs } => Some(Self::Now {
310                offset_secs: offset_secs + duration,
311            }),
312        }
313    }
314    /// Subtracts `duration` from `self`, returning `None` if `self` is a `Fixed(SystemTime)` and subtracting the duration is not possible
315    /// because the result would be outside the bounds of the underlying data structure (see [`SystemTime::checked_sub`]).
316    /// Otherwise returns `Some(time_expr)`.
317    pub fn checked_sub(&self, duration: f64) -> Option<Self> {
318        match self {
319            Self::Fixed(time) => checked_duration_add(*time, -duration).map(Self::Fixed),
320            Self::Now { offset_secs } => Some(Self::Now {
321                offset_secs: offset_secs - duration,
322            }),
323        }
324    }
325}
326
327impl Display for TimeExpr {
328    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329        match self {
330            TimeExpr::Fixed(time) => {
331                write!(f, "{}", format_rfc3339(*time))
332            }
333            TimeExpr::Now { offset_secs } => {
334                if *offset_secs == 0.0 {
335                    f.write_str("now()")
336                } else {
337                    write!(f, "now({offset_secs}s)")
338                }
339            }
340        }
341    }
342}
343
344impl FromStr for TimeExpr {
345    type Err = ZError;
346    fn from_str(s: &str) -> Result<Self, Self::Err> {
347        if s.starts_with("now(") && s.ends_with(')') {
348            let s = &s[4..s.len() - 1];
349            if s.is_empty() {
350                Ok(TimeExpr::Now { offset_secs: 0.0 })
351            } else {
352                match s.chars().next().unwrap() {
353                    '-' => parse_duration(&s[1..]).map(|f| TimeExpr::Now { offset_secs: -f }),
354                    _ => parse_duration(s).map(|f| TimeExpr::Now { offset_secs: f }),
355                }
356            }
357        } else {
358            parse_rfc3339_weak(s)
359                .map_err(|e| zerror!(e))
360                .map(TimeExpr::Fixed)
361        }
362        .map_err(|e| zerror!(r#"Invalid time "{}" ({})"#, s, e))
363    }
364}
365
366impl Add<f64> for TimeExpr {
367    type Output = Self;
368    fn add(self, duration: f64) -> Self {
369        match self {
370            Self::Fixed(time) => Self::Fixed(time + Duration::from_secs_f64(duration)),
371            Self::Now { offset_secs } => Self::Now {
372                offset_secs: offset_secs + duration,
373            },
374        }
375    }
376}
377
378impl Add<f64> for &TimeExpr {
379    type Output = TimeExpr;
380    fn add(self, duration: f64) -> TimeExpr {
381        match self {
382            TimeExpr::Fixed(time) => TimeExpr::Fixed((*time) + Duration::from_secs_f64(duration)),
383            TimeExpr::Now { offset_secs } => TimeExpr::Now {
384                offset_secs: offset_secs + duration,
385            },
386        }
387    }
388}
389
390fn checked_duration_add(t: SystemTime, duration: f64) -> Option<SystemTime> {
391    if duration >= 0.0 {
392        Duration::try_from_secs_f64(duration)
393            .ok()
394            .and_then(|d| t.checked_add(d))
395    } else {
396        Duration::try_from_secs_f64(-duration)
397            .ok()
398            .and_then(|d| t.checked_sub(d))
399    }
400}
401
402fn parse_time_bound(s: &str, inclusive: bool) -> Result<TimeBound<TimeExpr>, ZError> {
403    if s.is_empty() {
404        Ok(TimeBound::Unbounded)
405    } else if inclusive {
406        Ok(TimeBound::Inclusive(s.parse()?))
407    } else {
408        Ok(TimeBound::Exclusive(s.parse()?))
409    }
410}
411
412/// Parses a &str as a Duration.
413/// Expected format is a f64 in seconds, or "<f64><unit>" where <unit> is:
414///  - 'u'  => microseconds
415///  - "ms" => milliseconds
416///  - 's' => seconds
417///  - 'm' => minutes
418///  - 'h' => hours
419///  - 'd' => days
420///  - 'w' => weeks
421fn parse_duration(s: &str) -> Result<f64, ZError> {
422    if s.is_empty() {
423        bail!(
424            r#"Invalid duration: "" (expected format: <f64> (in seconds) or <f64><unit>. Accepted units: u, ms, s, m, h, d or w.)"#
425        );
426    }
427    let mut it = s.bytes().enumerate().rev();
428    match it.next().unwrap() {
429        (i, b'u') => s[..i].parse::<f64>().map(|u| U_TO_SECS * u),
430        (_, b's') => match it.next().unwrap() {
431            (i, b'm') => s[..i].parse::<f64>().map(|ms| MS_TO_SECS * ms),
432            (i, _) => s[..i + 1].parse::<f64>(),
433        },
434        (i, b'm') => s[..i].parse::<f64>().map(|m| M_TO_SECS * m),
435        (i, b'h') => s[..i].parse::<f64>().map(|h| H_TO_SECS * h),
436        (i, b'd') => s[..i].parse::<f64>().map(|d| D_TO_SECS * d),
437        (i, b'w') => s[..i].parse::<f64>().map(|w| W_TO_SECS * w),
438        _ => s.parse::<f64>(),
439    }
440    .map_err(|e| zerror!(r#"Invalid duration "{}" ({})"#, s, e))
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn test_time_range_contains() {
449        assert!("[now(-1s)..now(1s)]"
450            .parse::<TimeRange>()
451            .unwrap()
452            .contains(SystemTime::now()));
453        assert!(!"[now(-2s)..now(-1s)]"
454            .parse::<TimeRange>()
455            .unwrap()
456            .contains(SystemTime::now()));
457        assert!(!"[now(1s)..now(2s)]"
458            .parse::<TimeRange>()
459            .unwrap()
460            .contains(SystemTime::now()));
461
462        assert!("[now(-1m)..]"
463            .parse::<TimeRange>()
464            .unwrap()
465            .contains(SystemTime::now()));
466        assert!("[..now(1m)]"
467            .parse::<TimeRange>()
468            .unwrap()
469            .contains(SystemTime::now()));
470
471        assert!("[..]"
472            .parse::<TimeRange>()
473            .unwrap()
474            .contains(SystemTime::UNIX_EPOCH));
475        assert!("[..]"
476            .parse::<TimeRange>()
477            .unwrap()
478            .contains(SystemTime::now()));
479
480        assert!("[1970-01-01T00:00:00Z..]"
481            .parse::<TimeRange>()
482            .unwrap()
483            .contains(SystemTime::UNIX_EPOCH));
484        assert!("[..1970-01-01T00:00:00Z]"
485            .parse::<TimeRange>()
486            .unwrap()
487            .contains(SystemTime::UNIX_EPOCH));
488        assert!(!"]1970-01-01T00:00:00Z..]"
489            .parse::<TimeRange>()
490            .unwrap()
491            .contains(SystemTime::UNIX_EPOCH));
492        assert!(!"[..1970-01-01T00:00:00Z["
493            .parse::<TimeRange>()
494            .unwrap()
495            .contains(SystemTime::UNIX_EPOCH));
496    }
497
498    #[test]
499    fn test_parse_time_range() {
500        use TimeBound::*;
501        assert_eq!(
502            "[..]".parse::<TimeRange>().unwrap(),
503            TimeRange {
504                start: Unbounded,
505                end: Unbounded
506            }
507        );
508        assert_eq!(
509            "[now(-1h)..now(1h)]".parse::<TimeRange>().unwrap(),
510            TimeRange {
511                start: Inclusive(TimeExpr::Now {
512                    offset_secs: -3600.0
513                }),
514                end: Inclusive(TimeExpr::Now {
515                    offset_secs: 3600.0
516                })
517            }
518        );
519        assert_eq!(
520            "]now(-1h)..now(1h)[".parse::<TimeRange>().unwrap(),
521            TimeRange {
522                start: Exclusive(TimeExpr::Now {
523                    offset_secs: -3600.0
524                }),
525                end: Exclusive(TimeExpr::Now {
526                    offset_secs: 3600.0
527                })
528            }
529        );
530
531        assert!("".parse::<TimeExpr>().is_err());
532        assert!("[;]".parse::<TimeExpr>().is_err());
533        assert!("[;1h]".parse::<TimeExpr>().is_err());
534    }
535
536    #[test]
537    fn test_parse_time_expr() {
538        assert_eq!(
539            "2022-06-30T01:02:03.226942997Z"
540                .parse::<TimeExpr>()
541                .unwrap(),
542            TimeExpr::Fixed(humantime::parse_rfc3339("2022-06-30T01:02:03.226942997Z").unwrap())
543        );
544        assert_eq!(
545            "2022-06-30T01:02:03Z".parse::<TimeExpr>().unwrap(),
546            TimeExpr::Fixed(humantime::parse_rfc3339("2022-06-30T01:02:03Z").unwrap())
547        );
548        assert_eq!(
549            "2022-06-30T01:02:03".parse::<TimeExpr>().unwrap(),
550            TimeExpr::Fixed(humantime::parse_rfc3339("2022-06-30T01:02:03Z").unwrap())
551        );
552        assert_eq!(
553            "2022-06-30 01:02:03Z".parse::<TimeExpr>().unwrap(),
554            TimeExpr::Fixed(humantime::parse_rfc3339("2022-06-30T01:02:03Z").unwrap())
555        );
556        assert_eq!(
557            "now()".parse::<TimeExpr>().unwrap(),
558            TimeExpr::Now { offset_secs: 0.0 }
559        );
560        assert_eq!(
561            "now(0)".parse::<TimeExpr>().unwrap(),
562            TimeExpr::Now { offset_secs: 0.0 }
563        );
564        assert_eq!(
565            "now(123.45)".parse::<TimeExpr>().unwrap(),
566            TimeExpr::Now {
567                offset_secs: 123.45
568            }
569        );
570        assert_eq!(
571            "now(1h)".parse::<TimeExpr>().unwrap(),
572            TimeExpr::Now {
573                offset_secs: 3600.0
574            }
575        );
576        assert_eq!(
577            "now(-1h)".parse::<TimeExpr>().unwrap(),
578            TimeExpr::Now {
579                offset_secs: -3600.0
580            }
581        );
582
583        assert!("".parse::<TimeExpr>().is_err());
584        assert!("1h".parse::<TimeExpr>().is_err());
585        assert!("2020-11-05".parse::<TimeExpr>().is_err());
586    }
587
588    #[test]
589    fn test_add_time_expr() {
590        let t = TimeExpr::Now { offset_secs: 0.0 };
591        assert_eq!(
592            t.checked_add(3600.0),
593            Some(TimeExpr::Now {
594                offset_secs: 3600.0
595            })
596        );
597        assert_eq!(
598            t.checked_add(-3600.0),
599            Some(TimeExpr::Now {
600                offset_secs: -3600.0
601            })
602        );
603        assert_eq!(
604            t.checked_sub(3600.0),
605            Some(TimeExpr::Now {
606                offset_secs: -3600.0
607            })
608        );
609        assert_eq!(
610            t.checked_sub(-3600.0),
611            Some(TimeExpr::Now {
612                offset_secs: 3600.0
613            })
614        );
615
616        let t = TimeExpr::Fixed(SystemTime::UNIX_EPOCH);
617        assert_eq!(
618            t.checked_add(3600.0),
619            Some(TimeExpr::Fixed(
620                SystemTime::UNIX_EPOCH + Duration::from_secs_f64(3600.0)
621            ))
622        );
623        assert_eq!(
624            t.checked_add(-3600.0),
625            Some(TimeExpr::Fixed(
626                SystemTime::UNIX_EPOCH - Duration::from_secs_f64(3600.0)
627            ))
628        );
629        assert_eq!(
630            t.checked_sub(3600.0),
631            Some(TimeExpr::Fixed(
632                SystemTime::UNIX_EPOCH - Duration::from_secs_f64(3600.0)
633            ))
634        );
635        assert_eq!(
636            t.checked_sub(-3600.0),
637            Some(TimeExpr::Fixed(
638                SystemTime::UNIX_EPOCH + Duration::from_secs_f64(3600.0)
639            ))
640        );
641
642        assert_eq!(t.checked_add(f64::MAX), None);
643        assert_eq!(t.checked_sub(f64::MAX), None);
644    }
645
646    #[test]
647    fn test_resolve_time_expr() {
648        let now = SystemTime::now();
649
650        assert_eq!(
651            TimeExpr::Now { offset_secs: 0.0 }.checked_resolve_at(now),
652            Some(now)
653        );
654        assert_eq!(
655            TimeExpr::Now {
656                offset_secs: f64::MAX
657            }
658            .checked_resolve_at(now),
659            None
660        );
661
662        let t = TimeExpr::Fixed(SystemTime::UNIX_EPOCH);
663        assert_eq!(t.checked_resolve_at(now), Some(SystemTime::UNIX_EPOCH));
664    }
665
666    #[test]
667    fn test_parse_duration() {
668        assert_eq!(parse_duration("0").unwrap(), 0.0);
669        assert_eq!(parse_duration("1.2").unwrap(), 1.2);
670        assert_eq!(parse_duration("1u").unwrap(), 0.000001);
671        assert_eq!(parse_duration("2u").unwrap(), 0.000002);
672        assert_eq!(parse_duration("1.5ms").unwrap(), 0.0015);
673        assert_eq!(parse_duration("100ms").unwrap(), 0.1);
674        assert_eq!(parse_duration("10s").unwrap(), 10.0);
675        assert_eq!(parse_duration("0.5s").unwrap(), 0.5);
676        assert_eq!(parse_duration("1.1m").unwrap(), 66.0);
677        assert_eq!(parse_duration("1.5h").unwrap(), 5400.0);
678        assert_eq!(parse_duration("1d").unwrap(), 86400.0);
679        assert_eq!(parse_duration("1w").unwrap(), 604800.0);
680
681        assert!(parse_duration("").is_err());
682        assert!(parse_duration("1x").is_err());
683        assert!(parse_duration("abcd").is_err());
684        assert!(parse_duration("4mm").is_err());
685        assert!(parse_duration("1h4m").is_err());
686    }
687}