Skip to main content

shared/
time.rs

1pub fn decompose_unix_timestamp(unix_timestamp: i64) -> (u16, u8, u8, u8, u8, u8) {
2    // Constants for calculations
3    const SECONDS_IN_MINUTE: i64 = 60;
4    const SECONDS_IN_HOUR: i64 = 60 * SECONDS_IN_MINUTE;
5    const SECONDS_IN_DAY: i64 = 24 * SECONDS_IN_HOUR;
6    const DAYS_IN_YEAR: i64 = 365;
7    const DAYS_IN_LEAP_YEAR: i64 = 366;
8
9    // Start from 1970.
10    let mut year: i64 = 1970;
11    let mut days_since_epoch = unix_timestamp.div_euclid(SECONDS_IN_DAY);
12    let mut remaining_seconds = unix_timestamp.rem_euclid(SECONDS_IN_DAY);
13
14    // Determine the current year for timestamps on/after and before epoch.
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    // Determine the current month and day
40    let mut month = 0;
41    while days_since_epoch >= days_in_month(year, month) {
42        days_since_epoch -= days_in_month(year, month);
43        month += 1;
44    }
45
46    // Remaining days are the day of the month
47    let day = days_since_epoch + 1;
48
49    // Calculate hour, minute, and second from remaining seconds
50    let hour = remaining_seconds / SECONDS_IN_HOUR;
51    remaining_seconds %= SECONDS_IN_HOUR;
52    let minute = remaining_seconds / SECONDS_IN_MINUTE;
53    let second = remaining_seconds % SECONDS_IN_MINUTE;
54
55    (
56        year as u16,
57        month as u8 + 1,
58        day as u8,
59        hour as u8,
60        minute as u8,
61        second as u8,
62    )
63}
64
65pub fn is_leap_year(year: i64) -> bool {
66    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
67}
68
69pub fn days_in_month(year: i64, month: usize) -> i64 {
70    // Number of days in each month (non-leap year)
71    const DAYS_IN_MONTH: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
72
73    if month == 1 && is_leap_year(year) {
74        // February in a leap year
75        29
76    } else {
77        DAYS_IN_MONTH[month]
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn unix_epoch_is_correct() {
87        assert_eq!(decompose_unix_timestamp(0), (1970, 1, 1, 0, 0, 0));
88    }
89
90    #[test]
91    fn one_second_before_epoch_is_correct() {
92        assert_eq!(decompose_unix_timestamp(-1), (1969, 12, 31, 23, 59, 59));
93    }
94
95    #[test]
96    fn one_day_before_epoch_is_correct() {
97        assert_eq!(decompose_unix_timestamp(-86_400), (1969, 12, 31, 0, 0, 0));
98    }
99
100    #[test]
101    fn leap_day_2024_is_correct() {
102        // 2024-02-29 00:00:00 UTC
103        assert_eq!(
104            decompose_unix_timestamp(1_709_164_800),
105            (2024, 2, 29, 0, 0, 0)
106        );
107    }
108
109    #[test]
110    fn leap_year_rules_are_correct() {
111        assert!(is_leap_year(2000));
112        assert!(!is_leap_year(1900));
113        assert!(is_leap_year(2024));
114        assert!(!is_leap_year(2023));
115    }
116
117    #[test]
118    fn february_days_are_correct() {
119        assert_eq!(days_in_month(2024, 1), 29);
120        assert_eq!(days_in_month(2023, 1), 28);
121    }
122}