time.rs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. //! Timestamp formatting.
  2. use chrono::prelude::*;
  3. use core::cmp::max;
  4. use lazy_static::lazy_static;
  5. use std::time::Duration;
  6. use unicode_width::UnicodeWidthStr;
  7. /// Every timestamp in exa needs to be rendered by a **time format**.
  8. /// Formatting times is tricky, because how a timestamp is rendered can
  9. /// depend on one or more of the following:
  10. ///
  11. /// - The user’s locale, for printing the month name as “Feb”, or as “fév”,
  12. /// or as “2月”;
  13. /// - The current year, because certain formats will be less precise when
  14. /// dealing with dates far in the past;
  15. /// - The formatting style that the user asked for on the command-line.
  16. ///
  17. /// Because not all formatting styles need the same data, they all have their
  18. /// own enum variants. It’s not worth looking the locale up if the formatter
  19. /// prints month names as numbers.
  20. ///
  21. /// Also, eza supports *custom* styles, where the user enters a
  22. /// format string in an environment variable or something. Just these four.
  23. #[derive(PartialEq, Eq, Debug, Clone)]
  24. pub enum TimeFormat {
  25. /// The **default format** uses the user’s locale to print month names,
  26. /// and specifies the timestamp down to the minute for recent times, and
  27. /// day for older times.
  28. DefaultFormat,
  29. /// Use the **ISO format**, which specifies the timestamp down to the
  30. /// minute for recent times, and day for older times. It uses a number
  31. /// for the month so it doesn’t use the locale.
  32. ISOFormat,
  33. /// Use the **long ISO format**, which specifies the timestamp down to the
  34. /// minute using only numbers, without needing the locale or year.
  35. LongISO,
  36. /// Use the **full ISO format**, which specifies the timestamp down to the
  37. /// millisecond and includes its offset down to the minute. This too uses
  38. /// only numbers so doesn’t require any special consideration.
  39. FullISO,
  40. /// Use a relative but fixed width representation.
  41. Relative,
  42. /// Use a custom format
  43. Custom { fmt: String },
  44. }
  45. impl TimeFormat {
  46. pub fn format(self, time: &DateTime<FixedOffset>) -> String {
  47. #[rustfmt::skip]
  48. return match self {
  49. Self::DefaultFormat => default(time),
  50. Self::ISOFormat => iso(time),
  51. Self::LongISO => long(time),
  52. Self::FullISO => full(time),
  53. Self::Relative => relative(time),
  54. Self::Custom { fmt } => custom(time, &fmt),
  55. };
  56. }
  57. }
  58. fn default(time: &DateTime<FixedOffset>) -> String {
  59. let month = &*LOCALE.short_month_name(time.month0() as usize);
  60. let month_width = short_month_padding(*MAX_MONTH_WIDTH, month);
  61. let format = if time.year() == *CURRENT_YEAR {
  62. format!("%_d {month:<month_width$} %H:%M")
  63. } else {
  64. format!("%_d {month:<month_width$} %Y")
  65. };
  66. time.format(format.as_str()).to_string()
  67. }
  68. /// Convert between Unicode width and width in chars to use in format!.
  69. /// ex: in Japanese, 月 is one character, but it has the width of two.
  70. /// For alignement purposes, we take the real display width into account.
  71. /// So, `MAXIMUM_MONTH_WIDTH` (“12月”) = 4, but if we use `{:4}` in format!,
  72. /// it will add a space (“ 12月”) because format! counts characters.
  73. /// Conversely, a char can have a width of zero (like combining diacritics)
  74. fn short_month_padding(max_month_width: usize, month: &str) -> usize {
  75. let shift = month.chars().count() as isize - UnicodeWidthStr::width(month) as isize;
  76. (max_month_width as isize + shift) as usize
  77. }
  78. fn iso(time: &DateTime<FixedOffset>) -> String {
  79. if time.year() == *CURRENT_YEAR {
  80. time.format("%m-%d %H:%M").to_string()
  81. } else {
  82. time.format("%Y-%m-%d").to_string()
  83. }
  84. }
  85. fn long(time: &DateTime<FixedOffset>) -> String {
  86. time.format("%Y-%m-%d %H:%M").to_string()
  87. }
  88. // #[allow(trivial_numeric_casts)]
  89. fn relative(time: &DateTime<FixedOffset>) -> String {
  90. timeago::Formatter::new()
  91. .ago("")
  92. .convert(Duration::from_secs(
  93. max(0, Local::now().timestamp() - time.timestamp())
  94. // this .unwrap is safe since the call above can never result in a
  95. // value < 0
  96. .try_into()
  97. .unwrap(),
  98. ))
  99. }
  100. fn full(time: &DateTime<FixedOffset>) -> String {
  101. time.format("%Y-%m-%d %H:%M:%S.%f %z").to_string()
  102. }
  103. fn custom(time: &DateTime<FixedOffset>, fmt: &str) -> String {
  104. time.format(fmt).to_string()
  105. }
  106. lazy_static! {
  107. static ref CURRENT_YEAR: i32 = Local::now().year();
  108. static ref LOCALE: locale::Time = {
  109. locale::Time::load_user_locale()
  110. .unwrap_or_else(|_| locale::Time::english())
  111. };
  112. static ref MAX_MONTH_WIDTH: usize = {
  113. // Some locales use a three-character wide month name (Jan to Dec);
  114. // others vary between three to four (1月 to 12月, juil.). We check each month width
  115. // to detect the longest and set the output format accordingly.
  116. (0..11).map(|i| UnicodeWidthStr::width(&*LOCALE.short_month_name(i))).max().unwrap()
  117. };
  118. }
  119. #[cfg(test)]
  120. mod test {
  121. use super::*;
  122. #[test]
  123. fn short_month_width_japanese() {
  124. let max_month_width = 4;
  125. let month = "1\u{2F49}"; // 1月
  126. let padding = short_month_padding(max_month_width, month);
  127. let final_str = format!("{:<width$}", month, width = padding);
  128. assert_eq!(max_month_width, UnicodeWidthStr::width(final_str.as_str()));
  129. }
  130. #[test]
  131. fn short_month_width_hindi() {
  132. let max_month_width = 4;
  133. assert_eq!(
  134. true,
  135. [
  136. "\u{091C}\u{0928}\u{0970}", // जन॰
  137. "\u{092B}\u{093C}\u{0930}\u{0970}", // फ़र॰
  138. "\u{092E}\u{093E}\u{0930}\u{094D}\u{091A}", // मार्च
  139. "\u{0905}\u{092A}\u{094D}\u{0930}\u{0948}\u{0932}", // अप्रैल
  140. "\u{092E}\u{0908}", // मई
  141. "\u{091C}\u{0942}\u{0928}", // जून
  142. "\u{091C}\u{0941}\u{0932}\u{0970}", // जुल॰
  143. "\u{0905}\u{0917}\u{0970}", // अग॰
  144. "\u{0938}\u{093F}\u{0924}\u{0970}", // सित॰
  145. "\u{0905}\u{0915}\u{094D}\u{0924}\u{0942}\u{0970}", // अक्तू॰
  146. "\u{0928}\u{0935}\u{0970}", // नव॰
  147. "\u{0926}\u{093F}\u{0938}\u{0970}", // दिस॰
  148. ]
  149. .iter()
  150. .map(|month| format!(
  151. "{:<width$}",
  152. month,
  153. width = short_month_padding(max_month_width, month)
  154. ))
  155. .all(|string| UnicodeWidthStr::width(string.as_str()) == max_month_width)
  156. );
  157. }
  158. }