Vectrum
Sign In
89 lines · 3.2 KB · Rust
//! Clock times as the user wants to read them: 12- or 24-hour, or whatever
//! the locale prefers.

use std::time::SystemTime;

use chrono::{DateTime, Datelike as _, Local};

use crate::settings::{self, TimeFormat};

/// Whether times show as `2:32 PM` rather than `14:32`.
pub fn uses_12_hours() -> bool {
    match settings::get(|settings| settings.time_format) {
        TimeFormat::Hour12 => true,
        TimeFormat::Hour24 => false,
        TimeFormat::System => locale_uses_12_hours(),
    }
}

/// The locales that write the time with AM and PM.
fn locale_uses_12_hours() -> bool {
    let locale = ["LC_ALL", "LC_TIME", "LANG"]
        .iter()
        .find_map(|key| std::env::var(key).ok().filter(|value| !value.is_empty()))
        .unwrap_or_default();
    ["en_US", "en_CA", "en_AU", "en_NZ", "en_PH", "en_IN", "es_MX", "es_US", "hi_IN", "ar_", "ko_KR", "fil_PH"]
        .iter()
        .any(|prefix| locale.starts_with(prefix))
}

/// `14:32` or `2:32 PM`.
pub fn clock(time: SystemTime) -> String {
    let time: DateTime<Local> = time.into();
    if uses_12_hours() { time.format("%-I:%M %p").to_string() } else { time.format("%H:%M").to_string() }
}

/// A moment near now, as briefly as it reads clearly: `14:32`, `tomorrow
/// 09:00`, `Fri 09:00`, `Oct 21, 09:00`.
pub fn when(time: SystemTime) -> String {
    let local: DateTime<Local> = time.into();
    let today = Local::now().date_naive();
    let days = (local.date_naive() - today).num_days();
    let clock = clock(time);
    match days {
        0 => clock,
        1 => format!("tomorrow {clock}"),
        -1 => format!("yesterday {clock}"),
        2..=6 => format!("{} {clock}", local.format("%a")),
        _ if local.year() == today.year() => format!("{}, {clock}", local.format("%b %-d")),
        _ => format!("{}, {clock}", local.format("%b %-d %Y")),
    }
}

/// The next time the clock shows `hour`:00, `days` days from today.
pub fn at_hour(days: i64, hour: u32) -> SystemTime {
    let date = Local::now().date_naive() + chrono::Duration::days(days);
    date.and_hms_opt(hour, 0, 0)
        .and_then(|naive| naive.and_local_timezone(Local).earliest())
        .map(SystemTime::from)
        .unwrap_or_else(SystemTime::now)
}

/// A message's time as T3 Code's timeline shows it: `12:34 PM` today,
/// `yesterday at 12:34 PM`, `8/13 12:34 PM`, or `8/13/2025 12:34 PM` in
/// another year.
pub fn day_aware(time: SystemTime) -> String {
    let local: DateTime<Local> = time.into();
    let today = Local::now().date_naive();
    let clock = clock(time);
    match (local.date_naive() - today).num_days() {
        0 => clock,
        -1 => format!("yesterday at {clock}"),
        _ if local.year() == today.year() => format!("{} {clock}", local.format("%-m/%-d")),
        _ => format!("{} {clock}", local.format("%-m/%-d/%Y")),
    }
}

/// The long form for a tooltip: `12:04 PM, 4th June 2026`.
pub fn long_form(time: SystemTime) -> String {
    let local: DateTime<Local> = time.into();
    let day = local.day();
    let suffix = match (day % 10, day % 100) {
        (_, 11..=13) => "th",
        (1, _) => "st",
        (2, _) => "nd",
        (3, _) => "rd",
        _ => "th",
    };
    format!("{}, {day}{suffix} {}", clock(time), local.format("%B %Y"))
}