rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
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),
];

/// Return human-readable time since `d` (e.g. "2 days, 3 hours").
#[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)
}

/// Return human-readable time until `d`.
#[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");
    }
}