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 SystemTime::now()
48 .duration_since(UNIX_EPOCH)
49 .map_or(0, |d| d.as_secs())
50}
51
52#[must_use]
64pub fn utc_now_datetime() -> (u32, u32, u32, u32, u32, u32) {
65 secs_to_ymdhms(unix_secs())
66}
67
68#[must_use]
79pub fn utc_now_rfc3339() -> String {
80 let (y, mo, d, h, mi, s) = utc_now_datetime();
81 format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z")
82}
83
84#[must_use]
96pub fn utc_now_compact() -> String {
97 let (y, mo, d, h, mi, s) = utc_now_datetime();
98 format!("{y:04}{mo:02}{d:02}_{h:02}{mi:02}{s:02}")
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn rfc3339_format_is_correct() {
107 let ts = utc_now_rfc3339();
108 assert_eq!(ts.len(), 20);
109 assert!(ts.ends_with('Z'));
110 assert_eq!(&ts[10..11], "T");
111 assert!(ts[..4].chars().all(|c| c.is_ascii_digit()));
113 }
114
115 #[test]
116 fn compact_format_is_correct() {
117 let ts = utc_now_compact();
118 assert_eq!(ts.len(), 15);
119 assert_eq!(&ts[8..9], "_");
120 }
121
122 #[test]
123 fn secs_to_ymdhms_known_epoch() {
124 let (y, mo, d, h, mi, s) = secs_to_ymdhms(0);
126 assert_eq!((y, mo, d, h, mi, s), (1970, 1, 1, 0, 0, 0));
127 }
128
129 #[test]
130 fn secs_to_ymdhms_known_date() {
131 let (y, mo, d, h, mi, s) = secs_to_ymdhms(1_709_251_200);
133 assert_eq!((y, mo, d, h, mi, s), (2024, 3, 1, 0, 0, 0));
134 }
135
136 #[test]
137 fn secs_to_ymdhms_y2k_boundary() {
138 assert_eq!(secs_to_ymdhms(946_684_800), (2000, 1, 1, 0, 0, 0));
139 }
140
141 #[test]
142 fn secs_to_ymdhms_feb29_century_leap_year() {
143 assert_eq!(secs_to_ymdhms(951_782_400), (2000, 2, 29, 0, 0, 0));
144 }
145
146 #[test]
147 fn secs_to_ymdhms_feb29_regular_leap_year() {
148 assert_eq!(secs_to_ymdhms(1_709_164_800), (2024, 2, 29, 0, 0, 0));
149 }
150
151 #[test]
152 fn secs_to_ymdhms_year_end_boundary() {
153 assert_eq!(secs_to_ymdhms(1_704_067_199), (2023, 12, 31, 23, 59, 59));
154 }
155
156 #[test]
157 fn secs_to_ymdhms_nontrivial_hhmmss() {
158 assert_eq!(secs_to_ymdhms(1_718_458_245), (2024, 6, 15, 13, 30, 45));
159 }
160
161 #[test]
162 fn datetime_fields_are_in_range() {
163 let (y, mo, d, h, mi, s) = utc_now_datetime();
164 assert!(y >= 2024);
165 assert!((1..=12).contains(&mo));
166 assert!((1..=31).contains(&d));
167 assert!(h < 24 && mi < 60 && s < 60);
168 }
169}