simple_datetime_rs/
date.rs

1use std::str::FromStr;
2use core::{fmt, cmp};
3use crate::date_error::{DateError, DateErrorKind};
4use crate::constants::{MONTH_DAYS, SECONDS_IN_DAY, FOUR_YEARS_DAYS, LEAP_YEAR_DAYS, REGULAR_YEAR_DAYS, UNIX_EPOCH_YEAR, MONTHS_IN_YEAR, THREE_REGULAR_YEAR_DAYS, REGULAR_YEAR_MONTH_DAYS, LEAP_YEAR_MONTH_DAYS};
5use crate::utils::{crossplatform_util, date_util};
6use std::cmp::Ordering;
7use crate::utils::date_util::{leap_year, month_days, month_index};
8
9#[derive(Copy, Clone, Eq, PartialEq)]
10pub struct Date {
11    pub year:u64,
12    pub month:u64,
13    pub day:u64,
14}
15
16impl Date {
17    pub fn new(year:u64, month:u64, day:u64) -> Self{
18        let date = Date{year, month, day};
19        // debug_assert!(date.valid());
20        date
21    }
22
23    #[inline]
24    pub fn leap_year(&self) -> bool {
25        date_util::leap_year(self.year)
26    }
27
28    #[inline]
29    pub fn recent_leap_year(&self) -> u64 {
30        date_util::recent_leap_year(self.year)
31    }
32
33    #[inline]
34    pub fn next_leap_year(&self) -> u64 {
35        date_util::next_leap_year(self.year)
36    }
37
38    pub fn year_day(&self) -> u64 {
39        let is_leap = self.leap_year();
40        let month_days = if is_leap { &LEAP_YEAR_MONTH_DAYS } else { &REGULAR_YEAR_MONTH_DAYS };
41        
42        let mut days:u64 = 0;
43        for i in 0..(self.month_index()) {
44            days += month_days[i];
45        }
46        days + self.day
47    }
48
49    pub fn days_to_next_year(&self) -> u64 {
50        let total = if self.leap_year() {366u64} else {365};
51        total - self.year_day()
52    }
53
54    #[inline]
55    pub fn month_index(&self) -> usize {
56        date_util::month_index(self.month)
57    }
58
59    pub fn valid(&self) -> bool {
60        if self.month < 1 || self.month > 12 || self.day < 1 {
61            return false;
62        }
63        let max_day = MONTH_DAYS[self.month_index()];
64        self.day <= max_day || (self.month == 2 && self.leap_year() && self.day == 29)
65    }
66
67    pub fn from_ms_dos_date(mut ms_dos_date:u16) -> Self {
68        let day = ms_dos_date & 0x1f;
69        ms_dos_date >>= 5;
70        let month = ms_dos_date & 0xf;
71        ms_dos_date >>= 4;
72        Date::new(ms_dos_date as u64 + 1980, month as u64, day as u64)
73    }
74
75    pub fn add_days(&self, days:u64) -> Self{
76        let mut result_year = self.year;
77        let mut result_month = self.month;
78        let mut result_day = self.day + days;
79        
80        // Cache leap year calculation to avoid repeated calls
81        let mut is_leap = date_util::leap_year(result_year);
82        let mut month_days = if is_leap { &LEAP_YEAR_MONTH_DAYS } else { &REGULAR_YEAR_MONTH_DAYS };
83        
84        loop {
85            let days_in_current_month = month_days[(result_month.saturating_sub(1)) as usize];
86            
87            if result_day <= days_in_current_month {
88                break;
89            }
90            
91            result_day -= days_in_current_month;
92            result_month += 1;
93            
94            if result_month > 12 {
95                result_month = 1;
96                result_year += 1;
97                is_leap = date_util::leap_year(result_year);
98                month_days = if is_leap { &LEAP_YEAR_MONTH_DAYS } else { &REGULAR_YEAR_MONTH_DAYS };
99            }
100        }
101        
102        Date::new(result_year, result_month, result_day)
103    }
104
105    pub fn from_seconds_since_unix_epoch(seconds:u64) -> (Self, u64) {
106        let days = seconds / SECONDS_IN_DAY;
107        (Date::new(UNIX_EPOCH_YEAR,1, 1).add_days(days), seconds % SECONDS_IN_DAY)
108    }
109
110    pub fn to_seconds_from_unix_epoch(self, included:bool) -> u64 {
111        let days = self.to_days() - Date::new(UNIX_EPOCH_YEAR,1, 1).to_days();
112        (days + included as u64) * SECONDS_IN_DAY
113    }
114
115    pub fn today() -> Self {
116        let (date, _) = Self::from_seconds_since_unix_epoch(crossplatform_util::now_seconds());
117        date
118    }
119
120    pub fn quarter(&self) -> usize {
121        (self.month_index() / 3) + 1
122    }
123
124    pub fn add_months(&self, months:i64) -> Self {
125        let months = (self.year as i64 * 12 + self.month_index() as i64 + months) as u64;
126        let month = months % MONTHS_IN_YEAR + 1;
127        let year = months / MONTHS_IN_YEAR;
128        let day = cmp::min(date_util::month_days(month, date_util::leap_year(year)), self.day);
129        Date {
130            year,
131            month,
132            day
133        }
134    }
135
136    pub fn sub_months(&self, months:i64) -> Self {
137        self.add_months(-months)
138    }
139
140    pub fn is_month_last_day(&self) -> bool {
141        self.day == date_util::month_days(self.month, self.leap_year())
142    }
143
144    pub fn month_last_day(&self) -> Self {
145        Date {
146            year: self.year,
147            month: self.month,
148            day: date_util::month_days(self.month, self.leap_year())
149        }
150    }
151
152    pub fn sub_days(&self, days:u64) -> Self {
153        let mut result_year = self.year;
154        let mut result_month = self.month;
155        let mut result_day = self.day;
156        
157        // Cache leap year calculation to avoid repeated calls
158        let mut is_leap = date_util::leap_year(result_year);
159        let mut month_days = if is_leap { &LEAP_YEAR_MONTH_DAYS } else { &REGULAR_YEAR_MONTH_DAYS };
160        
161        let mut remaining_days = days;
162        while remaining_days > 0 {
163            if result_day > remaining_days {
164                result_day -= remaining_days;
165                break;
166            }
167            
168            remaining_days -= result_day;
169            result_month -= 1;
170            
171            if result_month == 0 {
172                result_month = 12;
173                result_year -= 1;
174                // Update leap year cache only when year changes
175                is_leap = date_util::leap_year(result_year);
176                month_days = if is_leap { &LEAP_YEAR_MONTH_DAYS } else { &REGULAR_YEAR_MONTH_DAYS };
177            }
178            
179            result_day = month_days[(result_month.saturating_sub(1)) as usize];
180        }
181        
182        Date::new(result_year, result_month, result_day)
183    }
184
185    /*
186        The function does not take "Adoption of the Gregorian calendar https://en.wikipedia.org/wiki/Adoption_of_the_Gregorian_calendar"
187        and similar things into consideration.
188    */
189    pub fn to_days(&self) -> u64 {
190        let year_elapsed = self.year - 1;
191        let leap_years = year_elapsed / 4;
192        let regular_years = year_elapsed - leap_years;
193        
194        let base_days = leap_years * LEAP_YEAR_DAYS + regular_years * REGULAR_YEAR_DAYS;
195        let is_leap = self.leap_year();
196        let month_days = if is_leap { &LEAP_YEAR_MONTH_DAYS } else { &REGULAR_YEAR_MONTH_DAYS };
197        
198        let mut days = 0;
199        for i in 0..(self.month_index()) {
200            days += month_days[i];
201        }
202        
203        base_days + days + self.day
204    }
205
206    pub fn from_days(days:u64) -> Self {
207        let days = days - 1;
208        let quarters = days / FOUR_YEARS_DAYS;
209        let days  = days % FOUR_YEARS_DAYS;
210        let (years, mut days) = if days / THREE_REGULAR_YEAR_DAYS == 0 {
211            (days / REGULAR_YEAR_DAYS, days % REGULAR_YEAR_DAYS)
212        } else {(3, days % THREE_REGULAR_YEAR_DAYS)};
213        let year = (quarters << 2) + years + 1;
214        
215        let is_leap = date_util::leap_year(year);
216        let month_days = if is_leap { &LEAP_YEAR_MONTH_DAYS } else { &REGULAR_YEAR_MONTH_DAYS };
217        
218        let mut month = 1;
219        for &days_in_month in month_days {
220            if days < days_in_month {
221                break;
222            }
223            days -= days_in_month;
224            month += 1;
225        }
226        Date::new(year, month, days + 1)
227    }
228
229    /*
230        The function does not take "Adoption of the Gregorian calendar https://en.wikipedia.org/wiki/Adoption_of_the_Gregorian_calendar"
231        and similar things into consideration.
232    */
233    #[inline]
234    fn weekday(&self) -> u8 {
235        (self.to_days() % 7) as u8
236    }
237
238    pub fn is_monday(&self) -> bool{
239        self.weekday() == 2
240    }
241
242    pub fn is_tuesday(&self) -> bool{
243        self.weekday() == 3
244    }
245
246    pub fn is_wednesday(&self) -> bool{
247        self.weekday() == 4
248    }
249
250    pub fn is_thursday(&self) -> bool{
251        self.weekday() == 5
252    }
253
254    pub fn is_friday(&self) -> bool{
255        self.weekday() == 6
256    }
257
258    pub fn is_saturday(&self) -> bool{
259        self.weekday() == 0
260    }
261
262    pub fn is_sunday(&self) -> bool{
263        self.weekday() == 1
264    }
265
266    pub fn is_weekend(&self) -> bool{
267        self.weekday() < 2
268    }
269
270    pub fn is_week_day(&self) -> bool{
271        self.weekday() > 1
272    }
273
274    pub fn normalize(&self) -> Date {
275        let month_index = month_index(self.month) as u64;
276        let year = month_index / 12 + self.year;
277        let month = month_index as u64 % 12 + 1;
278        let mut result = Self::new(year, month, self.day);
279        let max_days = month_days(month, leap_year(year));
280        if max_days < result.day {
281            let add = result.day - max_days;
282            result.day = max_days;
283            result = result.add_days(add);
284        }
285        result
286    }
287}
288
289impl std::ops::Sub for Date {
290    type Output = u64;
291
292    fn sub(self, rhs: Self) -> Self::Output {
293        self.to_days() - rhs.to_days()
294    }
295}
296
297impl fmt::Display for Date {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        write!(f, "{}-{:02}-{:02}", self.year, self.month, self.day)
300    }
301}
302
303impl fmt::Debug for Date {
304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        fmt::Display::fmt(self, f)
306    }
307}
308
309impl FromStr for Date {
310    type Err = DateError;
311
312    fn from_str(date_str: &str) -> Result<Self, Self::Err> {
313        // Optimized parsing without allocations
314        let bytes = date_str.as_bytes();
315        if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
316            return Err(DateErrorKind::WrongDateStringFormat.into());
317        }
318        
319        // Parse year (positions 0-3)
320        let year = parse_digits(&bytes[0..4])?;
321        
322        // Parse month (positions 5-6)
323        let month = parse_digits(&bytes[5..7])?;
324        
325        // Parse day (positions 8-9)
326        let day = parse_digits(&bytes[8..10])?;
327        
328        Ok(Date::new(year, month, day))
329    }
330}
331
332#[inline]
333fn parse_digits(bytes: &[u8]) -> Result<u64, DateError> {
334    let mut result = 0u64;
335    for &byte in bytes {
336        if byte < b'0' || byte > b'9' {
337            return Err(DateErrorKind::WrongDateStringFormat.into());
338        }
339        result = result * 10 + (byte - b'0') as u64;
340    }
341    Ok(result)
342}
343
344impl Ord for Date {
345    fn cmp(&self, other: &Self) -> Ordering {
346        if self.year != other.year {
347            return self.year.cmp(&other.year);
348        }
349        if self.month != other.month {
350            return self.month.cmp(&other.month);
351        }
352        self.day.cmp(&other.day)
353    }
354}
355
356impl PartialOrd for Date {
357    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
358        Some(self.cmp(other))
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_date_year_day(){
368        let date = Date::new(2020, 1,1);
369        assert_eq!(date.year_day(), 1);
370        let date = Date::new(2018, 2,28);
371        assert_eq!(date.year_day(), 59);
372        let date = Date::new(2016, 12,31);
373        assert_eq!(date.year_day(), 366);
374    }
375
376    #[test]
377    fn test_date_sub(){
378        let date_1 = Date::new(2020, 1,1);
379        let date_2 = Date::new(2019, 12,31);
380        assert_eq!(date_1 - date_2, 1);
381        let date_1 = Date::new(2020, 1,1);
382        let date_2 = Date::new(2016, 1,1);
383        assert_eq!(date_1 - date_2, 1461);
384        let date_1 = Date::new(2020, 1,1);
385        let date_2 = Date::new(2016, 3,1);
386        assert_eq!(date_1 - date_2, 1401);
387        let date_1 = Date::new(2020, 3,4);
388        let date_2 = Date::new(2016, 3,1);
389        assert_eq!(date_1 - date_2, 1464);
390        // let date_1 = Date::new(2019, 12,31);
391        // let date_2 = Date::new(2020, 1,1);
392        // assert_eq!(date_1 - date_2, Date::from_days(1));
393        let date_1 = Date::new(2021, 3,2);
394        let date_2 = Date::new(2019, 12,31);
395        assert_eq!(date_1 - date_2, 427);
396        let date_1 = Date::new(2021, 3,2);
397        let date_2 = Date::new(2020, 12,31);
398        assert_eq!(date_1 - date_2, 61);
399        let date_1 = Date::new(2021, 3,2);
400        let date_2 = Date::new(2020, 1,15);
401        assert_eq!(date_1 - date_2, 412);
402        let date_1 = Date::new(2020, 12,31);
403        let date_2 = Date::new(2020, 1,1);
404        assert_eq!(date_1 - date_2, 365);
405    }
406
407    #[test]
408    fn test_date_from_str() -> Result<(), DateError>{
409        assert_eq!(Date::from_str("2020-02-29")?, Date::new(2020, 2, 29));
410        Ok(())
411    }
412
413    #[test]
414    fn test_add_days(){
415        assert_eq!(Date::new(2019, 12,31).add_days(1), Date::new(2020, 1,1));
416        assert_eq!(Date::new(2019, 12,31).add_days(3753), Date::new(2030, 4,10));
417        assert_eq!(Date::new(2019, 2,28).add_days(365), Date::new(2020, 2,28));
418        assert_eq!(Date::new(2019, 2,28).add_days(366), Date::new(2020, 2,29));
419        assert_eq!(Date::new(2019, 3,1).add_days(366), Date::new(2020, 3,1));
420        assert_eq!(Date::new(2018, 1,1).add_days(1198), Date::new(2021, 4,13));
421    }
422
423    #[test]
424    fn test_ms_dos_date(){
425        assert_eq!(Date::from_ms_dos_date(0x354b), Date::new(2006, 10, 11));
426    }
427
428    #[test]
429    fn test_date_cmp() {
430        assert!(Date::new(2019, 12,31) < Date::new(2020, 1,1));
431        assert!(Date::new(2020, 2,1) > Date::new(2020, 1,31));
432        assert!(Date::new(2020, 3,31) > Date::new(2020, 3,30));
433        assert_eq!(Date::new(2020, 1,1), Date::new(2020, 1,1));
434    }
435
436    #[test]
437    fn test_add_months() {
438        assert_eq!(Date::new(2019, 12,31).add_months(2), Date::new(2020, 2,29));
439        assert_eq!(Date::new(2019, 12,31).add_months(26), Date::new(2022, 2,28));
440        assert_eq!(Date::new(2019, 12,31).add_months(1), Date::new(2020, 1,31));
441        assert_eq!(Date::new(2020, 2,29).add_months(-2), Date::new(2019, 12,29));
442    }
443
444    #[test]
445    fn test_is_month_last_day() {
446        assert!(Date::new(2019, 12,31).is_month_last_day());
447        assert!(!Date::new(2019, 12,30).is_month_last_day());
448        assert!(Date::new(2019, 2,28).is_month_last_day());
449        assert!(!Date::new(2020, 2,28).is_month_last_day());
450        assert!(Date::new(2020, 2,29).is_month_last_day());
451    }
452
453    #[test]
454    fn test_month_last_day() {
455        assert_eq!(Date::new(2019, 2,2).month_last_day(), Date::new(2019, 2,28));
456        assert_eq!(Date::new(2020, 2,2).month_last_day(), Date::new(2020, 2,29));
457    }
458
459    #[test]
460    fn test_to_seconds_from_unix_epoch() {
461        assert_eq!(Date::new(1970, 1,1).to_seconds_from_unix_epoch(false), 0);
462        assert_eq!(Date::new(1970, 1,1).to_seconds_from_unix_epoch(true), SECONDS_IN_DAY);
463    }
464
465    #[test]
466    fn test_to_days() {
467        assert_eq!(Date::new(1, 1,1).to_days(), 1);
468        assert_eq!(Date::new(1, 12,31).to_days(), 365);
469        assert_eq!(Date::new(4, 2,29).to_days(), 1155);
470        assert_eq!(Date::new(5, 1,1).to_days(), 1462);
471    }
472
473    #[test]
474    fn test_from_days() {
475        assert_eq!(Date::from_days(1), Date::new(1, 1,1));
476        assert_eq!(Date::from_days(365), Date::new(1, 12,31));
477        assert_eq!(Date::from_days(1155), Date::new(4, 2,29));
478        assert_eq!(Date::from_days(1462), Date::new(5, 1,1));
479    }
480
481    #[test]
482    fn test_is_monday() {
483        assert_eq!(Date::new(2021, 8,2).is_monday(), true);
484        assert_eq!(Date::new(2021, 8,3).is_monday(), false);
485    }
486
487    #[test]
488    fn test_is_tuesday() {
489        assert_eq!(Date::new(2021, 8,3).is_tuesday(), true);
490        assert_eq!(Date::new(2021, 8,4).is_tuesday(), false);
491    }
492
493    #[test]
494    fn test_is_wednesday() {
495        assert_eq!(Date::new(2021, 8,4).is_wednesday(), true);
496        assert_eq!(Date::new(2021, 8,5).is_wednesday(), false);
497    }
498
499    #[test]
500    fn test_is_thursday() {
501        assert_eq!(Date::new(2021, 8,5).is_thursday(), true);
502        assert_eq!(Date::new(2021, 8,6).is_thursday(), false);
503    }
504
505    #[test]
506    fn test_is_friday() {
507        assert_eq!(Date::new(2021, 8,6).is_friday(), true);
508        assert_eq!(Date::new(2021, 8,7).is_friday(), false);
509    }
510
511    #[test]
512    fn test_is_saturday() {
513        assert_eq!(Date::new(2021, 8,7).is_saturday(), true);
514        assert_eq!(Date::new(2021, 8,8).is_saturday(), false);
515    }
516
517    #[test]
518    fn test_is_sunday() {
519        assert_eq!(Date::new(2021, 8,8).is_sunday(), true);
520        assert_eq!(Date::new(2021, 8,9).is_sunday(), false);
521    }
522
523    #[test]
524    fn test_add_sub_days(){
525        let a = Date::new(2020,12,31);
526        assert_eq!(a.add_days(1).sub_days(1), a);
527    }
528
529    #[test]
530    fn test_sub_months() {
531        assert_eq!(Date::new(2020, 2,29).sub_months(2), Date::new(2019, 12,29));
532        assert_eq!(Date::new(2020, 4,30).sub_months(2), Date::new(2020, 2,29));
533        assert_eq!(Date::new(2022, 2,28).sub_months(26), Date::new(2019, 12,28));
534        assert_eq!(Date::new(2020, 1,31).sub_months(1), Date::new(2019, 12,31));
535    }
536
537    #[test]
538    fn test_normalize() {
539        assert_eq!(Date::new(2020, 49,32).normalize(), Date::new(2024, 2, 1));
540        assert_eq!(Date::new(2020, 49,60).normalize(), Date::new(2024, 2, 29));
541        assert_eq!(Date::new(2020, 49,61).normalize(), Date::new(2024, 3, 1));
542    }
543
544    #[test]
545    fn test_date_validation() {
546        // Valid dates
547        assert!(Date::new(2020, 2, 29).valid()); // Leap year
548        assert!(Date::new(2021, 2, 28).valid()); // Non-leap year
549        assert!(Date::new(2020, 12, 31).valid());
550        assert!(Date::new(2020, 1, 1).valid());
551        
552        // Invalid dates
553        assert!(!Date::new(2021, 2, 29).valid()); // Feb 29 in non-leap year
554        assert!(!Date::new(2020, 2, 30).valid()); // Feb 30
555        assert!(!Date::new(2020, 4, 31).valid()); // Apr 31
556        assert!(!Date::new(2020, 6, 31).valid()); // Jun 31
557        assert!(!Date::new(2020, 9, 31).valid()); // Sep 31
558        assert!(!Date::new(2020, 11, 31).valid()); // Nov 31
559        assert!(!Date::new(2020, 0, 1).valid()); // Month 0
560        assert!(!Date::new(2020, 13, 1).valid()); // Month 13
561        assert!(!Date::new(2020, 1, 0).valid()); // Day 0
562        assert!(!Date::new(2020, 1, 32).valid()); // Day 32
563    }
564
565    #[test]
566    fn test_date_from_str_invalid() {
567        assert!("invalid".parse::<Date>().is_err());
568        assert!("2020".parse::<Date>().is_err());
569        assert!("2020-13".parse::<Date>().is_err());
570        assert!("not-a-date".parse::<Date>().is_err());
571        assert!("2020/01/01".parse::<Date>().is_err());
572    }
573
574    #[test]
575    fn test_edge_cases() {
576        let date = Date::new(1, 1, 1);
577        assert!(date.valid());
578        assert_eq!(date.to_days(), 1);
579        
580        let date = Date::new(9999, 12, 31);
581        assert!(date.valid());
582        
583        assert!(Date::new(2000, 1, 1).leap_year());
584        assert!(!Date::new(1900, 1, 1).leap_year());
585        assert!(Date::new(2004, 1, 1).leap_year());
586        assert!(!Date::new(2001, 1, 1).leap_year());
587    }
588
589    #[test]
590    fn test_quarter_calculation() {
591        assert_eq!(Date::new(2020, 1, 1).quarter(), 1);
592        assert_eq!(Date::new(2020, 3, 31).quarter(), 1);
593        assert_eq!(Date::new(2020, 4, 1).quarter(), 2);
594        assert_eq!(Date::new(2020, 6, 30).quarter(), 2);
595        assert_eq!(Date::new(2020, 7, 1).quarter(), 3);
596        assert_eq!(Date::new(2020, 9, 30).quarter(), 3);
597        assert_eq!(Date::new(2020, 10, 1).quarter(), 4);
598        assert_eq!(Date::new(2020, 12, 31).quarter(), 4);
599    }
600
601    #[test]
602    fn test_weekend_weekday() {
603        assert!(Date::new(2021, 8, 7).is_weekend());
604        assert!(Date::new(2021, 8, 8).is_weekend());
605        assert!(!Date::new(2021, 8, 9).is_weekend());
606        assert!(Date::new(2021, 8, 9).is_week_day());
607        assert!(Date::new(2021, 8, 10).is_week_day());
608        assert!(!Date::new(2021, 8, 7).is_week_day());
609        assert!(!Date::new(2021, 8, 8).is_week_day());
610    }
611
612    #[test]
613    fn test_days_to_next_year() {
614        assert_eq!(Date::new(2020, 1, 1).days_to_next_year(), 365);
615        assert_eq!(Date::new(2021, 1, 1).days_to_next_year(), 364);
616        assert_eq!(Date::new(2020, 12, 31).days_to_next_year(), 0);
617        assert_eq!(Date::new(2020, 6, 15).days_to_next_year(), 199);
618    }
619
620    #[test]
621    fn test_year_day() {
622        assert_eq!(Date::new(2020, 1, 1).year_day(), 1);
623        assert_eq!(Date::new(2020, 1, 31).year_day(), 31);
624        assert_eq!(Date::new(2020, 2, 1).year_day(), 32);
625        assert_eq!(Date::new(2020, 2, 29).year_day(), 60);
626        assert_eq!(Date::new(2021, 2, 28).year_day(), 59);
627        assert_eq!(Date::new(2020, 12, 31).year_day(), 366);
628        assert_eq!(Date::new(2021, 12, 31).year_day(), 365);
629    }
630}