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 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}