Skip to main content

embedded_i18n/
time.rs

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; // 2000-01-01T00:00:00Z
259        assert_eq!(format_unix_timestamp(ts, "%Y"), "2000");
260    }
261
262    #[test]
263    fn leap_year_2000() {
264        let ts = 951782400; // 2000-02-29T00:00:00Z
265        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; // just after 2038-01-19T03:14:07Z
282        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; // 1969-01-01
301        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}