chrono_period/
lib.rs

1extern crate chrono;
2
3use chrono::{Duration, NaiveDateTime};
4
5use std::cmp::{min, max};
6
7/// A period of time between two ISO 8601 dates/times ([NaiveDateTime](NaiveDateTime)s).
8/// This is similar to a [`Duration`](chrono::Duration), except that it has a fixed start
9/// date/time (and thus a fixed end date/time), allowing clients to determine if specific
10/// segments of time intersect.
11///
12/// # Notes
13/// This period is not reliant on time zones. For the time being, offsets aren't considered at all.
14/// As such, if you need to interset two time periods that are of differing time zones, you're out
15/// of luck (patches accepted, though).
16#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
17pub struct NaivePeriod {
18    /// The date at which the period begins.
19    pub start: NaiveDateTime,
20
21    /// The date at which the period ends. This is inclusive, meaning that the period runs up to
22    /// and including this date/time.
23    pub end: NaiveDateTime
24}
25
26impl NaivePeriod {
27    /// Create a new `NaivePeriod` from two [`NaiveDateTime`](chrono::NaiveDateTime) objects.
28    ///
29    /// # Arguments
30    /// - `start`: A `NaiveDateTime` representing when the `NaivePeriod` will start.
31    /// - `end`: A `NaiveDateTime` representing when the `NaivePeriod` will end. Note that this
32    ///   date/time will be included in the period itself.
33    ///
34    /// # Returns
35    /// - A `NaivePeriod` object having the specified start and end `NaiveDateTime`s.
36    ///
37    /// # Example
38    /// ```
39    /// # use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
40    /// # use chrono_period::NaivePeriod;
41    /// let start = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
42    /// let end = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 1), NaiveTime::from_hms(0, 0, 0));
43    ///
44    /// let np1 = NaivePeriod::new(start, end);
45    ///
46    /// assert_eq!(np1.duration(), Duration::days(366));
47    /// ```
48    #[inline]
49    pub fn new(start: NaiveDateTime, end: NaiveDateTime) -> Self {
50        NaivePeriod { start: start, end: end }
51    }
52
53    /// Create a new `NaivePeriod` from a [`NaiveDateTime`](chrono::NaiveDateTime) object and a
54    /// [`Duration`](chrono::Duration) object.
55    ///
56    /// # Arguments
57    /// - `start`: A `NaiveDateTime` representing when the `NaivePeriod` will start.
58    /// - `duration`: A `Duration` object representing the the length of time this `NaivePeriod`
59    ///   will cover.
60    ///
61    /// # Returns
62    /// - A `NaivePeriod` object having the specified start `NaiveDateTime` and length of the
63    ///   specified `Duration`.
64    ///
65    /// # Example
66    /// ```
67    /// # use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
68    /// # use chrono_period::NaivePeriod;
69    /// let start = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(12, 0, 0));
70    ///
71    /// let np = NaivePeriod::from_start_duration(start, Duration::days(366));
72    ///
73    /// assert_eq!(Duration::days(366), np.duration());
74    /// ```
75    #[inline]
76    pub fn from_start_duration(start: NaiveDateTime, duration: Duration) -> Self {
77        NaivePeriod { start: start, end: start + duration }
78    }
79
80    /// Retrieve the [Duration](chrono::Duration) this `NaivePeriod` covers.
81    #[inline]
82    pub fn duration(&self) -> Duration {
83        self.end - self.start
84    }
85
86    /// Retrieve the intersection of this `NaivePeriod` with another `NaivePeriod`, if one exists.
87    ///
88    /// # Arguments
89    /// - `other`: A `NaivePeriod` to intersect with this `NaivePeriod`
90    ///
91    /// # Returns
92    /// - An `Option` containing either the intersection of the two `NaivePeriod`s, if they
93    ///   overlap; `None`, otherwise.
94    ///
95    /// # Examples
96    /// ```
97    /// # use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
98    /// # use chrono_period::NaivePeriod;
99    /// let start1 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
100    ///
101    /// let np1 = NaivePeriod::from_start_duration(start1, Duration::days(366));
102    ///
103    /// let start2 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
104    /// let end = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 1), NaiveTime::from_hms(0, 0, 0));
105    ///
106    /// let np2 = NaivePeriod::new(start2, end);
107    ///
108    /// let intersection = np1.get_intersection_with(np2);
109    ///
110    /// assert!(intersection.is_some());
111    /// assert_eq!(Duration::days(366), intersection.unwrap().duration())
112    /// ```
113    ///
114    /// ```
115    /// # use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
116    /// # use chrono_period::NaivePeriod;
117    ///
118    /// let start1 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
119    /// let end1 = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 1), NaiveTime::from_hms(0, 0, 0));
120    ///
121    /// let np1 = NaivePeriod::new(start1, end1);
122    ///
123    /// let start2 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 12, 1), NaiveTime::from_hms(0, 0, 0));
124    /// let end2 = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 14), NaiveTime::from_hms(0, 0, 0));
125    ///
126    /// let np2 = NaivePeriod::new(start2, end2);
127    ///
128    /// let start3 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 12, 1), NaiveTime::from_hms(0, 0, 0));
129    /// let end3 = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 1), NaiveTime::from_hms(0, 0, 0));
130    ///
131    /// let np3 = NaivePeriod::new(start3, end3);
132    ///
133    /// let intersection = np1.get_intersection_with(np2);
134    ///
135    /// assert_eq!(np3, intersection.unwrap());
136    /// ```
137    pub fn get_intersection_with(&self, other: NaivePeriod) -> Option<NaivePeriod> {
138        // If the start and end of other are both before self.start or both after self.end,
139        // then there is no intersection.
140        let other_start_ts = other.start.timestamp();
141        let other_end_ts = other.end.timestamp();
142
143        if (other_start_ts < self.start.timestamp() && other_end_ts < self.start.timestamp())
144           || (other_end_ts > self.end.timestamp() && other_start_ts > self.end.timestamp()) {
145           return None;
146        }
147
148        // The naive time period we want is from the maximum of other_start_ts and
149        // self.start.timestamp() and the minimum of other_end_ts and self.end.timestamp().
150        let start_ts = max(other_start_ts, self.start.timestamp());
151        let end_ts = min(other_end_ts, self.end.timestamp());
152
153        Some(NaivePeriod {
154            start: NaiveDateTime::from_timestamp(start_ts, 0),
155            end: NaiveDateTime::from_timestamp(end_ts, 0)
156        })
157    }
158
159    /// Determine if this `NaivePeriod` intersects with another `NaivePeriod`.
160    ///
161    /// # Arguments
162    /// - `other`: A `NaivePeriod` to intersect with this `NaivePeriod`
163    ///
164    /// # Returns
165    /// - `true`, if this `NaivePeriod` and `other` overlap in some finite time period; `false`,
166    ///   otherwise
167    ///
168    /// # Example
169    /// ```
170    /// # use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
171    /// # use chrono_period::NaivePeriod;
172    /// let start1 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
173    ///
174    /// let np1 = NaivePeriod::from_start_duration(start1, Duration::days(366));
175    ///
176    /// let start2 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
177    /// let end = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 1), NaiveTime::from_hms(0, 0, 0));
178    ///
179    /// let np2 = NaivePeriod::new(start2, end);
180    ///
181    /// assert!(np1.intersects_with(np2))
182    /// ```
183    #[inline]
184    pub fn intersects_with(&self, other: NaivePeriod) -> bool {
185        self.get_intersection_with(other).is_some()
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
192    use super::NaivePeriod;
193
194    #[test]
195    fn test_creation_of_naive_period() {
196        let start = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
197        let end = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 1), NaiveTime::from_hms(0, 0, 0));
198
199        let np1 = NaivePeriod::new(start, end);
200
201        assert_eq!(np1.duration(), Duration::days(366));
202    }
203
204    #[test]
205    fn test_intersection_of_year_and_single_day() {
206        let start1 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
207        let end1 = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 1), NaiveTime::from_hms(0, 0, 0));
208
209        let np1 = NaivePeriod::new(start1, end1);
210
211        let start2 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
212        let end2 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 2), NaiveTime::from_hms(0, 0, 0));
213
214        let np2 = NaivePeriod::new(start2, end2);
215
216        let intersection = np1.get_intersection_with(np2);
217
218        assert_eq!(intersection.unwrap(), np2);
219
220        // It should also be commutative
221        assert_eq!(intersection, np2.get_intersection_with(np1));
222    }
223
224    #[test]
225    fn test_intersection_that_creates_a_new_period() {
226        let start1 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
227        let end1 = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 1), NaiveTime::from_hms(0, 0, 0));
228
229        let np1 = NaivePeriod::new(start1, end1);
230
231        let start2 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 12, 1), NaiveTime::from_hms(0, 0, 0));
232        let end2 = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 14), NaiveTime::from_hms(0, 0, 0));
233
234        let np2 = NaivePeriod::new(start2, end2);
235
236        let start3 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 12, 1), NaiveTime::from_hms(0, 0, 0));
237        let end3 = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 1), NaiveTime::from_hms(0, 0, 0));
238
239        let np3 = NaivePeriod::new(start3, end3);
240
241        let intersection = np1.get_intersection_with(np2);
242
243        assert_eq!(np3, intersection.unwrap());
244    }
245
246    #[test]
247    fn test_intersection_of_disjoint_periods() {
248        let start1 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
249        let end1 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 4, 12), NaiveTime::from_hms(0, 0, 0));
250
251        let np1 = NaivePeriod::new(start1, end1);
252
253        let start2 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 9, 1), NaiveTime::from_hms(0, 0, 0));
254        let end2 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 9, 18), NaiveTime::from_hms(0, 0, 0));
255
256        let np2 = NaivePeriod::new(start2, end2);
257
258        let intersection = np2.get_intersection_with(np1);
259
260        assert!(intersection.is_none())
261    }
262
263    #[test]
264    fn test_creation_of_naive_period_from_duration() {
265        let start = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(12, 0, 0));
266
267        let np = NaivePeriod::from_start_duration(start, Duration::days(366));
268
269        assert_eq!(Duration::days(366), np.duration());
270    }
271
272    #[test]
273    fn test_intersects_with() {
274        let start1 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
275
276        let np1 = NaivePeriod::from_start_duration(start1, Duration::days(366));
277
278        let start2 = NaiveDateTime::new(NaiveDate::from_ymd(2020, 1, 1), NaiveTime::from_hms(0, 0, 0));
279        let end = NaiveDateTime::new(NaiveDate::from_ymd(2021, 1, 1), NaiveTime::from_hms(0, 0, 0));
280
281        let np2 = NaivePeriod::new(start2, end);
282
283        assert!(np1.intersects_with(np2))
284    }
285}