rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use chrono::{DateTime, Datelike, Timelike, Utc};
use std::fmt::Write as _;

const WEEKDAYS: [&str; 7] = [
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday",
    "Sunday",
];
const WEEKDAYS_ABBR: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const MONTHS_ABBR: [&str; 13] = [
    "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];

/// Format a UTC datetime using Django-style format characters.
#[must_use]
pub fn format(dt: DateTime<Utc>, format_string: &str) -> String {
    let mut output = String::with_capacity(format_string.len() + 16);
    let mut escaped = false;

    for ch in format_string.chars() {
        if escaped {
            output.push(ch);
            escaped = false;
            continue;
        }

        if ch == '\\' {
            escaped = true;
            continue;
        }

        write_token(&mut output, &dt, ch);
    }

    if escaped {
        output.push('\\');
    }

    output
}

fn write_token(output: &mut String, dt: &DateTime<Utc>, token: char) {
    match token {
        'd' => {
            let _ = write!(output, "{:02}", dt.day());
        }
        'D' => output.push_str(WEEKDAYS_ABBR[dt.weekday().num_days_from_monday() as usize]),
        'j' => {
            let _ = write!(output, "{}", dt.day());
        }
        'l' => output.push_str(WEEKDAYS[dt.weekday().num_days_from_monday() as usize]),
        'm' => {
            let _ = write!(output, "{:02}", dt.month());
        }
        'M' => output.push_str(MONTHS_ABBR[dt.month() as usize]),
        'n' => {
            let _ = write!(output, "{}", dt.month());
        }
        'Y' => {
            let _ = write!(output, "{:04}", dt.year());
        }
        'y' => {
            let _ = write!(output, "{:02}", dt.year().rem_euclid(100));
        }
        'H' => {
            let _ = write!(output, "{:02}", dt.hour());
        }
        'i' => {
            let _ = write!(output, "{:02}", dt.minute());
        }
        's' => {
            let _ = write!(output, "{:02}", dt.second());
        }
        'A' => output.push_str(if dt.hour() > 11 { "PM" } else { "AM" }),
        'a' => output.push_str(if dt.hour() > 11 { "pm" } else { "am" }),
        _ => output.push(token),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::TimeZone;

    #[test]
    fn format_supports_core_date_tokens() {
        let dt = Utc.with_ymd_and_hms(1979, 7, 8, 22, 0, 9).unwrap();
        assert_eq!(
            format(dt, "d D j l m M n Y y"),
            "08 Sun 8 Sunday 07 Jul 7 1979 79"
        );
    }

    #[test]
    fn format_supports_core_time_tokens() {
        let dt = Utc.with_ymd_and_hms(1979, 7, 8, 22, 5, 9).unwrap();
        assert_eq!(format(dt, "H:i:s A a"), "22:05:09 PM pm");
    }

    #[test]
    fn format_preserves_literals_and_escapes() {
        let dt = Utc.with_ymd_and_hms(1979, 7, 8, 22, 5, 9).unwrap();
        assert_eq!(format(dt, r"Y-m-d\TH:i:s"), "1979-07-08T22:05:09");
        assert_eq!(format(dt, "[Y] Y"), "[1979] 1979");
    }

    #[test]
    fn format_uses_correct_meridiem_boundaries() {
        let midnight = Utc.with_ymd_and_hms(1979, 7, 8, 0, 0, 0).unwrap();
        let noon = Utc.with_ymd_and_hms(1979, 7, 8, 12, 0, 0).unwrap();

        assert_eq!(format(midnight, "A a"), "AM am");
        assert_eq!(format(noon, "A a"), "PM pm");
    }

    #[test]
    fn format_matches_django_date_formats_subset() {
        let my_birthday = Utc.with_ymd_and_hms(1979, 7, 8, 22, 0, 0).unwrap();

        for (specifier, expected) in [
            ("d", "08"),
            ("D", "Sun"),
            ("j", "8"),
            ("l", "Sunday"),
            ("m", "07"),
            ("M", "Jul"),
            ("n", "7"),
            ("y", "79"),
            ("Y", "1979"),
        ] {
            assert_eq!(
                format(my_birthday, specifier),
                expected,
                "specifier {specifier}"
            );
        }
    }

    #[test]
    fn format_matches_django_time_formats_subset() {
        let my_birthday = Utc.with_ymd_and_hms(1979, 7, 8, 22, 0, 0).unwrap();

        for (specifier, expected) in [("A", "PM"), ("H", "22"), ("i", "00"), ("s", "00")] {
            assert_eq!(
                format(my_birthday, specifier),
                expected,
                "specifier {specifier}"
            );
        }
    }

    #[test]
    fn format_returns_empty_for_empty_format() {
        let my_birthday = Utc.with_ymd_and_hms(1979, 7, 8, 22, 0, 0).unwrap();
        assert_eq!(format(my_birthday, ""), "");
    }

    #[test]
    fn format_zero_pads_years_before_1000_like_django() {
        for (year, expected) in [(476, "76"), (42, "42"), (4, "04")] {
            let dt = Utc.with_ymd_and_hms(year, 9, 8, 5, 0, 0).unwrap();
            assert_eq!(format(dt, "y"), expected, "year {year}");
        }

        assert_eq!(
            format(Utc.with_ymd_and_hms(1, 1, 1, 0, 0, 0).unwrap(), "Y"),
            "0001"
        );
        assert_eq!(
            format(Utc.with_ymd_and_hms(999, 1, 1, 0, 0, 0).unwrap(), "Y"),
            "0999"
        );
    }
}