kronos/
utils.rs

1#![deny(warnings)]
2
3use chrono::Timelike;
4use chrono::Datelike;
5use chrono::Weekday;
6
7use crate::types::{Grain, Date, DateTime, Duration, Season};
8
9
10pub fn enclosing_grain_from_duration(duration: Duration) -> Grain {
11    if duration <= Duration::seconds(1) { return Grain::Second }
12    if duration <= Duration::minutes(1) { return Grain::Minute }
13    if duration <= Duration::hours(1) { return Grain::Hour }
14    if duration <= Duration::days(1) { return Grain::Day }
15    if duration <= Duration::days(7) { return Grain::Week }
16    if duration <= Duration::days(31) { return Grain::Month }
17    if duration <= Duration::days(92) { return Grain::Quarter }
18    if duration <= Duration::days(183) { return Grain::Half }
19    if duration <= Duration::days(366) { return Grain::Year }
20    if duration <= Duration::days(5 * 366) { return Grain::Lustrum }
21    if duration <= Duration::days(10 * 366) { return Grain::Decade }
22    if duration <= Duration::days(100 * 366) { return Grain::Century }
23    Grain::Millenium
24}
25
26pub fn grain_from_duration(duration: Duration) -> Grain {
27    let seconds = duration.num_seconds();
28    if seconds % 60 != 0 { return Grain::Second }
29    if (seconds/60) % 60 != 0 { return Grain::Minute }
30    if (seconds/3600) % 24 != 0 { return Grain::Hour }
31    if (seconds/3600/24) % 7 != 0 { return Grain::Day }
32    Grain::Week
33}
34
35pub fn truncate(d: DateTime, granularity: Grain) -> DateTime {
36    use crate::types::Grain::*;
37    match granularity {
38        Second => d.with_nanosecond(0).unwrap(),
39        Minute => d.date().and_hms(d.hour(), d.minute(), 0),
40        Hour => d.date().and_hms(d.hour(), 0, 0),
41        Day => d.date().and_hms(0, 0, 0),
42        Week => {
43            let days_from_sun = d.weekday().num_days_from_sunday();
44            (d.date() - Duration::days(i64::from(days_from_sun))).and_hms(0, 0, 0)
45        },
46        Month => Date::from_ymd(d.year(), d.month(), 1).and_hms(0, 0, 0),
47        Quarter => {
48            let quarter_start = 1 + 3 * ((d.month()-1) / 3);
49            Date::from_ymd(d.year(), quarter_start, 1).and_hms(0, 0, 0)
50        },
51        Half => {
52            let half_start = 1 + 6 * ((d.month() - 1) / 6);
53            Date::from_ymd(d.year(), half_start, 1).and_hms(0, 0, 0)
54        },
55        Year => Date::from_ymd(d.year(), 1, 1).and_hms(0, 0, 0),
56        Lustrum =>
57            Date::from_ymd(d.year() - d.year() % 5, 1, 1).and_hms(0, 0, 0),
58        Decade =>
59            Date::from_ymd(d.year() - d.year() % 10, 1, 1).and_hms(0, 0, 0),
60        Century =>
61            Date::from_ymd(d.year() - d.year() % 100, 1, 1).and_hms(0, 0, 0),
62        Millenium =>
63            Date::from_ymd(d.year() - d.year() % 1000, 1, 1).and_hms(0, 0, 0),
64    }
65}
66
67pub fn find_dow(mut date: Date, dow: u32, future: bool) -> Date {
68    while date.weekday().num_days_from_sunday() != dow {
69        date = if future { date.succ() } else { date.pred() }
70    }
71    date
72}
73
74pub fn find_month(mut date: Date, month: u32, future: bool) -> Date {
75    while date.month() != month {
76        date = if future {
77            dtshift::add(date, 0, 1, 0)
78        } else {
79            dtshift::sub(date, 0, 1, 0)
80        }
81    }
82    date
83}
84
85pub fn find_season(dt: Date, season: Season, future: bool, north: bool)
86    -> (Date, Date)
87{
88    let season_lookup = |date: Date| {
89        if date.month() < 3 || date.month() == 3 && date.day() < 21 {
90            Season::Winter
91        } else if date.month() < 6 || date.month() == 6 && date.day() < 21 {
92            Season::Spring
93        } else if date.month() < 9 || date.month() == 9 && date.day() < 21 {
94            Season::Summer
95        } else if date.month() < 12 || date.month() == 12 && date.day() < 21 {
96            Season::Autumn
97        } else {
98            Season::Winter
99        }
100    };
101    let search = |end_mo, mut date: Date| {
102        let inc = if future { Duration::days(1) } else { Duration::days(-1) };
103        while season_lookup(date) != season {
104            date += inc;
105        }
106        let end_date = Date::from_ymd(date.year(), end_mo, 21);
107        let start_date = dtshift::sub(end_date, 0, 3, 0);
108        (start_date, end_date)
109    };
110    match (season, north) {
111        (Season::Spring, true) |
112        (Season::Autumn, false) => search(6, dt),
113        (Season::Summer, true) |
114        (Season::Winter, false) => search(9, dt),
115        (Season::Autumn, true) |
116        (Season::Spring, false) => search(12, dt),
117        (Season::Winter, true) |
118        (Season::Summer, false) => search(3, dt),
119    }
120}
121
122pub fn find_weekend(mut date: Date, future: bool) -> Date {
123    if date.weekday() == Weekday::Sun { date = date.pred(); }
124    while date.weekday() != Weekday::Sat {
125        date = if future {
126            date.succ()
127        } else {
128            date.pred()
129        };
130    }
131    date
132}
133
134pub fn shift_datetime(d: DateTime, granularity: Grain, n: i32) -> DateTime {
135    use crate::types::Grain::*;
136    let m = if n >= 0 {n as u32} else {(-n) as u32};
137    let shiftfn = if n >= 0 {dtshift::add} else {dtshift::sub};
138    match granularity {
139        Second => d + Duration::seconds(i64::from(n)),
140        Minute => d + Duration::minutes(i64::from(n)),
141        Hour => d + Duration::hours(i64::from(n)),
142        Day => d + Duration::days(i64::from(n)),
143        Week => d + Duration::weeks(i64::from(n)),
144        Month => shiftfn(d.date(), 0, m, 0).and_time(d.time()),
145        Quarter => shiftfn(d.date(), 0, 3 * m, 0).and_time(d.time()),
146        Half => shiftfn(d.date(), 0, 6 * m, 0).and_time(d.time()),
147        Year => shiftfn(d.date(), m, 0, 0).and_time(d.time()),
148        Lustrum => shiftfn(d.date(), 5 * m, 0, 0).and_time(d.time()),
149        Decade => shiftfn(d.date(), 10 * m, 0, 0).and_time(d.time()),
150        Century => shiftfn(d.date(), 100 * m, 0, 0).and_time(d.time()),
151        Millenium => shiftfn(d.date(), 1000 * m, 0, 0).and_time(d.time()),
152    }
153}
154
155mod dtshift {
156    use super::*;
157    use std::cmp;
158
159    fn days_in_month(m: u32, y: i32) -> u32 {
160        static DIM: [u8;12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
161        assert!(m > 0 && m <= 12);
162        // check when february has 29 days
163        if m == 2 && y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) {return 29;}
164        u32::from(DIM[(m-1) as usize])
165    }
166
167    pub fn add(dt: Date, y: u32, mut m: u32, mut d: u32) -> Date {
168        let (mut day, mut month, mut year) = (dt.day(), dt.month(), dt.year());
169        while d > 0 {
170            let diff = cmp::min(days_in_month(month, year)-day, d);
171            day += diff;
172            d -= diff;
173            if d > 0 {
174                day = 0;
175                month += 1;
176                if month > 12 {
177                    year += 1;
178                    month = 1;
179                }
180            }
181        }
182        while m > 0 {
183            let diff = cmp::min(12 - month, m);
184            month += diff;
185            m -= diff;
186            if m > 0 {
187                month = 0;
188                year += 1;
189            }
190        }
191        year += y as i32;
192        day = cmp::min(day, days_in_month(month, year));
193        Date::from_ymd(year, month, day)
194    }
195
196    pub fn sub(dt: Date, y: u32, mut m: u32, mut d: u32) -> Date {
197        let (mut day, mut month, mut year) = (dt.day(), dt.month(), dt.year());
198        while d > 0 {
199            let diff = cmp::min(day-1, d);
200            day -= diff;
201            d -= diff;
202            if d > 0 {
203                month -= 1;
204                if month < 1 {
205                    month = 12;
206                    year -= 1;
207                }
208                day = 1 + days_in_month(month, year);
209            }
210        }
211        while m > 0 {
212            let diff = cmp::min(month-1, m);
213            month -= diff;
214            m -= diff;
215            if m > 0 {
216                month = 13;
217                year -= 1;
218            }
219        }
220        year -= y as i32;
221        day = cmp::min(day, days_in_month(month, year));
222        Date::from_ymd(year, month, day)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    fn dt(year: i32, month: u32, day: u32) -> Date {
230        Date::from_ymd(year, month, day)
231    }
232    fn dttm(year: i32, month: u32, day: u32) -> DateTime {
233        Date::from_ymd(year, month, day).and_hms(0, 0, 0)
234    }
235
236    #[test]
237    fn test_dateadd() {
238        let d = dt(2016, 9, 5);
239        assert_eq!(dtshift::add(d, 0, 0, 30), dt(2016, 10, 5));
240        assert_eq!(dtshift::add(d, 0, 0, 1234), dt(2020, 1, 22));
241        assert_eq!(dtshift::add(d, 0, 0, 365), dt(2017, 9, 5));
242        assert_eq!(dtshift::add(d, 0, 0, 2541), dt(2023, 8, 21));
243        assert_eq!(dtshift::add(d, 0, 1, 0), dt(2016, 10, 5));
244        let d = dt(2016, 1, 30);
245        assert_eq!(dtshift::add(d, 0, 1, 0), dt(2016, 2, 29));
246        assert_eq!(dtshift::add(d, 0, 2, 0), dt(2016, 3, 30));
247        assert_eq!(dtshift::add(d, 0, 12, 0), dt(2017, 1, 30));
248        let d = dt(2016, 12, 31);
249        assert_eq!(dtshift::add(d, 0, 1, 0), dt(2017, 1, 31));
250    }
251
252    #[test]
253    fn test_datesub() {
254        let d = dt(2016, 9, 5);
255        assert_eq!(dtshift::sub(d, 0, 0, 3), dt(2016, 9, 2));
256        assert_eq!(dtshift::sub(d, 0, 0, 6), dt(2016, 8, 30));
257        assert_eq!(dtshift::sub(d, 0, 0, 36), dt(2016, 7, 31));
258        assert_eq!(dtshift::sub(d, 0, 0, 1234), dt(2013, 4, 20));
259        assert_eq!(dtshift::sub(d, 0, 0, 365), dt(2015, 9, 6));
260        assert_eq!(dtshift::sub(d, 0, 1, 0), dt(2016, 8, 5));
261        let d = dt(2016, 9, 1);
262        assert_eq!(dtshift::sub(d, 0, 0, 1), dt(2016, 8, 31));
263        let d = dt(2016, 1, 31);
264        assert_eq!(dtshift::sub(d, 0, 1, 0), dt(2015, 12, 31));
265        let d = dt(2016, 3, 31);
266        assert_eq!(dtshift::sub(d, 0, 1, 0), dt(2016, 2, 29));
267        assert_eq!(dtshift::sub(d, 0, 2, 0), dt(2016, 1, 31));
268        assert_eq!(dtshift::sub(d, 0, 13, 0), dt(2015, 2, 28));
269    }
270
271    #[test]
272    fn test_shifts() {
273        let d = dttm(2016, 9, 5);
274        assert_eq!(shift_datetime(d, Grain::Day, -3), dttm(2016, 9, 2));
275        assert_eq!(shift_datetime(d, Grain::Day, -36), dttm(2016, 7, 31));
276        assert_eq!(shift_datetime(d, Grain::Day, -1234), dttm(2013, 4, 20));
277        assert_eq!(shift_datetime(d, Grain::Month, -1), dttm(2016, 8, 5));
278        let d = dttm(2016, 1, 31);
279        assert_eq!(shift_datetime(d, Grain::Month, -1), dttm(2015, 12, 31));
280        let d = dttm(2016, 3, 31);
281        assert_eq!(shift_datetime(d, Grain::Month, -27), dttm(2013, 12, 31));
282        assert_eq!(shift_datetime(d, Grain::Month, -2), dttm(2016, 1, 31));
283        assert_eq!(shift_datetime(d, Grain::Month, -13), dttm(2015, 2, 28));
284        let d = dttm(2016, 3, 31);
285        assert_eq!(shift_datetime(d, Grain::Week, 7), dttm(2016, 5, 19));
286        assert_eq!(shift_datetime(d, Grain::Year, -7), dttm(2009, 3, 31));
287        assert_eq!(shift_datetime(d, Grain::Quarter, 2), dttm(2016, 9, 30));
288    }
289}