1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6#[cfg_attr(feature = "defmt", derive(defmt::Format))]
7pub struct DateTime {
8 pub year: i32,
10 pub month: u8,
12 pub day: u8,
14 pub hour: u8,
16 pub minute: u8,
18 pub second: u8,
20 pub weekday: u8,
22}
23
24impl DateTime {
25 pub const fn from_unix(unix_seconds: i64, offset_minutes: i32) -> Self {
28 let t = unix_seconds + offset_minutes as i64 * 60;
29 let days = t.div_euclid(86_400);
30 let secs = t.rem_euclid(86_400);
31 let (year, month, day) = civil_from_days(days);
32 Self {
33 year,
34 month,
35 day,
36 hour: (secs / 3600) as u8,
37 minute: (secs / 60 % 60) as u8,
38 second: (secs % 60) as u8,
39 weekday: (days + 3).rem_euclid(7) as u8,
41 }
42 }
43
44 pub const fn weekday_name(&self) -> &'static str {
46 match self.weekday {
47 0 => "Mon",
48 1 => "Tue",
49 2 => "Wed",
50 3 => "Thu",
51 4 => "Fri",
52 5 => "Sat",
53 _ => "Sun",
54 }
55 }
56}
57
58const fn civil_from_days(z: i64) -> (i32, u8, u8) {
61 let z = z + 719_468;
62 let era = z.div_euclid(146_097);
63 let doe = z.rem_euclid(146_097); let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = (doy - (153 * mp + 2) / 5 + 1) as u8;
68 let month = if mp < 10 { mp + 3 } else { mp - 9 } as u8;
69 let year = (yoe + era * 400) as i32 + (month <= 2) as i32;
70 (year, month, day)
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 fn ymd_hms(t: i64, off: i32) -> (i32, u8, u8, u8, u8, u8) {
78 let d = DateTime::from_unix(t, off);
79 (d.year, d.month, d.day, d.hour, d.minute, d.second)
80 }
81
82 #[test]
83 fn epoch_and_known_dates() {
84 assert_eq!(ymd_hms(0, 0), (1970, 1, 1, 0, 0, 0));
85 assert_eq!(DateTime::from_unix(0, 0).weekday_name(), "Thu");
86 assert_eq!(ymd_hms(951_782_400, 0), (2000, 2, 29, 0, 0, 0)); assert_eq!(ymd_hms(1_767_225_600, 0), (2026, 1, 1, 0, 0, 0));
88 assert_eq!(DateTime::from_unix(1_767_225_600, 0).weekday_name(), "Thu");
89 assert_eq!(ymd_hms(4_102_444_799, 0), (2099, 12, 31, 23, 59, 59));
90 assert_eq!(ymd_hms(-1, 0), (1969, 12, 31, 23, 59, 59));
91 }
92
93 #[test]
94 fn utc_offsets_cross_midnight() {
95 assert_eq!(
97 ymd_hms(1_767_225_600 + 20 * 3600 + 1800, 420),
98 (2026, 1, 2, 3, 30, 0)
99 );
100 assert_eq!(
102 ymd_hms(1_767_225_600 + 20 * 3600 + 1800, -300),
103 (2026, 1, 1, 15, 30, 0)
104 );
105 }
106
107 #[test]
108 fn every_day_increments_by_one() {
109 let mut prev = DateTime::from_unix(0, 0);
111 for d in 1..(800 * 366) {
112 let cur = DateTime::from_unix(d * 86_400, 0);
113 let next_expected = (prev.day + 1, prev.month, prev.year);
114 assert!(
115 (cur.day, cur.month, cur.year) == next_expected
116 || (cur.day == 1
117 && (cur.month == prev.month + 1
118 || (cur.month == 1 && cur.year == prev.year + 1))),
119 "{prev:?} -> {cur:?}"
120 );
121 assert_eq!(cur.weekday, (prev.weekday + 1) % 7);
122 prev = cur;
123 }
124 }
125}