1use 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
41pub use parse::DateParser;
43pub use parse::RangeParser;
45pub type Weekday = chrono::Weekday;
47pub type Time = chrono::NaiveTime;
49
50pub use error::DateError;
52pub type Result<T> = std::result::Result<T, DateError>;
54
55#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
57pub struct Date(chrono::NaiveDate);
58
59impl Date {
61 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 #[must_use]
75 pub fn today() -> Self { Self(Local::now().date_naive()) }
76
77 pub fn parse(datespec: &str) -> Result<Self> { DateParser::default().parse(datespec) }
88}
89
90impl Date {
92 pub fn year(&self) -> i32 { self.0.year() }
94
95 pub fn month(&self) -> u32 { self.0.month() }
97
98 pub fn day(&self) -> u32 { self.0.day() }
100
101 pub fn weekday(&self) -> Weekday { self.0.weekday() }
103
104 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
118impl Date {
120 #[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 #[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 #[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 #[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 #[must_use]
156 pub fn month_start(&self) -> Date { Date(self.0.with_day(1).expect("Reasonable date range")) }
157
158 fn is_leap_year(year: i32) -> bool {
160 (year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0))
161 }
162
163 #[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 #[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 #[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 #[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 #[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 #[must_use]
212 pub fn succ(&self) -> Date { Self(self.0.succ_opt().expect("Not at end of time")) }
213
214 #[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 fn default() -> Date { Self::today() }
222}
223
224impl std::convert::TryFrom<&str> for Date {
225 type Error = DateError;
226
227 #[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 #[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 fn from(date: Date) -> Self { date.to_string() }
254}
255
256#[derive(Debug, PartialEq, Eq)]
258pub struct DateRange {
259 start: Date,
260 end: Date
261}
262
263impl DateRange {
264 pub fn new(start: Date, end: Date) -> Self {
267 Self::new_opt(start, end).unwrap_or(Self { start, end: start })
268 }
269
270 pub fn new_opt(start: Date, end: Date) -> Option<Self> {
272 (start < end).then_some(Self { start, end })
273 }
274
275 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 pub fn start(&self) -> Date { self.start }
292
293 pub fn end(&self) -> Date { self.end }
295
296 pub fn is_empty(&self) -> bool { self.start >= self.end }
298}
299
300impl From<Date> for DateRange {
301 fn from(date: Date) -> DateRange { Self { start: date, end: date.succ() } }
303}
304
305impl Default for DateRange {
306 fn default() -> Self {
308 let today = Date::today();
309 Self { start: today, end: today.succ() }
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
315pub struct DateTime(chrono::NaiveDateTime);
316
317impl DateTime {
318 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 pub fn now() -> Self { Self(Local::now().naive_local()) }
337
338 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 pub fn timestamp(&self) -> i64 { self.0.timestamp() }
347
348 pub fn date(&self) -> Date { Date(self.0.date()) }
350
351 pub fn hour(&self) -> u32 { self.0.hour() }
353
354 pub fn minute(&self) -> u32 { self.0.minute() }
356
357 pub fn second_offset(&self) -> u32 { self.0.minute() * 60 + self.0.second() }
359
360 pub fn hhmm(&self) -> String { format!("{:02}:{:02}", self.0.hour(), self.0.minute()) }
362}
363
364impl DateTime {
365 pub fn seconds(seconds: u64) -> Duration { Duration::from_secs(seconds) }
367
368 pub fn minutes(minutes: u64) -> Duration { Duration::from_secs(minutes * 60) }
370
371 pub fn hours(hours: u64) -> Duration { Duration::from_secs(hours * 3600) }
373
374 pub fn days(days: u64) -> Duration { Duration::from_secs(days * 86400) }
376
377 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 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 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 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 #[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 #[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 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 #[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 #[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}