chronoutil/
relative_duration.rs

1//! Implements a RelativeDuration extending Chrono's Duration to shift by months and years.
2use core::ops::{Add, Div, Mul, Neg, Sub};
3use std::time::Duration as StdDuration;
4
5use chrono::{Date, DateTime, Duration, NaiveDate, NaiveDateTime, TimeZone};
6
7use super::delta::shift_months;
8
9mod parse;
10
11/// Relative time duration extending Chrono's Duration.
12#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
13pub struct RelativeDuration {
14    months: i32, // Sorry, cosmologists..
15    duration: Duration,
16}
17
18impl From<Duration> for RelativeDuration {
19    /// Makes a new `RelativeDuration` from a `chrono::Duration`.
20    #[inline]
21    fn from(item: Duration) -> Self {
22        RelativeDuration {
23            months: 0,
24            duration: item,
25        }
26    }
27}
28
29impl From<StdDuration> for RelativeDuration {
30    /// Makes a new `RelativeDuration` from a std `Duration`.
31    #[inline]
32    fn from(item: StdDuration) -> Self {
33        RelativeDuration::from(
34            Duration::from_std(item).expect("RelativeDuration::from_std OutOfRangeError"),
35        )
36    }
37}
38
39impl RelativeDuration {
40    /// Makes a new `RelativeDuration` with given number of years.
41    ///
42    /// Equivalent to `RelativeDuration::months(years * 12)` with overflow checks.
43    /// Panics when the duration is out of bounds.
44    #[inline]
45    pub fn years(years: i32) -> RelativeDuration {
46        let months = years
47            .checked_mul(12)
48            .expect("RelativeDuration::years out of bounds");
49        RelativeDuration::months(months)
50    }
51
52    /// Makes a new `RelativeDuration` with given number of months.
53    /// Panics when the duration is out of bounds.
54    #[inline]
55    pub fn months(months: i32) -> RelativeDuration {
56        RelativeDuration {
57            months,
58            duration: Duration::zero(),
59        }
60    }
61
62    /// Makes a new `RelativeDuration` with given number of weeks.
63    /// Panics when the duration is out of bounds.
64    #[inline]
65    pub fn weeks(weeks: i64) -> RelativeDuration {
66        RelativeDuration {
67            months: 0,
68            duration: Duration::weeks(weeks),
69        }
70    }
71
72    /// Makes a new `RelativeDuration` with given number of days.
73    /// Panics when the duration is out of bounds.
74    #[inline]
75    pub fn days(days: i64) -> RelativeDuration {
76        RelativeDuration {
77            months: 0,
78            duration: Duration::days(days),
79        }
80    }
81
82    /// Makes a new `RelativeDuration` with given number of hours.
83    /// Panics when the duration is out of bounds.
84    #[inline]
85    pub fn hours(hours: i64) -> RelativeDuration {
86        RelativeDuration {
87            months: 0,
88            duration: Duration::hours(hours),
89        }
90    }
91
92    /// Makes a new `RelativeDuration` with given number of minutes.
93    /// Panics when the duration is out of bounds.
94    #[inline]
95    pub fn minutes(minutes: i64) -> RelativeDuration {
96        RelativeDuration {
97            months: 0,
98            duration: Duration::minutes(minutes),
99        }
100    }
101
102    /// Makes a new `RelativeDuration` with given number of seconds.
103    /// Panics when the duration is out of bounds.
104    #[inline]
105    pub fn seconds(seconds: i64) -> RelativeDuration {
106        RelativeDuration {
107            months: 0,
108            duration: Duration::seconds(seconds),
109        }
110    }
111
112    /// Makes a new `RelativeDuration` with given number of milliseconds.
113    #[inline]
114    pub fn milliseconds(milliseconds: i64) -> RelativeDuration {
115        RelativeDuration {
116            months: 0,
117            duration: Duration::milliseconds(milliseconds),
118        }
119    }
120
121    /// Makes a new `RelativeDuration` with given number of microseconds.
122    #[inline]
123    pub fn microseconds(microseconds: i64) -> RelativeDuration {
124        RelativeDuration {
125            months: 0,
126            duration: Duration::microseconds(microseconds),
127        }
128    }
129
130    /// Makes a new `RelativeDuration` with given number of nanoseconds.
131    #[inline]
132    pub fn nanoseconds(nanos: i64) -> RelativeDuration {
133        RelativeDuration {
134            months: 0,
135            duration: Duration::nanoseconds(nanos),
136        }
137    }
138
139    /// Update the `Duration` part of the current `RelativeDuration`.
140    #[inline]
141    pub fn with_duration(self, duration: Duration) -> RelativeDuration {
142        RelativeDuration {
143            months: self.months,
144            duration,
145        }
146    }
147
148    /// A `RelativeDuration` representing zero.
149    #[inline]
150    pub fn zero() -> RelativeDuration {
151        RelativeDuration {
152            months: 0,
153            duration: Duration::zero(),
154        }
155    }
156
157    /// Returns true if the duration equals RelativeDuration::zero().
158    #[inline]
159    pub fn is_zero(&self) -> bool {
160        self.months == 0 && self.duration.is_zero()
161    }
162}
163
164impl Neg for RelativeDuration {
165    type Output = RelativeDuration;
166
167    #[inline]
168    fn neg(self) -> RelativeDuration {
169        RelativeDuration {
170            months: -self.months,
171            duration: -self.duration,
172        }
173    }
174}
175
176impl Add<RelativeDuration> for RelativeDuration {
177    type Output = RelativeDuration;
178
179    #[inline]
180    fn add(self, rhs: RelativeDuration) -> RelativeDuration {
181        RelativeDuration {
182            months: self.months + rhs.months,
183            duration: self.duration + rhs.duration,
184        }
185    }
186}
187
188impl Add<Duration> for RelativeDuration {
189    type Output = RelativeDuration;
190
191    #[inline]
192    fn add(self, rhs: Duration) -> RelativeDuration {
193        self + RelativeDuration {
194            months: 0,
195            duration: rhs,
196        }
197    }
198}
199
200impl Add<RelativeDuration> for Duration {
201    type Output = RelativeDuration;
202
203    #[inline]
204    fn add(self, rhs: RelativeDuration) -> RelativeDuration {
205        rhs + self
206    }
207}
208
209impl Sub for RelativeDuration {
210    type Output = RelativeDuration;
211
212    #[inline]
213    fn sub(self, rhs: RelativeDuration) -> RelativeDuration {
214        self + (-rhs)
215    }
216}
217
218impl Sub<RelativeDuration> for Duration {
219    type Output = RelativeDuration;
220
221    #[inline]
222    fn sub(self, rhs: RelativeDuration) -> RelativeDuration {
223        -rhs + self
224    }
225}
226
227impl Sub<Duration> for RelativeDuration {
228    type Output = RelativeDuration;
229
230    #[inline]
231    fn sub(self, rhs: Duration) -> RelativeDuration {
232        self + (-rhs)
233    }
234}
235
236impl Mul<i32> for RelativeDuration {
237    type Output = RelativeDuration;
238
239    #[inline]
240    fn mul(self, rhs: i32) -> RelativeDuration {
241        RelativeDuration {
242            months: self.months * rhs,
243            duration: self.duration * rhs,
244        }
245    }
246}
247
248impl Div<i32> for RelativeDuration {
249    type Output = RelativeDuration;
250
251    #[inline]
252    fn div(self, rhs: i32) -> RelativeDuration {
253        RelativeDuration {
254            months: self.months / rhs,
255            duration: self.duration / rhs,
256        }
257    }
258}
259
260// The following is just copy-pasta, mostly because we
261// can't impl<T> Add<RelativeDuration> for T with T: Datelike
262impl Add<RelativeDuration> for NaiveDate {
263    type Output = NaiveDate;
264
265    #[inline]
266    fn add(self, rhs: RelativeDuration) -> NaiveDate {
267        shift_months(self, rhs.months) + rhs.duration
268    }
269}
270
271impl Add<RelativeDuration> for NaiveDateTime {
272    type Output = NaiveDateTime;
273
274    #[inline]
275    fn add(self, rhs: RelativeDuration) -> NaiveDateTime {
276        shift_months(self, rhs.months) + rhs.duration
277    }
278}
279
280impl<Tz> Add<RelativeDuration> for Date<Tz>
281where
282    Tz: TimeZone,
283{
284    type Output = Date<Tz>;
285
286    #[inline]
287    fn add(self, rhs: RelativeDuration) -> Date<Tz> {
288        shift_months(self, rhs.months) + rhs.duration
289    }
290}
291
292impl<Tz> Add<RelativeDuration> for DateTime<Tz>
293where
294    Tz: TimeZone,
295{
296    type Output = DateTime<Tz>;
297
298    #[inline]
299    fn add(self, rhs: RelativeDuration) -> DateTime<Tz> {
300        shift_months(self, rhs.months) + rhs.duration
301    }
302}
303
304impl Sub<RelativeDuration> for NaiveDate {
305    type Output = NaiveDate;
306
307    #[inline]
308    fn sub(self, rhs: RelativeDuration) -> NaiveDate {
309        self + (-rhs)
310    }
311}
312
313impl Sub<RelativeDuration> for NaiveDateTime {
314    type Output = NaiveDateTime;
315
316    #[inline]
317    fn sub(self, rhs: RelativeDuration) -> NaiveDateTime {
318        self + (-rhs)
319    }
320}
321
322impl<Tz> Sub<RelativeDuration> for Date<Tz>
323where
324    Tz: TimeZone,
325{
326    type Output = Date<Tz>;
327
328    #[inline]
329    fn sub(self, rhs: RelativeDuration) -> Date<Tz> {
330        self + (-rhs)
331    }
332}
333
334impl<Tz> Sub<RelativeDuration> for DateTime<Tz>
335where
336    Tz: TimeZone,
337{
338    type Output = DateTime<Tz>;
339
340    #[inline]
341    fn sub(self, rhs: RelativeDuration) -> DateTime<Tz> {
342        self + (-rhs)
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_duration_arithmetic() {
352        let x = RelativeDuration {
353            months: 5 * 12 + 7,
354            duration: Duration::seconds(100),
355        };
356        let y = RelativeDuration {
357            months: 3 * 12 + 6,
358            duration: Duration::seconds(300),
359        };
360        let z = Duration::days(100);
361
362        assert_eq!(
363            x + y,
364            RelativeDuration {
365                months: 9 * 12 + 1,
366                duration: Duration::seconds(400)
367            }
368        );
369        assert_eq!(
370            x - y,
371            RelativeDuration {
372                months: 2 * 12 + 1,
373                duration: Duration::seconds(-200)
374            }
375        );
376        assert_eq!(
377            x + z,
378            RelativeDuration {
379                months: 5 * 12 + 7,
380                duration: Duration::days(100) + Duration::seconds(100)
381            }
382        );
383
384        assert_eq!(y + x, y + x, "Addition should be symmetric");
385        assert_eq!(x - y, -(y - x), "Subtraction should be anti-symmetric");
386        assert_eq!(y + z, z + y, "Addition should be symmetric");
387        assert_eq!(y - z, -(z - y), "Subtraction should be anti-symmetric");
388
389        assert_eq!(
390            x / 2,
391            RelativeDuration {
392                months: 5 * 6 + 3,
393                duration: Duration::seconds(50)
394            }
395        );
396        assert_eq!(
397            x * 2,
398            RelativeDuration {
399                months: 10 * 12 + 14,
400                duration: Duration::seconds(200)
401            }
402        );
403    }
404
405    #[test]
406    fn test_date_arithmetic() {
407        let base = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
408
409        assert_eq!(
410            base + RelativeDuration {
411                months: 24,
412                duration: Duration::zero()
413            },
414            NaiveDate::from_ymd_opt(2022, 2, 28).unwrap()
415        );
416        assert_eq!(
417            base + RelativeDuration {
418                months: 48,
419                duration: Duration::zero()
420            },
421            NaiveDate::from_ymd_opt(2024, 2, 29).unwrap()
422        );
423
424        let not_leap = NaiveDate::from_ymd_opt(2020, 2, 28).unwrap();
425        let tricky_delta = RelativeDuration {
426            months: 24,
427            duration: Duration::days(1),
428        };
429        assert_eq!(
430            base + tricky_delta,
431            NaiveDate::from_ymd_opt(2022, 3, 1).unwrap()
432        );
433        assert_eq!(base + tricky_delta, not_leap + tricky_delta);
434    }
435
436    #[test]
437    fn test_date_negative_arithmetic() {
438        let base = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
439
440        assert_eq!(
441            base - RelativeDuration {
442                months: 24,
443                duration: Duration::zero()
444            },
445            NaiveDate::from_ymd_opt(2018, 2, 28).unwrap()
446        );
447        assert_eq!(
448            base - RelativeDuration {
449                months: 48,
450                duration: Duration::zero()
451            },
452            NaiveDate::from_ymd_opt(2016, 2, 29).unwrap()
453        );
454
455        let not_leap = NaiveDate::from_ymd_opt(2020, 2, 28).unwrap();
456        let tricky_delta = RelativeDuration {
457            months: 24,
458            duration: Duration::days(-1),
459        };
460        assert_eq!(
461            base - tricky_delta,
462            NaiveDate::from_ymd_opt(2018, 3, 1).unwrap()
463        );
464        assert_eq!(base - tricky_delta, not_leap - tricky_delta);
465    }
466
467    #[test]
468    fn test_constructors() {
469        assert_eq!(RelativeDuration::years(5), RelativeDuration::months(60));
470        assert_eq!(RelativeDuration::weeks(5), RelativeDuration::days(35));
471        assert_eq!(RelativeDuration::days(5), RelativeDuration::hours(120));
472        assert_eq!(RelativeDuration::hours(5), RelativeDuration::minutes(300));
473        assert_eq!(RelativeDuration::minutes(5), RelativeDuration::seconds(300));
474        assert_eq!(
475            RelativeDuration::months(1).with_duration(Duration::weeks(3)),
476            RelativeDuration {
477                months: 1,
478                duration: Duration::weeks(3)
479            },
480        );
481    }
482}