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",
];
#[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"
);
}
}