diff options
| author | Joe Carstairs <me@joeac.net> | 2024-12-12 08:28:04 +0000 |
|---|---|---|
| committer | Joe Carstairs <me@joeac.net> | 2024-12-12 08:28:04 +0000 |
| commit | 54894f9de9bad4fecb2a116d59a03cdd003f84c0 (patch) | |
| tree | 6ce5517cecf23d4e93d7a381a374e1fa6c810cee /schist_models/src/date_utc.rs | |
| parent | 8a30bcbdf9264d235bf93da3df8e170cfde53d02 (diff) | |
Moves rust workspace to root
Diffstat (limited to 'schist_models/src/date_utc.rs')
| -rw-r--r-- | schist_models/src/date_utc.rs | 547 |
1 files changed, 547 insertions, 0 deletions
diff --git a/schist_models/src/date_utc.rs b/schist_models/src/date_utc.rs new file mode 100644 index 0000000..2b36ec0 --- /dev/null +++ b/schist_models/src/date_utc.rs @@ -0,0 +1,547 @@ +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<Utc>, +} + +impl DateUtc { + pub fn from_ymd(year: i32, month: u32, day: u32) -> Result<Self> { + 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> { + 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<Self> { + 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<Self> { + 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<DateTime<Utc>> = 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<DateTime<Utc>> = 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<DatetimeUtc> 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::<DateTime<Utc>>().unwrap() + .with_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()).unwrap(), + } + } +} + +impl<T> From<T> for DateUtc where T: Borrow<DateTime<Utc>> { + 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<Self> { + 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<Text, Sqlite> 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<Text, Sqlite> for DateUtc { + fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<DateUtc> { + let str = <String as FromSql<Text, Sqlite>>::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<DateUtc> = "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<DateUtc> = "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<DateUtc> = "1970-00-23".parse(); + let date_utc_2: Result<DateUtc> = "1970-13-23".parse(); + let date_utc_3: Result<DateUtc> = "1970-11-0".parse(); + let date_utc_4: Result<DateUtc> = "1970-11-31".parse(); + let date_utc_5: Result<DateUtc> = "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<DateUtc> { + (1..31).map(|day| format!("2024-01-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } + + fn get_dates_in_feb_2024() -> Vec<DateUtc> { + (1..29).map(|day| format!("2024-02-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } + + fn get_dates_in_mar_2024() -> Vec<DateUtc> { + (1..31).map(|day| format!("2024-03-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } + + fn get_dates_in_apr_2024() -> Vec<DateUtc> { + (1..30).map(|day| format!("2024-04-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } + + fn get_dates_in_may_2024() -> Vec<DateUtc> { + (1..31).map(|day| format!("2024-05-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } + + fn get_dates_in_jun_2024() -> Vec<DateUtc> { + (1..30).map(|day| format!("2024-06-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } + + fn get_dates_in_jul_2024() -> Vec<DateUtc> { + (1..31).map(|day| format!("2024-07-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } + + fn get_dates_in_aug_2024() -> Vec<DateUtc> { + (1..31).map(|day| format!("2024-08-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } + + fn get_dates_in_sep_2024() -> Vec<DateUtc> { + (1..30).map(|day| format!("2024-09-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } + + fn get_dates_in_oct_2024() -> Vec<DateUtc> { + (1..31).map(|day| format!("2024-10-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } + + fn get_dates_in_nov_2024() -> Vec<DateUtc> { + (1..30).map(|day| format!("2024-11-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } + + fn get_dates_in_dec_2024() -> Vec<DateUtc> { + (1..31).map(|day| format!("2024-12-{:02}", day).parse::<DateUtc>().unwrap()).collect::<Vec<DateUtc>>() + } +} |
