use chrono::prelude::*;
use core::cmp::max;
use std::sync::LazyLock;
use std::time::Duration;
use unicode_width::UnicodeWidthStr;
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum TimeFormat {
DefaultFormat,
ISOFormat,
LongISO,
FullISO,
Relative,
Custom {
non_recent: String,
recent: Option<String>,
},
}
impl TimeFormat {
#[must_use]
pub fn format(self, time: &DateTime<FixedOffset>) -> String {
#[rustfmt::skip]
return match self {
Self::DefaultFormat => default(time),
Self::ISOFormat => iso(time),
Self::LongISO => long(time),
Self::FullISO => full(time),
Self::Relative => relative(time),
Self::Custom { non_recent, recent } => custom(
time, non_recent.as_str(), recent.as_deref()
),
};
}
}
fn default(time: &DateTime<FixedOffset>) -> String {
let month = &*LOCALE.short_month_name(time.month0() as usize);
let month_width = short_month_padding(*MAX_MONTH_WIDTH, month);
let format = if time.year() == *CURRENT_YEAR {
format!("%_d {month:<month_width$} %H:%M")
} else {
format!("%_d {month:<month_width$} %Y")
};
time.format(format.as_str()).to_string()
}
fn short_month_padding(max_month_width: usize, month: &str) -> usize {
let shift = month.chars().count() as isize - UnicodeWidthStr::width(month) as isize;
(max_month_width as isize + shift) as usize
}
fn iso(time: &DateTime<FixedOffset>) -> String {
if time.year() == *CURRENT_YEAR {
time.format("%m-%d %H:%M").to_string()
} else {
time.format("%Y-%m-%d").to_string()
}
}
fn long(time: &DateTime<FixedOffset>) -> String {
time.format("%Y-%m-%d %H:%M").to_string()
}
fn relative(time: &DateTime<FixedOffset>) -> String {
timeago::Formatter::new()
.ago("")
.convert(Duration::from_secs(
max(0, Local::now().timestamp() - time.timestamp())
.try_into()
.unwrap(),
))
}
fn full(time: &DateTime<FixedOffset>) -> String {
time.format("%Y-%m-%d %H:%M:%S.%f %z").to_string()
}
fn custom(time: &DateTime<FixedOffset>, non_recent_fmt: &str, recent_fmt: Option<&str>) -> String {
if let Some(recent_fmt) = recent_fmt {
if time.year() == *CURRENT_YEAR {
time.format(recent_fmt).to_string()
} else {
time.format(non_recent_fmt).to_string()
}
} else {
time.format(non_recent_fmt).to_string()
}
}
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 MAX_MONTH_WIDTH: LazyLock<usize> = LazyLock::new(|| {
(0..11)
.map(|i| UnicodeWidthStr::width(&*LOCALE.short_month_name(i)))
.max()
.unwrap()
});
#[cfg(test)]
mod test {
use super::*;
#[test]
fn short_month_width_japanese() {
let max_month_width = 4;
let month = "1\u{2F49}"; let padding = short_month_padding(max_month_width, month);
let final_str = format!("{month:<padding$}");
assert_eq!(max_month_width, UnicodeWidthStr::width(final_str.as_str()));
}
#[test]
fn short_month_width_hindi() {
let max_month_width = 4;
assert!([
"\u{091C}\u{0928}\u{0970}", "\u{092B}\u{093C}\u{0930}\u{0970}", "\u{092E}\u{093E}\u{0930}\u{094D}\u{091A}", "\u{0905}\u{092A}\u{094D}\u{0930}\u{0948}\u{0932}", "\u{092E}\u{0908}", "\u{091C}\u{0942}\u{0928}", "\u{091C}\u{0941}\u{0932}\u{0970}", "\u{0905}\u{0917}\u{0970}", "\u{0938}\u{093F}\u{0924}\u{0970}", "\u{0905}\u{0915}\u{094D}\u{0924}\u{0942}\u{0970}", "\u{0928}\u{0935}\u{0970}", "\u{0926}\u{093F}\u{0938}\u{0970}", ]
.iter()
.map(|month| format!(
"{:<width$}",
month,
width = short_month_padding(max_month_width, month)
))
.all(|string| UnicodeWidthStr::width(string.as_str()) == max_month_width));
}
}