emit_core/
extent.rs

1/*!
2The [`Extent`] type.
3
4An extent is the time for which an event is active. It may be either a point for an event that occurred at a particular time, or a range for an event that was active over a particular period.
5
6Extents can be constructed directly, or generically through the [`ToExtent`] trait.
7*/
8
9use crate::{
10    empty::Empty,
11    props::Props,
12    str::{Str, ToStr},
13    timestamp::Timestamp,
14    value::{ToValue, Value},
15    well_known::{KEY_TS, KEY_TS_START},
16};
17use core::{fmt, ops::ControlFlow, ops::Range, time::Duration};
18
19/**
20Either a single [`Timestamp`] for a point in time, or a pair of [`Timestamp`]s for a range.
21*/
22#[derive(Clone)]
23pub struct Extent {
24    range: Range<Timestamp>,
25    is_range: bool,
26}
27
28impl Extent {
29    /**
30    Create an extent for a point in time.
31    */
32    pub fn point(ts: Timestamp) -> Self {
33        Extent {
34            range: ts..ts,
35            is_range: false,
36        }
37    }
38
39    /**
40    Create an extent for a range.
41
42    The end of the range should be after the start, but an empty range is still considered a range.
43    */
44    pub fn range(ts: Range<Timestamp>) -> Self {
45        Extent {
46            range: ts,
47            is_range: true,
48        }
49    }
50
51    /**
52    Get the extent as a point in time.
53
54    For point extents, this will return exactly the value the extent was created from. For range extents, this will return the end bound.
55    */
56    pub fn as_point(&self) -> &Timestamp {
57        &self.range.end
58    }
59
60    /**
61    Try get the extent as a range.
62
63    This method will return `Some` if the extent is a range, even if that range is empty. It will return `None` for point extents.
64    */
65    pub fn as_range(&self) -> Option<&Range<Timestamp>> {
66        if self.is_range() {
67            Some(&self.range)
68        } else {
69            None
70        }
71    }
72
73    /**
74    Try get the length of the extent.
75
76    This method will return `Some` if the extent is a range, even if that range is empty. It will return `None` for point extents.
77    */
78    pub fn len(&self) -> Option<Duration> {
79        if self.is_range() {
80            self.range.end.duration_since(self.range.start)
81        } else {
82            None
83        }
84    }
85
86    /**
87    Whether the extent is a single point in time.
88    */
89    pub fn is_point(&self) -> bool {
90        !self.is_range()
91    }
92
93    /**
94    Whether the extent is a range.
95    */
96    pub fn is_range(&self) -> bool {
97        self.is_range
98    }
99}
100
101impl fmt::Debug for Extent {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        if self.is_range() {
104            fmt::Debug::fmt(&self.range.start, f)?;
105            f.write_str("..")?;
106            fmt::Debug::fmt(&self.range.end, f)
107        } else {
108            fmt::Debug::fmt(&self.range.end, f)
109        }
110    }
111}
112
113impl fmt::Display for Extent {
114    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
115        if self.is_range() {
116            fmt::Display::fmt(&self.range.start, f)?;
117            f.write_str("..")?;
118            fmt::Display::fmt(&self.range.end, f)
119        } else {
120            fmt::Display::fmt(&self.range.end, f)
121        }
122    }
123}
124
125/**
126Try convert a value into an [`Extent`].
127*/
128pub trait ToExtent {
129    /**
130    Perform the conversion.
131    */
132    fn to_extent(&self) -> Option<Extent>;
133}
134
135impl<'a, T: ToExtent + ?Sized> ToExtent for &'a T {
136    fn to_extent(&self) -> Option<Extent> {
137        (**self).to_extent()
138    }
139}
140
141impl ToExtent for Empty {
142    fn to_extent(&self) -> Option<Extent> {
143        None
144    }
145}
146
147impl<T: ToExtent> ToExtent for Option<T> {
148    fn to_extent(&self) -> Option<Extent> {
149        self.as_ref().and_then(|ts| ts.to_extent())
150    }
151}
152
153impl ToExtent for Extent {
154    fn to_extent(&self) -> Option<Extent> {
155        Some(self.clone())
156    }
157}
158
159impl ToExtent for Timestamp {
160    fn to_extent(&self) -> Option<Extent> {
161        Some(Extent::point(*self))
162    }
163}
164
165impl ToExtent for Range<Timestamp> {
166    fn to_extent(&self) -> Option<Extent> {
167        Some(Extent::range(self.clone()))
168    }
169}
170
171impl ToExtent for Range<Option<Timestamp>> {
172    fn to_extent(&self) -> Option<Extent> {
173        match (self.start, self.end) {
174            (Some(start), Some(end)) => (start..end).to_extent(),
175            (Some(start), None) => start.to_extent(),
176            (None, Some(end)) => end.to_extent(),
177            (None, None) => None::<Timestamp>.to_extent(),
178        }
179    }
180}
181
182impl Props for Extent {
183    fn for_each<'kv, F: FnMut(Str<'kv>, Value<'kv>) -> ControlFlow<()>>(
184        &'kv self,
185        mut for_each: F,
186    ) -> ControlFlow<()> {
187        if let Some(range) = self.as_range() {
188            for_each(KEY_TS_START.to_str(), range.start.to_value())?;
189            for_each(KEY_TS.to_str(), range.end.to_value())
190        } else {
191            for_each(KEY_TS.to_str(), self.as_point().to_value())
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn point() {
202        let ts = Extent::point(Timestamp::MIN);
203
204        assert!(ts.is_point());
205        assert!(!ts.is_range());
206
207        assert_eq!(&Timestamp::MIN, ts.as_point());
208
209        assert_eq!(None, ts.as_range());
210        assert_eq!(None, ts.len());
211    }
212
213    #[test]
214    fn range() {
215        let ts = Extent::range(Timestamp::MIN..Timestamp::MIN + Duration::from_secs(1));
216
217        assert!(!ts.is_point());
218        assert!(ts.is_range());
219
220        assert_eq!(&(Timestamp::MIN + Duration::from_secs(1)), ts.as_point());
221
222        assert_eq!(
223            &(Timestamp::MIN..Timestamp::MIN + Duration::from_secs(1)),
224            ts.as_range().unwrap(),
225        );
226        assert_eq!(
227            Some(&(Timestamp::MIN..Timestamp::MIN + Duration::from_secs(1))),
228            ts.as_range()
229        );
230        assert_eq!(Some(Duration::from_secs(1)), ts.len());
231    }
232
233    #[test]
234    fn range_empty() {
235        let ts = Extent::range(Timestamp::MIN..Timestamp::MIN);
236
237        assert!(!ts.is_point());
238        assert!(ts.is_range());
239
240        assert_eq!(&Timestamp::MIN, ts.as_point());
241
242        assert_eq!(Some(Duration::from_secs(0)), ts.len());
243    }
244
245    #[test]
246    fn range_backwards() {
247        let ts = Extent::range(Timestamp::MAX..Timestamp::MIN);
248
249        assert!(!ts.is_point());
250        assert!(ts.is_range());
251
252        assert_eq!(&Timestamp::MIN, ts.as_point());
253
254        assert!(ts.len().is_none());
255    }
256
257    #[test]
258    fn as_props() {
259        let ts = Extent::point(Timestamp::MIN);
260
261        assert_eq!(Timestamp::MIN, ts.pull::<Timestamp, _>("ts").unwrap());
262
263        let ts = Extent::range(Timestamp::MIN..Timestamp::MAX);
264
265        assert_eq!(Timestamp::MAX, ts.pull::<Timestamp, _>("ts").unwrap());
266        assert_eq!(Timestamp::MIN, ts.pull::<Timestamp, _>("ts_start").unwrap());
267    }
268}