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}