//! Timestamp formatting. use chrono::prelude::*; use core::cmp::max; use lazy_static::lazy_static; use std::time::Duration; use unicode_width::UnicodeWidthStr; /// Every timestamp in exa needs to be rendered by a **time format**. /// Formatting times is tricky, because how a timestamp is rendered can /// depend on one or more of the following: /// /// - The user’s locale, for printing the month name as “Feb”, or as “fév”, /// or as “2月”; /// - The current year, because certain formats will be less precise when /// dealing with dates far in the past; /// - The formatting style that the user asked for on the command-line. /// /// Because not all formatting styles need the same data, they all have their /// own enum variants. It’s not worth looking the locale up if the formatter /// prints month names as numbers. /// /// Also, eza supports *custom* styles, where the user enters a /// format string in an environment variable or something. Just these four. #[derive(PartialEq, Eq, Debug, Clone)] pub enum TimeFormat { /// The **default format** uses the user’s locale to print month names, /// and specifies the timestamp down to the minute for recent times, and /// day for older times. DefaultFormat, /// Use the **ISO format**, which specifies the timestamp down to the /// minute for recent times, and day for older times. It uses a number /// for the month so it doesn’t use the locale. ISOFormat, /// Use the **long ISO format**, which specifies the timestamp down to the /// minute using only numbers, without needing the locale or year. LongISO, /// Use the **full ISO format**, which specifies the timestamp down to the /// millisecond and includes its offset down to the minute. This too uses /// only numbers so doesn’t require any special consideration. FullISO, /// Use a relative but fixed width representation. Relative, /// Use a custom format Custom { fmt: String }, } impl TimeFormat { pub fn format(self, time: &DateTime) -> 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 { fmt } => custom(time, &fmt), }; } } fn default(time: &DateTime) -> 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: 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) -> 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) -> String { time.format("%Y-%m-%d %H:%M").to_string() } // #[allow(trivial_numeric_casts)] fn relative(time: &DateTime) -> String { timeago::Formatter::new() .ago("") .convert(Duration::from_secs( max(0, Local::now().timestamp() - time.timestamp()) // this .unwrap is safe since the call above can never result in a // value < 0 .try_into() .unwrap(), )) } fn full(time: &DateTime) -> String { time.format("%Y-%m-%d %H:%M:%S.%f %z").to_string() } fn custom(time: &DateTime, fmt: &str) -> String { time.format(fmt).to_string() } lazy_static! { static ref CURRENT_YEAR: i32 = Local::now().year(); static ref LOCALE: locale::Time = { locale::Time::load_user_locale() .unwrap_or_else(|_| locale::Time::english()) }; static ref MAX_MONTH_WIDTH: usize = { // Some locales use a three-character wide month name (Jan to Dec); // others vary between three to four (1月 to 12月, juil.). We check each month width // to detect the longest and set the output format accordingly. (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}"; // 1月 let padding = short_month_padding(max_month_width, month); let final_str = format!("{: