use std::{borrow::Borrow, fmt, str::FromStr}; use anyhow::{Context, Error, Result}; use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, Utc}; use diesel::{ backend::Backend, deserialize::{self, FromSql, FromSqlRow}, expression::AsExpression, serialize::{self, ToSql}, sql_types::Text, sqlite::Sqlite, }; use serde::{Deserialize, Serialize}; use schist_traits::{Dateable, Nowlike}; use crate::datetime_utc::DatetimeUtc; #[derive( Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Clone, Copy, Debug, Hash, AsExpression, FromSqlRow, )] #[diesel(sql_type=Text)] pub struct DateUtc { chrono_datetime_utc: DateTime, } impl DateUtc { pub fn from_ymd(year: i32, month: u32, day: u32) -> Result { Ok(Self { chrono_datetime_utc: NaiveDate::from_ymd_opt(year, month, day) .with_context(|| { format!( "failed to construct date from ymd {:04}-{:02}-{:02}", year, month, day ) })? .and_hms_opt(0, 0, 0) .unwrap() .and_utc(), }) } pub fn with_hms(&mut self, hour: u32, min: u32, sec: u32) -> Result { DatetimeUtc::from_ymd_and_hms(self.year(), self.month(), self.day(), hour, min, sec) } pub fn day_diff(&self, other: &DateUtc) -> u32 { self.chrono_datetime_utc .num_days_from_ce() .abs_diff(other.chrono_datetime_utc.num_days_from_ce()) } pub fn days_in_month(&self) -> u32 { self.first_day_in_next_month() .day_diff(&self.first_day_in_month()) } pub fn add_days(&self, days: u64) -> Result { let chrono_datetime_utc = self .chrono_datetime_utc .checked_add_days(chrono::Days::new(days)) .with_context(|| format!("failed to add {} days to {}", days, self)) .unwrap(); Ok(Self { chrono_datetime_utc, }) } pub fn sub_days(&self, days: u64) -> Result { let chrono_datetime_utc = self .chrono_datetime_utc .checked_sub_days(chrono::Days::new(days)) .with_context(|| format!("failed to subtract {} days from {}", days, self)) .unwrap(); Ok(Self { chrono_datetime_utc, }) } fn is_day_str(str: &str) -> bool { if str.len() != 10 { return false; } for (ix, ch) in str.chars().enumerate() { if ix == 4 || ix == 7 { if ch != '-' { return false; } } else { if ch < '0' || ch > '9' { return false; } } } return true; } pub fn first_day_in_next_month(&self) -> Self { let month = self.month(); let mut curr_guess: Option> = None; let mut curr_add = 1; while curr_guess.is_none() || curr_guess.unwrap().month() == month { curr_guess = self .chrono_datetime_utc .checked_add_days(chrono::Days::new(curr_add)); curr_add = curr_add + 1; } return Self { chrono_datetime_utc: curr_guess.unwrap(), }; } pub fn first_day_in_month(&self) -> Self { let month = self.month(); let mut prev_guess = self.clone(); let mut curr_guess = self.clone(); let mut curr_subtractor = 1; while curr_guess.month() == month { prev_guess = curr_guess.clone(); let mut next: Option> = None; while next.is_none() { if curr_subtractor > 31 { return prev_guess; } next = self .chrono_datetime_utc .checked_sub_days(chrono::Days::new(curr_subtractor)); curr_subtractor = curr_subtractor + 1; } let next = next.unwrap(); curr_guess = Self { chrono_datetime_utc: next, }; } return prev_guess; } pub fn format(&self, fmt: &str) -> String { format!("{}", self.chrono_datetime_utc.format(fmt)) } } impl Nowlike for DateUtc { fn now() -> Self { let now = Utc::now(); Self::from_ymd(now.year(), now.month(), now.day()).unwrap() } } impl Dateable for DateUtc { fn year(&self) -> i32 { self.chrono_datetime_utc.year() } fn month(&self) -> u32 { self.chrono_datetime_utc.month() } fn day(&self) -> u32 { self.chrono_datetime_utc.day() } } impl From for DateUtc { fn from(datetime_utc: DatetimeUtc) -> Self { Self::from(&datetime_utc) } } impl From<&DatetimeUtc> for DateUtc { fn from(datetime_utc: &DatetimeUtc) -> Self { Self { chrono_datetime_utc: datetime_utc .to_string() .parse::>() .unwrap() .with_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()) .unwrap(), } } } impl From for DateUtc where T: Borrow>, { fn from(chrono_datetime_utc: T) -> Self { Self { chrono_datetime_utc: chrono_datetime_utc .borrow() .with_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()) .unwrap(), } } } impl fmt::Display for DateUtc { /// Converts to a day string, YYYY-MM-DD, in UTC /// Example: 10.30am 24 Oct 2024 UTC -> "2024-10-24" fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { let date_str = self.chrono_datetime_utc.to_rfc3339(); let day_str = date_str.split_at_checked(10).unwrap().0; write!(f, "{}", day_str) } } impl FromStr for DateUtc { type Err = anyhow::Error; /// Constructs DatetimeUtc from a day string, YYYY-MM-DD, in UTC /// Example: "2024-10-24" -> Midnight 24 Oct 2024 UTC fn from_str(date_str: &str) -> Result { if !Self::is_day_str(&date_str) { Err(Error::msg(format!( "{} is not a day string in YYYY-MM-DD format", date_str ))) } else { let mut datetime_str = String::from(date_str); datetime_str.push_str("T00:00:00.000Z"); Ok(Self { chrono_datetime_utc: datetime_str.parse()?, }) } } } impl ToSql for DateUtc { fn to_sql<'b>( &'b self, out: &mut diesel::serialize::Output<'b, '_, Sqlite>, ) -> serialize::Result { out.set_value(self.to_string()); Ok(serialize::IsNull::No) } } impl FromSql for DateUtc { fn from_sql(bytes: ::RawValue<'_>) -> deserialize::Result { let str = >::from_sql(bytes)?; Ok(str.parse()?) } } #[cfg(test)] mod test { use schist_traits::Timeable; use super::*; #[test] fn when_from_ymd_then_ymd_are_as_expected() { let date_utc = DateUtc::from_ymd(1970, 11, 23); assert!(date_utc.is_ok()); let date_utc = date_utc.unwrap(); assert_eq!(date_utc.year(), 1970); assert_eq!(date_utc.month(), 11); assert_eq!(date_utc.day(), 23); } #[test] fn when_from_ymd_then_to_string_as_expected() { let date_utc = DateUtc::from_ymd(1970, 11, 23); assert!(date_utc.is_ok()); assert_eq!(date_utc.unwrap().to_string(), "1970-11-23"); } #[test] fn when_from_invalid_ymd_then_err() { let date_utc_1 = DateUtc::from_ymd(1970, 0, 23); let date_utc_2 = DateUtc::from_ymd(1970, 13, 23); let date_utc_3 = DateUtc::from_ymd(1970, 11, 0); let date_utc_4 = DateUtc::from_ymd(1970, 11, 31); let date_utc_5 = DateUtc::from_ymd(1971, 2, 29); assert!(date_utc_1.is_err()); assert_eq!( date_utc_1.unwrap_err().to_string(), "failed to construct date from ymd 1970-00-23" ); assert!(date_utc_2.is_err()); assert_eq!( date_utc_2.unwrap_err().to_string(), "failed to construct date from ymd 1970-13-23" ); assert!(date_utc_3.is_err()); assert_eq!( date_utc_3.unwrap_err().to_string(), "failed to construct date from ymd 1970-11-00" ); assert!(date_utc_4.is_err()); assert_eq!( date_utc_4.unwrap_err().to_string(), "failed to construct date from ymd 1970-11-31" ); assert!(date_utc_5.is_err()); assert_eq!( date_utc_5.unwrap_err().to_string(), "failed to construct date from ymd 1971-02-29" ); } #[test] fn when_from_string_then_ymd_are_as_expected() { let date_utc: Result = "1970-11-23".parse(); assert!(date_utc.is_ok()); let date_utc = date_utc.unwrap(); assert_eq!(date_utc.year(), 1970); assert_eq!(date_utc.month(), 11); assert_eq!(date_utc.day(), 23); } #[test] fn when_from_string_then_to_string_as_expected() { let date_utc: Result = "1970-11-23".parse(); assert!(date_utc.is_ok()); assert_eq!(date_utc.unwrap().to_string(), "1970-11-23"); } #[test] fn when_from_invalid_string_then_err() { let date_utc_1: Result = "1970-00-23".parse(); let date_utc_2: Result = "1970-13-23".parse(); let date_utc_3: Result = "1970-11-0".parse(); let date_utc_4: Result = "1970-11-31".parse(); let date_utc_5: Result = "1971-02-29".parse(); assert!(date_utc_1.is_err()); assert!(date_utc_2.is_err()); assert!(date_utc_3.is_err()); assert!(date_utc_4.is_err()); assert!(date_utc_5.is_err()); } #[test] fn with_with_hms_then_ymd_hms_are_as_expected() { let datetime_utc = DateUtc::from_ymd(1970, 11, 23) .unwrap() .with_hms(12, 55, 16); assert!(datetime_utc.is_ok()); let datetime_utc = datetime_utc.unwrap(); assert_eq!(datetime_utc.year(), 1970); assert_eq!(datetime_utc.month(), 11); assert_eq!(datetime_utc.day(), 23); assert_eq!(datetime_utc.hour(), 12); assert_eq!(datetime_utc.minute(), 55); assert_eq!(datetime_utc.second(), 16); } #[test] fn when_with_invalid_hms_then_err() { let datetime_utc_1 = DateUtc::from_ymd(1970, 11, 23).unwrap().with_hms(25, 0, 0); let datetime_utc_2 = DateUtc::from_ymd(1970, 11, 23).unwrap().with_hms(0, 61, 0); let datetime_utc_3 = DateUtc::from_ymd(1970, 11, 23).unwrap().with_hms(0, 0, 61); assert!(datetime_utc_1.is_err()); assert!(datetime_utc_2.is_err()); assert!(datetime_utc_3.is_err()); } #[test] fn when_from_datetime_utc_at_different_times_on_same_day_then_eq() { let datetime_utc1 = DateUtc::from_ymd(1970, 11, 23) .unwrap() .with_hms(0, 0, 0) .unwrap(); let datetime_utc2 = DateUtc::from_ymd(1970, 11, 23) .unwrap() .with_hms(23, 59, 59) .unwrap(); let date_utc1 = DateUtc::from(datetime_utc1); let date_utc2 = DateUtc::from(datetime_utc2); assert_eq!(date_utc1, date_utc2); } #[test] fn given_today_when_day_diff_today_then_return_0() { let start = DateUtc::from_ymd(2000, 1, 1).unwrap(); let end = DateUtc::from_ymd(2000, 1, 1).unwrap(); let day_diff = start.day_diff(&end); let neg_day_diff = end.day_diff(&start); assert_eq!(day_diff, 0); assert_eq!(day_diff, neg_day_diff); } #[test] fn given_today_when_day_diff_tomorrow_then_return_1() { let start = DateUtc::from_ymd(2000, 1, 1).unwrap(); let end = DateUtc::from_ymd(2000, 1, 2).unwrap(); let day_diff = start.day_diff(&end); let neg_day_diff = end.day_diff(&start); assert_eq!(day_diff, 1); assert_eq!(day_diff, neg_day_diff); } #[test] fn when_any_date_in_2024_then_first_day_in_month_is_correct() { let jan_1_2024: DateUtc = "2024-01-01".parse().unwrap(); for date in get_dates_in_jan_2024() { assert_eq!(jan_1_2024, date.first_day_in_month()); } let feb_1_2024: DateUtc = "2024-02-01".parse().unwrap(); for date in get_dates_in_feb_2024() { assert_eq!(feb_1_2024, date.first_day_in_month()); } let mar_1_2024: DateUtc = "2024-03-01".parse().unwrap(); for date in get_dates_in_mar_2024() { assert_eq!(mar_1_2024, date.first_day_in_month()); } let apr_1_2024: DateUtc = "2024-04-01".parse().unwrap(); for date in get_dates_in_apr_2024() { assert_eq!(apr_1_2024, date.first_day_in_month()); } let may_1_2024: DateUtc = "2024-05-01".parse().unwrap(); for date in get_dates_in_may_2024() { assert_eq!(may_1_2024, date.first_day_in_month()); } let jun_1_2024: DateUtc = "2024-06-01".parse().unwrap(); for date in get_dates_in_jun_2024() { assert_eq!(jun_1_2024, date.first_day_in_month()); } let jul_1_2024: DateUtc = "2024-07-01".parse().unwrap(); for date in get_dates_in_jul_2024() { assert_eq!(jul_1_2024, date.first_day_in_month()); } let aug_1_2024: DateUtc = "2024-08-01".parse().unwrap(); for date in get_dates_in_aug_2024() { assert_eq!(aug_1_2024, date.first_day_in_month()); } let sep_1_2024: DateUtc = "2024-09-01".parse().unwrap(); for date in get_dates_in_sep_2024() { assert_eq!(sep_1_2024, date.first_day_in_month()); } let oct_1_2024: DateUtc = "2024-10-01".parse().unwrap(); for date in get_dates_in_oct_2024() { assert_eq!(oct_1_2024, date.first_day_in_month()); } let nov_1_2024: DateUtc = "2024-11-01".parse().unwrap(); for date in get_dates_in_nov_2024() { assert_eq!(nov_1_2024, date.first_day_in_month()); } let dec_1_2024: DateUtc = "2024-12-01".parse().unwrap(); for date in get_dates_in_dec_2024() { assert_eq!(dec_1_2024, date.first_day_in_month()); } } #[test] fn when_any_date_in_2024_then_first_day_in_next_month_is_correct() { let feb_1_2024: DateUtc = "2024-02-01".parse().unwrap(); for date in get_dates_in_jan_2024() { assert_eq!(feb_1_2024, date.first_day_in_next_month()); } let mar_1_2024: DateUtc = "2024-03-01".parse().unwrap(); for date in get_dates_in_feb_2024() { assert_eq!(mar_1_2024, date.first_day_in_next_month()); } let apr_1_2024: DateUtc = "2024-04-01".parse().unwrap(); for date in get_dates_in_mar_2024() { assert_eq!(apr_1_2024, date.first_day_in_next_month()); } let may_1_2024: DateUtc = "2024-05-01".parse().unwrap(); for date in get_dates_in_apr_2024() { assert_eq!(may_1_2024, date.first_day_in_next_month()); } let jun_1_2024: DateUtc = "2024-06-01".parse().unwrap(); for date in get_dates_in_may_2024() { assert_eq!(jun_1_2024, date.first_day_in_next_month()); } let jul_1_2024: DateUtc = "2024-07-01".parse().unwrap(); for date in get_dates_in_jun_2024() { assert_eq!(jul_1_2024, date.first_day_in_next_month()); } let aug_1_2024: DateUtc = "2024-08-01".parse().unwrap(); for date in get_dates_in_jul_2024() { assert_eq!(aug_1_2024, date.first_day_in_next_month()); } let sep_1_2024: DateUtc = "2024-09-01".parse().unwrap(); for date in get_dates_in_aug_2024() { assert_eq!(sep_1_2024, date.first_day_in_next_month()); } let oct_1_2024: DateUtc = "2024-10-01".parse().unwrap(); for date in get_dates_in_sep_2024() { assert_eq!(oct_1_2024, date.first_day_in_next_month()); } let nov_1_2024: DateUtc = "2024-11-01".parse().unwrap(); for date in get_dates_in_oct_2024() { assert_eq!(nov_1_2024, date.first_day_in_next_month()); } let dec_1_2024: DateUtc = "2024-12-01".parse().unwrap(); for date in get_dates_in_nov_2024() { assert_eq!(dec_1_2024, date.first_day_in_next_month()); } let jan_1_2025: DateUtc = "2025-01-01".parse().unwrap(); for date in get_dates_in_dec_2024() { assert_eq!(jan_1_2025, date.first_day_in_next_month()); } } fn get_dates_in_jan_2024() -> Vec { (1..31) .map(|day| format!("2024-01-{:02}", day).parse::().unwrap()) .collect::>() } fn get_dates_in_feb_2024() -> Vec { (1..29) .map(|day| format!("2024-02-{:02}", day).parse::().unwrap()) .collect::>() } fn get_dates_in_mar_2024() -> Vec { (1..31) .map(|day| format!("2024-03-{:02}", day).parse::().unwrap()) .collect::>() } fn get_dates_in_apr_2024() -> Vec { (1..30) .map(|day| format!("2024-04-{:02}", day).parse::().unwrap()) .collect::>() } fn get_dates_in_may_2024() -> Vec { (1..31) .map(|day| format!("2024-05-{:02}", day).parse::().unwrap()) .collect::>() } fn get_dates_in_jun_2024() -> Vec { (1..30) .map(|day| format!("2024-06-{:02}", day).parse::().unwrap()) .collect::>() } fn get_dates_in_jul_2024() -> Vec { (1..31) .map(|day| format!("2024-07-{:02}", day).parse::().unwrap()) .collect::>() } fn get_dates_in_aug_2024() -> Vec { (1..31) .map(|day| format!("2024-08-{:02}", day).parse::().unwrap()) .collect::>() } fn get_dates_in_sep_2024() -> Vec { (1..30) .map(|day| format!("2024-09-{:02}", day).parse::().unwrap()) .collect::>() } fn get_dates_in_oct_2024() -> Vec { (1..31) .map(|day| format!("2024-10-{:02}", day).parse::().unwrap()) .collect::>() } fn get_dates_in_nov_2024() -> Vec { (1..30) .map(|day| format!("2024-11-{:02}", day).parse::().unwrap()) .collect::>() } fn get_dates_in_dec_2024() -> Vec { (1..31) .map(|day| format!("2024-12-{:02}", day).parse::().unwrap()) .collect::>() } }