Skip to main content

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 = "2021-07-02".parse()?;
11//! print!("Date: {}", day);
12//!
13//! let mut stamp: DateTime = "2021-07-02 12:34:00".parse()?;
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::str::FromStr for Date {
225    type Err = 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 from_str(date: &str) -> std::result::Result<Self, Self::Err> {
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.and_utc().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::str::FromStr for DateTime {
428    type Err = 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 from_str(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 assert2::{assert, let_assert};
464    use rstest::rstest;
465
466    use super::*;
467
468    #[test]
469    fn test_date_new() {
470        let_assert!(Ok(date) = Date::new(2021, 11, 20));
471        assert!(date.to_string() == String::from("2021-11-20"));
472    }
473
474    #[rstest]
475    #[case(2021, 0,  20, "bad month zero")]
476    #[case(2021, 13, 20, "bad month too high")]
477    #[case(2021, 11, 0,  "bad day zero")]
478    #[case(2021, 11, 32, "bad day too high")]
479    fn test_date_new_unsuccess(
480        #[case]year:  i32,
481        #[case]month: u32,
482        #[case]day:   u32,
483        #[case]msg:   &str
484    ) {
485        assert!(Err(DateError::InvalidDate) == Date::new(year, month, day), "{msg}");
486    }
487
488    #[test]
489    fn test_date_day_end() {
490        let_assert!(Ok(date) = Date::new(2021, 11, 20));
491        let_assert!(Ok(expected) = DateTime::new((2021, 11, 20), (23, 59, 59)));
492        assert!(date.day_end() == expected);
493    }
494
495    #[test]
496    fn test_date_day_start() {
497        let_assert!(Ok(date) = Date::new(2021, 11, 20));
498        let_assert!(Ok(expected) = DateTime::new((2021, 11, 20), (0, 0, 0)));
499        assert!(date.day_start() == expected);
500    }
501
502    #[test]
503    fn test_month_start() {
504        let_assert!(Ok(date) = Date::new(2022, 11, 20));
505        let_assert!(Ok(expected) = Date::new(2022, 11, 1));
506        assert!(date.month_start() == expected);
507    }
508
509    #[rstest]
510    #[case("jan", 1, 31)]
511    #[case("feb", 2, 28)]
512    #[case("mar", 3, 31)]
513    #[case("apr", 4, 30)]
514    #[case("may", 5, 31)]
515    #[case("jun", 6, 30)]
516    #[case("jul", 7, 31)]
517    #[case("aug", 8, 31)]
518    #[case("sep", 9, 30)]
519    #[case("oct", 10, 31)]
520    #[case("nov", 11, 30)]
521    #[case("dec", 12, 31)]
522    fn test_month_end(#[case]name: &str, #[case]mon: u32, #[case]day: u32) {
523        let_assert!(Ok(date) = Date::new(2022, mon, 20));
524        let_assert!(Ok(expected) = Date::new(2022, mon, day));
525        assert!(date.month_end() == expected, "{name}");
526    }
527
528    #[test]
529    fn test_month_end_leap_year() {
530        let_assert!(Ok(date) = Date::new(2020, 2, 20));
531        let_assert!(Ok(expected) = Date::new(2020, 2, 29));
532        assert!(date.month_end() == expected);
533    }
534
535    #[test]
536    fn test_date_week_start() {
537        let_assert!(Ok(date) = Date::new(2022, 12, 20));
538        let_assert!(Ok(expected) = Date::new(2022, 12, 18));
539        assert!(date.week_start() == expected);
540    }
541
542    #[test]
543    fn test_date_week_start_no_change() {
544        let_assert!(Ok(date) = Date::new(2022, 12, 18));
545        let_assert!(Ok(expected) = Date::new(2022, 12, 18));
546        assert!(date.week_start() == expected);
547    }
548
549    #[test]
550    fn test_date_week_end() {
551        let_assert!(Ok(date) = Date::new(2022, 12, 15));
552        let_assert!(Ok(expected) = Date::new(2022, 12, 17));
553        assert!(date.week_end() == expected);
554    }
555
556    #[test]
557    fn test_date_week_end_no_change() {
558        let_assert!(Ok(date) = Date::new(2022, 12, 17));
559        let_assert!(Ok(expected) = Date::new(2022, 12, 17));
560        assert!(date.week_end() == expected);
561    }
562
563    #[test]
564    fn test_date_year_start() {
565        let_assert!(Ok(date) = Date::new(2022, 12, 20));
566        let_assert!(Ok(expected) = Date::new(2022, 1, 1));
567        assert!(date.year_start() == expected);
568    }
569
570    #[test]
571    fn test_date_year_end() {
572        let_assert!(Ok(date) = Date::new(2022, 12, 20));
573        let_assert!(Ok(expected) = Date::new(2022, 12, 31));
574        assert!(date.year_end() == expected);
575    }
576
577    #[test]
578    fn test_date_succ() {
579        let_assert!(Ok(date) = Date::new(2021, 11, 20));
580        let_assert!(Ok(expected) = Date::new(2021, 11, 21));
581        assert!(date.succ() == expected);
582    }
583
584    #[test]
585    fn test_date_pred() {
586        let_assert!(Ok(date) = Date::new(2021, 11, 20));
587        let_assert!(Ok(expected) = Date::new(2021, 11, 19));
588        assert!(date.pred() == expected);
589    }
590
591    #[test]
592    fn test_date_parse() {
593        let_assert!(Ok(date) = "2021-11-20".parse::<Date>());
594        let_assert!(Ok(expected) = Date::new(2021, 11, 20));
595        assert!(date == expected);
596    }
597
598    #[test]
599    fn test_date_parse_bad() {
600        let date = "fred".parse::<Date>();
601        assert!(&date.is_err());
602    }
603
604    // DateRange
605
606    #[test]
607    fn test_date_range_default() {
608        let range = DateRange::default();
609        assert!(range.start() == Date::today());
610        assert!(range.end() == Date::today().succ());
611    }
612
613    #[test]
614    fn test_date_range_new_opt() {
615        let_assert!(Some(range) = DateRange::new_opt(Date::today(), Date::today().succ()));
616        assert!(range.start() == Date::today());
617        assert!(range.end() == Date::today().succ());
618    }
619
620    #[test]
621    fn test_date_range_new_opt_backwards() {
622        let range = DateRange::new_opt(Date::today(), Date::today().pred());
623        assert!(range.is_none());
624    }
625
626    #[test]
627    fn test_date_range_new_opt_empty() {
628        let range = DateRange::new_opt(Date::today(), Date::today());
629        assert!(range.is_none());
630    }
631
632    #[test]
633    fn test_date_range_new() {
634        let range = DateRange::new(Date::today(), Date::today().succ());
635        assert!(range.start() == Date::today());
636        assert!(range.end() == Date::today().succ());
637        assert!(range.is_empty() == false);
638    }
639
640    #[test]
641    fn test_date_range_new_backwards() {
642        let range = DateRange::new(Date::today(), Date::today().pred());
643        assert!(range.start() == Date::today());
644        assert!(range.end() == Date::today());
645        assert!(range.is_empty() == true);
646    }
647
648    #[test]
649    fn test_date_range_new_empty() {
650        let range = DateRange::new(Date::today(), Date::today());
651        assert!(range.start() == Date::today());
652        assert!(range.end() == Date::today());
653        assert!(range.is_empty() == true);
654    }
655
656    #[test]
657    fn test_date_range_from_date() {
658        let_assert!(Ok(date) = Date::new(2022, 12, 1));
659        let range: DateRange = date.into();
660        let expect = DateRange::new(date, date.succ());
661
662        assert!(range == expect);
663    }
664
665    // DateTime
666
667    #[test]
668    fn test_datetime_new() {
669        let_assert!(Ok(date) = DateTime::new((2021, 11, 20), (11, 32, 18)));
670        assert!(date.to_string() == String::from("2021-11-20 11:32:18"));
671    }
672
673    #[test]
674    fn test_datetime_new_bad_date() {
675        let date = DateTime::new((2021, 13, 20), (11, 32, 18));
676        assert!(date.is_err());
677    }
678
679    #[test]
680    fn test_datetime_new_bad_time() {
681        let date = DateTime::new((2021, 11, 20), (11, 82, 18));
682        assert!(date.is_err());
683    }
684
685    #[test]
686    fn test_datetime_parse() {
687        let_assert!(Ok(date) = "2021-11-20 11:32:18".parse::<DateTime>());
688        let_assert!(Ok(expected) = DateTime::new((2021, 11, 20), (11, 32, 18)));
689        assert!(date == expected);
690    }
691
692    #[test]
693    fn test_datetime_diff() {
694        let_assert!(Ok(date) = DateTime::new((2021, 11, 20), (11, 32, 18)));
695        let_assert!(Ok(old) = DateTime::new((2021, 11, 18), (12, 00, 00)));
696        let_assert!(Ok(dur) = date - old);
697        assert!(dur == Duration::from_secs(2 * 86400 - 28 * 60 + 18));
698    }
699
700    #[test]
701    fn test_datetime_diff_bad() {
702        let_assert!(Ok(date) = DateTime::new((2021, 11, 18), (12, 00, 00)));
703        let_assert!(Ok(old) = DateTime::new((2021, 11, 20), (11, 32, 18)));
704        let_assert!(Err(_) = date - old);
705    }
706
707    #[test]
708    fn test_datetime_add_time() {
709        let_assert!(Ok(date) = DateTime::new((2021, 11, 18), (12, 00, 00)));
710        let_assert!(Ok(new) = date + DateTime::minutes(10));
711        let_assert!(Ok(expected) = DateTime::new((2021, 11, 18), (12, 10, 00)));
712        assert!(new == expected);
713    }
714
715    #[test]
716    fn test_datetime_add_days() {
717        let_assert!(Ok(date) = DateTime::new((2021, 11, 18), (12, 00, 00)));
718        let_assert!(Ok(new) = date + DateTime::days(3));
719        let_assert!(Ok(expected) = DateTime::new((2021, 11, 21), (12, 00, 00)));
720        assert!(new == expected);
721    }
722
723    #[test]
724    fn test_datetime_hhmm() {
725        let_assert!(Ok(date) = DateTime::new((2021, 11, 18), (8, 5, 13)));
726        assert!(date.hhmm() == String::from("08:05"));
727    }
728
729    #[test]
730    fn test_datetime_parse_bad() {
731        let date = "fred".parse::<DateTime>();
732        assert!(&date.is_err());
733    }
734}