date_calculations/
lib.rs

1#![deny(missing_docs)]
2
3//! This crate provides helper functions for calculating shifts in Chrono's NaiveDate values for
4//! various periods (week, month, quarter, year) for common shifts in direction (beginning_of_*,
5//! end_of_*, previous_*, and next_*).
6//!
7//! The dates passed to these functions should be Gregorian dates to ensure proper calcuation.
8//!
9//! ```
10//! use chrono::prelude::*;
11//! use date_calculations::*;
12//!
13//! let twenty_twenty_one = NaiveDate::from_ymd_opt(2021, 1, 31).unwrap();
14//!
15//! assert_eq!(next_year(&twenty_twenty_one).unwrap().year(), 2022);
16//! assert_eq!(next_year(&twenty_twenty_one).unwrap().month(), 1);
17//! assert_eq!(next_year(&twenty_twenty_one).unwrap().day(), 1);
18//!
19//! assert_eq!(previous_quarter(&twenty_twenty_one).unwrap().year(), 2020);
20//! assert_eq!(previous_quarter(&twenty_twenty_one).unwrap().month(), 10);
21//! assert_eq!(previous_quarter(&twenty_twenty_one).unwrap().day(), 1);
22//! ```
23
24use chrono::prelude::*;
25
26// weeks
27
28/// Returns the beginning of the week relative to the provided date.
29///
30/// Weeks begin on Sunday.
31pub fn beginning_of_week(date: &NaiveDate) -> Option<NaiveDate> {
32    if date.weekday() == Weekday::Sun {
33        Some(date.clone())
34    } else {
35        NaiveDate::from_isoywd_opt(date.iso_week().year(), date.iso_week().week(), Weekday::Sun)
36            .map(|d| d - chrono::Duration::weeks(1))
37    }
38}
39
40/// Returns the end of the week relative to the provided date.
41///
42/// Weeks end on Saturday.
43pub fn end_of_week(date: &NaiveDate) -> Option<NaiveDate> {
44    beginning_of_week(date).map(|d| d + chrono::Duration::days(6))
45}
46
47/// Returns the beginning of the next week.
48///
49/// Weeks begin on Sunday.
50pub fn next_week(date: &NaiveDate) -> Option<NaiveDate> {
51    beginning_of_week(date).map(|d| d + chrono::Duration::weeks(1))
52}
53
54/// Returns the end of the next week.
55///
56/// Weeks end on Saturday.
57pub fn previous_week(date: &NaiveDate) -> Option<NaiveDate> {
58    beginning_of_week(date).map(|d| d - chrono::Duration::weeks(1))
59}
60
61/// Returns the first day of the current month and year.
62pub fn beginning_of_month(date: &NaiveDate) -> Option<NaiveDate> {
63    date.with_day(1)
64}
65
66/// Returns the last day of the current month and year.
67pub fn end_of_month(date: &NaiveDate) -> Option<NaiveDate> {
68    next_month(date).map(|d| d - chrono::Duration::days(1))
69}
70
71/// Returns the first day of the next month.
72///
73/// If the current month is December, this will shift to the next year.
74pub fn next_month(date: &NaiveDate) -> Option<NaiveDate> {
75    if date.month() == 12 {
76        next_year(date)
77    } else {
78        beginning_of_month(date)?.with_month(date.month() + 1)
79    }
80}
81
82/// Returns the first day of the previous month.
83///
84/// If the current month is January, this will shift to the previous year.
85pub fn previous_month(date: &NaiveDate) -> Option<NaiveDate> {
86    if date.month() == 1 {
87        beginning_of_month(date)?
88            .with_month(12)?
89            .with_year(date.year() - 1)
90    } else {
91        beginning_of_month(date)?.with_month(date.month() - 1)
92    }
93}
94
95/// Returns the first day of the current quarter and year.
96///
97/// This will either be January 1, April 1, July 1, or October 1 of the current year.
98pub fn beginning_of_quarter(date: &NaiveDate) -> Option<NaiveDate> {
99    beginning_of_month(date)?.with_month(quarter_month(date))
100}
101
102/// Returns the last day of the current quarter and year.
103///
104/// This will either be March 31, June 30, September 30, or December 31 of the current year.
105pub fn end_of_quarter(date: &NaiveDate) -> Option<NaiveDate> {
106    next_quarter(date).map(|d| d - chrono::Duration::days(1))
107}
108
109/// Returns the first day of the next quarter.
110///
111/// If the current date falls in the last quarter of the year, this will shift to the first quarter
112/// of the next year.
113pub fn next_quarter(date: &NaiveDate) -> Option<NaiveDate> {
114    if date.month() >= 10 {
115        beginning_of_year(date)?.with_year(date.year() + 1)
116    } else {
117        beginning_of_month(date)?.with_month(quarter_month(date) + 3)
118    }
119}
120
121/// Returns the first day of the previous quarter.
122///
123/// If the current date falls in the first quarter of the year, this will shift to the last quarter
124/// of the previous year.
125pub fn previous_quarter(date: &NaiveDate) -> Option<NaiveDate> {
126    if date.month() < 4 {
127        beginning_of_month(date)?
128            .with_year(date.year() - 1)?
129            .with_month(10)
130    } else {
131        beginning_of_month(date)?.with_month(quarter_month(date) - 3)
132    }
133}
134
135fn quarter_month(date: &NaiveDate) -> u32 {
136    1 + 3 * ((date.month() - 1) / 3)
137}
138
139/// Returns the first day of the year (January 1) of the current year.
140pub fn beginning_of_year(date: &NaiveDate) -> Option<NaiveDate> {
141    beginning_of_month(date)?.with_month(1)
142}
143
144/// Returns the last day of the year (December 31) of the current year.
145pub fn end_of_year(date: &NaiveDate) -> Option<NaiveDate> {
146    date.with_month(12)?.with_day(31)
147}
148
149/// Returns the first day of the year (January 1) of the next year.
150pub fn next_year(date: &NaiveDate) -> Option<NaiveDate> {
151    beginning_of_year(date)?.with_year(date.year() + 1)
152}
153
154/// Returns the first day of the year (January 1) of the previous year.
155pub fn previous_year(date: &NaiveDate) -> Option<NaiveDate> {
156    beginning_of_year(date)?.with_year(date.year() - 1)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use num::clamp;
163    use quickcheck::{Arbitrary, Gen};
164    use quickcheck_macros::quickcheck;
165
166    #[derive(Clone, Debug)]
167    struct NaiveDateWrapper(NaiveDate);
168
169    #[quickcheck]
170    fn beginning_of_week_works(d: NaiveDateWrapper) -> bool {
171        let since = d.0.signed_duration_since(beginning_of_week(&d.0).unwrap());
172
173        beginning_of_week(&d.0).unwrap().weekday() == Weekday::Sun
174            && since.num_days() >= 0
175            && since.num_days() < 7
176    }
177
178    #[quickcheck]
179    fn end_of_week_works(d: NaiveDateWrapper) -> bool {
180        end_of_week(&d.0).unwrap().weekday() == Weekday::Sat
181    }
182
183    #[quickcheck]
184    fn next_week_works(d: NaiveDateWrapper) -> bool {
185        let since = next_week(&d.0).unwrap().signed_duration_since(d.0);
186        next_week(&d.0).unwrap().weekday() == Weekday::Sun
187            && since.num_days() > 0
188            && since.num_days() <= 7
189    }
190
191    #[quickcheck]
192    fn previous_week_works(d: NaiveDateWrapper) -> bool {
193        let since = previous_week(&d.0).unwrap().signed_duration_since(d.0);
194        previous_week(&d.0).unwrap().weekday() == Weekday::Sun
195            && since.num_days() <= -7
196            && since.num_days() > -14
197    }
198
199    #[quickcheck]
200    fn beginning_of_month_works(d: NaiveDateWrapper) -> bool {
201        beginning_of_month(&d.0).unwrap().day() == 1
202            && beginning_of_month(&d.0).unwrap().month() == d.0.month()
203            && beginning_of_month(&d.0).unwrap().year() == d.0.year()
204    }
205
206    #[quickcheck]
207    fn end_of_month_works(d: NaiveDateWrapper) -> bool {
208        end_of_month(&d.0).unwrap().month() == d.0.month()
209            && end_of_month(&d.0).unwrap().year() == d.0.year()
210            && (end_of_month(&d.0).unwrap() + chrono::Duration::days(1))
211                == next_month(&d.0).unwrap()
212    }
213
214    #[quickcheck]
215    fn beginning_of_year_works(d: NaiveDateWrapper) -> bool {
216        beginning_of_year(&d.0).unwrap().month() == 1
217            && beginning_of_year(&d.0).unwrap().day() == 1
218            && beginning_of_year(&d.0).unwrap().year() == d.0.year()
219    }
220
221    #[quickcheck]
222    fn end_of_year_works(d: NaiveDateWrapper) -> bool {
223        end_of_year(&d.0).unwrap().month() == 12
224            && end_of_year(&d.0).unwrap().day() == 31
225            && end_of_year(&d.0).unwrap().year() == d.0.year()
226    }
227
228    #[quickcheck]
229    fn next_year_works(d: NaiveDateWrapper) -> bool {
230        next_year(&d.0).unwrap().month() == 1
231            && next_year(&d.0).unwrap().day() == 1
232            && next_year(&d.0).unwrap().year() == d.0.year() + 1
233    }
234
235    #[quickcheck]
236    fn previous_year_works(d: NaiveDateWrapper) -> bool {
237        previous_year(&d.0).unwrap().month() == 1
238            && previous_year(&d.0).unwrap().day() == 1
239            && previous_year(&d.0).unwrap().year() == d.0.year() - 1
240    }
241
242    #[quickcheck]
243    fn beginning_of_quarter_works(d: NaiveDateWrapper) -> bool {
244        [1, 4, 7, 10].contains(&beginning_of_quarter(&d.0).unwrap().month())
245            && beginning_of_quarter(&d.0).unwrap().day() == 1
246            && beginning_of_quarter(&d.0).unwrap().year() == d.0.year()
247    }
248
249    #[quickcheck]
250    fn end_of_quarter_works(d: NaiveDateWrapper) -> bool {
251        [3, 6, 9, 12].contains(&end_of_quarter(&d.0).unwrap().month())
252            && end_of_quarter(&d.0)
253                .map(|x| x + chrono::Duration::days(1))
254                .unwrap()
255                == next_quarter(&d.0).unwrap()
256            && end_of_quarter(&d.0).unwrap().year() == d.0.year()
257    }
258
259    #[quickcheck]
260    fn next_quarter_works(d: NaiveDateWrapper) -> bool {
261        let current_month = d.0.month();
262        let year = if current_month >= 10 {
263            d.0.year() + 1
264        } else {
265            d.0.year()
266        };
267
268        [1, 4, 7, 10].contains(&next_quarter(&d.0).unwrap().month())
269            && next_quarter(&d.0).unwrap().day() == 1
270            && next_quarter(&d.0).unwrap().year() == year
271    }
272
273    #[quickcheck]
274    fn previous_quarter_works(d: NaiveDateWrapper) -> bool {
275        let current_month = d.0.month();
276        let year = if current_month <= 3 {
277            d.0.year() - 1
278        } else {
279            d.0.year()
280        };
281
282        [1, 4, 7, 10].contains(&previous_quarter(&d.0).unwrap().month())
283            && previous_quarter(&d.0).unwrap().day() == 1
284            && previous_quarter(&d.0).unwrap().year() == year
285    }
286
287    impl Arbitrary for NaiveDateWrapper {
288        fn arbitrary<G: Gen>(g: &mut G) -> NaiveDateWrapper {
289            let year = clamp(i32::arbitrary(g), 1584, 2800);
290            let month = 1 + u32::arbitrary(g) % 12;
291            let day = 1 + u32::arbitrary(g) % 31;
292
293            let first_date = NaiveDate::from_ymd_opt(year, month, day);
294            if day > 27 {
295                let result = vec![
296                    first_date,
297                    NaiveDate::from_ymd_opt(year, month, day - 1),
298                    NaiveDate::from_ymd_opt(year, month, day - 2),
299                ]
300                .into_iter()
301                .filter_map(|v| v)
302                .nth(0)
303                .unwrap();
304
305                NaiveDateWrapper(result)
306            } else {
307                NaiveDateWrapper(first_date.unwrap())
308            }
309        }
310    }
311}