use chrono::{ DateTime, Datelike, Duration, Local, Months, NaiveDate, TimeZone, Timelike, Utc, Weekday, }; /// 🧉 » `LoolDate` /// -- /// /// wrapper around `chrono::DateTime` to provide more date manipulation methods pub struct LoolDate { date: DateTime, } pub enum TimeUnit { Year, Month, Day, Hour, Minute, Second, } impl LoolDate { /// creates a new `LoolDate` from a `DateTime` pub fn new(date: DateTime) -> Self { Self { date } } // creates a new `LoolDate` in the `Local` timezone pub fn now() -> LoolDate { LoolDate { date: Local::now() } } /// creates a new `LoolDate` in the `Utc` timezone pub fn utc_now() -> LoolDate { LoolDate { date: Utc::now() } } /// returns a clone of the inner `DateTime` pub fn date(&self) -> DateTime { self.date.clone() } /// adds `1` year to the current date pub fn add_year(&mut self) { self.date = self.date.clone() + Months::new(12); self.set_start_of(TimeUnit::Year); } /// adds `1` month to the current date pub fn add_month(&mut self) { self.date = self.date.clone() + Months::new(1); self.set_start_of(TimeUnit::Month); } /// adds `1` day to the current date pub fn add_day(&mut self) { self.date += Duration::days(1); self.set_start_of(TimeUnit::Day); } /// adds `1` hour to the current date pub fn add_hour(&mut self) { self.date += Duration::hours(1); self.set_start_of(TimeUnit::Hour); } /// adds `1` minute to the current date pub fn add_minute(&mut self) { self.date += Duration::minutes(1); self.set_start_of(TimeUnit::Minute); } /// adds `1` second to the current date pub fn add_second(&mut self) { self.date += Duration::seconds(1); self.set_start_of(TimeUnit::Second); } /// subtracts `1` year from the current date pub fn subs_year(&mut self) { self.date = self.date.clone() - Months::new(12); } /// subtracts `1` month from the current date pub fn subs_month(&mut self) { self.date = self.date.clone() - Months::new(1); } /// subtracts `1` day from the current date pub fn subs_day(&mut self) { self.date -= Duration::days(1); } /// subtracts `1` hour from the current date pub fn subs_hour(&mut self) { self.date -= Duration::hours(1); } /// subtracts `1` minute from the current date pub fn subs_minute(&mut self) { self.date -= Duration::minutes(1); } /// subtracts `1` second from the current date pub fn subs_second(&mut self) { self.date -= Duration::seconds(1); } /// returns the day of the month starting from `1` pub fn day(&self) -> u32 { self.date.day() } /// returns the day of the week pub fn weekday(&self) -> Weekday { self.date.weekday() } /// returns the day of the week starting from `0` (monday) pub fn weekday_from_monday(&self) -> u32 { self.date.weekday().num_days_from_monday() } /// returns the day of the week starting from `0` (sunday) pub fn weekday_from_sunday(&self) -> u32 { self.date.weekday().num_days_from_sunday() } /// returns the month starting from 1 pub fn month(&self) -> u32 { self.date.month() } /// returns the year number in the [calendar date](./naive/struct.NaiveDate.html#calendar-date). pub fn year(&self) -> i32 { self.date.year() } /// returns the hour `(0..23)` pub fn hour(&self) -> u32 { self.date.hour() } /// returns the minute `(0..59)` pub fn minute(&self) -> u32 { self.date.minute() } /// returns the second number `(0..59)` pub fn second(&self) -> u32 { self.date.second() } /// returns the number of milliseconds since the last second boundary /// /// In event of a [leap second](https://en.wikipedia.org/wiki/Leap_second) this may exceed /// `999`. pub fn millis(&self) -> u64 { self.date.timestamp_subsec_millis() as u64 } /// Returns the number of microseconds since the last second boundary. /// /// In event of a [leap second](https://en.wikipedia.org/wiki/Leap_second) this may exceed /// `999999`. pub fn micros(&self) -> u32 { self.date.timestamp_subsec_micros() } /// Returns the number of nanoseconds since the last second boundary. /// /// In event of a [leap second](https://en.wikipedia.org/wiki/Leap_second) this may exceed /// `999999999`. pub fn nanos(&self) -> u32 { self.date.timestamp_subsec_nanos() } /// sets the nanoseconds since the last second change /// /// values greater than `2000,000,000` will be clamped to `1999,999,999` pub fn set_nanos(&mut self, nanos: u32) { // avoid `whith_nanosecond` returning None for > 2_000_000_000 values // so we can safely unwrap the result let nanos = if nanos > 2_000_000_000 { 1_999_999_999 } else { nanos }; self.date = self.date.with_nanosecond(nanos).unwrap(); } /// sets the microseconds since the last second change /// /// values greater than `2,000,000` will be clamped to `1,999,999` pub fn set_micros(&mut self, micros: u32) { let micros = if micros > 2_000_000 { 1_999_999 } else { micros }; self.date = self.date.with_nanosecond(micros * 1_000).unwrap(); } /// sets the milliseconds since the last second change /// /// values greater than `2,000` will be clamped to `1,999` pub fn set_millis(&mut self, millis: u64) { let millis = if millis > 2_000 { 1_999 } else { millis }; self.date = self.date.with_nanosecond(millis as u32 * 1_000_000).unwrap(); } /// sets the second number of the date /// /// values >= `60` will be clamped to `0` pub fn set_second(&mut self, second: u32) { let second = if second >= 60 { 0 } else { second }; self.date = self.date.with_second(second).unwrap(); } /// sets the minute number of the date /// /// values >= `60` will be clamped to `0` pub fn set_minute(&mut self, minute: u32) { let minute = if minute >= 60 { 0 } else { minute }; self.date = self.date.with_minute(minute).unwrap(); } /// sets the hour number of the date `(0..23)` /// /// values >= `24` will be clamped to `0` pub fn set_hour(&mut self, hour: u32) { let hour = if hour >= 24 { 0 } else { hour }; self.date = self.date.with_hour(hour).unwrap(); } /// sets the hour, minute and second of the date at once /// /// - `hour` must be in the range `(0..23)` /// - `minute` must be in the range `(0..59)` /// - `second` must be in the range `(0..59)` /// /// values greater than the maximum will be clamped (see `set_hour`, `set_minute`, `set_second`) /// documentation for more information pub fn set_hms(&mut self, hour: u32, minute: u32, second: u32) { self.set_hour(hour); self.set_minute(minute); self.set_second(second); } /// sets the day of the month `(1..31)` /// /// values greater than the maximum day of the month will be clamped to the last day of the /// month, depending on the current year and month and taking leap years into account. pub fn set_day(&mut self, day: u32) { let day = if day == 0 { 1 } else { day }; let days_in_month = get_days_from_month(self.date.year(), self.date.month()); if day > days_in_month { self.date = self.date.with_day(days_in_month).unwrap(); } else { self.date = self.date.with_day(day).unwrap(); } } /// sets the month of the year `(1..12)` /// /// - values greater than the maximum month will be clamped to `12` /// - values less than `1` will be clamped to `1` /// /// if the current day is greater than the last day of the new month, the day will be clamped /// to the last day of the month, depending on the current year and month and taking leap years /// into account. So, be aware that changing the month may imply a change in the day. /// This is done to avoid invalid dates like `2024-02-30` which would cause an error in the /// `chrono` library. pub fn set_month(&mut self, month: u32) { let month = if month > 12 { 12 } else if month == 0 { 1 } else { month }; let day = self.date.day(); let days_in_month = get_days_from_month(self.date.year(), month); if day > days_in_month { self.date = self.date.with_day(days_in_month).unwrap(); self.date = self.date.with_month(month).unwrap(); } else { self.date = self.date.with_month(month).unwrap(); } } /// sets the month and day of the date at once /// /// check the `set_month` and `set_day` documentation for more information about the clamping /// behavior. pub fn set_md(&mut self, month: u32, day: u32) { self.set_month(month); self.set_day(day); } /// sets the year of the date /// /// **warning**: changing the year may imply a change in the day. /// For example, if the current date is `2024-02-29` and you set the year to `2023`, the date /// date `2023-02-29` will be invalid, because `2023` is not a leap year. In this case, the day /// will be clamped to `2023-02-28`. /// /// check the `set_day` documentation for more information about the clamping behavior. pub fn set_year(&mut self, year: i32) { let month = self.date.month(); let day = self.date.day(); let days_in_month = get_days_from_month(year, month); if day > days_in_month { self.date = self.date.with_day(days_in_month).unwrap(); self.date = self.date.with_year(year).unwrap(); } else { self.date = self.date.with_year(year).unwrap(); } } /// sets the year, month and day of the date at once /// /// check the `set_year`, `set_month` and `set_day` documentation for more information about the /// clamping behavior. pub fn set_ymd(&mut self, year: i32, month: u32, day: u32) { self.set_year(year); self.set_month(month); self.set_day(day); } /// returns true when the current date is the last day of the month. pub fn is_last_day_of_month(&self) -> bool { self.date.day() == get_days_from_month(self.date.year(), self.date.month()) } /// returns true when the current weekday is the last occurrence of this weekday /// for the present month. pub fn is_last_weekday_of_month(&self) -> bool { // check this by adding 7 days to the current date and seeing if it's // a different month let next_weekday = self.date.clone() + Duration::days(7); self.date.month() != next_weekday.month() } pub fn set_start_of(&mut self, unit: TimeUnit) { match unit { TimeUnit::Year => { self.set_month(1); self.set_day(1); self.set_hour(0); self.set_minute(0); self.set_second(0); self.set_nanos(0); } TimeUnit::Month => { self.set_day(1); self.set_hour(0); self.set_minute(0); self.set_second(0); self.set_nanos(0); } TimeUnit::Day => { self.set_hour(0); self.set_minute(0); self.set_second(0); self.set_nanos(0); } TimeUnit::Hour => { self.set_minute(0); self.set_second(0); self.set_nanos(0); } TimeUnit::Minute => { self.set_second(0); self.set_nanos(0); } TimeUnit::Second => { self.set_nanos(0); } }; } } fn get_days_from_month(year: i32, month: u32) -> u32 { NaiveDate::from_ymd_opt( match month { 12 => year + 1, _ => year, }, match month { 12 => 1, _ => month + 1, }, 1, ) .unwrap() .signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap()) .num_days() as u32 } #[cfg(test)] mod tests { use {super::*, chrono::Utc}; #[test] fn test_get_days_from_month() { assert_eq!(get_days_from_month(2024, 1), 31); assert_eq!(get_days_from_month(2024, 2), 29); assert_eq!(get_days_from_month(2024, 3), 31); assert_eq!(get_days_from_month(2024, 4), 30); assert_eq!(get_days_from_month(2024, 5), 31); assert_eq!(get_days_from_month(2024, 6), 30); assert_eq!(get_days_from_month(2024, 7), 31); assert_eq!(get_days_from_month(2024, 8), 31); assert_eq!(get_days_from_month(2024, 9), 30); assert_eq!(get_days_from_month(2024, 10), 31); assert_eq!(get_days_from_month(2024, 11), 30); assert_eq!(get_days_from_month(2024, 12), 31); } #[test] fn test_set_day() { let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()); date.set_day(31); assert_eq!(date.day(), 31); assert_eq!(date.month(), 1); assert_eq!(date.year(), 2024); date.set_day(1); assert_eq!(date.day(), 1); assert_eq!(date.month(), 1); assert_eq!(date.year(), 2024); } #[test] fn test_set_day_invalid_values() { let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()); date.set_day(35); assert_eq!(date.day(), 31); assert_eq!(date.month(), 1); assert_eq!(date.year(), 2024); date.set_day(0); assert_eq!(date.day(), 1); assert_eq!(date.month(), 1); assert_eq!(date.year(), 2024); let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 2, 1, 0, 0, 0).unwrap()); date.set_day(30); assert_eq!(date.day(), 29); assert_eq!(date.month(), 2); assert_eq!(date.year(), 2024); } #[test] fn test_set_month() { let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()); date.set_month(12); assert_eq!(date.month(), 12); date.set_month(13); assert_eq!(date.month(), 12); date.set_month(1); assert_eq!(date.month(), 1); } #[test] fn test_set_month_invalid_values() { let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 10, 0, 0, 0).unwrap()); date.set_month(13); assert_eq!(date.month(), 12); assert_eq!(date.year(), 2024); assert_eq!(date.day(), 10); date.set_month(0); assert_eq!(date.month(), 1); assert_eq!(date.year(), 2024); assert_eq!(date.day(), 10); } #[test] fn test_set_month_leap_years() { let mut date = LoolDate::new(Utc.with_ymd_and_hms(2023, 1, 29, 0, 0, 0).unwrap()); date.set_month(2); assert_eq!(date.month(), 2); assert_eq!(date.year(), 2023); assert_eq!(date.day(), 28); // 2023 is not a leap year so, can't keep the original 29 let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 29, 0, 0, 0).unwrap()); date.set_month(2); assert_eq!(date.month(), 2); assert_eq!(date.year(), 2024); assert_eq!(date.day(), 29); // 2024 is a leap year so, 29 is a valid day } #[test] fn test_set_month_month_overflow() { let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 31, 0, 0, 0).unwrap()); date.set_month(4); assert_eq!(date.month(), 4); assert_eq!(date.year(), 2024); assert_eq!(date.day(), 30); // since april has 30 days, it cant keep the original 31 let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 31, 0, 0, 0).unwrap()); date.set_month(8); assert_eq!(date.month(), 8); assert_eq!(date.year(), 2024); assert_eq!(date.day(), 31); // august has 31 days, so it can keep the original 31 let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 31, 0, 0, 0).unwrap()); date.set_month(11); assert_eq!(date.month(), 11); assert_eq!(date.year(), 2024); assert_eq!(date.day(), 30); // november has 30 days, so it cant keep the original 31 } #[test] fn test_set_year() { let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()); date.set_year(2021); assert_eq!(date.year(), 2021); assert_eq!(date.month(), 1); assert_eq!(date.day(), 1); date.set_year(2050); assert_eq!(date.year(), 2050); assert_eq!(date.month(), 1); assert_eq!(date.day(), 1); date.set_year(-5); assert_eq!(date.year(), -5); assert_eq!(date.month(), 1); assert_eq!(date.day(), 1); } #[test] fn test_set_year_leap_years() { let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 2, 29, 0, 0, 0).unwrap()); date.set_year(2023); assert_eq!(date.year(), 2023); assert_eq!(date.month(), 2); assert_eq!(date.day(), 28); // 2023 is not a leap year so, can't keep the original 29 let mut date = LoolDate::new(Utc.with_ymd_and_hms(2023, 2, 28, 0, 0, 0).unwrap()); date.set_year(2024); assert_eq!(date.year(), 2024); assert_eq!(date.month(), 2); assert_eq!(date.day(), 28); } }