use chrono::{DateTime, Duration, Local, NaiveDateTime, TimeZone, Timelike};
const TIMESTAMP_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%:z";
const HAND_TYPED_FORMATS: [&str; 4] = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"];
pub fn now() -> DateTime<Local> {
let now = Local::now();
now.with_nanosecond(0).unwrap_or(now)
}
pub fn fmt_ts(dt: DateTime<Local>) -> String {
dt.format(TIMESTAMP_FORMAT).to_string()
}
pub fn parse_ts(s: &str) -> Option<DateTime<Local>> {
let s = s.trim();
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Some(dt.with_timezone(&Local));
}
HAND_TYPED_FORMATS
.iter()
.find_map(|format| NaiveDateTime::parse_from_str(s, format).ok())
.and_then(|naive| Local.from_local_datetime(&naive).earliest())
}
pub fn fmt_age(d: Duration) -> String {
let minutes = d.num_minutes().max(0);
match minutes {
..60 => format!("{minutes}m"),
60..1440 => format!("{}h", minutes / 60),
1440..20160 => format!("{}d", minutes / 1440),
_ => format!("{}w", minutes / 10080),
}
}
pub fn fmt_dur(d: Duration) -> String {
let minutes = d.num_minutes().max(0);
if minutes >= 60 { format!("{}h {:02}m", minutes / 60, minutes % 60) } else { format!("{minutes}m") }
}
pub fn fmt_clock(d: Duration) -> String {
let s = d.num_seconds().max(0);
format!("{:02}:{:02}:{:02}", s / 3600, s / 60 % 60, s % 60)
}
pub fn covered(spans: impl IntoIterator<Item = (DateTime<Local>, DateTime<Local>)>) -> Duration {
let mut spans: Vec<_> = spans.into_iter().filter(|(start, end)| end > start).collect();
spans.sort_unstable();
let mut total = Duration::zero();
let mut open: Option<(DateTime<Local>, DateTime<Local>)> = None;
for (start, end) in spans {
match &mut open {
Some((_, until)) if start <= *until => *until = (*until).max(end),
_ => {
if let Some((from, until)) = open.replace((start, end)) {
total += until - from;
}
}
}
}
total + open.map_or_else(Duration::zero, |(from, until)| until - from)
}
#[cfg(test)]
#[path = "../../tests/model/time.rs"]
mod tests;