time.rs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. //! Timestamp formatting.
  2. use std::convert::TryInto;
  3. use std::cmp::max;
  4. use std::time::{SystemTime, UNIX_EPOCH, Duration};
  5. use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece, Instant};
  6. use datetime::fmt::DateFormat;
  7. use lazy_static::lazy_static;
  8. use unicode_width::UnicodeWidthStr;
  9. /// Every timestamp in exa needs to be rendered by a **time format**.
  10. /// Formatting times is tricky, because how a timestamp is rendered can
  11. /// depend on one or more of the following:
  12. ///
  13. /// - The user’s locale, for printing the month name as “Feb”, or as “fév”,
  14. /// or as “2月”;
  15. /// - The current year, because certain formats will be less precise when
  16. /// dealing with dates far in the past;
  17. /// - The formatting style that the user asked for on the command-line.
  18. ///
  19. /// Because not all formatting styles need the same data, they all have their
  20. /// own enum variants. It’s not worth looking the locale up if the formatter
  21. /// prints month names as numbers.
  22. ///
  23. /// Currently exa does not support *custom* styles, where the user enters a
  24. /// format string in an environment variable or something. Just these four.
  25. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  26. pub enum TimeFormat {
  27. /// The **default format** uses the user’s locale to print month names,
  28. /// and specifies the timestamp down to the minute for recent times, and
  29. /// day for older times.
  30. DefaultFormat,
  31. /// Use the **ISO format**, which specifies the timestamp down to the
  32. /// minute for recent times, and day for older times. It uses a number
  33. /// for the month so it doesn’t use the locale.
  34. ISOFormat,
  35. /// Use the **long ISO format**, which specifies the timestamp down to the
  36. /// minute using only numbers, without needing the locale or year.
  37. LongISO,
  38. /// Use the **full ISO format**, which specifies the timestamp down to the
  39. /// millisecond and includes its offset down to the minute. This too uses
  40. /// only numbers so doesn’t require any special consideration.
  41. FullISO,
  42. /// Use a relative but fixed width representation.
  43. Relative,
  44. }
  45. // There are two different formatting functions because local and zoned
  46. // timestamps are separate types.
  47. impl TimeFormat {
  48. pub fn format_local(self, time: SystemTime) -> String {
  49. match self {
  50. Self::DefaultFormat => default_local(time),
  51. Self::ISOFormat => iso_local(time),
  52. Self::LongISO => long_local(time),
  53. Self::FullISO => full_local(time),
  54. Self::Relative => relative(time),
  55. }
  56. }
  57. pub fn format_zoned(self, time: SystemTime, zone: &TimeZone) -> String {
  58. match self {
  59. Self::DefaultFormat => default_zoned(time, zone),
  60. Self::ISOFormat => iso_zoned(time, zone),
  61. Self::LongISO => long_zoned(time, zone),
  62. Self::FullISO => full_zoned(time, zone),
  63. Self::Relative => relative(time),
  64. }
  65. }
  66. }
  67. #[allow(trivial_numeric_casts)]
  68. fn default_local(time: SystemTime) -> String {
  69. let date = LocalDateTime::at(systemtime_epoch(time));
  70. let date_format = get_dateformat(&date);
  71. date_format.format(&date, &LOCALE)
  72. }
  73. #[allow(trivial_numeric_casts)]
  74. fn default_zoned(time: SystemTime, zone: &TimeZone) -> String {
  75. let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
  76. let date_format = get_dateformat(&date);
  77. date_format.format(&date, &LOCALE)
  78. }
  79. fn get_dateformat(date: &LocalDateTime) -> &'static DateFormat<'static> {
  80. match (is_recent(date), *MAXIMUM_MONTH_WIDTH) {
  81. (true, 4) => &FOUR_WIDE_DATE_TIME,
  82. (true, 5) => &FIVE_WIDE_DATE_TIME,
  83. (true, _) => &OTHER_WIDE_DATE_TIME,
  84. (false, 4) => &FOUR_WIDE_DATE_YEAR,
  85. (false, 5) => &FIVE_WIDE_DATE_YEAR,
  86. (false, _) => &OTHER_WIDE_DATE_YEAR,
  87. }
  88. }
  89. #[allow(trivial_numeric_casts)]
  90. fn long_local(time: SystemTime) -> String {
  91. let date = LocalDateTime::at(systemtime_epoch(time));
  92. format!("{:04}-{:02}-{:02} {:02}:{:02}",
  93. date.year(), date.month() as usize, date.day(),
  94. date.hour(), date.minute())
  95. }
  96. #[allow(trivial_numeric_casts)]
  97. fn long_zoned(time: SystemTime, zone: &TimeZone) -> String {
  98. let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
  99. format!("{:04}-{:02}-{:02} {:02}:{:02}",
  100. date.year(), date.month() as usize, date.day(),
  101. date.hour(), date.minute())
  102. }
  103. #[allow(trivial_numeric_casts)]
  104. fn relative(time: SystemTime) -> String {
  105. timeago::Formatter::new()
  106. .ago("")
  107. .convert(
  108. Duration::from_secs(
  109. max(0, Instant::now().seconds() - systemtime_epoch(time))
  110. // this .unwrap is safe since the call above can never result in a
  111. // value < 0
  112. .try_into().unwrap()
  113. )
  114. )
  115. }
  116. #[allow(trivial_numeric_casts)]
  117. fn full_local(time: SystemTime) -> String {
  118. let date = LocalDateTime::at(systemtime_epoch(time));
  119. format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}",
  120. date.year(), date.month() as usize, date.day(),
  121. date.hour(), date.minute(), date.second(), systemtime_nanos(time))
  122. }
  123. #[allow(trivial_numeric_casts)]
  124. fn full_zoned(time: SystemTime, zone: &TimeZone) -> String {
  125. use datetime::Offset;
  126. let local = LocalDateTime::at(systemtime_epoch(time));
  127. let date = zone.to_zoned(local);
  128. let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range");
  129. format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {:+03}{:02}",
  130. date.year(), date.month() as usize, date.day(),
  131. date.hour(), date.minute(), date.second(), systemtime_nanos(time),
  132. offset.hours(), offset.minutes().abs())
  133. }
  134. #[allow(trivial_numeric_casts)]
  135. fn iso_local(time: SystemTime) -> String {
  136. let date = LocalDateTime::at(systemtime_epoch(time));
  137. if is_recent(&date) {
  138. format!("{:02}-{:02} {:02}:{:02}",
  139. date.month() as usize, date.day(),
  140. date.hour(), date.minute())
  141. }
  142. else {
  143. format!("{:04}-{:02}-{:02}",
  144. date.year(), date.month() as usize, date.day())
  145. }
  146. }
  147. #[allow(trivial_numeric_casts)]
  148. fn iso_zoned(time: SystemTime, zone: &TimeZone) -> String {
  149. let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
  150. if is_recent(&date) {
  151. format!("{:02}-{:02} {:02}:{:02}",
  152. date.month() as usize, date.day(),
  153. date.hour(), date.minute())
  154. }
  155. else {
  156. format!("{:04}-{:02}-{:02}",
  157. date.year(), date.month() as usize, date.day())
  158. }
  159. }
  160. fn systemtime_epoch(time: SystemTime) -> i64 {
  161. time.duration_since(UNIX_EPOCH)
  162. .map(|t| t.as_secs() as i64)
  163. .unwrap_or_else(|e| {
  164. let diff = e.duration();
  165. let mut secs = diff.as_secs();
  166. if diff.subsec_nanos() > 0 {
  167. secs += 1;
  168. }
  169. -(secs as i64)
  170. })
  171. }
  172. fn systemtime_nanos(time: SystemTime) -> u32 {
  173. time.duration_since(UNIX_EPOCH)
  174. .map(|t| t.subsec_nanos())
  175. .unwrap_or_else(|e| {
  176. let nanos = e.duration().subsec_nanos();
  177. if nanos > 0 {
  178. 1_000_000_000 - nanos
  179. } else {
  180. nanos
  181. }
  182. })
  183. }
  184. fn is_recent(date: &LocalDateTime) -> bool {
  185. date.year() == *CURRENT_YEAR
  186. }
  187. lazy_static! {
  188. static ref CURRENT_YEAR: i64 = LocalDateTime::now().year();
  189. static ref LOCALE: locale::Time = {
  190. locale::Time::load_user_locale()
  191. .unwrap_or_else(|_| locale::Time::english())
  192. };
  193. static ref MAXIMUM_MONTH_WIDTH: usize = {
  194. // Some locales use a three-character wide month name (Jan to Dec);
  195. // others vary between three to four (1月 to 12月, juil.). We check each month width
  196. // to detect the longest and set the output format accordingly.
  197. let mut maximum_month_width = 0;
  198. for i in 0..11 {
  199. let current_month_width = UnicodeWidthStr::width(&*LOCALE.short_month_name(i));
  200. maximum_month_width = std::cmp::max(maximum_month_width, current_month_width);
  201. }
  202. maximum_month_width
  203. };
  204. static ref FOUR_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
  205. "{2>:D} {4<:M} {02>:h}:{02>:m}"
  206. ).unwrap();
  207. static ref FIVE_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
  208. "{2>:D} {5<:M} {02>:h}:{02>:m}"
  209. ).unwrap();
  210. static ref OTHER_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
  211. "{2>:D} {:M} {02>:h}:{02>:m}"
  212. ).unwrap();
  213. static ref FOUR_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse(
  214. "{2>:D} {4<:M} {5>:Y}"
  215. ).unwrap();
  216. static ref FIVE_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse(
  217. "{2>:D} {5<:M} {5>:Y}"
  218. ).unwrap();
  219. static ref OTHER_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse(
  220. "{2>:D} {:M} {5>:Y}"
  221. ).unwrap();
  222. }