1use 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#[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 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 pub fn resolve(self) -> TimeRange<SystemTime> {
70 self.resolve_at(SystemTime::now())
71 }
72
73 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 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 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 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 pub fn resolve_at(&self, now: SystemTime) -> SystemTime {
291 self.checked_resolve_at(now).unwrap()
292 }
293
294 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 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 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
412fn 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}