gistools/util/
date.rs

1use alloc::{fmt, format, string::String};
2use serde::{Deserialize, Serialize};
3
4/// Helper function to check if a year is a leap year
5const fn is_leap_year(year: u16) -> bool {
6    (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
7}
8
9/// Days in each month for non-leap and leap years
10const DAYS_IN_MONTH: [[u16; 12]; 2] = [
11    [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], // Non-leap year
12    [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], // Leap year
13];
14
15/// # Date Structure
16///
17/// ## Description
18/// Convenience Date structure to model like a Javascript Date object.
19///
20/// ## Usage
21///
22/// The methods you have access to:
23/// - [`Date::new`]: Create a new Date
24/// - [`Date::new_full`]: Creates a full Date
25/// - [`Date::from_time`]: Create date given number of milliseconds since 1970-01-01T00:00:00Z (UTC)
26/// - [`Date::set_time`]: Set the date fields from milliseconds since 1970-01-01T00:00:00Z
27/// - [`Date::get_time`]: Returns the number of milliseconds since 1970-01-01T00:00:00Z (UTC)
28/// - [`Date::to_iso_string`]: Returns a string representing the Date in ISO 8601 extended format.
29///
30/// ```rust
31/// use gistools::util::Date;
32///
33/// let date = Date::new(2022, 1, 1);
34/// assert_eq!(date.to_iso_string(), "2022-01-01T00:00:00.000Z");
35/// ```
36#[derive(Debug, PartialEq, Ord, PartialOrd, Eq, Clone, Copy, Default, Serialize, Deserialize)]
37pub struct Date {
38    /// Year
39    pub year: u16,
40    /// Month
41    pub month: u8,
42    /// Day
43    pub day: u8,
44    /// Hour
45    pub hour: u8,
46    /// Minute
47    pub minute: u8,
48    /// Second
49    pub second: u8,
50}
51impl Date {
52    /// Creates a new Date
53    pub fn new(year: u16, month: u8, day: u8) -> Date {
54        Date { year, month, day, hour: 0, minute: 0, second: 0 }
55    }
56
57    /// Creates a full Date
58    pub fn new_full(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Date {
59        Date { year, month, day, hour, minute, second }
60    }
61
62    /// Create date given number of milliseconds since 1970-01-01T00:00:00Z (UTC)
63    pub fn from_time(time: i64) -> Date {
64        let mut date = Date::default();
65        date.set_time(time);
66        date
67    }
68
69    /// Set the date fields from milliseconds since 1970-01-01T00:00:00Z
70    pub fn set_time(&mut self, time: i64) {
71        // Break into days and remaining milliseconds
72        let mut days = time / 86_400_000;
73        let mut ms_remaining = time % 86_400_000;
74        if ms_remaining < 0 {
75            ms_remaining += 86_400_000;
76            days -= 1;
77        }
78
79        // Determine year
80        let mut year = 1970;
81        loop {
82            let year_days = if is_leap_year(year) { 366 } else { 365 };
83            if days < year_days {
84                break;
85            }
86            days -= year_days;
87            year += 1;
88        }
89
90        // Determine month
91        let leap = is_leap_year(year) as usize;
92        let mut month = 0;
93        while days >= DAYS_IN_MONTH[leap][month] as i64 {
94            days -= DAYS_IN_MONTH[leap][month] as i64;
95            month += 1;
96        }
97
98        self.year = year;
99        self.month = (month + 1) as u8;
100        self.day = (days + 1) as u8;
101
102        // Convert remaining milliseconds to time of day
103        self.hour = (ms_remaining / 3_600_000) as u8;
104        self.minute = ((ms_remaining % 3_600_000) / 60_000) as u8;
105        self.second = ((ms_remaining % 60_000) / 1_000) as u8;
106    }
107
108    /// Returns the number of milliseconds since 1970-01-01T00:00:00Z (UTC)
109    pub fn get_time(&self) -> i64 {
110        let mut days = 0;
111
112        // Sum up days for all previous years
113        for y in 1970..self.year {
114            days += if is_leap_year(y) { 366 } else { 365 };
115        }
116
117        // Sum up days for previous months in the current year
118        let leap = is_leap_year(self.year) as usize;
119        for m in 0..(self.month as usize - 1) {
120            days += DAYS_IN_MONTH[leap][m % 12] as i64;
121        }
122
123        // Add days of the current month
124        days += self.day as i64 - 1;
125
126        // Convert to milliseconds
127        days * 86_400_000
128            + (self.hour as i64 * 3_600_000)
129            + (self.minute as i64 * 60_000)
130            + (self.second as i64 * 1_000)
131    }
132
133    /// Returns a string representing the Date in ISO 8601 extended format.
134    pub fn to_iso_string(&self) -> String {
135        format!(
136            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.000Z",
137            self.year, self.month, self.day, self.hour, self.minute, self.second
138        )
139    }
140}
141impl From<&str> for Date {
142    fn from(s: &str) -> Date {
143        // Try parse as milliseconds
144        if let Ok(ms) = s.parse::<i64>() {
145            return Date::from_time(ms);
146        }
147
148        // Otherwise, parse as ISO8601-like string
149        // Expected form: "YYYY-MM-DDTHH:MM:SSZ" or shorter like "YYYY-MM-DD"
150        let mut date = Date::default();
151
152        let year = s[0..4].parse().unwrap();
153        let month = s[5..7].parse().unwrap();
154        let day = s[8..10].parse().unwrap();
155
156        let mut hour = 0;
157        let mut minute = 0;
158        let mut second = 0;
159
160        if s.len() >= 19 {
161            hour = s[11..13].parse().unwrap();
162            minute = s[14..16].parse().unwrap();
163            second = s[17..19].parse().unwrap();
164        }
165
166        date.year = year;
167        date.month = month;
168        date.day = day;
169        date.hour = hour;
170        date.minute = minute;
171        date.second = second;
172
173        date
174    }
175}
176impl fmt::Display for Date {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(f, "{:04}{:02}{:02}", self.year, self.month + 1, self.day)
179    }
180}