1use alloc::format;
2use alloc::string::String;
3
4fn decompose_unix_timestamp(unix_timestamp: i64) -> (u16, u8, u8, u8, u8, u8) {
5 const SECONDS_IN_MINUTE: i64 = 60;
6 const SECONDS_IN_HOUR: i64 = 60 * SECONDS_IN_MINUTE;
7 const SECONDS_IN_DAY: i64 = 24 * SECONDS_IN_HOUR;
8 const DAYS_IN_YEAR: i64 = 365;
9 const DAYS_IN_LEAP_YEAR: i64 = 366;
10
11 let mut year: i64 = 1970;
12 let mut days_since_epoch = unix_timestamp.div_euclid(SECONDS_IN_DAY);
13 let mut remaining_seconds = unix_timestamp.rem_euclid(SECONDS_IN_DAY);
14
15 while days_since_epoch
16 >= if is_leap_year(year) {
17 DAYS_IN_LEAP_YEAR
18 } else {
19 DAYS_IN_YEAR
20 }
21 {
22 days_since_epoch -= if is_leap_year(year) {
23 DAYS_IN_LEAP_YEAR
24 } else {
25 DAYS_IN_YEAR
26 };
27 year += 1;
28 }
29
30 while days_since_epoch < 0 {
31 year -= 1;
32 days_since_epoch += if is_leap_year(year) {
33 DAYS_IN_LEAP_YEAR
34 } else {
35 DAYS_IN_YEAR
36 };
37 }
38
39 let mut month = 0;
40 while days_since_epoch >= days_in_month(year, month) {
41 days_since_epoch -= days_in_month(year, month);
42 month += 1;
43 }
44
45 let day = days_since_epoch + 1;
46
47 let hour = remaining_seconds / SECONDS_IN_HOUR;
48 remaining_seconds %= SECONDS_IN_HOUR;
49 let minute = remaining_seconds / SECONDS_IN_MINUTE;
50 let second = remaining_seconds % SECONDS_IN_MINUTE;
51
52 (
53 year as u16,
54 month as u8 + 1,
55 day as u8,
56 hour as u8,
57 minute as u8,
58 second as u8,
59 )
60}
61
62fn is_leap_year(year: i64) -> bool {
63 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
64}
65
66fn days_in_month(year: i64, month: usize) -> i64 {
67 const DAYS_IN_MONTH: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
68
69 if month == 1 && is_leap_year(year) {
70 29
71 } else {
72 DAYS_IN_MONTH[month]
73 }
74}
75
76pub fn format_unix_timestamp(unix_timestamp: i64, pattern: &str) -> String {
77 let (year, month, day, hour, minute, second) = decompose_unix_timestamp(unix_timestamp);
78
79 let mut output = String::with_capacity(pattern.len() + 16);
80 let mut characters = pattern.chars();
81
82 while let Some(character) = characters.next() {
83 if character != '%' {
84 output.push(character);
85 continue;
86 }
87
88 match characters.next() {
89 Some('Y') => output.push_str(&format!("{:04}", year)),
90 Some('m') => output.push_str(&format!("{:02}", month)),
91 Some('d') => output.push_str(&format!("{:02}", day)),
92 Some('H') => output.push_str(&format!("{:02}", hour)),
93 Some('I') => output.push_str(&format!("{:02}", hour_12(hour))),
94 Some('M') => output.push_str(&format!("{:02}", minute)),
95 Some('S') => output.push_str(&format!("{:02}", second)),
96 Some('p') => output.push_str(if hour < 12 { "AM" } else { "PM" }),
97 Some('%') => output.push('%'),
98 Some(other) => {
99 output.push('%');
100 output.push(other);
101 }
102 None => output.push('%'),
103 }
104 }
105
106 output
107}
108
109const fn hour_12(hour_24: u8) -> u8 {
110 match hour_24 % 12 {
111 0 => 12,
112 value => value,
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn format_24_hour_time() {
122 let timestamp = 13 * 3600 + 5 * 60;
123 assert_eq!(format_unix_timestamp(timestamp, "%H:%M"), "13:05");
124 }
125
126 #[test]
127 fn format_12_hour_time_with_am_pm() {
128 let midnight = 0;
129 let afternoon = 13 * 3600 + 5 * 60;
130
131 assert_eq!(format_unix_timestamp(midnight, "%I:%M %p"), "12:00 AM");
132 assert_eq!(format_unix_timestamp(afternoon, "%I:%M %p"), "01:05 PM");
133 }
134
135 #[test]
136 fn format_date_and_time() {
137 assert_eq!(
138 format_unix_timestamp(0, "%Y-%m-%d %H:%M:%S"),
139 "1970-01-01 00:00:00"
140 );
141 }
142
143 #[test]
144 fn format_negative_unix_time() {
145 assert_eq!(
146 format_unix_timestamp(-1, "%Y-%m-%d %H:%M:%S"),
147 "1969-12-31 23:59:59"
148 );
149 }
150
151 #[test]
152 fn unix_epoch_is_correct() {
153 assert_eq!(decompose_unix_timestamp(0), (1970, 1, 1, 0, 0, 0));
154 }
155
156 #[test]
157 fn one_second_before_epoch_is_correct() {
158 assert_eq!(decompose_unix_timestamp(-1), (1969, 12, 31, 23, 59, 59));
159 }
160
161 #[test]
162 fn one_day_before_epoch_is_correct() {
163 assert_eq!(decompose_unix_timestamp(-86_400), (1969, 12, 31, 0, 0, 0));
164 }
165
166 #[test]
167 fn leap_day_2024_is_correct() {
168 assert_eq!(
169 decompose_unix_timestamp(1_709_164_800),
170 (2024, 2, 29, 0, 0, 0)
171 );
172 }
173
174 #[test]
175 fn leap_year_rules_are_correct() {
176 assert!(is_leap_year(2000));
177 assert!(!is_leap_year(1900));
178 assert!(is_leap_year(2024));
179 assert!(!is_leap_year(2023));
180 }
181
182 #[test]
183 fn february_days_are_correct() {
184 assert_eq!(days_in_month(2024, 1), 29);
185 assert_eq!(days_in_month(2023, 1), 28);
186 }
187
188 #[test]
189 fn only_time() {
190 assert_eq!(format_unix_timestamp(3661, "%H:%M:%S"), "01:01:01");
191 }
192
193 #[test]
194 fn only_date() {
195 assert_eq!(format_unix_timestamp(0, "%Y-%m-%d"), "1970-01-01");
196 }
197
198 #[test]
199 fn literal_percent() {
200 assert_eq!(format_unix_timestamp(0, "100%%"), "100%");
201 }
202
203 #[test]
204 fn unknown_format_token() {
205 assert_eq!(format_unix_timestamp(0, "%x"), "%x");
206 }
207
208 #[test]
209 fn partial_percent_at_end() {
210 assert_eq!(format_unix_timestamp(0, "end%"), "end%");
211 }
212
213 #[test]
214 fn mixed_literal_and_format() {
215 assert_eq!(
216 format_unix_timestamp(0, "Date: %Y-%m-%d, Time: %H:%M:%S"),
217 "Date: 1970-01-01, Time: 00:00:00"
218 );
219 }
220
221 #[test]
222 fn noon_is_12_pm() {
223 let noon = 12 * 3600;
224 assert_eq!(decompose_unix_timestamp(noon), (1970, 1, 1, 12, 0, 0));
225 }
226
227 #[test]
228 fn hour_12_midnight() {
229 assert_eq!(hour_12(0), 12);
230 }
231
232 #[test]
233 fn hour_12_noon() {
234 assert_eq!(hour_12(12), 12);
235 }
236
237 #[test]
238 fn hour_12_various() {
239 assert_eq!(hour_12(1), 1);
240 assert_eq!(hour_12(11), 11);
241 assert_eq!(hour_12(13), 1);
242 assert_eq!(hour_12(23), 11);
243 }
244
245 #[test]
246 fn format_12_hour_noon() {
247 let noon = 12 * 3600;
248 assert_eq!(format_unix_timestamp(noon, "%I:%M %p"), "12:00 PM");
249 }
250
251 #[test]
252 fn format_12_hour_midnight() {
253 assert_eq!(format_unix_timestamp(0, "%I:%M %p"), "12:00 AM");
254 }
255
256 #[test]
257 fn year_2000_timestamp() {
258 let ts = 946684800; assert_eq!(format_unix_timestamp(ts, "%Y"), "2000");
260 }
261
262 #[test]
263 fn leap_year_2000() {
264 let ts = 951782400; assert_eq!(decompose_unix_timestamp(ts), (2000, 2, 29, 0, 0, 0));
266 assert_eq!(format_unix_timestamp(ts, "%Y-%m-%d"), "2000-02-29");
267 }
268
269 #[test]
270 fn non_leap_year_1900() {
271 assert!(!is_leap_year(1900));
272 }
273
274 #[test]
275 fn century_boundary_2000_is_leap() {
276 assert!(is_leap_year(2000));
277 }
278
279 #[test]
280 fn year_2038_problem() {
281 let ts = 2147483648i64; assert_eq!(format_unix_timestamp(ts, "%Y"), "2038");
283 }
284
285 #[test]
286 fn empty_pattern() {
287 assert_eq!(format_unix_timestamp(0, ""), "");
288 }
289
290 #[test]
291 fn no_format_tokens() {
292 assert_eq!(
293 format_unix_timestamp(0, "just literal text"),
294 "just literal text"
295 );
296 }
297
298 #[test]
299 fn negative_timestamp_bce_date() {
300 let ts = -31536000i64; assert_eq!(format_unix_timestamp(ts, "%Y-%m-%d"), "1969-01-01");
302 }
303
304 #[test]
305 fn multiple_percent_escape() {
306 assert_eq!(format_unix_timestamp(0, "%%Y%%m%%d%%"), "%Y%m%d%");
307 }
308}