use crate::Date; use chrono::{ DateTime, Duration, FixedOffset as Offset, Local, NaiveDateTime, NaiveTime, TimeZone, Timelike, Utc, }; use std::{cmp::Ordering, ops::Sub}; /// A convenience wrapper around [DateTime][t] with fixed timezone /// /// [t]: chrono::DateTime #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Time { inner: DateTime, } impl From> for Time { fn from(inner: DateTime) -> Self { Self { inner } } } impl<'t> Sub for &'t Time { type Output = Duration; fn sub(self, o: &'t Time) -> Self::Output { self.inner - o.inner } } impl ToString for Time { fn to_string(&self) -> String { format!("{}", self.inner.format("%Y-%m-%d %H:%M:%S%:z")) } } impl Time { /// Get the current local time and pin it to a fixed Tz offset pub fn now() -> Self { let now = Local::now(); Self { inner: build_datetime( now.time() .with_second(0) .and_then(|t| t.with_nanosecond(0)) .unwrap(), ), } } /// Get the time that might be rounded to the next 15 minutes pub(crate) fn rounded(r: bool) -> Self { if r { Time::now().round() } else { Time::now() } } pub(crate) fn date(&self) -> chrono::Date { self.inner.date() } /// Check if a time stamp happened _after_ a date pub fn after(&self, date: &Date) -> bool { &Date::from(self.date()) > date } #[cfg(test)] pub(crate) fn fixed(hour: u32, min: u32, sec: u32) -> Self { Self { inner: build_datetime(NaiveTime::from_hms(hour, min, sec)), } } /// Return a new instance that is rounded to nearest 15 minutes /// /// It uses the internally provided offset to do rounding, meaning /// that the timezone information will not change, even when /// rounding values that were created in a different timezone. pub fn round(&self) -> Self { let naive = self.inner.time(); let (new_min, incr_hour) = match naive.minute() { // 0-7 => (0, false) m if m < 7 => (0, false), // 7-22 => (15, false) m if m >= 7 && m < 22 => (15, false), // 22-37 => (30, false) m if m >= 22 && m < 37 => (30, false), // 37-52 => (45, false) m if m >= 37 && m < 52 => (45, false), // 52-59 => (0, true) m if m >= 52 && m <= 59 => (0, true), _ => unreachable!(), }; let hour = naive.hour(); let new = NaiveTime::from_hms(if incr_hour { hour + 1 } else { hour }, new_min, 0); let offset = self.inner.offset(); let date = self.inner.date(); Self { inner: DateTime::from_utc(NaiveDateTime::new(date.naive_local(), new), *offset), } } pub fn hour(&self) -> u32 { self.inner.hour() } pub fn minute(&self) -> u32 { self.inner.minute() } pub fn second(&self) -> u32 { self.inner.second() } } /// Build a DateTime with the current local fixed offset fn build_datetime(nt: NaiveTime) -> DateTime { let date = Utc::now().date().naive_local(); let offset = Local.offset_from_utc_date(&date); DateTime::from_utc(NaiveDateTime::new(date, nt), offset) } #[test] fn simple() { let t = Time::fixed(10, 44, 0); let round = t.round(); assert_eq!(round.minute(), 45); let t = Time::fixed(6, 8, 0); let round = t.round(); assert_eq!(round.minute(), 15); let t = Time::fixed(6, 55, 0); let round = t.round(); assert_eq!(round.minute(), 0); assert_eq!(round.hour(), 7); }