irox_time/
gregorian.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2025 IROX Contributors
3//
4
5//!
6//! Contains [`Date`] and associated elements to represent a Proleptic Gregorian Date.
7//!
8
9extern crate alloc;
10use core::fmt::{Display, Formatter};
11use core::ops::{Add, AddAssign, Sub};
12
13use irox_enums::{EnumIterItem, EnumName, EnumTryFromStr};
14use irox_units::bounds::{GreaterThanEqualToValueError, LessThanValue, Range};
15use irox_units::units::duration::{Duration, DurationUnit};
16
17use crate::epoch::{UnixTimestamp, JULIAN_DAY_1_JAN_YR0, LEAPOCH_TIMESTAMP, UNIX_EPOCH};
18use crate::format::iso8601::ExtendedDateFormat;
19use crate::format::{Format, FormatError, FormatParser};
20use crate::julian::JulianDate;
21use crate::SECONDS_IN_DAY;
22
23pub use alloc::string::String;
24
25/// Days per 4 Year Window
26///
27/// Each window has 1 extra leap day in it
28pub const DAYS_PER_4YEAR: u32 = 365 * 4 + 1;
29
30/// Days per 100 Year Window
31///
32/// There are 25x 4Y Windows in a 100Y Window, except the 100th isn't a Leap.
33pub const DAYS_PER_100YEAR: u32 = DAYS_PER_4YEAR * 25 - 1;
34
35///
36/// Days per 400 Year Window
37///
38/// There are 4x 100Y Windows in a 400Y Window, with an extra leap day
39pub const DAYS_PER_400YEAR: u32 = DAYS_PER_100YEAR * 4 + 1;
40
41///
42/// Gregorian Month enumeration
43#[derive(
44    Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, EnumName, EnumIterItem, EnumTryFromStr,
45)]
46pub enum Month {
47    January = 1,
48    February = 2,
49    March = 3,
50    April = 4,
51    May = 5,
52    June = 6,
53    July = 7,
54    August = 8,
55    September = 9,
56    October = 10,
57    November = 11,
58    December = 12,
59}
60
61impl Display for Month {
62    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
63        f.write_fmt(format_args!("{}", self.name()))
64    }
65}
66
67impl Month {
68    ///
69    /// Returns the total number of days in the month for the indicated gregorian
70    /// year.
71    #[allow(clippy::match_same_arms)]
72    pub const fn days_in_month(&self, year: i32) -> u8 {
73        match self {
74            Month::January => 31,
75            Month::February => {
76                if is_leap_year(year) {
77                    29
78                } else {
79                    28
80                }
81            }
82            Month::March => 31,
83            Month::April => 30,
84            Month::May => 31,
85            Month::June => 30,
86            Month::July => 31,
87            Month::August => 31,
88            Month::September => 30,
89            Month::October => 31,
90            Month::November => 30,
91            Month::December => 31,
92        }
93    }
94
95    ///
96    /// Returns the start day of the year for this month for the indicated year, zero-based 01-JAN is 00.
97    #[must_use]
98    pub const fn start_day_of_year(&self, year: i32) -> u16 {
99        if is_leap_year(year) {
100            match self {
101                Month::January => 0,
102                Month::February => 31,
103                Month::March => 60,
104                Month::April => 91,
105                Month::May => 121,
106                Month::June => 152,
107                Month::July => 182,
108                Month::August => 213,
109                Month::September => 244,
110                Month::October => 274,
111                Month::November => 305,
112                Month::December => 335,
113            }
114        } else {
115            match self {
116                Month::January => 0,
117                Month::February => 31,
118                Month::March => 59,
119                Month::April => 90,
120                Month::May => 120,
121                Month::June => 151,
122                Month::July => 181,
123                Month::August => 212,
124                Month::September => 243,
125                Month::October => 273,
126                Month::November => 304,
127                Month::December => 334,
128            }
129        }
130    }
131
132    ///
133    /// Returns the end day of the year for this month for the indicated year, zero-based, JAN-31 is 30.
134    #[must_use]
135    pub const fn end_day_of_year(&self, year: i32) -> u16 {
136        if is_leap_year(year) {
137            match self {
138                Month::January => 30,
139                Month::February => 59,
140                Month::March => 90,
141                Month::April => 120,
142                Month::May => 151,
143                Month::June => 181,
144                Month::July => 212,
145                Month::August => 243,
146                Month::September => 273,
147                Month::October => 304,
148                Month::November => 334,
149                Month::December => 365,
150            }
151        } else {
152            match self {
153                Month::January => 30,
154                Month::February => 58,
155                Month::March => 89,
156                Month::April => 119,
157                Month::May => 150,
158                Month::June => 180,
159                Month::July => 211,
160                Month::August => 242,
161                Month::September => 272,
162                Month::October => 303,
163                Month::November => 333,
164                Month::December => 364,
165            }
166        }
167    }
168
169    ///
170    /// Returns a range verifier to check if the indicate day number is valid for
171    /// the particular month and year
172    #[must_use]
173    pub const fn valid_day_number(&self, year: i32) -> LessThanValue<u8> {
174        let upper_lim = self.days_in_month(year);
175        LessThanValue::new(upper_lim + 1)
176    }
177
178    ///
179    /// Returns true if the indicate day of the year is within this month
180    #[must_use]
181    pub const fn day_is_within_month(&self, year: i32, day_of_year: u16) -> bool {
182        day_of_year >= self.start_day_of_year(year) && day_of_year <= self.end_day_of_year(year)
183    }
184
185    ///
186    /// Returns true if the indicated date is within this month
187    #[must_use]
188    pub const fn date_is_within_month(&self, date: &Date) -> bool {
189        self.day_is_within_month(date.year, date.day_of_year)
190    }
191}
192
193impl TryFrom<u8> for Month {
194    type Error = GreaterThanEqualToValueError<u8>;
195
196    fn try_from(value: u8) -> Result<Self, Self::Error> {
197        let range = LessThanValue::new(13);
198        Ok(match value {
199            1 => Month::January,
200            2 => Month::February,
201            3 => Month::March,
202            4 => Month::April,
203            5 => Month::May,
204            6 => Month::June,
205            7 => Month::July,
206            8 => Month::August,
207            9 => Month::September,
208            10 => Month::October,
209            11 => Month::November,
210            12 => Month::December,
211            e => return Self::Error::err(e, range),
212        })
213    }
214}
215
216///
217/// Gregorian Date - a specific date on a calendar.
218#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
219pub struct Date {
220    ///
221    /// Year is the Proleptic Gregorian Year
222    pub(crate) year: i32,
223
224    ///
225    /// Day of Year is the day index into the specified year, range [0, 366)
226    pub(crate) day_of_year: u16,
227}
228
229impl Display for Date {
230    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
231        write!(f, "{}", ExtendedDateFormat.format(self))
232    }
233}
234
235impl Date {
236    ///
237    /// Constructs a new date given the provided gregorian year and day of year.
238    /// Returns `Err(GreaterThanEqualToValueError)` if `day_of_year` is outside the valid range.
239    pub fn new(year: i32, day_of_year: u16) -> Result<Date, GreaterThanEqualToValueError<u16>> {
240        let valid_num_days = if is_leap_year(year) { 366 } else { 365 };
241
242        LessThanValue::new(valid_num_days).check_value_is_valid(&day_of_year)?;
243
244        Ok(Date { year, day_of_year })
245    }
246
247    ///
248    /// Constructs a new date given the provided values.  If month or day is out
249    /// of range, will return `Err(OutsideRangeError)`.
250    ///
251    /// Note: The 'day' parameter here is in the range of `1..=31` as would be shown on a calendar
252    pub fn try_from_values(
253        year: i32,
254        month: u8,
255        day: u8,
256    ) -> Result<Date, GreaterThanEqualToValueError<u8>> {
257        let month: Month = month.try_into()?;
258        Self::try_from(year, month, day)
259    }
260
261    ///
262    /// Constructs a new date given the provided values, if day is out of range,
263    /// will return `Err(OutsideRangeError)`
264    ///
265    /// Note: The 'day' parameter here is in the range of `1..=31` as would be shown on a calendar
266    pub fn try_from(
267        year: i32,
268        month: Month,
269        day: u8,
270    ) -> Result<Date, GreaterThanEqualToValueError<u8>> {
271        month.valid_day_number(year).check_value_is_valid(&day)?;
272        let day_of_year = (month.start_day_of_year(year) + day as u16) - 1;
273        Ok(Date { year, day_of_year })
274    }
275
276    ///
277    /// Returns the gregorian year this date represents
278    #[must_use]
279    pub fn year(&self) -> i32 {
280        self.year
281    }
282
283    ///
284    /// Returns the day offset into the year.  January 1 is '0', January 31 is '30',
285    /// February 1 is '31', and so on.
286    #[must_use]
287    pub fn day_of_year_offset(&self) -> u16 {
288        self.day_of_year
289    }
290
291    ///
292    /// Returns the calendar day of the year.  January 1 is '1', January 31 is '31',
293    /// February 1 is '32', and so on.
294    #[must_use]
295    pub fn day_of_year(&self) -> u16 {
296        self.day_of_year + 1
297    }
298
299    ///
300    /// Returns the [`Month`] this date is contained within
301    #[must_use]
302    pub fn month_of_year(&self) -> Month {
303        for month in Month::iter_items() {
304            if month.date_is_within_month(self) {
305                return month;
306            }
307        }
308
309        // TODO: Proper normalization here.
310        Month::January
311    }
312
313    ///
314    /// Returns the day index into the current month in the range [0,31)
315    #[must_use]
316    pub fn day_of_month(&self) -> u8 {
317        (self.day_of_year - self.month_of_year().start_day_of_year(self.year)) as u8
318    }
319
320    /// Adds the specified number of days to this date.
321    #[must_use]
322    pub const fn add_days(&self, days: u32) -> Date {
323        let mut days_remaining = days;
324        let mut years = self.year;
325        let mut days = self.day_of_year as u32;
326
327        loop {
328            let days_in_year = days_in_year(years) as u32;
329            if days + days_remaining >= days_in_year {
330                years += 1;
331                days_remaining -= days_in_year - days;
332                days = 0;
333                continue;
334            }
335            days += days_remaining;
336            break;
337        }
338        Date {
339            year: years,
340            day_of_year: days as u16,
341        }
342    }
343
344    /// Subtracts the given number of days from this date
345    #[must_use]
346    pub const fn sub_days(&self, days: u16) -> Date {
347        let mut days_remaining = days;
348        let mut years = self.year;
349        let mut days = self.day_of_year;
350
351        loop {
352            if days_remaining > days {
353                years -= 1;
354                let days_in_year = days_in_year(years);
355                days_remaining -= days;
356                days += days_in_year;
357                continue;
358            }
359            days -= days_remaining;
360            break;
361        }
362        Date {
363            year: years,
364            day_of_year: days,
365        }
366    }
367
368    /// Adds the specified number of years to this date.
369    #[must_use]
370    pub const fn add_years(&self, years: u16) -> Date {
371        Date {
372            year: self.year + years as i32,
373            day_of_year: self.day_of_year,
374        }
375    }
376
377    /// Subtracts the specified number of years from this date.
378    #[must_use]
379    pub const fn sub_years(&self, years: u16) -> Date {
380        Date {
381            year: self.year - years as i32,
382            day_of_year: self.day_of_year,
383        }
384    }
385
386    ///
387    /// Returns the [`UnixTimestamp`] of this Date
388    #[must_use]
389    pub fn as_unix_timestamp(&self) -> UnixTimestamp {
390        self.into()
391    }
392
393    ///
394    /// Returns the [`JulianDate`] of this date
395    #[must_use]
396    pub fn as_julian_day(&self) -> JulianDate {
397        self.into()
398    }
399
400    ///
401    /// Formats this date using the specified formatter
402    #[must_use]
403    pub fn format<F: Format<Self>>(&self, format: &F) -> String {
404        format.format(self)
405    }
406
407    ///
408    /// Attempts to parse a date from the string using the specified formatter
409    pub fn parse_from<F: FormatParser<Self>>(
410        format: &F,
411        string: &str,
412    ) -> Result<Self, FormatError> {
413        format.try_from(string)
414    }
415
416    ///
417    /// Returns the day-of-the-week name of this date, using ISO8601 convention that the week starts on Monday.
418    pub fn day_of_week(&self) -> DayOfWeek {
419        let prime = self.as_julian_day().as_prime_date();
420        let dow = prime.get_day_number() as i32 % 7;
421        match dow {
422            1 => DayOfWeek::Tuesday,
423            2 => DayOfWeek::Wednesday,
424            3 => DayOfWeek::Thursday,
425            4 => DayOfWeek::Friday,
426            5 => DayOfWeek::Saturday,
427            6 => DayOfWeek::Sunday,
428            _ => DayOfWeek::Monday,
429        }
430    }
431
432    ///
433    /// Returns a pair (year number, week of year)
434    pub fn week_number(&self) -> (i32, u8) {
435        let jan01 = Date {
436            year: self.year,
437            day_of_year: 0,
438        };
439        let dow = self.day_of_week() as i32;
440        let wkno = (9 + self.day_of_year as i32 - dow) / 7;
441
442        if wkno == 0 {
443            let dow = jan01.day_of_week() as i32;
444            if (0..=3).contains(&dow) {
445                return (self.year, 1);
446            }
447            let year = self.year - 1;
448            if dow == 4 {
449                // friday, always W53 of prev year
450                return (year, 53);
451            }
452            if dow == 5 {
453                // saturday, W53 in leaps, W52 otherwise.
454                let prev_yr_starts_on = Date {
455                    year,
456                    day_of_year: 0,
457                }
458                .day_of_week();
459                if is_leap_year(year) && prev_yr_starts_on == DayOfWeek::Thursday {
460                    return (year, 53);
461                }
462            }
463            // sunday, always W52
464            return (year, 52);
465        }
466        if wkno == 53 {
467            // only actually 53 if is a long year
468            if is_long_year(self.year) {
469                return (self.year, 53);
470            }
471            // otherwise is Year 01 of the following year.
472            return (self.year + 1, 1);
473        }
474
475        (self.year, wkno as u8)
476    }
477}
478
479///
480/// Day of the week enumeration, following ISO8601 convention of "Monday is the start of the week"
481#[derive(
482    Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, EnumName, EnumIterItem, EnumTryFromStr,
483)]
484pub enum DayOfWeek {
485    Monday = 0,
486    Tuesday = 1,
487    Wednesday = 2,
488    Thursday = 3,
489    Friday = 4,
490    Saturday = 5,
491    Sunday = 6,
492}
493
494impl TryFrom<u8> for DayOfWeek {
495    type Error = GreaterThanEqualToValueError<u8>;
496
497    fn try_from(value: u8) -> Result<Self, Self::Error> {
498        Ok(match value {
499            0 => DayOfWeek::Monday,
500            1 => DayOfWeek::Tuesday,
501            2 => DayOfWeek::Wednesday,
502            3 => DayOfWeek::Thursday,
503            4 => DayOfWeek::Friday,
504            5 => DayOfWeek::Saturday,
505            6 => DayOfWeek::Sunday,
506            e => return GreaterThanEqualToValueError::err(e, LessThanValue::new(8)),
507        })
508    }
509}
510
511///
512/// Returns true if the indicated year is a ISO8601 "Long Year" with 53 Weeks in it.
513pub fn is_long_year(year: i32) -> bool {
514    let start_day = Date {
515        year,
516        day_of_year: 0,
517    }
518    .day_of_week();
519    let is_leap = is_leap_year(year);
520    if start_day == DayOfWeek::Thursday {
521        return true;
522    }
523    if start_day == DayOfWeek::Wednesday && is_leap {
524        return true;
525    }
526    false
527}
528
529///
530/// Checks if a gregorian year is considered a "leap year"
531///
532/// Every year that is exactly divisible by four is a leap year, except for
533/// years that are exactly divisible by 100, but these centurial years are leap
534/// years if they are exactly divisible by 400. For example, the years 1700,
535/// 1800, and 1900 are not leap years, but the years 1600 and 2000 are.
536///
537/// `365 + 1/4 - 1/100 + 1/400 = 365 + 97/400 = 365.2425`
538///
539/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Leap_year#Gregorian_calendar)
540pub const fn is_leap_year(year: i32) -> bool {
541    if year % 400 == 0 {
542        return true;
543    }
544    if year % 100 == 0 {
545        return false;
546    }
547    year % 4 == 0
548}
549
550///
551/// Returns the total number of days in the indicated calendar year
552pub const fn days_in_year(year: i32) -> u16 {
553    if is_leap_year(year) {
554        366
555    } else {
556        365
557    }
558}
559
560///
561/// Returns the total number of seconds in the indicated calendar year
562pub const fn seconds_in_year(year: i32) -> u32 {
563    days_in_year(year) as u32 * SECONDS_IN_DAY
564}
565
566impl From<&Date> for UnixTimestamp {
567    fn from(value: &Date) -> Self {
568        let years_duration = value.year - UNIX_EPOCH.0.year;
569        if years_duration < 0 {
570            return UnixTimestamp::default();
571        }
572        let mut secs_duration: u64 = value.day_of_year as u64 * SECONDS_IN_DAY as u64;
573        for year in UNIX_EPOCH.0.year..value.year {
574            secs_duration += seconds_in_year(year) as u64;
575        }
576
577        UnixTimestamp::from_seconds_f64(secs_duration as f64)
578    }
579}
580
581impl From<&UnixTimestamp> for Date {
582    #[allow(clippy::integer_division)]
583    fn from(value: &UnixTimestamp) -> Self {
584        // Algorithm impl based on libmusl __secs_to_tm.c
585        let sec_in_day = SECONDS_IN_DAY as i64;
586        let leapoch = LEAPOCH_TIMESTAMP.get_offset().as_seconds() as i64;
587        let offset = value.get_offset().as_seconds() as i64;
588
589        // clever impl - the leapoch is a nice round 400 cycle leap year
590        // so we compute the negative offset (for dates before the leapoch)
591        // and the positive offset (for dates after the leapoch).  This ensures
592        // that the "day calculation" is always 0 aligned to a round leap cycle.
593        // Therefore, there's no weird offsets and calculations and even division.
594        let seconds = offset - leapoch;
595        let mut days = seconds / sec_in_day;
596        if seconds % sec_in_day < 0 {
597            days -= 1;
598        }
599
600        // compute the number of 400 leap year (qc) cycles
601        let mut qc_cycles = days / DAYS_PER_400YEAR as i64;
602        let mut rem_days = days % DAYS_PER_400YEAR as i64;
603        if rem_days < 0 {
604            rem_days += DAYS_PER_400YEAR as i64;
605            qc_cycles -= 1;
606        }
607
608        // compute the remaining number of 100 non-leap year (c) cycles
609        let mut c_cycles = rem_days / DAYS_PER_100YEAR as i64;
610        if c_cycles == 4 {
611            c_cycles -= 1;
612        }
613        rem_days -= c_cycles * DAYS_PER_100YEAR as i64;
614
615        // compute the remaining number 4 leap year (q) cycles
616        let mut q_cycles = rem_days / DAYS_PER_4YEAR as i64;
617        if q_cycles == 25 {
618            q_cycles -= 1;
619        }
620        rem_days -= q_cycles * DAYS_PER_4YEAR as i64;
621
622        // and lastly, the number of non-leap years
623        let mut rem_years = rem_days / 365;
624        if rem_years == 4 {
625            rem_years -= 1;
626        }
627        rem_days -= rem_years * 365;
628
629        let mut year = rem_years + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles + 2000;
630        let mut yday = rem_days + 31 + 28; // because the LEAPOCH is 1-MAR
631        if is_leap_year(year as i32) {
632            if yday + 1 > 365 {
633                year += 1;
634                yday -= 366;
635            }
636            yday += 1;
637        } else if yday > 364 {
638            year += 1;
639            yday -= 365;
640        }
641
642        Date {
643            year: year as i32,
644            day_of_year: yday as u16,
645        }
646    }
647}
648
649impl From<&Date> for JulianDate {
650    #[allow(clippy::integer_division)]
651    fn from(value: &Date) -> Self {
652        let mut years = value.year - 1;
653        let qc_years = years / 400;
654        years -= qc_years * 400;
655        let c_years = years / 100;
656        years -= c_years * 100;
657        let q_years = years / 4 + 1;
658        let leap_days = qc_years * 97 + c_years * 24 + q_years;
659
660        let duration_days = value.year * 365 + leap_days + value.day_of_year as i32;
661
662        let duration_days = duration_days as f64 + JULIAN_DAY_1_JAN_YR0;
663        JulianDate::new(duration_days)
664    }
665}
666
667impl From<Date> for JulianDate {
668    fn from(value: Date) -> Self {
669        From::<&Date>::from(&value)
670    }
671}
672
673impl From<&JulianDate> for Date {
674    fn from(value: &JulianDate) -> Self {
675        value.get_julian_epoch().get_epoch().0
676            + Duration::new(value.get_day_number(), DurationUnit::Day)
677    }
678}
679
680impl From<JulianDate> for Date {
681    fn from(value: JulianDate) -> Self {
682        From::<&JulianDate>::from(&value)
683    }
684}
685
686impl Sub<&Date> for Date {
687    type Output = Duration;
688
689    fn sub(self, rhs: &Date) -> Self::Output {
690        if self == *rhs {
691            return Duration::new_seconds(0.0);
692        }
693        let duration = self.as_julian_day().get_day_number() - rhs.as_julian_day().get_day_number();
694        Duration::new(duration, DurationUnit::Day)
695    }
696}
697
698impl Sub<Date> for Date {
699    type Output = Duration;
700
701    fn sub(self, rhs: Date) -> Self::Output {
702        if self == rhs {
703            return Duration::new_seconds(0.0);
704        }
705        let duration = self.as_julian_day().get_day_number() - rhs.as_julian_day().get_day_number();
706        Duration::new(duration, DurationUnit::Day)
707    }
708}
709
710impl Add<&mut Duration> for Date {
711    type Output = Date;
712
713    fn add(self, rhs: &mut Duration) -> Self::Output {
714        let days = rhs.as_days();
715        self.add_days(days as u32)
716    }
717}
718
719impl Add<&Duration> for Date {
720    type Output = Date;
721
722    fn add(self, rhs: &Duration) -> Self::Output {
723        let days = rhs.as_days();
724        self.add_days(days as u32)
725    }
726}
727
728impl Add<Duration> for Date {
729    type Output = Date;
730
731    fn add(self, rhs: Duration) -> Self::Output {
732        let days = rhs.as_days();
733        self.add_days(days as u32)
734    }
735}
736
737impl AddAssign<Duration> for Date {
738    fn add_assign(&mut self, rhs: Duration) {
739        let date = *self + rhs;
740        self.year = date.year;
741        self.day_of_year = date.day_of_year;
742    }
743}
744impl AddAssign<&Duration> for Date {
745    fn add_assign(&mut self, rhs: &Duration) {
746        let date = *self + *rhs;
747        self.year = date.year;
748        self.day_of_year = date.day_of_year;
749    }
750}
751
752#[cfg(test)]
753mod tests {
754    use irox_enums::EnumIterItem;
755    use irox_units::bounds::GreaterThanEqualToValueError;
756
757    use crate::epoch::{UnixTimestamp, GPS_EPOCH, PRIME_EPOCH, UNIX_EPOCH};
758    use crate::gregorian::{is_leap_year, Date, Month};
759
760    #[test]
761    pub fn leap_year_test() {
762        assert!(is_leap_year(1996));
763        assert!(!is_leap_year(1997));
764        assert!(!is_leap_year(1998));
765        assert!(!is_leap_year(1999));
766        assert!(is_leap_year(2000));
767        assert!(!is_leap_year(2001));
768        assert!(!is_leap_year(2002));
769        assert!(!is_leap_year(2003));
770        assert!(is_leap_year(2004));
771        assert!(is_leap_year(2008));
772        assert!(is_leap_year(2012));
773        assert!(is_leap_year(1600));
774        assert!(!is_leap_year(1700));
775        assert!(!is_leap_year(1800));
776        assert!(!is_leap_year(1900));
777        assert!(!is_leap_year(2100));
778    }
779
780    #[test]
781    pub fn test_timestamp_to_date() -> Result<(), GreaterThanEqualToValueError<u16>> {
782        assert_eq!(
783            UnixTimestamp::from_seconds(1697299822).as_date(),
784            Date::new(2023, 286)?
785        );
786
787        assert_eq!(
788            UnixTimestamp::from_seconds(0).as_date(),
789            Date::new(1970, 0)?
790        );
791        assert_eq!(
792            UnixTimestamp::from_seconds(915148800).as_date(),
793            Date::new(1999, 0)?
794        );
795        assert_eq!(
796            UnixTimestamp::from_seconds(1095379200).as_date(),
797            Date::try_from(2004, Month::September, 17).unwrap()
798        );
799        Ok(())
800    }
801
802    #[test]
803    pub fn test_date_subtract() {
804        assert_eq!(
805            3657.0,
806            (GPS_EPOCH.get_gregorian_date() - UNIX_EPOCH.get_gregorian_date()).value()
807        );
808        assert_eq!(
809            0.0,
810            (GPS_EPOCH.get_gregorian_date() - GPS_EPOCH.get_gregorian_date()).value()
811        );
812        assert_eq!(
813            -3657.0,
814            (UNIX_EPOCH.get_gregorian_date() - GPS_EPOCH.get_gregorian_date()).value()
815        );
816        assert_eq!(
817            25567.0,
818            (UNIX_EPOCH.get_gregorian_date() - PRIME_EPOCH.get_gregorian_date()).value()
819        );
820        assert_eq!(
821            -25567.0,
822            (PRIME_EPOCH.get_gregorian_date() - UNIX_EPOCH.get_gregorian_date()).value()
823        );
824    }
825
826    #[test]
827    pub fn test_date_add() {
828        assert_eq!(
829            GPS_EPOCH.get_gregorian_date(),
830            UNIX_EPOCH.get_gregorian_date().add_days(3657)
831        );
832    }
833
834    #[test]
835    #[ignore]
836    pub fn test_print_year() -> Result<(), GreaterThanEqualToValueError<u8>> {
837        let year = 2019;
838        for month in Month::iter_items() {
839            for day in 1..=month.days_in_month(year) {
840                let date = Date::try_from(year, month, day)?;
841                println!(
842                    "{month} {day} {year}-{} {}",
843                    date.day_of_year,
844                    date.as_julian_day().get_day_number()
845                );
846            }
847        }
848
849        Ok(())
850    }
851
852    #[test]
853    #[ignore]
854    pub fn test_print_leap_days() {
855        let mut year = 0;
856        let mut leaps = 0;
857        loop {
858            println!("{year} : {leaps}");
859            if year > 2100 {
860                break;
861            }
862            if is_leap_year(year) {
863                leaps += 1;
864            }
865            let mut sum_leaps = 0;
866            for y in 0..year {
867                if is_leap_year(y) {
868                    sum_leaps += 1;
869                }
870            }
871            println!("sum: {year} {sum_leaps}");
872            year += 4;
873        }
874    }
875
876    #[test]
877    pub fn test_julian_day() -> Result<(), GreaterThanEqualToValueError<u8>> {
878        assert_eq!(
879            Date::try_from_values(1970, 1, 1)?
880                .as_julian_day()
881                .get_day_number(),
882            2440587.5
883        );
884        assert_eq!(
885            Date::try_from_values(1980, 1, 6)?
886                .as_julian_day()
887                .get_day_number(),
888            2444244.5
889        );
890        assert_eq!(
891            Date::try_from_values(1858, 11, 17)?
892                .as_julian_day()
893                .get_day_number(),
894            2400000.5
895        );
896        assert_eq!(
897            Date::try_from_values(1950, 1, 1)?
898                .as_julian_day()
899                .get_day_number(),
900            2433282.5
901        );
902        assert_eq!(
903            Date::try_from_values(2000, 3, 1)?
904                .as_julian_day()
905                .get_day_number(),
906            2451604.5
907        );
908        assert_eq!(
909            Date::try_from_values(2020, 1, 1)?
910                .as_julian_day()
911                .get_day_number(),
912            2458849.5
913        );
914        assert_eq!(
915            Date::try_from_values(2020, 10, 1)?
916                .as_julian_day()
917                .get_day_number(),
918            2459123.5
919        );
920
921        assert_eq!(
922            Date::try_from_values(2021, 12, 31)?
923                .as_julian_day()
924                .get_day_number(),
925            2459579.5
926        );
927        assert_eq!(
928            Date::try_from_values(2022, 1, 1)?
929                .as_julian_day()
930                .get_day_number(),
931            2459580.5
932        );
933        assert_eq!(
934            Date::try_from_values(2022, 10, 1)?
935                .as_julian_day()
936                .get_day_number(),
937            2459853.5
938        );
939        assert_eq!(
940            Date::try_from_values(2023, 10, 18)?
941                .as_julian_day()
942                .get_day_number(),
943            2460235.5
944        );
945
946        Ok(())
947    }
948
949    #[test]
950    pub fn test_day_of_year() -> Result<(), GreaterThanEqualToValueError<u8>> {
951        let date = Date {
952            year: 2021,
953            day_of_year: 90,
954        };
955        assert_eq!("2021-04-01", date.to_string());
956        let date = Date {
957            year: 2021,
958            day_of_year: 91,
959        };
960        assert_eq!("2021-04-02", date.to_string());
961        Ok(())
962    }
963}