use crate::enums::named_enum;
use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike};
named_enum! {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Unit {
Hour => "hour",
Day => "day",
Week => "week",
Month => "month",
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Period {
pub unit: Unit,
pub start: NaiveDateTime,
}
impl Period {
pub fn containing(unit: Unit, at: NaiveDateTime) -> Self {
let d = at.date();
let day = match unit {
Unit::Hour | Unit::Day => d,
Unit::Week => d - Duration::days(i64::from(d.weekday().num_days_from_monday())),
Unit::Month => d.with_day(1).expect("day 1 exists"),
};
let hour = if unit == Unit::Hour { at.hour() } else { 0 };
Self { unit, start: day.and_hms_opt(hour, 0, 0).expect("valid hour") }
}
pub fn current(unit: Unit) -> Self {
Self::containing(unit, Local::now().naive_local())
}
pub fn end(self) -> NaiveDateTime {
match self.unit {
Unit::Hour => self.start + Duration::hours(1),
Unit::Day => self.start + Duration::days(1),
Unit::Week => self.start + Duration::days(7),
Unit::Month => {
let d = self.start.date();
let (y, m) = if d.month() == 12 { (d.year() + 1, 1) } else { (d.year(), d.month() + 1) };
NaiveDate::from_ymd_opt(y, m, 1).expect("valid month").and_time(NaiveTime::MIN)
}
}
}
pub fn next(self) -> Self {
Self { unit: self.unit, start: self.end() }
}
pub fn prev(self) -> Self {
Self::containing(self.unit, self.start - Duration::seconds(1))
}
pub fn contains(self, at: NaiveDateTime) -> bool {
self.start <= at && at < self.end()
}
pub fn with_unit(self, unit: Unit) -> Self {
let now = Local::now().naive_local();
Self::containing(unit, if self.contains(now) { now } else { self.start })
}
pub fn bounds(self) -> (DateTime<Local>, DateTime<Local>) {
(to_local(self.start), to_local(self.end()))
}
pub fn label(self) -> String {
match self.unit {
Unit::Hour => format!(
"{} {}–{}",
self.start.format("%a %Y-%m-%d"),
self.start.format("%H:%M"),
self.end().format("%H:%M")
),
Unit::Day => self.start.format("%A %Y-%m-%d").to_string(),
Unit::Week => format!(
"week {} · {} – {}",
self.start.iso_week().week(),
self.start.format("%b %d"),
(self.end() - Duration::days(1)).format("%b %d %Y")
),
Unit::Month => self.start.format("%B %Y").to_string(),
}
}
pub fn columns(self) -> Vec<(String, Period)> {
let sub = match self.unit {
Unit::Week => Unit::Day,
Unit::Month => Unit::Week,
Unit::Hour | Unit::Day => return Vec::new(),
};
let mut cols = Vec::new();
let mut p = Period::containing(sub, self.start);
while p.start < self.end() {
let label = match sub {
Unit::Day => p.start.format("%a %d").to_string(),
_ => format!("W{}", p.start.iso_week().week()),
};
cols.push((label, p));
p = p.next();
}
cols
}
}
fn to_local(naive: NaiveDateTime) -> DateTime<Local> {
(0..=4)
.find_map(|quarter| Local.from_local_datetime(&(naive + Duration::minutes(15 * quarter))).earliest())
.unwrap_or_else(|| Local.from_utc_datetime(&naive))
}
#[cfg(test)]
#[path = "../tests/period.rs"]
mod tests;