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::Dateable, nowlike::Nowlike}; use crate::datetime_utc::DatetimeUtc; #[derive( Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Clone, Copy, Debug, 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; } 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() }; } 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; } } 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::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 test_first_day_in_month() { todo!(); } #[test] fn test_first_day_in_next_month() { todo!(); } #[test] fn test_days_in_month() { todo!(); } }