proto_types/common/
interval.rs

1use std::cmp::Ordering;
2
3use thiserror::Error;
4
5use crate::{common::Interval, constants::NANOS_PER_SECOND, Duration, Timestamp};
6
7/// Errors that can occur during the creation, conversion or validation of an [`Interval`].
8#[derive(Debug, Error, PartialEq, Eq, Clone)]
9pub enum IntervalError {
10  #[error("Start and end time must be both defined or undefined")]
11  InvalidPairing,
12  #[error("Interval contains an invalid Timestamp")]
13  InvalidTimestamp,
14  #[error("Interval's end_time is before its start_time")]
15  EndTimeBeforeStartTime,
16  #[error("Interval arithmetic resulted in a value outside its representable range")]
17  OutOfRange,
18  #[error("Interval conversion error: {0}")]
19  ConversionError(String),
20}
21
22fn validate_interval(
23  start: Option<Timestamp>,
24  end: Option<Timestamp>,
25) -> Result<(), IntervalError> {
26  if end < start {
27    Err(IntervalError::EndTimeBeforeStartTime)
28  } else if !((start.is_some() && end.is_some()) || start.is_none() && end.is_none()) {
29    Err(IntervalError::InvalidPairing)
30  } else {
31    Ok(())
32  }
33}
34
35impl Interval {
36  /// Creates a new [`Interval`] instance with validation.
37  /// `end_time` must not be before `start_time`, and they must be both either set or unset.
38  pub fn new(
39    start_time: Option<Timestamp>,
40    end_time: Option<Timestamp>,
41  ) -> Result<Self, IntervalError> {
42    validate_interval(start_time, end_time)?;
43
44    Ok(Interval {
45      start_time,
46      end_time,
47    })
48  }
49
50  /// Checks that `end_time` is not before `start_time`. And that start and `end_time` are both either unspecified or specified at the same time.
51  pub fn is_valid(&self) -> bool {
52    validate_interval(self.start_time, self.end_time).is_ok()
53  }
54
55  /// Returns `true` if the `Interval` is empty (`start_time` equals `end_time`).
56  pub fn is_empty(&self) -> bool {
57    self
58      .start_time
59      .as_ref()
60      .zip(self.end_time.as_ref())
61      .map_or_else(|| false, |(start, end)| start == end)
62  }
63
64  /// Returns `true` if the `Interval` is unspecified (no `start_time` and no `end_time`)
65  pub fn is_unspecified(&self) -> bool {
66    self.start_time.is_none() && self.end_time.is_none()
67  }
68}
69
70impl TryFrom<Interval> for Duration {
71  type Error = IntervalError;
72  fn try_from(value: Interval) -> Result<Self, Self::Error> {
73    let result = value.start_time.zip(value.end_time).map(|(start, end)| {
74      let mut seconds_diff = end.seconds - start.seconds;
75      let mut nanos_diff = end.nanos - start.nanos;
76
77      if nanos_diff < 0 {
78        seconds_diff -= 1;
79        nanos_diff += NANOS_PER_SECOND;
80      } else if nanos_diff >= NANOS_PER_SECOND {
81        seconds_diff += 1;
82        nanos_diff -= NANOS_PER_SECOND;
83      }
84
85      Duration {
86        seconds: seconds_diff,
87        nanos: nanos_diff,
88      }
89    });
90
91    result.ok_or(IntervalError::ConversionError(
92      "Cannot convert to Duration due to missing start or end time".to_string(),
93    ))
94  }
95}
96
97impl PartialOrd for Interval {
98  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
99    if !self.is_valid() || !other.is_valid() {
100      return None;
101    }
102
103    if self.is_unspecified() {
104      if other.is_unspecified() {
105        Some(Ordering::Equal)
106      } else {
107        Some(Ordering::Less)
108      }
109    } else if other.is_unspecified() {
110      Some(Ordering::Greater)
111    } else {
112      let self_as_duration: Result<Duration, IntervalError> = (*self).try_into();
113      let other_as_duration: Result<Duration, IntervalError> = (*other).try_into();
114
115      if self_as_duration.is_ok() && other_as_duration.is_err() {
116        Some(Ordering::Greater)
117      } else if self_as_duration.is_err() && other_as_duration.is_ok() {
118        Some(Ordering::Less)
119      } else if self_as_duration.is_err() && other_as_duration.is_err() {
120        Some(Ordering::Equal)
121      } else {
122        self_as_duration
123          .unwrap()
124          .partial_cmp(&other_as_duration.unwrap())
125      }
126    }
127  }
128}