1use std::time::{SystemTime, UNIX_EPOCH};
11
12pub(crate) const fn secs_to_ymdhms(secs: u64) -> (u32, u32, u32, u32, u32, u32) {
18 const SECS_PER_MIN: u64 = 60;
19 const DAYS_PER_400Y: u64 = 146_097;
20
21 let s = (secs % SECS_PER_MIN) as u32;
22 let total_mins = secs / SECS_PER_MIN;
23 let mi = (total_mins % 60) as u32;
24 let total_hours = total_mins / 60;
25 let h = (total_hours % 24) as u32;
26 let mut days = total_hours / 24;
27
28 days += 719_468;
30 let era = days / DAYS_PER_400Y;
31 let doe = days % DAYS_PER_400Y;
32 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
33 let y = yoe + era * 400;
34 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
35 let mp = (5 * doy + 2) / 153;
36 #[allow(clippy::cast_possible_truncation)]
38 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
39 #[allow(clippy::cast_possible_truncation)]
40 let mo = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
41 #[allow(clippy::cast_possible_truncation)]
42 let y = if mo <= 2 { y + 1 } else { y } as u32;
43 (y, mo, d, h, mi, s)
44}
45
46fn unix_secs() -> u64 {
47 unix_secs_of(SystemTime::now())
48}
49
50fn unix_secs_of(time: SystemTime) -> u64 {
51 time.duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs())
52}
53
54#[must_use]
69pub fn rfc3339_from(time: SystemTime) -> String {
70 let (y, mo, d, h, mi, s) = secs_to_ymdhms(unix_secs_of(time));
71 format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z")
72}
73
74#[must_use]
86pub fn utc_now_datetime() -> (u32, u32, u32, u32, u32, u32) {
87 secs_to_ymdhms(unix_secs())
88}
89
90#[must_use]
101pub fn utc_now_rfc3339() -> String {
102 rfc3339_from(SystemTime::now())
103}
104
105#[must_use]
117pub fn utc_now_compact() -> String {
118 let (y, mo, d, h, mi, s) = utc_now_datetime();
119 format!("{y:04}{mo:02}{d:02}_{h:02}{mi:02}{s:02}")
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn rfc3339_format_is_correct() {
128 let ts = utc_now_rfc3339();
129 assert_eq!(ts.len(), 20);
130 assert!(ts.ends_with('Z'));
131 assert_eq!(&ts[10..11], "T");
132 assert!(ts[..4].chars().all(|c| c.is_ascii_digit()));
134 }
135
136 #[test]
137 fn rfc3339_from_fixed_epoch() {
138 let t = UNIX_EPOCH + std::time::Duration::from_secs(1_700_000_000);
139 assert_eq!(rfc3339_from(t), "2023-11-14T22:13:20Z");
140 }
141
142 #[test]
143 fn rfc3339_from_unix_epoch_saturates_for_pre_epoch_instants() {
144 let before_epoch = UNIX_EPOCH - std::time::Duration::from_secs(1);
145 assert_eq!(rfc3339_from(before_epoch), "1970-01-01T00:00:00Z");
146 }
147
148 #[test]
149 fn compact_format_is_correct() {
150 let ts = utc_now_compact();
151 assert_eq!(ts.len(), 15);
152 assert_eq!(&ts[8..9], "_");
153 }
154
155 #[test]
156 fn secs_to_ymdhms_known_epoch() {
157 let (y, mo, d, h, mi, s) = secs_to_ymdhms(0);
159 assert_eq!((y, mo, d, h, mi, s), (1970, 1, 1, 0, 0, 0));
160 }
161
162 #[test]
163 fn secs_to_ymdhms_known_date() {
164 let (y, mo, d, h, mi, s) = secs_to_ymdhms(1_709_251_200);
166 assert_eq!((y, mo, d, h, mi, s), (2024, 3, 1, 0, 0, 0));
167 }
168
169 #[test]
170 fn secs_to_ymdhms_y2k_boundary() {
171 assert_eq!(secs_to_ymdhms(946_684_800), (2000, 1, 1, 0, 0, 0));
172 }
173
174 #[test]
175 fn secs_to_ymdhms_feb29_century_leap_year() {
176 assert_eq!(secs_to_ymdhms(951_782_400), (2000, 2, 29, 0, 0, 0));
177 }
178
179 #[test]
180 fn secs_to_ymdhms_feb29_regular_leap_year() {
181 assert_eq!(secs_to_ymdhms(1_709_164_800), (2024, 2, 29, 0, 0, 0));
182 }
183
184 #[test]
185 fn secs_to_ymdhms_year_end_boundary() {
186 assert_eq!(secs_to_ymdhms(1_704_067_199), (2023, 12, 31, 23, 59, 59));
187 }
188
189 #[test]
190 fn secs_to_ymdhms_nontrivial_hhmmss() {
191 assert_eq!(secs_to_ymdhms(1_718_458_245), (2024, 6, 15, 13, 30, 45));
192 }
193
194 #[test]
195 fn datetime_fields_are_in_range() {
196 let (y, mo, d, h, mi, s) = utc_now_datetime();
197 assert!(y >= 2024);
198 assert!((1..=12).contains(&mo));
199 assert!((1..=31).contains(&d));
200 assert!(h < 24 && mi < 60 && s < 60);
201 }
202}