timelog/
date.rs

1//! Utilities for working with dates and times
2//!
3//! # Examples
4//!
5//! ```rust
6//! use timelog::date::{Date, DateTime};
7//! use timelog::Result;
8//!
9//! # fn main() -> Result<()> {
10//! let mut day = Date::try_from("2021-07-02")?;
11//! print!("Date: {}", day);
12//!
13//! let mut stamp = DateTime::try_from("2021-07-02 12:34:00")?;
14//! print!("Stamp: {}", stamp);
15//!
16//! let mut today = Date::parse("today")?;
17//! print!("Today: {}", today);
18//! #   Ok(())
19//! #  }
20//! ```
21//!
22//! # Description
23//!
24//! The [`Date`] struct represents a date in the local time zone. The module also
25//! provides tools for manipulating [`Date`]s.
26//!
27//! The [`DateRange`] struct represents a pair of dates in the local time zone.
28//! The range represents a half-open range.
29//!
30//! The [`DateTime`] struct represents a date and time in the local time zone. The
31//! module also provides tools for manipulating [`DateTime`]s.
32
33use std::fmt::{self, Display};
34use std::time::Duration;
35
36use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
37
38pub mod error;
39pub mod parse;
40
41/// Type shortcut for [`parse::DateParser`]
42pub use parse::DateParser;
43/// Type shortcut for [`parse::RangeParser`]
44pub use parse::RangeParser;
45/// Enum listing the days of the week. (From [`chrono::Weekday`])
46pub type Weekday = chrono::Weekday;
47/// Type for our internal time format.
48pub type Time = chrono::NaiveTime;
49
50/// Error type for errors in date functionality.
51pub use error::DateError;
52/// Result type for errors in date functionality.
53pub type Result<T> = std::result::Result<T, DateError>;
54
55/// Representation of a date in the local time zone.
56#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
57pub struct Date(chrono::NaiveDate);
58
59// Create Dates
60impl Date {
61    /// Create a [`Date`] out of a year, month, and day, returning a [`Result`].
62    ///
63    /// # Errors
64    ///
65    /// If one of the parameters is outside the legal range for a date, returns an
66    /// [`DateError::InvalidDate`].
67    pub fn new(year: i32, month: u32, day: u32) -> Result<Self> {
68        Ok(Self(
69            NaiveDate::from_ymd_opt(year, month, day).ok_or(DateError::InvalidDate)?
70        ))
71    }
72
73    /// Create a [`Date`] for today.
74    #[must_use]
75    pub fn today() -> Self { Self(Local::now().date_naive()) }
76
77    /// Create a [`Date`] object from the supplied date specification.
78    ///
79    /// Legal specifications include "today" and "yesterday", the days of the week "sunday"
80    /// through "saturday", and a date in the form "YYYY-MM-DD".
81    /// The days of the week result in the date representing the previous instance of that day
82    /// (last Monday for "monday", etc.).
83    ///
84    /// # Errors
85    ///
86    /// Return [`DateError::InvalidDaySpec`] if the supplied spec is not valid.
87    pub fn parse(datespec: &str) -> Result<Self> { DateParser::default().parse(datespec) }
88}
89
90// Accessors
91impl Date {
92    /// Return the year portion of the [`Date`]
93    pub fn year(&self) -> i32 { self.0.year() }
94
95    /// Return the month portion of the [`Date`]
96    pub fn month(&self) -> u32 { self.0.month() }
97
98    /// Return the day portion of the [`Date`]
99    pub fn day(&self) -> u32 { self.0.day() }
100
101    /// Return the day of the week enum.
102    pub fn weekday(&self) -> Weekday { self.0.weekday() }
103
104    /// Return the day of the week as a string.
105    pub fn weekday_str(&self) -> &'static str {
106        match self.0.weekday() {
107            Weekday::Sun => "Sunday",
108            Weekday::Mon => "Monday",
109            Weekday::Tue => "Tuesday",
110            Weekday::Wed => "Wednesday",
111            Weekday::Thu => "Thursday",
112            Weekday::Fri => "Friday",
113            Weekday::Sat => "Saturday"
114        }
115    }
116}
117
118// Relative date methods
119impl Date {
120    /// Create a [`DateTime`] object for the first second of the current [`Date`]
121    #[must_use]
122    pub fn day_start(&self) -> DateTime {
123        DateTime(self.0.and_hms_opt(0, 0, 0).expect("Midnight exists"))
124    }
125
126    /// Create a [`DateTime`] object for the last second of the current [`Date`]
127    #[must_use]
128    pub fn day_end(&self) -> DateTime {
129        DateTime(self.0.and_hms_opt(23, 59, 59).expect("Midnight exists"))
130    }
131
132    // Find the last date before this one where the day of the week was
133    // weekday.
134    #[must_use]
135    fn find_previous(&self, weekday: Weekday) -> Self {
136        let mut day = self.0.pred_opt().expect("Not at beginning of time");
137        while day.weekday() != weekday {
138            day = day.pred_opt().expect("Not at beginning of time");
139        }
140        Self(day)
141    }
142
143    // Find the next date after this one where the day of the week was
144    // weekday.
145    #[must_use]
146    fn find_next(&self, weekday: Weekday) -> Self {
147        let mut day = self.0.succ_opt().expect("Not at end of time");
148        while day.weekday() != weekday {
149            day = day.succ_opt().expect("Not at end of time");
150        }
151        Self(day)
152    }
153
154    /// Create a [`Date`] object for the last day of the month.
155    #[must_use]
156    pub fn month_start(&self) -> Date { Date(self.0.with_day(1).expect("Reasonable date range")) }
157
158    // Return true if the supplied year is a leap year
159    fn is_leap_year(year: i32) -> bool {
160        (year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0))
161    }
162
163    /// Create a [`Date`] object for the last day of the month.
164    #[must_use]
165    #[rustfmt::skip]
166    pub fn month_end(&self) -> Date {
167        let last_day = match self.0.month() {
168            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
169            4 | 6 | 9 | 11 => 30,
170            2 => if Self::is_leap_year(self.0.year()) { 29 } else { 28 },
171            _ => unreachable!()
172        };
173        Date(self.0.with_day(last_day).expect("End of month should work"))
174    }
175
176    /// Create a [`Date`] object for the first day of the week.
177    #[must_use]
178    pub fn week_start(&self) -> Date {
179        match self.0.weekday() {
180            Weekday::Sun => *self,
181            _ => self.find_previous(Weekday::Sun)
182        }
183    }
184
185    /// Create a [`Date`] object for the last day of the week.
186    #[must_use]
187    pub fn week_end(&self) -> Date {
188        match self.0.weekday() {
189            Weekday::Sat => *self,
190            _ => self.find_next(Weekday::Sat)
191        }
192    }
193
194    /// Create a [`Date`] object for the beginning of the year containing the date.
195    #[must_use]
196    pub fn year_start(&self) -> Date {
197        Self(self.0
198            .with_month(1).expect("Within reasonable dates")
199            .with_day(1).expect("Within reasonable dates"))
200    }
201
202    /// Create a [`Date`] object for the end of the year containing the date.
203    #[must_use]
204    pub fn year_end(&self) -> Date {
205        Self(self.0
206            .with_month(12).expect("Within reasonable dates")
207            .with_day(31).expect("Within reasonable dates"))
208    }
209
210    /// Create a [`Date`] for the day after the current date.
211    #[must_use]
212    pub fn succ(&self) -> Date { Self(self.0.succ_opt().expect("Not at end of time")) }
213
214    /// Create a [`Date`] for the day before the current date.
215    #[must_use]
216    pub fn pred(&self) -> Date { Self(self.0.pred_opt().expect("Not at beginnning of time")) }
217}
218
219impl Default for Date {
220    /// The default [`Date`] is the current day.
221    fn default() -> Date { Self::today() }
222}
223
224impl std::convert::TryFrom<&str> for Date {
225    type Error = DateError;
226
227    /// Create a [`Date`] out of a string, returning a [`Result`]
228    ///
229    /// # Errors
230    ///
231    /// If the date is not formatted as 'YYYY-MM-DD', returns an [`DateError::InvalidDate`].
232    /// Also if the date string cannot be converted into a [`Date`], returns an
233    /// [`DateError::InvalidDate`].
234    #[rustfmt::skip]
235    fn try_from(date: &str) -> Result<Self> {
236        let Ok(parsed) = NaiveDate::parse_from_str(date, "%Y-%m-%d") else {
237            return Err(DateError::InvalidDate);
238        };
239        Ok(Self(parsed))
240    }
241}
242
243impl Display for Date {
244    /// Format a [`Date`] object in "YYYY-MM-DD" format.
245    #[rustfmt::skip]
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        write!(f, "{}-{:02}-{:02}", self.0.year(), self.0.month(), self.0.day())
248    }
249}
250
251impl From<Date> for String {
252    /// Convert a [`Date`] into a [`String`]
253    fn from(date: Date) -> Self { date.to_string() }
254}
255
256/// Representation of a half-open range of dates in the local time zone.
257#[derive(Debug, PartialEq, Eq)]
258pub struct DateRange {
259    start: Date,
260    end:   Date
261}
262
263impl DateRange {
264    /// Create [`DateRange`] with the supplied start and end dates. If
265    /// The end is <= the start, create an empty [`DateRange`]
266    pub fn new(start: Date, end: Date) -> Self {
267        Self::new_opt(start, end).unwrap_or(Self { start, end: start })
268    }
269
270    /// Create [`DateRange`] if the start date is less than the end date.
271    pub fn new_opt(start: Date, end: Date) -> Option<Self> {
272        (start < end).then_some(Self { start, end })
273    }
274
275    /// Create [`DateRange`] from an iterator returning parts of a date range descriptor.
276    ///
277    /// # Errors
278    ///
279    /// - Return [`DateError::InvalidDate`] if the specification for a single day is not valid.
280    /// - Return [`DateError::InvalidDaySpec`] if overall range specification is not valid.
281    pub fn parse<'a, I>(datespec: &mut I) -> Result<Self>
282    where
283        I: Iterator<Item = &'a str>
284    {
285        RangeParser::default().parse(datespec).map(|(dr, _)| dr)
286    }
287}
288
289impl DateRange {
290    /// Return a copy of the start date for the range.
291    pub fn start(&self) -> Date { self.start }
292
293    /// Return a copy of the end date for the range.
294    pub fn end(&self) -> Date { self.end }
295
296    /// Return true if the range is empty.
297    pub fn is_empty(&self) -> bool { self.start >= self.end }
298}
299
300impl From<Date> for DateRange {
301    /// Create [`DateRange`] covering the supplied date.
302    fn from(date: Date) -> DateRange { Self { start: date, end: date.succ() } }
303}
304
305impl Default for DateRange {
306    /// The default [`DateRange`] covers just today.
307    fn default() -> Self {
308        let today = Date::today();
309        Self { start: today, end: today.succ() }
310    }
311}
312
313/// Representation of a date and time in the local time zone.
314#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
315pub struct DateTime(chrono::NaiveDateTime);
316
317impl DateTime {
318    /// Create a [`DateTime`] from two tuples, one representing the date (year, month, day) and
319    /// the second representing the time (hour, minute, second).
320    ///
321    /// # Errors
322    ///
323    /// Return an [`DateError::InvalidDate`] if the values in the tuples are not appropriate for the
324    /// data types.
325    pub fn new(date: (i32, u32, u32), time: (u32, u32, u32)) -> Result<Self> {
326        let Some(d) = NaiveDate::from_ymd_opt(date.0, date.1, date.2) else {
327            return Err(DateError::InvalidDate);
328        };
329        let Some(t) = NaiveTime::from_hms_opt(time.0, time.1, time.2) else {
330            return Err(DateError::InvalidDate);
331        };
332        Ok(Self(NaiveDateTime::new(d, t)))
333    }
334
335    /// Create a [`Date`] for right now.
336    pub fn now() -> Self { Self(Local::now().naive_local()) }
337
338    // Create a new [`DateTime`] from a [`Date`] and a [`NaiveTime`]
339    pub(crate) fn new_from_date_time(date: Date, time: NaiveTime) -> Self {
340        Self(NaiveDateTime::new(date.0, time))
341    }
342}
343
344impl DateTime {
345    /// Return the epoch time representing the current value of the [`DateTime`] object.
346    pub fn timestamp(&self) -> i64 { self.0.timestamp() }
347
348    /// Return a copy of just the [`Date`] portion of the [`DateTime`] object.
349    pub fn date(&self) -> Date { Date(self.0.date()) }
350
351    /// Return the hour of the day.
352    pub fn hour(&self) -> u32 { self.0.hour() }
353
354    /// Return the minute of the hour.
355    pub fn minute(&self) -> u32 { self.0.minute() }
356
357    /// Return the number of seconds after the hour.
358    pub fn second_offset(&self) -> u32 { self.0.minute() * 60 + self.0.second() }
359
360    /// Return the formatted time as a [`String`]
361    pub fn hhmm(&self) -> String { format!("{:02}:{:02}", self.0.hour(), self.0.minute()) }
362}
363
364impl DateTime {
365    /// Return a [`Duration`] lasting the supplied number of minutes.
366    pub fn seconds(seconds: u64) -> Duration { Duration::from_secs(seconds) }
367
368    /// Return a [`Duration`] lasting the supplied number of minutes.
369    pub fn minutes(minutes: u64) -> Duration { Duration::from_secs(minutes * 60) }
370
371    /// Return a [`Duration`] lasting the supplied number of hours.
372    pub fn hours(hours: u64) -> Duration { Duration::from_secs(hours * 3600) }
373
374    /// Return a [`Duration`] lasting the supplied number of days.
375    pub fn days(days: u64) -> Duration { Duration::from_secs(days * 86400) }
376
377    /// Return a [`Duration`] lasting the supplied number of weeks.
378    pub fn weeks(weeks: u64) -> Duration { Self::days(weeks * 7) }
379}
380
381impl std::ops::Add<Duration> for DateTime {
382    type Output = Result<DateTime>;
383
384    /// Return a [`DateTime`] as a [`Result`] resulting from adding the `rhs` [`Duration`] to the
385    /// object.
386    ///
387    /// # Errors
388    ///
389    /// Return an [`DateError::InvalidDate`] if adding the duration results in a bad date.
390    fn add(self, rhs: Duration) -> Result<Self> {
391        Ok(Self(
392            self.0 + chrono::Duration::from_std(rhs).map_err(|_| DateError::InvalidDate)?
393        ))
394    }
395}
396
397impl std::ops::Sub<Self> for DateTime {
398    type Output = Result<Duration>;
399
400    /// Return the [`Duration`] as a [`Result`] resulting from subtracting the `rhs` from the
401    /// object.
402    ///
403    /// # Errors
404    ///
405    /// Return an [`DateError::EntryOrder`] if the `rhs` is larger than the [`DateTime`].
406    fn sub(self, rhs: Self) -> Result<Duration> {
407        (self.0 - rhs.0).to_std().map_err(|_| DateError::EntryOrder)
408    }
409}
410
411impl std::ops::Sub<Duration> for DateTime {
412    type Output = Result<DateTime>;
413
414    /// Return a [`DateTime`] as a [`Result`] resulting from subtracting the `rhs` [`Duration`]
415    /// from the object.
416    ///
417    /// # Errors
418    ///
419    /// Return an [`DateError::InvalidDate`] if adding the duration results in a bad date.
420    fn sub(self, rhs: Duration) -> Result<Self> {
421        Ok(Self(
422            self.0 - chrono::Duration::from_std(rhs).map_err(|_| DateError::InvalidDate)?
423        ))
424    }
425}
426
427impl std::convert::TryFrom<&str> for DateTime {
428    type Error = DateError;
429
430    /// Parse the [`DateTime`] out of a string, returning a [`Result`]
431    ///
432    /// # Errors
433    ///
434    /// If the datetime is not formatted as 'YYYY-MM-DD HH:MM:SS', returns an
435    /// [`DateError::InvalidDate`]. Also if the datetime cannot be converted into a [`DateTime`],
436    /// returns an [`DateError::InvalidDate`].
437    #[rustfmt::skip]
438    fn try_from(datetime: &str) -> Result<Self> {
439        let Ok(parsed) = NaiveDateTime::parse_from_str(datetime, "%Y-%m-%d %H:%M:%S") else {
440            return Err(DateError::InvalidDate);
441        };
442        Ok(Self(parsed))
443    }
444}
445
446impl Display for DateTime {
447    /// Format a [`DateTime`] object in "YYYY-MM-DD HH:MM:DD" format.
448    #[rustfmt::skip]
449    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
450        write!(f, "{}-{:02}-{:02} {:02}:{:02}:{:02}",
451            self.0.year(), self.0.month(), self.0.day(),
452            self.0.hour(), self.0.minute(), self.0.second())
453    }
454}
455
456impl From<DateTime> for String {
457    /// Convert a [`Date`] into a [`String`]
458    fn from(datetime: DateTime) -> Self { datetime.to_string() }
459}
460
461#[cfg(test)]
462mod tests {
463    use spectral::prelude::*;
464
465    use super::*;
466
467    #[test]
468    fn test_date_new() {
469        let date = Date::new(2021, 11, 20);
470        assert_that!(&date).is_ok();
471        assert_that!(date.unwrap().to_string()).is_equal_to(&String::from("2021-11-20"));
472    }
473
474    #[test]
475    fn test_date_new_bad_month() {
476        assert_that!(Date::new(2021, 0, 20)).is_err_containing(&DateError::InvalidDate);
477        assert_that!(Date::new(2021, 13, 20)).is_err_containing(&DateError::InvalidDate);
478    }
479
480    #[test]
481    fn test_date_new_bad_day() {
482        assert_that!(Date::new(2021, 11, 0)).is_err_containing(&DateError::InvalidDate);
483        assert_that!(Date::new(2021, 11, 32)).is_err_containing(&DateError::InvalidDate);
484    }
485
486    #[test]
487    fn test_date_day_end() {
488        let date = Date::new(2021, 11, 20).unwrap();
489        assert_that!(date.day_end())
490            .is_equal_to(&DateTime::new((2021, 11, 20), (23, 59, 59)).unwrap());
491    }
492
493    #[test]
494    fn test_date_day_start() {
495        let date = Date::new(2021, 11, 20).unwrap();
496        assert_that!(date.day_start())
497            .is_equal_to(&DateTime::new((2021, 11, 20), (0, 0, 0)).unwrap());
498    }
499
500    #[test]
501    fn test_month_start() {
502        let date = Date::new(2022, 11, 20).unwrap();
503        assert_that!(date.month_start())
504            .is_equal_to(&Date::new(2022, 11, 1).unwrap());
505    }
506
507    #[test]
508    fn test_month_end() {
509        let tests = [
510            ("jan", 1, 31),
511            ("feb", 2, 28),
512            ("mar", 3, 31),
513            ("apr", 4, 30),
514            ("may", 5, 31),
515            ("jun", 6, 30),
516            ("jul", 7, 31),
517            ("aug", 8, 31),
518            ("sep", 9, 30),
519            ("oct", 10, 31),
520            ("nov", 11, 30),
521            ("dec", 12, 31)
522        ];
523        for (name, mon, day) in tests.iter() {
524            let date = Date::new(2022, *mon, 20).unwrap();
525            assert_that!(date.month_end())
526                .named(name)
527                .is_equal_to(&Date::new(2022, *mon, *day).unwrap());
528        }
529    }
530
531    #[test]
532    fn test_month_end_leap_year() {
533        let date = Date::new(2020, 2, 20).unwrap();
534        assert_that!(date.month_end())
535            .is_equal_to(&Date::new(2020, 2, 29).unwrap());
536    }
537
538    #[test]
539    fn test_date_week_start() {
540        let date = Date::new(2022, 12, 20).unwrap();
541        assert_that!(date.week_start())
542            .is_equal_to(&Date::new(2022, 12, 18).unwrap());
543    }
544
545    #[test]
546    fn test_date_week_start_no_change() {
547        let date = Date::new(2022, 12, 18).unwrap();
548        assert_that!(date.week_start())
549            .is_equal_to(&Date::new(2022, 12, 18).unwrap());
550    }
551
552    #[test]
553    fn test_date_week_end() {
554        let date = Date::new(2022, 12, 15).unwrap();
555        assert_that!(date.week_end())
556            .is_equal_to(&Date::new(2022, 12, 17).unwrap());
557    }
558
559    #[test]
560    fn test_date_week_end_no_change() {
561        let date = Date::new(2022, 12, 17).unwrap();
562        assert_that!(date.week_end())
563            .is_equal_to(&Date::new(2022, 12, 17).unwrap());
564    }
565
566    #[test]
567    fn test_date_year_start() {
568        let date = Date::new(2022, 12, 20).unwrap();
569        assert_that!(date.year_start())
570            .is_equal_to(&Date::new(2022, 1, 1).unwrap());
571    }
572
573    #[test]
574    fn test_date_year_end() {
575        let date = Date::new(2022, 12, 20).unwrap();
576        assert_that!(date.year_end())
577            .is_equal_to(&Date::new(2022, 12, 31).unwrap());
578    }
579
580    #[test]
581    fn test_date_succ() {
582        let date = Date::new(2021, 11, 20).unwrap();
583        assert_that!(date.succ()).is_equal_to(&Date::new(2021, 11, 21).unwrap());
584    }
585
586    #[test]
587    fn test_date_pred() {
588        let date = Date::new(2021, 11, 20).unwrap();
589        assert_that!(date.pred()).is_equal_to(&Date::new(2021, 11, 19).unwrap());
590    }
591
592    #[test]
593    fn test_date_try_from() {
594        let date = Date::try_from("2021-11-20");
595        assert_that!(&date).is_ok();
596        assert_that!(date.unwrap()).is_equal_to(&Date::new(2021, 11, 20).unwrap());
597    }
598
599    #[test]
600    fn test_date_try_from_bad() {
601        let date = Date::try_from("fred");
602        assert_that!(&date).is_err();
603    }
604
605    // DateRange
606
607    #[test]
608    fn test_date_range_default() {
609        let range = DateRange::default();
610        assert_that!(range.start()).is_equal_to(&Date::today());
611        assert_that!(range.end()).is_equal_to(&Date::today().succ());
612    }
613
614    #[test]
615    fn test_date_range_new_opt() {
616        let range = DateRange::new_opt(Date::today(), Date::today().succ());
617        assert_that!(range).is_some();
618        let range = range.unwrap();
619        assert_that!(range.start()).is_equal_to(&Date::today());
620        assert_that!(range.end()).is_equal_to(&Date::today().succ());
621    }
622
623    #[test]
624    fn test_date_range_new_opt_backwards() {
625        let range = DateRange::new_opt(Date::today(), Date::today().pred());
626        assert_that!(range).is_none();
627    }
628
629    #[test]
630    fn test_date_range_new_opt_empty() {
631        let range = DateRange::new_opt(Date::today(), Date::today());
632        assert_that!(range).is_none();
633    }
634
635    #[test]
636    fn test_date_range_new() {
637        let range = DateRange::new(Date::today(), Date::today().succ());
638        assert_that!(range.start()).is_equal_to(&Date::today());
639        assert_that!(range.end()).is_equal_to(&Date::today().succ());
640        assert_that!(range.is_empty()).is_equal_to(&false);
641    }
642
643    #[test]
644    fn test_date_range_new_backwards() {
645        let range = DateRange::new(Date::today(), Date::today().pred());
646        assert_that!(range.start()).is_equal_to(&Date::today());
647        assert_that!(range.end()).is_equal_to(&Date::today());
648        assert_that!(range.is_empty()).is_equal_to(&true);
649    }
650
651    #[test]
652    fn test_date_range_new_empty() {
653        let range = DateRange::new(Date::today(), Date::today());
654        assert_that!(range.start()).is_equal_to(&Date::today());
655        assert_that!(range.end()).is_equal_to(&Date::today());
656        assert_that!(range.is_empty()).is_equal_to(&true);
657    }
658
659    #[test]
660    fn test_date_range_from_date() {
661        let date = Date::new(2022, 12, 1).expect("Hard coded date won't fail.");
662        let range: DateRange = date.into();
663        let expect = DateRange::new(date, date.succ());
664
665        assert_that!(range).is_equal_to(&expect);
666    }
667
668    // DateTime
669
670    #[test]
671    fn test_datetime_new() {
672        let date = DateTime::new((2021, 11, 20), (11, 32, 18));
673        assert_that!(date).is_ok();
674        assert_that!(date.unwrap().to_string()).is_equal_to(&String::from("2021-11-20 11:32:18"));
675    }
676
677    #[test]
678    fn test_datetime_new_bad_date() {
679        let date = DateTime::new((2021, 13, 20), (11, 32, 18));
680        assert_that!(date).is_err();
681    }
682
683    #[test]
684    fn test_datetime_new_bad_time() {
685        let date = DateTime::new((2021, 11, 20), (11, 82, 18));
686        assert_that!(date).is_err();
687    }
688
689    #[test]
690    fn test_datetime_try_from() {
691        let date = DateTime::try_from("2021-11-20 11:32:18");
692        assert_that!(&date).is_ok();
693        assert_that!(date)
694            .is_ok()
695            .is_equal_to(&DateTime::new((2021, 11, 20), (11, 32, 18)).unwrap());
696    }
697
698    #[test]
699    fn test_datetime_diff() {
700        let date = DateTime::new((2021, 11, 20), (11, 32, 18)).unwrap();
701        let old = DateTime::new((2021, 11, 18), (12, 00, 00)).unwrap();
702        assert_that!(date - old)
703            .is_ok()
704            .is_equal_to(&Duration::from_secs(2 * 86400 - 28 * 60 + 18));
705    }
706
707    #[test]
708    fn test_datetime_diff_bad() {
709        let date = DateTime::new((2021, 11, 18), (12, 00, 00)).unwrap();
710        let old  = DateTime::new((2021, 11, 20), (11, 32, 18)).unwrap();
711        assert_that!(date - old).is_err();
712    }
713
714    #[test]
715    fn test_datetime_add_time() {
716        let date = DateTime::new((2021, 11, 18), (12, 00, 00)).unwrap();
717        let new = date + DateTime::minutes(10);
718        assert_that!(new)
719            .is_ok()
720            .is_equal_to(&DateTime::new((2021, 11, 18), (12, 10, 00)).unwrap());
721    }
722
723    #[test]
724    fn test_datetime_add_days() {
725        let date = DateTime::new((2021, 11, 18), (12, 00, 00)).unwrap();
726        let new = date + DateTime::days(3);
727        assert_that!(new)
728            .is_ok()
729            .is_equal_to(&DateTime::new((2021, 11, 21), (12, 00, 00)).unwrap());
730    }
731
732    #[test]
733    fn test_datetime_hhmm() {
734        let date = DateTime::new((2021, 11, 18), (8, 5, 13)).unwrap();
735        assert_that!(date.hhmm()).is_equal_to(&String::from("08:05"));
736    }
737
738    #[test]
739    fn test_datetime_try_from_bad() {
740        let date = DateTime::try_from("fred");
741        assert_that!(&date).is_err();
742    }
743}