use std::sync::LazyLock;
use std::time::SystemTime;
use chrono::{DateTime, Datelike, Local, Timelike};
use unicode_width::UnicodeWidthStr;
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum TimeFormat {
DefaultFormat,
ISOFormat,
LongISO,
FullISO,
Relative,
Custom(String),
}
impl TimeFormat {
pub fn format(&self, time: SystemTime) -> String {
let dt: DateTime<Local> = time.into();
match self {
Self::DefaultFormat => default(dt),
Self::ISOFormat => iso(dt),
Self::LongISO => long(dt),
Self::FullISO => full(dt),
Self::Relative => relative(dt),
Self::Custom(fmt) => dt.format(fmt).to_string(),
}
}
}
fn default(date: DateTime<Local>) -> String {
let month_name = LOCALE.short_month_name(date.month0() as usize);
if is_recent(&date) {
match *MAXIMUM_MONTH_WIDTH {
4 => format!(
"{:>2} {:<4} {:02}:{:02}",
date.day(),
month_name,
date.hour(),
date.minute()
),
5 => format!(
"{:>2} {:<5} {:02}:{:02}",
date.day(),
month_name,
date.hour(),
date.minute()
),
_ => format!(
"{:>2} {} {:02}:{:02}",
date.day(),
month_name,
date.hour(),
date.minute()
),
}
} else {
match *MAXIMUM_MONTH_WIDTH {
4 => format!("{:>2} {:<4} {:>5}", date.day(), month_name, date.year()),
5 => format!("{:>2} {:<5} {:>5}", date.day(), month_name, date.year()),
_ => format!("{:>2} {} {:>5}", date.day(), month_name, date.year()),
}
}
}
fn long(date: DateTime<Local>) -> String {
format!(
"{:04}-{:02}-{:02} {:02}:{:02}",
date.year(),
date.month(),
date.day(),
date.hour(),
date.minute()
)
}
fn full(date: DateTime<Local>) -> String {
let offset = date.offset().local_minus_utc();
let offset_hours = offset / 3600;
let offset_minutes = (offset % 3600).abs() / 60;
let nanos = date.timestamp_subsec_nanos();
format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {:+03}{:02}",
date.year(),
date.month(),
date.day(),
date.hour(),
date.minute(),
date.second(),
nanos,
offset_hours,
offset_minutes
)
}
fn iso(date: DateTime<Local>) -> String {
if is_recent(&date) {
format!(
"{:02}-{:02} {:02}:{:02}",
date.month(),
date.day(),
date.hour(),
date.minute()
)
} else {
format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.day())
}
}
fn relative(date: DateTime<Local>) -> String {
let now = Local::now();
let duration = now.signed_duration_since(date);
if duration.num_seconds() < 0 {
return "in the future".to_string();
}
let seconds = duration.num_seconds();
let minutes = duration.num_minutes();
let hours = duration.num_hours();
let days = duration.num_days();
let weeks = days / 7;
let months = days / 30;
let years = days / 365;
if seconds < 60 {
"just now".to_string()
} else if minutes == 1 {
"1 min ago".to_string()
} else if minutes < 60 {
format!("{minutes} mins ago")
} else if hours == 1 {
"1 hour ago".to_string()
} else if hours < 24 {
format!("{hours} hours ago")
} else if days == 1 {
"1 day ago".to_string()
} else if days < 7 {
format!("{days} days ago")
} else if weeks == 1 {
"1 week ago".to_string()
} else if weeks < 5 {
format!("{weeks} weeks ago")
} else if months == 1 {
"1 month ago".to_string()
} else if months < 12 {
format!("{months} months ago")
} else if years == 1 {
"1 year ago".to_string()
} else {
format!("{years} years ago")
}
}
fn is_recent(date: &DateTime<Local>) -> bool {
date.year() == *CURRENT_YEAR
}
static CURRENT_YEAR: LazyLock<i32> = LazyLock::new(|| Local::now().year());
static LOCALE: LazyLock<locale::Time> =
LazyLock::new(|| locale::Time::load_user_locale().unwrap_or_else(|_| locale::Time::english()));
static MAXIMUM_MONTH_WIDTH: LazyLock<usize> = LazyLock::new(|| {
let mut maximum_month_width = 0;
for i in 0..12 {
let current_month_width = UnicodeWidthStr::width(&*LOCALE.short_month_name(i));
maximum_month_width = std::cmp::max(maximum_month_width, current_month_width);
}
maximum_month_width
});