1extern 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
25pub const DAYS_PER_4YEAR: u32 = 365 * 4 + 1;
29
30pub const DAYS_PER_100YEAR: u32 = DAYS_PER_4YEAR * 25 - 1;
34
35pub const DAYS_PER_400YEAR: u32 = DAYS_PER_100YEAR * 4 + 1;
40
41#[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 #[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 #[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 #[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 #[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 #[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 #[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#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
219pub struct Date {
220 pub(crate) year: i32,
223
224 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 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 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 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 #[must_use]
279 pub fn year(&self) -> i32 {
280 self.year
281 }
282
283 #[must_use]
287 pub fn day_of_year_offset(&self) -> u16 {
288 self.day_of_year
289 }
290
291 #[must_use]
295 pub fn day_of_year(&self) -> u16 {
296 self.day_of_year + 1
297 }
298
299 #[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 Month::January
311 }
312
313 #[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 #[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 #[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 #[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 #[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 #[must_use]
389 pub fn as_unix_timestamp(&self) -> UnixTimestamp {
390 self.into()
391 }
392
393 #[must_use]
396 pub fn as_julian_day(&self) -> JulianDate {
397 self.into()
398 }
399
400 #[must_use]
403 pub fn format<F: Format<Self>>(&self, format: &F) -> String {
404 format.format(self)
405 }
406
407 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 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 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 return (year, 53);
451 }
452 if dow == 5 {
453 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 return (year, 52);
465 }
466 if wkno == 53 {
467 if is_long_year(self.year) {
469 return (self.year, 53);
470 }
471 return (self.year + 1, 1);
473 }
474
475 (self.year, wkno as u8)
476 }
477}
478
479#[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
511pub 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
529pub 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
550pub const fn days_in_year(year: i32) -> u16 {
553 if is_leap_year(year) {
554 366
555 } else {
556 365
557 }
558}
559
560pub 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 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 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 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 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 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 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; 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}