use chrono::{DateTime, Datelike, Local, NaiveDate, Timelike};
#[must_use]
pub fn format_relative_timestamp(timestamp: DateTime<Local>, today: NaiveDate) -> String {
let msg_date = timestamp.date_naive();
let time_str = format_time(timestamp);
if msg_date == today {
return format!("Today at {time_str}");
}
let yesterday = today.pred_opt().unwrap_or(today);
if msg_date == yesterday {
return format!("Yesterday at {time_str}");
}
let date_str = format_date(msg_date);
format!("{date_str} at {time_str}")
}
fn format_time(timestamp: DateTime<Local>) -> String {
let hour = timestamp.hour();
let minute = timestamp.minute();
let (hour_12, period) = match hour {
0 => (12, "am"),
1..=11 => (hour, "am"),
12 => (12, "pm"),
_ => (hour - 12, "pm"),
};
format!("{hour_12:}:{minute:02}{period}")
}
fn format_date(date: chrono::NaiveDate) -> String {
let month = date.format("%B").to_string();
let day = date.day();
let year = date.year();
let ordinal = match day {
1 | 21 | 31 => "st",
2 | 22 => "nd",
3 | 23 => "rd",
_ => "th",
};
format!("{month} {day}{ordinal}, {year}")
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Local;
#[test]
fn test_format_time() {
let ts = Local::now().with_hour(18).unwrap().with_minute(41).unwrap();
let time = format_time(ts);
assert_eq!(time, "6:41pm");
}
#[test]
fn test_format_time_am() {
let ts = Local::now().with_hour(9).unwrap().with_minute(26).unwrap();
let time = format_time(ts);
assert_eq!(time, "9:26am");
}
#[test]
fn test_format_date() {
use chrono::NaiveDate;
let date = NaiveDate::from_ymd_opt(2025, 3, 21).unwrap();
let formatted = format_date(date);
assert_eq!(formatted, "March 21st, 2025");
let date = NaiveDate::from_ymd_opt(2025, 3, 22).unwrap();
let formatted = format_date(date);
assert_eq!(formatted, "March 22nd, 2025");
let date = NaiveDate::from_ymd_opt(2025, 3, 23).unwrap();
let formatted = format_date(date);
assert_eq!(formatted, "March 23rd, 2025");
let date = NaiveDate::from_ymd_opt(2025, 3, 24).unwrap();
let formatted = format_date(date);
assert_eq!(formatted, "March 24th, 2025");
}
#[test]
fn test_format_relative_today() {
let now = Local::now();
let relative = format_relative_timestamp(now, now.date_naive());
assert!(relative.starts_with("Today at"));
}
#[test]
fn the_branch_follows_the_passed_date_not_the_clock() {
use chrono::TimeZone;
let stamp = Local
.with_ymd_and_hms(2026, 1, 2, 15, 4, 5)
.earliest()
.expect("fixture wall clock exists in the local timezone");
let day = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).expect("real date");
assert_eq!(
format_relative_timestamp(stamp, day(2026, 1, 2)),
"Today at 3:04pm"
);
assert_eq!(
format_relative_timestamp(stamp, day(2026, 1, 3)),
"Yesterday at 3:04pm"
);
assert_eq!(
format_relative_timestamp(stamp, day(2026, 3, 21)),
"January 2nd, 2026 at 3:04pm"
);
}
}