embedded_utils/time/
datetime.rs

1use crate::time::TimeZone;
2
3/// DateTime is a Unix and UTC conversion util.
4///
5/// Example of converting a Unix to a DateTime UTC.
6/// ```rust,no_run
7/// use embedded_utils::time::{DateTime, TimeZone};
8///
9/// let datetime = DateTime::from_unix_millis(1704067199998, TimeZone::UTC);
10/// println!("datetime: {:?}", datetime);
11/// ```
12#[derive(core::fmt::Debug)]
13pub struct DateTime {
14    /// Year
15    pub year: u32,
16    /// Month
17    pub month: u8,
18    /// Day
19    pub day: u8,
20    /// Hour
21    pub hour: u8,
22    /// Minute
23    pub minute: u8,
24    /// Second
25    pub second: u8,
26    /// Millisecond
27    pub millisecond: u16,
28    /// Timezone
29    pub timezone: TimeZone,
30}
31
32const MILLISECONDS_PER_SECOND: u64 = 1000;
33const SECONDS_PER_MINUTE: u64 = 60;
34const MINUTES_PER_HOUR: i32 = 60;
35const HOURS_PER_DAY: i32 = 24;
36const DAYS_PER_YEAR: u64 = 365;
37const DAYS_PER_LEAP_YEAR: u64 = 366;
38
39const MONTHS: [[u8; 12]; 2] = [
40    [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], // Normal year
41    [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], // Leap year
42];
43
44impl DateTime {
45    /// Create a new DateTime from a Unix timestamp in seconds.
46    #[inline]
47    pub fn from_unix_secs(timestamp: u64, timezone: TimeZone) -> DateTime {
48        return Self::from_unix_millis(timestamp * 1000, timezone);
49    }
50
51    /// Create a new DateTime from a Unix timestamp in milliseconds.
52    pub fn from_unix_millis(timestamp: u64, timezone: TimeZone) -> DateTime {
53        let mut milliseconds = timestamp;
54        let mut seconds = milliseconds / MILLISECONDS_PER_SECOND;
55        let mut minutes = (seconds / SECONDS_PER_MINUTE) as i32;
56        let mut hours = minutes / MINUTES_PER_HOUR;
57        let mut days = hours / HOURS_PER_DAY;
58
59        milliseconds %= MILLISECONDS_PER_SECOND;
60        seconds %= SECONDS_PER_MINUTE;
61        minutes %= MINUTES_PER_HOUR;
62        hours %= HOURS_PER_DAY;
63        hours += timezone.get_offset() / 3600;
64        if hours < 0 {
65            hours += HOURS_PER_DAY as i32;
66            days -= 1;
67        } else if hours >= HOURS_PER_DAY as i32 {
68            hours -= HOURS_PER_DAY as i32;
69            days += 1;
70        }
71
72        let mut years = 1970;
73        while days
74            >= if is_leap_year(years) {
75                DAYS_PER_LEAP_YEAR
76            } else {
77                DAYS_PER_YEAR
78            } as i32
79        {
80            days -= if is_leap_year(years) {
81                DAYS_PER_LEAP_YEAR
82            } else {
83                DAYS_PER_YEAR
84            } as i32;
85            years += 1;
86        }
87
88        let mut month = 0;
89        while days >= MONTHS[is_leap_year(years) as usize][month as usize] as i32 {
90            days -= MONTHS[is_leap_year(years) as usize][month as usize] as i32;
91            month += 1;
92        }
93
94        DateTime {
95            year: years,
96            month: month + 1,
97            day: days as u8 + 1,
98            hour: hours as u8,
99            minute: minutes as u8,
100            second: (seconds % SECONDS_PER_MINUTE) as u8,
101            millisecond: milliseconds as u16,
102            timezone,
103        }
104    }
105}
106
107/// Check if a year is a leap year.
108fn is_leap_year(year: u32) -> bool {
109    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
110}
111
112#[cfg(test)]
113mod tests {
114    use crate::time::{DateTime, TimeZone};
115
116    #[test]
117    fn from_unix_secs_works() {
118        // converting a Unix to a DateTime UTC.
119        let datetime = DateTime::from_unix_secs(1704067199, TimeZone::UTC);
120        assert_eq!(datetime.year, 2023, "year: {}", datetime.year);
121        assert_eq!(datetime.month, 12, "month: {}", datetime.month);
122        assert_eq!(datetime.day, 31, "day: {}", datetime.day);
123        assert_eq!(datetime.hour, 23, "hour: {}", datetime.hour);
124        assert_eq!(datetime.minute, 59, "minute: {}", datetime.minute);
125        assert_eq!(datetime.second, 59, "second: {}", datetime.second);
126        assert_eq!(
127            datetime.millisecond, 0,
128            "millisecond: {}",
129            datetime.millisecond
130        );
131        assert_eq!(
132            datetime.timezone.get_offset(),
133            TimeZone::UTC.get_offset(),
134            "timezone: {:?}",
135            datetime.timezone.get_offset()
136        );
137    }
138
139    #[test]
140    fn from_unix_millis_works() {
141        // converting a Unix to a DateTime AsiaShanghai.
142        let datetime = DateTime::from_unix_millis(1704067199998, TimeZone::AsiaShanghai);
143        assert_eq!(datetime.year, 2024, "year: {}", datetime.year);
144        assert_eq!(datetime.month, 1, "month: {}", datetime.month);
145        assert_eq!(datetime.day, 1, "day: {}", datetime.day);
146        assert_eq!(datetime.hour, 7, "hour: {}", datetime.hour);
147        assert_eq!(datetime.minute, 59, "minute: {}", datetime.minute);
148        assert_eq!(datetime.second, 59, "second: {}", datetime.second);
149        assert_eq!(
150            datetime.millisecond, 998,
151            "millisecond: {}",
152            datetime.millisecond
153        );
154        assert_eq!(
155            datetime.timezone.get_offset(),
156            TimeZone::AsiaShanghai.get_offset(),
157            "timezone: {:?}",
158            datetime.timezone.get_offset()
159        );
160    }
161}