1pub const SECONDS_PER_DAY: i64 = 86_400;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct DateTime {
27 pub year: i64,
28 pub month: u32,
30 pub day: u32,
32 pub hour: u32,
33 pub minute: u32,
34 pub second: u32,
35 pub weekday: u32,
37}
38
39pub fn unix_now() -> i64 {
44 match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
45 Ok(elapsed) => elapsed.as_secs() as i64,
46 Err(before) => -(before.duration().as_secs() as i64),
47 }
48}
49
50pub fn from_unix(seconds: i64) -> DateTime {
52 let days = seconds.div_euclid(SECONDS_PER_DAY);
54 let time_of_day = seconds.rem_euclid(SECONDS_PER_DAY);
55 let (year, month, day) = civil_from_days(days);
56
57 DateTime {
58 year,
59 month,
60 day,
61 hour: (time_of_day / 3600) as u32,
62 minute: ((time_of_day % 3600) / 60) as u32,
63 second: (time_of_day % 60) as u32,
64 weekday: weekday_of(days),
65 }
66}
67
68pub fn to_unix(year: i64, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> i64 {
70 days_from_civil(year, month, day) * SECONDS_PER_DAY
71 + i64::from(hour) * 3600
72 + i64::from(minute) * 60
73 + i64::from(second)
74}
75
76pub fn start_of_day(seconds: i64) -> i64 {
78 seconds - seconds.rem_euclid(SECONDS_PER_DAY)
79}
80
81pub fn start_of_minute(seconds: i64) -> i64 {
83 seconds - seconds.rem_euclid(60)
84}
85
86fn weekday_of(days: i64) -> u32 {
91 (days + 4).rem_euclid(7) as u32
92}
93
94fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
99 let month = i64::from(month);
100 let year = year - i64::from(month <= 2);
101
102 let era = year.div_euclid(400);
103 let year_of_era = year - era * 400;
104 let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(day) - 1;
105 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
106
107 era * 146_097 + day_of_era - 719_468
108}
109
110fn civil_from_days(days: i64) -> (i64, u32, u32) {
112 let shifted = days + 719_468;
113 let era = shifted.div_euclid(146_097);
114 let day_of_era = shifted - era * 146_097;
115 let year_of_era =
116 (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
117 let year = year_of_era + era * 400;
118 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
119 let shifted_month = (5 * day_of_year + 2) / 153;
120 let day = day_of_year - (153 * shifted_month + 2) / 5 + 1;
121 let month = shifted_month + if shifted_month < 10 { 3 } else { -9 };
122
123 (year + i64::from(month <= 2), month as u32, day as u32)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn converts_known_dates_in_both_directions() {
132 let epoch = from_unix(0);
134 assert_eq!((epoch.year, epoch.month, epoch.day), (1970, 1, 1));
135 assert_eq!(epoch.weekday, 4);
136 assert_eq!(to_unix(1970, 1, 1, 0, 0, 0), 0);
137
138 let known = to_unix(2026, 8, 29, 13, 45, 30);
140 let back = from_unix(known);
141 assert_eq!((back.year, back.month, back.day), (2026, 8, 29));
142 assert_eq!((back.hour, back.minute, back.second), (13, 45, 30));
143 assert_eq!(back.weekday, 6, "29 August 2026 is a Saturday");
144 }
145
146 #[test]
147 fn handles_leap_days_and_century_rules() {
148 let leap = from_unix(to_unix(2000, 2, 29, 0, 0, 0));
150 assert_eq!((leap.year, leap.month, leap.day), (2000, 2, 29));
151
152 let march_1900 = from_unix(to_unix(1900, 2, 28, 0, 0, 0) + SECONDS_PER_DAY);
153 assert_eq!((march_1900.year, march_1900.month, march_1900.day), (1900, 3, 1));
154 }
155
156 #[test]
157 fn round_trips_every_day_for_a_decade() {
158 let mut seconds = to_unix(2020, 1, 1, 0, 0, 0);
159 let end = to_unix(2030, 1, 1, 0, 0, 0);
160 let mut previous_weekday = from_unix(seconds).weekday;
161
162 while seconds < end {
163 let date = from_unix(seconds);
164 assert_eq!(
165 to_unix(date.year, date.month, date.day, date.hour, date.minute, date.second),
166 seconds
167 );
168 seconds += SECONDS_PER_DAY;
169
170 let weekday = from_unix(seconds).weekday;
171 assert_eq!(weekday, (previous_weekday + 1) % 7, "weekdays must advance by one");
172 previous_weekday = weekday;
173 }
174 }
175
176 #[test]
177 fn dates_before_the_epoch_do_not_round_the_wrong_way() {
178 let date = from_unix(-1);
179 assert_eq!((date.year, date.month, date.day), (1969, 12, 31));
180 assert_eq!((date.hour, date.minute, date.second), (23, 59, 59));
181 }
182
183 #[test]
184 fn truncates_to_day_and_minute_boundaries() {
185 let noon = to_unix(2026, 8, 29, 12, 34, 56);
186 assert_eq!(start_of_day(noon), to_unix(2026, 8, 29, 0, 0, 0));
187 assert_eq!(start_of_minute(noon), to_unix(2026, 8, 29, 12, 34, 0));
188 }
189}