use sefy_core::time::civil_from_days;
pub fn moment(at: i64, now: i64) -> String {
format!("{} ({})", date_time(at), ago(now - at))
}
pub fn date_time(at: i64) -> String {
let days = at.div_euclid(86_400);
let seconds = at.rem_euclid(86_400);
let (year, month, day) = civil_from_days(days);
format!(
"{year:04}-{month:02}-{day:02} {:02}:{:02} UTC",
seconds / 3600,
(seconds % 3600) / 60
)
}
pub fn date(at: i64) -> String {
let (year, month, day) = civil_from_days(at.div_euclid(86_400));
format!("{year:04}-{month:02}-{day:02}")
}
pub fn days_away(days: i64) -> String {
match days {
0 => "today".to_owned(),
1 => "tomorrow".to_owned(),
-1 => "yesterday".to_owned(),
days if days > 1 => format!("in {days} days"),
days => format!("{} days ago", -days),
}
}
pub fn due(days: i64) -> String {
if days < 0 {
format!("expired {}", days_away(days))
} else {
format!("expires {}", days_away(days))
}
}
fn ago(seconds: i64) -> String {
if seconds < 0 {
return "in the future".to_owned();
}
const MINUTE: i64 = 60;
const HOUR: i64 = 60 * MINUTE;
const DAY: i64 = 24 * HOUR;
if seconds < MINUTE {
"just now".to_owned()
} else if seconds < HOUR {
plural(seconds / MINUTE, "minute")
} else if seconds < DAY {
plural(seconds / HOUR, "hour")
} else {
plural(seconds / DAY, "day")
}
}
fn plural(n: i64, noun: &str) -> String {
if n == 1 {
format!("1 {noun} ago")
} else {
format!("{n} {noun}s ago")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_epoch_and_some_dates_after_it() {
assert_eq!(date_time(0), "1970-01-01 00:00 UTC");
assert_eq!(date_time(1_709_164_800), "2024-02-29 00:00 UTC");
assert_eq!(date_time(1_709_251_200), "2024-03-01 00:00 UTC");
assert_eq!(date_time(951_782_400), "2000-02-29 00:00 UTC");
}
#[test]
fn the_time_of_day_comes_out_too() {
assert_eq!(date_time(3600 * 13 + 60 * 45), "1970-01-01 13:45 UTC");
assert_eq!(date_time(86_399), "1970-01-01 23:59 UTC");
}
#[test]
fn a_date_before_the_epoch_lands_on_the_right_day() {
assert_eq!(date_time(-1), "1969-12-31 23:59 UTC");
}
#[test]
fn distances_pick_the_unit_that_reads() {
assert_eq!(ago(0), "just now");
assert_eq!(ago(59), "just now");
assert_eq!(ago(60), "1 minute ago");
assert_eq!(ago(119), "1 minute ago");
assert_eq!(ago(3600), "1 hour ago");
assert_eq!(ago(86_400), "1 day ago");
assert_eq!(ago(86_400 * 3), "3 days ago");
}
#[test]
fn a_stamp_from_the_future_is_said_plainly() {
assert_eq!(ago(-5), "in the future");
assert!(moment(100, 50).ends_with("(in the future)"));
}
#[test]
fn a_date_is_the_day_alone() {
assert_eq!(date(1_709_164_800 + 13 * 3600), "2024-02-29");
assert_eq!(date(-1), "1969-12-31");
}
#[test]
fn days_either_way_are_said_the_way_people_say_them() {
assert_eq!(days_away(0), "today");
assert_eq!(days_away(1), "tomorrow");
assert_eq!(days_away(-1), "yesterday");
assert_eq!(days_away(12), "in 12 days");
assert_eq!(days_away(-3), "3 days ago");
assert_eq!(due(5), "expires in 5 days");
assert_eq!(due(0), "expires today");
assert_eq!(due(-1), "expired yesterday");
assert_eq!(due(-40), "expired 40 days ago");
}
#[test]
fn a_moment_carries_both_readings() {
let rendered = moment(1_709_164_800, 1_709_164_800 + 7200);
assert_eq!(rendered, "2024-02-29 00:00 UTC (2 hours ago)");
}
}