1use crate::MILLIS_PER_DAY;
2
3const SECONDS_PER_DAY: i32 = 86400;
40const DAYS_PER_CYCLE: i32 = 146097;
41const DAYS_0000_TO_1970: i32 = (DAYS_PER_CYCLE * 5) - (30 * 365 + 7);
42
43const SUPPORT_NEGATIVE_YEAR: bool = false;
44
45static DAYS_PER_MONTH: [[i32; 12]; 2] = [
47 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
48 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
49];
50
51#[inline]
53fn is_leap_year(year: i32) -> bool {
54 ((year % 4) == 0) & ((year % 100) != 0) | ((year % 400) == 0)
55}
56
57#[inline]
58fn days_per_month(year: i32, zero_based_month: i32) -> i32 {
59 let is_leap = is_leap_year(year);
60 let is_feb = zero_based_month == 1;
61 let mut days = 30 + ((zero_based_month % 2) != (zero_based_month <= 6) as i32) as i32;
62 days -= (2 - is_leap as i32) * (is_feb as i32);
63 days
64}
65
66#[derive(PartialEq, Eq, Clone, Copy, Debug)]
68pub struct EpochDays(i32);
69
70impl EpochDays {
71 #[inline]
72 pub fn new(epoch_days: i32) -> Self {
73 Self(epoch_days)
74 }
75
76 #[inline]
77 pub fn days(&self) -> i32 {
78 self.0
79 }
80
81 #[inline]
84 pub fn from_ymd(year: i32, month: i32, day: i32) -> Self {
85 let y = year;
86 let m = month;
87 let mut total = 365 * y;
88
89 total += if y < 0 && SUPPORT_NEGATIVE_YEAR {
90 -(y / -4 - y / -100 + y / -400)
91 } else {
92 (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400
93 };
94
95 total += ((367 * m - 362) / 12);
96 total += day - 1;
97
98 total -= 0_i32.wrapping_sub((m > 2) as i32) & (1 + (!is_leap_year(year) as i32));
99
100 Self(total - DAYS_0000_TO_1970)
101 }
102
103 #[inline]
107 pub fn to_ymd(&self) -> (i32, i32, i32) {
108 let epoch_days = self.0;
109 let mut zero_day = epoch_days + DAYS_0000_TO_1970;
110 zero_day -= 60; let mut adjust = 0;
113 if zero_day < 0 && SUPPORT_NEGATIVE_YEAR {
114 let adjust_cycles = (zero_day + 1) / DAYS_PER_CYCLE - 1;
116 adjust = adjust_cycles * 400;
117 zero_day += -adjust_cycles * DAYS_PER_CYCLE;
118 }
119 let mut year_est = (400 * zero_day + 591) / DAYS_PER_CYCLE;
120
121 if !SUPPORT_NEGATIVE_YEAR {
122 year_est &= i32::MAX;
123 }
124
125 let mut doy_est = zero_day - (365 * year_est + year_est / 4 - year_est / 100 + year_est / 400);
126
127 year_est -= (doy_est < 0) as i32;
129 if !SUPPORT_NEGATIVE_YEAR {
130 year_est &= i32::MAX;
131 }
132
133 doy_est = zero_day - (365 * year_est + year_est / 4 - year_est / 100 + year_est / 400);
134
135 year_est += adjust; let march_doy0 = doy_est;
137
138 let march_month0 = (march_doy0 * 5 + 2) / 153;
140 let month = (march_month0 + 2) % 12 + 1;
141 let dom = march_doy0 - (march_month0 * 306 + 5) / 10 + 1;
142 year_est += march_month0 / 10;
143
144 (year_est, month, dom)
145 }
146
147 #[inline]
148 pub fn from_timestamp_millis(ts: i64) -> Self {
149 Self::from_timestamp_millis_float(ts as f64)
151 }
152
153 #[inline]
154 pub fn from_timestamp_millis_float(ts: f64) -> Self {
155 let epoch_days = (ts * (1.0 / MILLIS_PER_DAY as f64)).floor();
156 Self(unsafe { epoch_days.to_int_unchecked() })
157 }
158
159 #[inline]
160 pub fn to_timestamp_millis(&self) -> i64 {
161 (self.0 as i64) * MILLIS_PER_DAY
162 }
163
164 #[inline]
165 pub fn to_timestamp_millis_float(&self) -> f64 {
166 (self.0 as f64) * (MILLIS_PER_DAY as f64)
167 }
168
169 #[inline]
175 pub fn add_months(&self, months: i32) -> Self {
176 let (mut y, mut m, mut d) = self.to_ymd();
177 let mut m0 = m - 1;
178 m0 += months;
179 y += m0.div_euclid(12);
180 m0 = m0.rem_euclid(12);
181 d = d.min(days_per_month(y, m0));
182 m = m0 + 1;
183 Self::from_ymd(y, m, d)
184 }
185
186 #[inline]
192 pub fn add_years(&self, years: i32) -> Self {
193 let (mut y, m, mut d) = self.to_ymd();
194 y += years;
195 d = d.min(days_per_month(y, m - 1));
196 Self::from_ymd(y, m, d)
197 }
198
199 #[inline]
200 pub fn diff_months(&self, other: EpochDays) -> i32 {
201 let (y0, m0, d0) = self.to_ymd();
202 let (y1, m1, d1) = other.to_ymd();
203
204 (y1 * 12 + m1) - (y0 * 12 + m0) - (d1 < d0) as i32
206 }
207
208 #[inline]
209 pub fn diff_years(&self, other: EpochDays) -> i32 {
210 let (y0, m0, d0) = self.to_ymd();
211 let (y1, m1, d1) = other.to_ymd();
212
213 y1 - y0 - ((m1, d1) < (m0, d0)) as i32
215 }
216
217 #[inline]
218 pub fn date_trunc_month(&self) -> Self {
219 let (y, m, d) = self.to_ymd();
220 Self::from_ymd(y, m, 1)
221 }
222
223 #[inline]
224 pub fn date_trunc_year(&self) -> Self {
225 let (y, m, d) = self.to_ymd();
226 Self::from_ymd(y, 1, 1)
227 }
228
229 #[inline]
230 pub fn date_trunc_quarter(&self) -> Self {
231 let (y, m, d) = self.to_ymd();
232 Self::from_ymd(y, (m - 1) / 3 * 3 + 1, 1)
233 }
234
235 #[inline]
236 pub fn extract_year(&self) -> i32 {
237 self.to_ymd().0
238 }
239
240 #[inline]
241 pub fn extract_month(&self) -> i32 {
242 self.to_ymd().1
243 }
244
245 #[inline]
246 pub fn extract_quarter(&self) -> i32 {
247 (self.to_ymd().1 - 1) / 3 + 1
248 }
249
250 #[inline]
251 pub fn extract_day_of_month(&self) -> i32 {
252 self.to_ymd().2
253 }
254
255 #[inline]
256 pub fn days_in_month(&self) -> i32 {
257 let (y, m, _) = self.to_ymd();
258 days_per_month(y, m)
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use crate::epoch_days::{days_per_month, is_leap_year, DAYS_PER_MONTH};
265 use crate::EpochDays;
266
267 #[test]
268 fn test_is_leap_year() {
269 assert!(!is_leap_year(1900));
270 assert!(!is_leap_year(1999));
271
272 assert!(is_leap_year(2000));
273
274 assert!(!is_leap_year(2001));
275 assert!(!is_leap_year(2002));
276 assert!(!is_leap_year(2003));
277
278 assert!(is_leap_year(2004));
279 assert!(is_leap_year(2020));
280 }
281
282 #[test]
283 fn test_days_per_month() {
284 for i in 0..12 {
285 assert_eq!(days_per_month(2023, i as i32), DAYS_PER_MONTH[0][i], "non-leap: {i}");
286 }
287 for i in 0..12 {
288 assert_eq!(days_per_month(1900, i as i32), DAYS_PER_MONTH[0][i], "non-leap (%100): {i}");
289 }
290 for i in 0..12 {
291 assert_eq!(days_per_month(2020, i as i32), DAYS_PER_MONTH[1][i], "leap: {i}");
292 }
293 for i in 0..12 {
294 assert_eq!(days_per_month(2000, i as i32), DAYS_PER_MONTH[1][i], "leap (%400): {i}");
295 }
296 }
297
298 #[test]
299 fn test_to_epoch_day() {
300 assert_eq!(0, EpochDays::from_ymd(1970, 1, 1).0);
301 assert_eq!(1, EpochDays::from_ymd(1970, 1, 2).0);
302 assert_eq!(365, EpochDays::from_ymd(1971, 1, 1).0);
303 assert_eq!(365 * 2, EpochDays::from_ymd(1972, 1, 1).0);
304 assert_eq!(365 * 2 + 366, EpochDays::from_ymd(1973, 1, 1).0);
305
306 assert_eq!(18998, EpochDays::from_ymd(2022, 1, 6).0);
307 assert_eq!(19198, EpochDays::from_ymd(2022, 7, 25).0);
308 }
309
310 #[test]
311 fn test_date_trunc_year_epoch_days() {
312 assert_eq!(18993, EpochDays::new(19198).date_trunc_year().days());
313 }
314
315 #[test]
316 fn test_date_trunc_month_epoch_days() {
317 assert_eq!(19174, EpochDays::new(19198).date_trunc_month().days());
318 }
319
320 #[test]
321 fn test_date_diff_month_epoch_days() {
322 assert_eq!(
323 EpochDays::from_ymd(2023, 10, 1).diff_months(EpochDays::from_ymd(2023, 11, 1)),
324 1
325 );
326 assert_eq!(
327 EpochDays::from_ymd(2023, 10, 1).diff_months(EpochDays::from_ymd(2023, 12, 1)),
328 2
329 );
330 assert_eq!(
331 EpochDays::from_ymd(2023, 10, 1).diff_months(EpochDays::from_ymd(2023, 12, 31)),
332 2
333 );
334 assert_eq!(
335 EpochDays::from_ymd(2023, 10, 22).diff_months(EpochDays::from_ymd(2023, 11, 22)),
336 1
337 );
338 assert_eq!(
339 EpochDays::from_ymd(2023, 10, 22).diff_months(EpochDays::from_ymd(2023, 11, 21)),
340 0
341 );
342 assert_eq!(
343 EpochDays::from_ymd(2023, 10, 31).diff_months(EpochDays::from_ymd(2023, 11, 30)),
344 0
345 );
346 }
347
348 #[test]
349 fn test_date_diff_month_epoch_days_negative() {
350 assert_eq!(
351 EpochDays::from_ymd(2023, 11, 1).diff_months(EpochDays::from_ymd(2023, 10, 1)),
352 -1
353 );
354 }
355
356 #[test]
357 fn test_date_diff_year_epoch_days() {
358 assert_eq!(
359 EpochDays::from_ymd(2023, 10, 1).diff_years(EpochDays::from_ymd(2023, 10, 1)),
360 0
361 );
362 assert_eq!(
363 EpochDays::from_ymd(2023, 10, 1).diff_years(EpochDays::from_ymd(2023, 11, 1)),
364 0
365 );
366 assert_eq!(
367 EpochDays::from_ymd(2023, 1, 1).diff_years(EpochDays::from_ymd(2024, 1, 1)),
368 1
369 );
370 assert_eq!(
371 EpochDays::from_ymd(2023, 2, 28).diff_years(EpochDays::from_ymd(2024, 2, 28)),
372 1
373 );
374 assert_eq!(
375 EpochDays::from_ymd(2023, 2, 28).diff_years(EpochDays::from_ymd(2024, 2, 29)),
376 1
377 );
378 assert_eq!(
379 EpochDays::from_ymd(2023, 6, 15).diff_years(EpochDays::from_ymd(2024, 6, 14)),
380 0
381 );
382 assert_eq!(
383 EpochDays::from_ymd(2023, 6, 15).diff_years(EpochDays::from_ymd(2025, 6, 14)),
384 1
385 );
386 assert_eq!(
387 EpochDays::from_ymd(2023, 6, 15).diff_years(EpochDays::from_ymd(2025, 6, 16)),
388 2
389 );
390 }
391
392 #[test]
393 fn test_extract_year() {
394 assert_eq!(2022, EpochDays::from_ymd(2022, 1, 1).extract_year());
395 assert_eq!(2022, EpochDays::from_ymd(2022, 8, 24).extract_year());
396 assert_eq!(2022, EpochDays::from_ymd(2022, 12, 31).extract_year());
397 }
398
399 #[test]
400 fn test_extract_month() {
401 assert_eq!(1, EpochDays::from_ymd(2000, 1, 1).extract_month());
402 assert_eq!(2, EpochDays::from_ymd(2000, 2, 1).extract_month());
403 assert_eq!(2, EpochDays::from_ymd(2000, 2, 29).extract_month());
404 assert_eq!(1, EpochDays::from_ymd(2022, 1, 1).extract_month());
405 assert_eq!(8, EpochDays::from_ymd(2022, 8, 24).extract_month());
406 assert_eq!(12, EpochDays::from_ymd(2022, 12, 31).extract_month());
407 }
408
409 #[test]
410 fn test_extract_day() {
411 assert_eq!(1, EpochDays::from_ymd(2000, 1, 1).extract_day_of_month());
412 assert_eq!(1, EpochDays::from_ymd(2000, 2, 1).extract_day_of_month());
413 assert_eq!(29, EpochDays::from_ymd(2000, 2, 29).extract_day_of_month());
414 assert_eq!(1, EpochDays::from_ymd(2000, 3, 1).extract_day_of_month());
415 }
416
417 #[test]
418 fn test_extract_quarter() {
419 assert_eq!(1, EpochDays::from_ymd(2000, 1, 1).extract_quarter());
420 assert_eq!(1, EpochDays::from_ymd(2000, 2, 1).extract_quarter());
421 assert_eq!(1, EpochDays::from_ymd(2000, 3, 31).extract_quarter());
422 assert_eq!(2, EpochDays::from_ymd(2000, 4, 1).extract_quarter());
423 assert_eq!(3, EpochDays::from_ymd(2000, 7, 1).extract_quarter());
424 assert_eq!(4, EpochDays::from_ymd(2000, 10, 1).extract_quarter());
425 assert_eq!(4, EpochDays::from_ymd(2000, 12, 31).extract_quarter());
426 }
427}