use chrono::{DateTime, Duration, Utc};
const CHUNKS: [(&str, u64); 6] = [
("year", 365 * 24 * 3_600),
("month", 30 * 24 * 3_600),
("week", 7 * 24 * 3_600),
("day", 24 * 3_600),
("hour", 3_600),
("minute", 60),
];
#[must_use]
pub fn timesince(d: DateTime<Utc>, now: Option<DateTime<Utc>>, depth: usize) -> String {
let now = now.unwrap_or_else(Utc::now);
let delta = now.signed_duration_since(d);
format_timedelta(delta, depth)
}
#[must_use]
pub fn timeuntil(d: DateTime<Utc>, now: Option<DateTime<Utc>>, depth: usize) -> String {
let now = now.unwrap_or_else(Utc::now);
let delta = d.signed_duration_since(now);
format_timedelta(delta, depth)
}
fn format_timedelta(delta: Duration, depth: usize) -> String {
if delta <= Duration::zero() || depth == 0 {
return String::from("0 minutes");
}
let mut remaining_seconds = delta.num_seconds() as u64;
let mut parts = Vec::with_capacity(depth.min(CHUNKS.len()));
for (name, chunk_seconds) in CHUNKS {
if remaining_seconds < chunk_seconds {
continue;
}
let count = remaining_seconds / chunk_seconds;
remaining_seconds %= chunk_seconds;
parts.push(format_part(name, count));
if parts.len() == depth {
break;
}
}
if parts.is_empty() {
String::from("0 minutes")
} else {
parts.join(", ")
}
}
fn format_part(name: &str, count: u64) -> String {
if count == 1 {
format!("1 {name}")
} else {
format!("{count} {name}s")
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn timesince_formats_days_and_hours() {
let now = Utc.with_ymd_and_hms(2024, 3, 10, 12, 0, 0).unwrap();
let past = now - Duration::days(2) - Duration::hours(3);
assert_eq!(timesince(past, Some(now), 2), "2 days, 3 hours");
}
#[test]
fn timesince_formats_years_and_months() {
let now = Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap();
let past = now - Duration::days(365 + 90);
assert_eq!(timesince(past, Some(now), 2), "1 year, 3 months");
}
#[test]
fn timesince_limits_depth() {
let now = Utc.with_ymd_and_hms(2024, 3, 10, 12, 0, 0).unwrap();
let past = now - Duration::days(14) - Duration::days(3) - Duration::hours(2);
assert_eq!(timesince(past, Some(now), 1), "2 weeks");
}
#[test]
fn timeuntil_formats_future_date() {
let now = Utc.with_ymd_and_hms(2024, 3, 10, 12, 0, 0).unwrap();
let future = now + Duration::days(1) + Duration::minutes(30);
assert_eq!(timeuntil(future, Some(now), 2), "1 day, 30 minutes");
}
#[test]
fn timesince_clamps_future_to_zero_minutes() {
let now = Utc.with_ymd_and_hms(2024, 3, 10, 12, 0, 0).unwrap();
let future = now + Duration::minutes(5);
assert_eq!(timesince(future, Some(now), 2), "0 minutes");
}
#[test]
fn timesince_returns_zero_minutes_for_small_delta() {
let now = Utc.with_ymd_and_hms(2024, 3, 10, 12, 0, 0).unwrap();
let past = now - Duration::seconds(59);
assert_eq!(timesince(past, Some(now), 2), "0 minutes");
}
}