date_time/
date_tuple.rs

1use date_utils;
2use month_tuple::MonthTuple;
3use regex::Regex;
4use std::cmp::Ordering;
5use std::fmt;
6use std::str::FromStr;
7
8const DAYS_IN_A_COMMON_YEAR: u32 = 365;
9const DAYS_IN_A_LEAP_YEAR: u32 = 366;
10
11pub type Date = DateTuple;
12
13/// Holds a specific date by year, month, and day.
14///
15/// Handles values from 01 Jan 0000 to 31 Dec 9999.
16#[cfg_attr(
17    feature = "serde_support",
18    derive(serde::Serialize, serde::Deserialize)
19)]
20#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)]
21pub struct DateTuple {
22    y: u16,
23    m: u8,
24    d: u8,
25}
26
27impl DateTuple {
28    /// Takes a year, month, and day and converts them into a DateTuple.
29    ///
30    /// Will not overlap - the date entered must be valid without further calculation.
31    pub fn new(y: u16, m: u8, d: u8) -> Result<DateTuple, String> {
32        if y > 9999 {
33            return Err(format!(
34                "Invalid year in DateTuple {:?}: year must be <= 9999.",
35                DateTuple { y, m, d }
36            ));
37        }
38        if (1..=12).contains(&m) {
39            if d == 0 || d > date_utils::get_last_date_in_month(m, y) {
40                return Err(format!(
41                    "Invalid date in DateTuple: {:?}",
42                    DateTuple { y, m, d }
43                ));
44            }
45            Ok(DateTuple { y, m, d })
46        } else {
47            Err(format!(
48                "Invalid month in DateTuple: {:?}\nMonth must be between 1 and 12; Note that months are ONE-BASED since version 2.0.0.",
49                DateTuple { y, m, d }
50            ))
51        }
52    }
53
54    /// Returns the minimum date handled - 1st January 0000.
55    pub fn min_value() -> DateTuple {
56        DateTuple::new(0, 1, 1).unwrap()
57    }
58
59    /// Returns the maximum date handled - 31st December 9999.
60    pub fn max_value() -> DateTuple {
61        DateTuple::new(9999, 12, 31).unwrap()
62    }
63
64    /// Returns a `DateTuple` of the current date according to the system clock.
65    pub fn today() -> DateTuple {
66        date_utils::now_as_datetuple()
67    }
68
69    pub fn get_year(self) -> u16 {
70        self.y
71    }
72
73    pub fn get_month(self) -> u8 {
74        self.m
75    }
76
77    pub fn get_date(self) -> u8 {
78        self.d
79    }
80
81    /// Gets a DateTuple representing the date immediately following
82    /// the current one. Will not go past Dec 9999.
83    pub fn next_date(self) -> DateTuple {
84        if self.y == 9999 && self.m == 12 && self.d == 31 {
85            return self;
86        }
87        if self.d == date_utils::get_last_date_in_month(self.m, self.y) {
88            if self.m == 12 {
89                DateTuple {
90                    y: self.y + 1,
91                    m: 1,
92                    d: 1,
93                }
94            } else {
95                DateTuple {
96                    y: self.y,
97                    m: self.m + 1,
98                    d: 1,
99                }
100            }
101        } else {
102            DateTuple {
103                y: self.y,
104                m: self.m,
105                d: self.d + 1,
106            }
107        }
108    }
109
110    /// Gets a DateTuple representing the date immediately preceding
111    /// the current one. Will not go past 1 Jan 0000.
112    pub fn previous_date(self) -> DateTuple {
113        if self.y == 0 && self.m == 1 && self.d == 1 {
114            return self;
115        }
116        if self.d == 1 {
117            if self.m == 1 {
118                DateTuple {
119                    y: self.y - 1,
120                    m: 12,
121                    d: date_utils::get_last_date_in_month(12, self.y - 1),
122                }
123            } else {
124                DateTuple {
125                    y: self.y,
126                    m: self.m - 1,
127                    d: date_utils::get_last_date_in_month(self.m - 1, self.y),
128                }
129            }
130        } else {
131            DateTuple {
132                y: self.y,
133                m: self.m,
134                d: self.d - 1,
135            }
136        }
137    }
138
139    /// Adds a number of days to a DateTuple.
140    pub fn add_days(&mut self, days: u32) {
141        for _ in 0..days {
142            *self = self.next_date();
143        }
144    }
145
146    /// Subtracts a number of days from a DateTuple.
147    pub fn subtract_days(&mut self, days: u32) {
148        for _ in 0..days {
149            *self = self.previous_date();
150        }
151    }
152
153    /// Adds a number of months to a DateTuple.
154    ///
155    /// If the day of month is beyond the last date in the resulting month, the day of
156    /// month will be set to the last day of that month.
157    pub fn add_months(&mut self, months: u32) {
158        let mut new_month = MonthTuple::from(*self);
159        new_month.add_months(months);
160        let last_date_in_month =
161            date_utils::get_last_date_in_month(new_month.get_month(), new_month.get_year());
162        if self.d > last_date_in_month {
163            self.d = last_date_in_month;
164        }
165        self.y = new_month.get_year();
166        self.m = new_month.get_month();
167    }
168
169    /// Subtracts a number of months from a DateTuple.
170    ///
171    /// If the day of month is beyond the last date in the resulting month, the day of
172    /// month will be set to the last day of that month.
173    pub fn subtract_months(&mut self, months: u32) {
174        let mut new_month = MonthTuple::from(*self);
175        new_month.subtract_months(months);
176        let last_date_in_month =
177            date_utils::get_last_date_in_month(new_month.get_month(), new_month.get_year());
178        if self.d > last_date_in_month {
179            self.d = last_date_in_month;
180        }
181        self.y = new_month.get_year();
182        self.m = new_month.get_month();
183    }
184
185    /// Adds a number of years to a DateTuple.
186    ///
187    /// If the date is set to Feb 29 and the resulting year is not a leap year,
188    /// it will be changed to Feb 28.
189    pub fn add_years(&mut self, years: u16) {
190        let mut new_years = self.y + years;
191        if new_years > 9999 {
192            new_years = 9999;
193        }
194        if self.m == 2 && self.d == 29 && !date_utils::is_leap_year(new_years) {
195            self.d = 28
196        }
197        self.y = new_years;
198    }
199
200    /// Subtracts a number of years from a DateTuple.
201    ///
202    /// If the date is set to Feb 29 and the resulting year is not a leap year,
203    /// it will be changed to Feb 28.
204    pub fn subtract_years(&mut self, years: u16) {
205        let mut new_years = i32::from(self.y) - i32::from(years);
206        if new_years < 0 {
207            new_years = 0;
208        }
209        let new_years = new_years as u16;
210        if self.m == 2 && self.d == 29 && !date_utils::is_leap_year(new_years) {
211            self.d = 28
212        }
213        self.y = new_years;
214    }
215
216    /// Produces a readable date.
217    ///
218    /// ## Examples
219    /// * 2 Oct 2018
220    /// * 13 Jan 2019
221    pub fn to_readable_string(self) -> String {
222        let month = MonthTuple::from(self);
223        format!("{} {}", self.d, month.to_readable_string())
224    }
225
226    /// Gets the total number of days in the tuple,
227    /// with the first being `DateTuple::min_value()`.
228    pub fn to_days(self) -> u32 {
229        let mut total_days = 0u32;
230        for y in 0..self.y {
231            total_days += if date_utils::is_leap_year(y) {
232                DAYS_IN_A_LEAP_YEAR
233            } else {
234                DAYS_IN_A_COMMON_YEAR
235            }
236        }
237        for m in 1..self.m {
238            total_days += u32::from(date_utils::get_last_date_in_month(m, self.y));
239        }
240        total_days + u32::from(self.d)
241    }
242
243    /// Calculates years, months, and days from a total number of
244    /// days, with the first being `DateTuple::min_value()`.
245    pub fn from_days(mut total_days: u32) -> Result<DateTuple, String> {
246        let mut years = 0u16;
247        let mut months = 1u8;
248        while total_days
249            > if date_utils::is_leap_year(years) {
250                DAYS_IN_A_LEAP_YEAR
251            } else {
252                DAYS_IN_A_COMMON_YEAR
253            }
254        {
255            total_days -= if date_utils::is_leap_year(years) {
256                DAYS_IN_A_LEAP_YEAR
257            } else {
258                DAYS_IN_A_COMMON_YEAR
259            };
260            years += 1;
261        }
262        while total_days > u32::from(date_utils::get_last_date_in_month(months, years)) {
263            total_days -= u32::from(date_utils::get_last_date_in_month(months, years));
264            months += 1;
265        }
266        DateTuple::new(years, months, total_days as u8)
267    }
268}
269
270impl fmt::Display for DateTuple {
271    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
272        write!(f, "{:04}-{:02}-{:02}", self.y, self.m, self.d)
273    }
274}
275
276impl FromStr for DateTuple {
277    type Err = String;
278
279    /// Expects a string formatted like 2018-11-02.
280    ///
281    /// Also accepts the legacy crate format of 20181102.
282    fn from_str(s: &str) -> Result<DateTuple, Self::Err> {
283        lazy_static! {
284            static ref VALID_FORMAT: Regex = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
285            static ref LEGACY_FORMAT: Regex = Regex::new(r"^\d{8}$").unwrap();
286        }
287
288        if VALID_FORMAT.is_match(s) {
289            match DateTuple::new(
290                u16::from_str(&s[0..4]).unwrap(),
291                u8::from_str(&s[5..7]).unwrap(),
292                u8::from_str(&s[8..10]).unwrap(),
293            ) {
294                Ok(d) => Ok(d),
295                Err(e) => Err(format!("Invalid date passed to from_str: {}", e)),
296            }
297        } else if LEGACY_FORMAT.is_match(s) {
298            let (s1, s2) = s.split_at(4);
299            let (s2, s3) = s2.split_at(2);
300            match DateTuple::new(
301                u16::from_str(s1).unwrap(),
302                u8::from_str(s2).unwrap(),
303                u8::from_str(s3).unwrap(),
304            ) {
305                Ok(d) => Ok(d),
306                Err(e) => Err(format!("Invalid date passed to from_str: {}", e)),
307            }
308        } else {
309            Err(format!("Invalid str formatting of DateTuple: {}\nExpects a string formatted like 2018-11-02.", s))
310        }
311    }
312}
313
314impl PartialOrd for DateTuple {
315    fn partial_cmp(&self, other: &DateTuple) -> Option<Ordering> {
316        if self.y == other.y {
317            if self.m == other.m {
318                self.d.partial_cmp(&other.d)
319            } else {
320                self.m.partial_cmp(&other.m)
321            }
322        } else {
323            self.y.partial_cmp(&other.y)
324        }
325    }
326}
327
328#[cfg_attr(tarpaulin, skip)]
329impl Ord for DateTuple {
330    fn cmp(&self, other: &DateTuple) -> Ordering {
331        if self.y == other.y {
332            if self.m == other.m {
333                self.d.cmp(&other.d)
334            } else {
335                self.m.cmp(&other.m)
336            }
337        } else {
338            self.y.cmp(&other.y)
339        }
340    }
341}
342
343#[cfg(test)]
344mod tests {
345
346    use super::Date;
347
348    #[test]
349    fn test_next_date() {
350        let tuple1 = Date::new(2000, 6, 10).unwrap();
351        let tuple2 = Date::new(2000, 3, 31).unwrap();
352        let tuple3 = Date::max_value();
353        assert_eq!(
354            Date {
355                y: 2000,
356                m: 6,
357                d: 11
358            },
359            tuple1.next_date()
360        );
361        assert_eq!(
362            Date {
363                y: 2000,
364                m: 4,
365                d: 1
366            },
367            tuple2.next_date()
368        );
369        assert_eq!(
370            Date {
371                y: 9999,
372                m: 12,
373                d: 31
374            },
375            tuple3.next_date()
376        );
377    }
378
379    #[test]
380    fn test_previous_date() {
381        let tuple1 = Date::new(2000, 6, 10).unwrap();
382        let tuple2 = Date::new(2000, 3, 1).unwrap();
383        let tuple3 = Date::new(0, 1, 1).unwrap();
384        let tuple4 = Date::new(2000, 1, 1).unwrap();
385        assert_eq!(
386            Date {
387                y: 2000,
388                m: 6,
389                d: 9
390            },
391            tuple1.previous_date()
392        );
393        assert_eq!(
394            Date {
395                y: 2000,
396                m: 2,
397                d: 29
398            },
399            tuple2.previous_date()
400        );
401        assert_eq!(Date { y: 0, m: 1, d: 1 }, tuple3.previous_date());
402        assert_eq!(
403            Date {
404                y: 1999,
405                m: 12,
406                d: 31
407            },
408            tuple4.previous_date()
409        );
410    }
411}