time.rs 9.4 KB

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