Skip to main content

datetimeutils/
lib.rs

1use std::fmt;
2use std::time::{Duration, SystemTime, SystemTimeError};
3
4/// Enum with the seven days of the week.
5#[derive(Debug, Clone, Copy)]
6pub enum Day {
7    Sunday,
8    Monday,
9    Tuesday,
10    Wednesday,
11    Thursday,
12    Friday,
13    Saturday,
14}
15
16/// Maps the `Day` enum to a string representation, e.g. "Monday".
17pub fn day_string(day: Day) -> &'static str {
18    match day {
19        Day::Sunday => "Sunday",
20        Day::Monday => "Monday",
21        Day::Tuesday => "Tuesday",
22        Day::Wednesday => "Wednesday",
23        Day::Thursday => "Thursday",
24        Day::Friday => "Friday",
25        Day::Saturday => "Saturday",
26    }
27}
28
29/// Maps the `Day` enum to a shortened string representation, e.g. "Mon".
30pub fn day_abbrev_string(day: Day) -> &'static str {
31    &day_string(day)[0..3]
32}
33
34impl fmt::Display for Day {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(f, "{}", day_string(*self))
37    }
38}
39
40/// Enum with the months of the year.
41#[derive(Debug, Clone, Copy)]
42pub enum Month {
43    January,
44    February,
45    March,
46    April,
47    May,
48    June,
49    July,
50    August,
51    September,
52    October,
53    November,
54    December,
55}
56
57/// Maps the `Month` enum to a string representation, e.g. "January".
58pub fn month_string(month: Month) -> &'static str {
59    match month {
60        Month::January => "January",
61        Month::February => "February",
62        Month::March => "March",
63        Month::April => "April",
64        Month::May => "May",
65        Month::June => "June",
66        Month::July => "July",
67        Month::August => "August",
68        Month::September => "September",
69        Month::October => "October",
70        Month::November => "November",
71        Month::December => "December",
72    }
73}
74
75/// Maps the `Month` enum to a shortened string representation, e.g. "Jan".
76pub fn month_abbrev_string(month: Month) -> &'static str {
77    &month_string(month)[0..3]
78}
79
80impl fmt::Display for Month {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(f, "{}", month_string(*self))
83    }
84}
85
86/// Takes in a year (e.g. 2019) and returns the number of days in that year.
87pub fn days_in_year(year: u64) -> u64 {
88    if year % 400 == 0 {
89        366
90    } else if year % 100 == 0 {
91        365
92    } else if year % 4 == 0 {
93        366
94    } else {
95        365
96    }
97}
98
99/// Takes in a year and month (e.g. 2020, February) and returns the number of days in that month.
100pub fn days_in_month(year: u64, month: Month) -> u64 {
101    match month {
102        Month::January => 31,
103        Month::February if days_in_year(year) == 366 => 29,
104        Month::February => 28,
105        Month::March => 31,
106        Month::April => 30,
107        Month::May => 31,
108        Month::June => 30,
109        Month::July => 31,
110        Month::August => 31,
111        Month::September => 30,
112        Month::October => 31,
113        Month::November => 30,
114        Month::December => 31,
115    }
116}
117
118/// Converts a `Month` enum to an integer in the range 1-12.
119pub fn index_from_month(month: Month) -> u64 {
120    match month {
121        Month::January => 1,
122        Month::February => 2,
123        Month::March => 3,
124        Month::April => 4,
125        Month::May => 5,
126        Month::June => 6,
127        Month::July => 7,
128        Month::August => 8,
129        Month::September => 9,
130        Month::October => 10,
131        Month::November => 11,
132        Month::December => 12,
133    }
134}
135
136/// Converts an integer in the range 1-12 into the corresponding `Month` enum.
137/// Values outside the 1-12 range are converted to `None`.
138pub fn month_from_index(index: u64) -> Option<Month> {
139    match index {
140        1 => Some(Month::January),
141        2 => Some(Month::February),
142        3 => Some(Month::March),
143        4 => Some(Month::April),
144        5 => Some(Month::May),
145        6 => Some(Month::June),
146        7 => Some(Month::July),
147        8 => Some(Month::August),
148        9 => Some(Month::September),
149        10 => Some(Month::October),
150        11 => Some(Month::November),
151        12 => Some(Month::December),
152        _ => None,
153    }
154}
155
156/// Returns the number of seconds in a day.
157pub fn seconds_in_day() -> u64 {
158    24 * 60 * 60
159}
160
161/// Returns the number of seconds in an hour.
162pub fn seconds_in_hour() -> u64 {
163    60 * 60
164}
165
166/// Returns the number of seconds in a minute.
167pub fn seconds_in_minute() -> u64 {
168    60
169}
170
171/// Conceptually this is a thin wrapper for `std::time::SystemTime`, but provides
172/// more useful functions. The impl of this struct has functions that allow easily
173/// extracting the year/month/date/etc. for the given point in time. In actual fact
174/// the internal representation of this struct is a `Duration` since the unix epoch,
175/// so that error-handling is only required once upon creating the instance, and
176/// not for each attempt at extracting date/time fields.
177pub struct PostEpochTime {
178    delta: Duration,
179}
180
181impl PostEpochTime {
182    /// Create a `PostEpochTime` from a `SystemTime`. The `SystemTime` must be temporally
183    /// in the future relative to the unix epoch, or an error will be returned.
184    pub fn from(st: &SystemTime) -> Result<Self, SystemTimeError> {
185        Ok(PostEpochTime {
186            delta: st.duration_since(SystemTime::UNIX_EPOCH)?
187        })
188    }
189
190    /// Create a `PostEpochTime` for the current instant. The current instant must be
191    /// in the future relative to the unix epoch, or an error will be returned.
192    pub fn now() -> Result<Self, SystemTimeError> {
193        Self::from(&SystemTime::now())
194    }
195
196    /// Returns the number of milliseconds passed since the unix epoch.
197    pub fn milliseconds_since_epoch(&self) -> u128 {
198        self.delta.as_millis()
199    }
200
201    /// Returns the number of microseconds passed since the unix epoch.
202    pub fn microseconds_since_epoch(&self) -> u128 {
203        self.delta.as_micros()
204    }
205
206    /// Returns the number of nanoseconds passed since the unix epoch.
207    pub fn nanoseconds_since_epoch(&self) -> u128 {
208        self.delta.as_nanos()
209    }
210
211    /// Returns the number of complete seconds passed since the unix epoch.
212    pub fn seconds_since_epoch(&self) -> u64 {
213        self.delta.as_secs()
214    }
215
216    /// Returns the number of complete days passed since the unix epoch.
217    pub fn days_since_epoch(&self) -> u64 {
218        self.delta.as_secs() / seconds_in_day()
219    }
220
221    /// Returns the day of the week that this point in time falls on.
222    pub fn day_of_week(&self) -> Day {
223        match self.days_since_epoch() % 7 {
224            0 => Day::Thursday,
225            1 => Day::Friday,
226            2 => Day::Saturday,
227            3 => Day::Sunday,
228            4 => Day::Monday,
229            5 => Day::Tuesday,
230            6 => Day::Wednesday,
231            _ => panic!("Modulo operator is broken"),
232        }
233    }
234
235    fn year_split(&self) -> (u64, u64) {
236        let mut days = self.days_since_epoch();
237        let mut year = 1970;
238        loop {
239            let in_year = days_in_year(year);
240            if days < in_year {
241                break;
242            }
243            days -= in_year;
244            year += 1;
245        }
246        (year, days)
247    }
248
249    /// Returns the year (e.g. 2020) this point in time falls on.
250    pub fn year(&self) -> u64 {
251        self.year_split().0
252    }
253
254    /// Returns the day of the year for this point in time (1-indexed).
255    /// A return value of 1 indicates January 1, a value of 2 indicates January 2,
256    /// and so on. If the year is a leap year the largest returned value
257    /// would be 366, and for non-leap years it would be 365.
258    pub fn day_of_year(&self) -> u64 {
259        self.year_split().1 + 1
260    }
261
262    fn month_split(&self) -> (Month, u64) {
263        let (year, mut days) = self.year_split();
264        let mut month = Month::January;
265        loop {
266            let in_month = days_in_month(year, month);
267            if days < in_month {
268                break;
269            }
270            days -= in_month;
271            month = month_from_index(index_from_month(month) + 1).expect("Month should never overflow");
272        }
273        (month, days)
274    }
275
276    /// Returns the month this point in time falls on.
277    pub fn month(&self) -> Month {
278        self.month_split().0
279    }
280
281    /// Returns the day of the month for this point in time (1-indexed).
282    /// A return value of 1 means it falls on the first of the month. The maximum
283    /// returned value will be 31.
284    pub fn day_of_month(&self) -> u64 {
285        self.month_split().1 + 1
286    }
287
288    /// Returns the second within the day (0-indexed). This will be in the range
289    /// 0..86399 (inclusive).
290    pub fn second_in_day(&self) -> u64 {
291        self.delta.as_secs() % seconds_in_day()
292    }
293
294    /// Returns the hour within the day (0-indexed). This will be in the range
295    /// 0..23 (inclusive).
296    pub fn hour(&self) -> u64 {
297        self.second_in_day() / seconds_in_hour()
298    }
299
300    /// Returns the second within the hour (0-indexed). This will be in the range
301    /// 0..3599 (inclusive).
302    pub fn second_in_hour(&self) -> u64 {
303        self.second_in_day() % seconds_in_hour()
304    }
305
306    /// Returns the minute within the hour (0-indexed). This will be in the range
307    /// 0..59 (inclusive).
308    pub fn minute(&self) -> u64 {
309        self.second_in_hour() / seconds_in_minute()
310    }
311
312    /// Returns the second within the minute (0-indexed). This will be in the range
313    /// 0..59 (inclusive).
314    pub fn second(&self) -> u64 {
315        self.delta.as_secs() % seconds_in_minute()
316    }
317}
318
319impl fmt::Display for PostEpochTime {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        write!(f, "{}, {} {} {} {:02}:{:02}:{:02}", 
322            day_abbrev_string(self.day_of_week()),
323            self.day_of_month(),
324            month_abbrev_string(self.month()),
325            self.year(),
326            self.hour(),
327            self.minute(),
328            self.second())
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn smoke_test() {
338        let timestamp = SystemTime::UNIX_EPOCH + Duration::new(1580610340, 123);
339        let pet = PostEpochTime::from(&timestamp).unwrap();
340        assert_eq!(format!("{}", pet), "Sun, 2 Feb 2020 02:25:40".to_string());
341    }
342}