use std::fmt::{self, Display};
use std::time::Duration;
use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
pub mod error;
pub mod parse;
pub use parse::DateParser;
pub use parse::RangeParser;
pub type Weekday = chrono::Weekday;
pub type Time = chrono::NaiveTime;
pub use error::DateError;
pub type Result<T> = std::result::Result<T, DateError>;
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub struct Date(chrono::NaiveDate);
impl Date {
pub fn new(year: i32, month: u32, day: u32) -> Result<Self> {
Ok(Self(
NaiveDate::from_ymd_opt(year, month, day).ok_or(DateError::InvalidDate)?
))
}
#[must_use]
pub fn today() -> Self { Self(Local::now().date_naive()) }
pub fn parse(datespec: &str) -> Result<Self> { DateParser::default().parse(datespec) }
}
impl Date {
pub fn year(&self) -> i32 { self.0.year() }
pub fn month(&self) -> u32 { self.0.month() }
pub fn day(&self) -> u32 { self.0.day() }
pub fn weekday(&self) -> Weekday { self.0.weekday() }
pub fn weekday_str(&self) -> &'static str {
match self.0.weekday() {
Weekday::Sun => "Sunday",
Weekday::Mon => "Monday",
Weekday::Tue => "Tuesday",
Weekday::Wed => "Wednesday",
Weekday::Thu => "Thursday",
Weekday::Fri => "Friday",
Weekday::Sat => "Saturday"
}
}
}
impl Date {
#[must_use]
pub fn day_start(&self) -> DateTime {
DateTime(self.0.and_hms_opt(0, 0, 0).expect("Midnight exists"))
}
#[must_use]
pub fn day_end(&self) -> DateTime {
DateTime(self.0.and_hms_opt(23, 59, 59).expect("Midnight exists"))
}
#[must_use]
fn find_previous(&self, weekday: Weekday) -> Self {
let mut day = self.0.pred_opt().expect("Not at beginning of time");
while day.weekday() != weekday {
day = day.pred_opt().expect("Not at beginning of time");
}
Self(day)
}
#[must_use]
fn find_next(&self, weekday: Weekday) -> Self {
let mut day = self.0.succ_opt().expect("Not at end of time");
while day.weekday() != weekday {
day = day.succ_opt().expect("Not at end of time");
}
Self(day)
}
#[must_use]
pub fn month_start(&self) -> Date { Date(self.0.with_day(1).expect("Reasonable date range")) }
fn is_leap_year(year: i32) -> bool {
(year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0))
}
#[must_use]
#[rustfmt::skip]
pub fn month_end(&self) -> Date {
let last_day = match self.0.month() {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => if Self::is_leap_year(self.0.year()) { 29 } else { 28 },
_ => unreachable!()
};
Date(self.0.with_day(last_day).expect("End of month should work"))
}
#[must_use]
pub fn week_start(&self) -> Date {
match self.0.weekday() {
Weekday::Sun => *self,
_ => self.find_previous(Weekday::Sun)
}
}
#[must_use]
pub fn week_end(&self) -> Date {
match self.0.weekday() {
Weekday::Sat => *self,
_ => self.find_next(Weekday::Sat)
}
}
#[must_use]
pub fn year_start(&self) -> Date {
Self(self.0
.with_month(1).expect("Within reasonable dates")
.with_day(1).expect("Within reasonable dates"))
}
#[must_use]
pub fn year_end(&self) -> Date {
Self(self.0
.with_month(12).expect("Within reasonable dates")
.with_day(31).expect("Within reasonable dates"))
}
#[must_use]
pub fn succ(&self) -> Date { Self(self.0.succ_opt().expect("Not at end of time")) }
#[must_use]
pub fn pred(&self) -> Date { Self(self.0.pred_opt().expect("Not at beginnning of time")) }
}
impl Default for Date {
fn default() -> Date { Self::today() }
}
impl std::str::FromStr for Date {
type Err = DateError;
#[rustfmt::skip]
fn from_str(date: &str) -> std::result::Result<Self, Self::Err> {
let Ok(parsed) = NaiveDate::parse_from_str(date, "%Y-%m-%d") else {
return Err(DateError::InvalidDate);
};
Ok(Self(parsed))
}
}
impl Display for Date {
#[rustfmt::skip]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{:02}-{:02}", self.0.year(), self.0.month(), self.0.day())
}
}
impl From<Date> for String {
fn from(date: Date) -> Self { date.to_string() }
}
#[derive(Debug, PartialEq, Eq)]
pub struct DateRange {
start: Date,
end: Date
}
impl DateRange {
pub fn new(start: Date, end: Date) -> Self {
Self::new_opt(start, end).unwrap_or(Self { start, end: start })
}
pub fn new_opt(start: Date, end: Date) -> Option<Self> {
(start < end).then_some(Self { start, end })
}
pub fn parse<'a, I>(datespec: &mut I) -> Result<Self>
where
I: Iterator<Item = &'a str>
{
RangeParser::default().parse(datespec).map(|(dr, _)| dr)
}
}
impl DateRange {
pub fn start(&self) -> Date { self.start }
pub fn end(&self) -> Date { self.end }
pub fn is_empty(&self) -> bool { self.start >= self.end }
}
impl From<Date> for DateRange {
fn from(date: Date) -> DateRange { Self { start: date, end: date.succ() } }
}
impl Default for DateRange {
fn default() -> Self {
let today = Date::today();
Self { start: today, end: today.succ() }
}
}
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub struct DateTime(chrono::NaiveDateTime);
impl DateTime {
pub fn new(date: (i32, u32, u32), time: (u32, u32, u32)) -> Result<Self> {
let Some(d) = NaiveDate::from_ymd_opt(date.0, date.1, date.2) else {
return Err(DateError::InvalidDate);
};
let Some(t) = NaiveTime::from_hms_opt(time.0, time.1, time.2) else {
return Err(DateError::InvalidDate);
};
Ok(Self(NaiveDateTime::new(d, t)))
}
pub fn now() -> Self { Self(Local::now().naive_local()) }
pub(crate) fn new_from_date_time(date: Date, time: NaiveTime) -> Self {
Self(NaiveDateTime::new(date.0, time))
}
}
impl DateTime {
pub fn timestamp(&self) -> i64 { self.0.and_utc().timestamp() }
pub fn date(&self) -> Date { Date(self.0.date()) }
pub fn hour(&self) -> u32 { self.0.hour() }
pub fn minute(&self) -> u32 { self.0.minute() }
pub fn second_offset(&self) -> u32 { self.0.minute() * 60 + self.0.second() }
pub fn hhmm(&self) -> String { format!("{:02}:{:02}", self.0.hour(), self.0.minute()) }
}
impl DateTime {
pub fn seconds(seconds: u64) -> Duration { Duration::from_secs(seconds) }
pub fn minutes(minutes: u64) -> Duration { Duration::from_secs(minutes * 60) }
pub fn hours(hours: u64) -> Duration { Duration::from_secs(hours * 3600) }
pub fn days(days: u64) -> Duration { Duration::from_secs(days * 86400) }
pub fn weeks(weeks: u64) -> Duration { Self::days(weeks * 7) }
}
impl std::ops::Add<Duration> for DateTime {
type Output = Result<DateTime>;
fn add(self, rhs: Duration) -> Result<Self> {
Ok(Self(
self.0 + chrono::Duration::from_std(rhs).map_err(|_| DateError::InvalidDate)?
))
}
}
impl std::ops::Sub<Self> for DateTime {
type Output = Result<Duration>;
fn sub(self, rhs: Self) -> Result<Duration> {
(self.0 - rhs.0).to_std().map_err(|_| DateError::EntryOrder)
}
}
impl std::ops::Sub<Duration> for DateTime {
type Output = Result<DateTime>;
fn sub(self, rhs: Duration) -> Result<Self> {
Ok(Self(
self.0 - chrono::Duration::from_std(rhs).map_err(|_| DateError::InvalidDate)?
))
}
}
impl std::str::FromStr for DateTime {
type Err = DateError;
#[rustfmt::skip]
fn from_str(datetime: &str) -> Result<Self> {
let Ok(parsed) = NaiveDateTime::parse_from_str(datetime, "%Y-%m-%d %H:%M:%S") else {
return Err(DateError::InvalidDate);
};
Ok(Self(parsed))
}
}
impl Display for DateTime {
#[rustfmt::skip]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{:02}-{:02} {:02}:{:02}:{:02}",
self.0.year(), self.0.month(), self.0.day(),
self.0.hour(), self.0.minute(), self.0.second())
}
}
impl From<DateTime> for String {
fn from(datetime: DateTime) -> Self { datetime.to_string() }
}
#[cfg(test)]
mod tests {
use assert2::{assert, let_assert};
use rstest::rstest;
use super::*;
#[test]
fn test_date_new() {
let_assert!(Ok(date) = Date::new(2021, 11, 20));
assert!(date.to_string() == String::from("2021-11-20"));
}
#[rstest]
#[case(2021, 0, 20, "bad month zero")]
#[case(2021, 13, 20, "bad month too high")]
#[case(2021, 11, 0, "bad day zero")]
#[case(2021, 11, 32, "bad day too high")]
fn test_date_new_unsuccess(
#[case]year: i32,
#[case]month: u32,
#[case]day: u32,
#[case]msg: &str
) {
assert!(Err(DateError::InvalidDate) == Date::new(year, month, day), "{msg}");
}
#[test]
fn test_date_day_end() {
let_assert!(Ok(date) = Date::new(2021, 11, 20));
let_assert!(Ok(expected) = DateTime::new((2021, 11, 20), (23, 59, 59)));
assert!(date.day_end() == expected);
}
#[test]
fn test_date_day_start() {
let_assert!(Ok(date) = Date::new(2021, 11, 20));
let_assert!(Ok(expected) = DateTime::new((2021, 11, 20), (0, 0, 0)));
assert!(date.day_start() == expected);
}
#[test]
fn test_month_start() {
let_assert!(Ok(date) = Date::new(2022, 11, 20));
let_assert!(Ok(expected) = Date::new(2022, 11, 1));
assert!(date.month_start() == expected);
}
#[rstest]
#[case("jan", 1, 31)]
#[case("feb", 2, 28)]
#[case("mar", 3, 31)]
#[case("apr", 4, 30)]
#[case("may", 5, 31)]
#[case("jun", 6, 30)]
#[case("jul", 7, 31)]
#[case("aug", 8, 31)]
#[case("sep", 9, 30)]
#[case("oct", 10, 31)]
#[case("nov", 11, 30)]
#[case("dec", 12, 31)]
fn test_month_end(#[case]name: &str, #[case]mon: u32, #[case]day: u32) {
let_assert!(Ok(date) = Date::new(2022, mon, 20));
let_assert!(Ok(expected) = Date::new(2022, mon, day));
assert!(date.month_end() == expected, "{name}");
}
#[test]
fn test_month_end_leap_year() {
let_assert!(Ok(date) = Date::new(2020, 2, 20));
let_assert!(Ok(expected) = Date::new(2020, 2, 29));
assert!(date.month_end() == expected);
}
#[test]
fn test_date_week_start() {
let_assert!(Ok(date) = Date::new(2022, 12, 20));
let_assert!(Ok(expected) = Date::new(2022, 12, 18));
assert!(date.week_start() == expected);
}
#[test]
fn test_date_week_start_no_change() {
let_assert!(Ok(date) = Date::new(2022, 12, 18));
let_assert!(Ok(expected) = Date::new(2022, 12, 18));
assert!(date.week_start() == expected);
}
#[test]
fn test_date_week_end() {
let_assert!(Ok(date) = Date::new(2022, 12, 15));
let_assert!(Ok(expected) = Date::new(2022, 12, 17));
assert!(date.week_end() == expected);
}
#[test]
fn test_date_week_end_no_change() {
let_assert!(Ok(date) = Date::new(2022, 12, 17));
let_assert!(Ok(expected) = Date::new(2022, 12, 17));
assert!(date.week_end() == expected);
}
#[test]
fn test_date_year_start() {
let_assert!(Ok(date) = Date::new(2022, 12, 20));
let_assert!(Ok(expected) = Date::new(2022, 1, 1));
assert!(date.year_start() == expected);
}
#[test]
fn test_date_year_end() {
let_assert!(Ok(date) = Date::new(2022, 12, 20));
let_assert!(Ok(expected) = Date::new(2022, 12, 31));
assert!(date.year_end() == expected);
}
#[test]
fn test_date_succ() {
let_assert!(Ok(date) = Date::new(2021, 11, 20));
let_assert!(Ok(expected) = Date::new(2021, 11, 21));
assert!(date.succ() == expected);
}
#[test]
fn test_date_pred() {
let_assert!(Ok(date) = Date::new(2021, 11, 20));
let_assert!(Ok(expected) = Date::new(2021, 11, 19));
assert!(date.pred() == expected);
}
#[test]
fn test_date_parse() {
let_assert!(Ok(date) = "2021-11-20".parse::<Date>());
let_assert!(Ok(expected) = Date::new(2021, 11, 20));
assert!(date == expected);
}
#[test]
fn test_date_parse_bad() {
let date = "fred".parse::<Date>();
assert!(&date.is_err());
}
#[test]
fn test_date_range_default() {
let range = DateRange::default();
assert!(range.start() == Date::today());
assert!(range.end() == Date::today().succ());
}
#[test]
fn test_date_range_new_opt() {
let_assert!(Some(range) = DateRange::new_opt(Date::today(), Date::today().succ()));
assert!(range.start() == Date::today());
assert!(range.end() == Date::today().succ());
}
#[test]
fn test_date_range_new_opt_backwards() {
let range = DateRange::new_opt(Date::today(), Date::today().pred());
assert!(range.is_none());
}
#[test]
fn test_date_range_new_opt_empty() {
let range = DateRange::new_opt(Date::today(), Date::today());
assert!(range.is_none());
}
#[test]
fn test_date_range_new() {
let range = DateRange::new(Date::today(), Date::today().succ());
assert!(range.start() == Date::today());
assert!(range.end() == Date::today().succ());
assert!(range.is_empty() == false);
}
#[test]
fn test_date_range_new_backwards() {
let range = DateRange::new(Date::today(), Date::today().pred());
assert!(range.start() == Date::today());
assert!(range.end() == Date::today());
assert!(range.is_empty() == true);
}
#[test]
fn test_date_range_new_empty() {
let range = DateRange::new(Date::today(), Date::today());
assert!(range.start() == Date::today());
assert!(range.end() == Date::today());
assert!(range.is_empty() == true);
}
#[test]
fn test_date_range_from_date() {
let_assert!(Ok(date) = Date::new(2022, 12, 1));
let range: DateRange = date.into();
let expect = DateRange::new(date, date.succ());
assert!(range == expect);
}
#[test]
fn test_datetime_new() {
let_assert!(Ok(date) = DateTime::new((2021, 11, 20), (11, 32, 18)));
assert!(date.to_string() == String::from("2021-11-20 11:32:18"));
}
#[test]
fn test_datetime_new_bad_date() {
let date = DateTime::new((2021, 13, 20), (11, 32, 18));
assert!(date.is_err());
}
#[test]
fn test_datetime_new_bad_time() {
let date = DateTime::new((2021, 11, 20), (11, 82, 18));
assert!(date.is_err());
}
#[test]
fn test_datetime_parse() {
let_assert!(Ok(date) = "2021-11-20 11:32:18".parse::<DateTime>());
let_assert!(Ok(expected) = DateTime::new((2021, 11, 20), (11, 32, 18)));
assert!(date == expected);
}
#[test]
fn test_datetime_diff() {
let_assert!(Ok(date) = DateTime::new((2021, 11, 20), (11, 32, 18)));
let_assert!(Ok(old) = DateTime::new((2021, 11, 18), (12, 00, 00)));
let_assert!(Ok(dur) = date - old);
assert!(dur == Duration::from_secs(2 * 86400 - 28 * 60 + 18));
}
#[test]
fn test_datetime_diff_bad() {
let_assert!(Ok(date) = DateTime::new((2021, 11, 18), (12, 00, 00)));
let_assert!(Ok(old) = DateTime::new((2021, 11, 20), (11, 32, 18)));
let_assert!(Err(_) = date - old);
}
#[test]
fn test_datetime_add_time() {
let_assert!(Ok(date) = DateTime::new((2021, 11, 18), (12, 00, 00)));
let_assert!(Ok(new) = date + DateTime::minutes(10));
let_assert!(Ok(expected) = DateTime::new((2021, 11, 18), (12, 10, 00)));
assert!(new == expected);
}
#[test]
fn test_datetime_add_days() {
let_assert!(Ok(date) = DateTime::new((2021, 11, 18), (12, 00, 00)));
let_assert!(Ok(new) = date + DateTime::days(3));
let_assert!(Ok(expected) = DateTime::new((2021, 11, 21), (12, 00, 00)));
assert!(new == expected);
}
#[test]
fn test_datetime_hhmm() {
let_assert!(Ok(date) = DateTime::new((2021, 11, 18), (8, 5, 13)));
assert!(date.hhmm() == String::from("08:05"));
}
#[test]
fn test_datetime_parse_bad() {
let date = "fred".parse::<DateTime>();
assert!(&date.is_err());
}
}