2017-08-09 16:14:16 +00:00
|
|
|
|
//! Timestamp formatting.
|
|
|
|
|
|
2017-07-05 23:01:45 +00:00
|
|
|
|
use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece};
|
2017-07-03 07:45:14 +00:00
|
|
|
|
use datetime::fmt::DateFormat;
|
|
|
|
|
use locale;
|
2017-08-06 01:59:46 +00:00
|
|
|
|
use std::cmp;
|
2017-07-03 07:45:14 +00:00
|
|
|
|
|
2017-07-05 22:07:27 +00:00
|
|
|
|
use fs::fields::Time;
|
2017-07-03 07:45:14 +00:00
|
|
|
|
|
|
|
|
|
|
2017-08-09 16:14:16 +00:00
|
|
|
|
/// 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.
|
|
|
|
|
///
|
|
|
|
|
/// Currently exa does not support *custom* styles, where the user enters a
|
|
|
|
|
/// format string in an environment variable or something. Just these four.
|
|
|
|
|
#[derive(Debug)]
|
2017-07-05 22:27:48 +00:00
|
|
|
|
pub enum TimeFormat {
|
2017-08-09 16:14:16 +00:00
|
|
|
|
|
|
|
|
|
/// 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.
|
2017-07-05 22:27:48 +00:00
|
|
|
|
DefaultFormat(DefaultFormat),
|
2017-08-09 16:14:16 +00:00
|
|
|
|
|
|
|
|
|
/// 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 need a locale.
|
2017-07-05 23:39:54 +00:00
|
|
|
|
ISOFormat(ISOFormat),
|
2017-08-09 16:14:16 +00:00
|
|
|
|
|
|
|
|
|
/// Use the **long ISO format**, which specifies the timestamp down to the
|
|
|
|
|
/// minute using only numbers, without needing the locale or year.
|
2017-07-05 23:21:38 +00:00
|
|
|
|
LongISO,
|
2017-08-09 16:14:16 +00:00
|
|
|
|
|
|
|
|
|
/// 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.
|
2017-07-05 23:21:38 +00:00
|
|
|
|
FullISO,
|
2017-07-05 22:27:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-09 16:14:16 +00:00
|
|
|
|
// There are two different formatting functions because local and zoned
|
|
|
|
|
// timestamps are separate types.
|
|
|
|
|
|
2017-07-05 22:27:48 +00:00
|
|
|
|
impl TimeFormat {
|
|
|
|
|
pub fn format_local(&self, time: Time) -> String {
|
|
|
|
|
match *self {
|
|
|
|
|
TimeFormat::DefaultFormat(ref fmt) => fmt.format_local(time),
|
2017-07-05 23:39:54 +00:00
|
|
|
|
TimeFormat::ISOFormat(ref iso) => iso.format_local(time),
|
2017-07-05 23:21:38 +00:00
|
|
|
|
TimeFormat::LongISO => long_local(time),
|
|
|
|
|
TimeFormat::FullISO => full_local(time),
|
2017-07-05 22:27:48 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn format_zoned(&self, time: Time, zone: &TimeZone) -> String {
|
|
|
|
|
match *self {
|
|
|
|
|
TimeFormat::DefaultFormat(ref fmt) => fmt.format_zoned(time, zone),
|
2017-07-05 23:39:54 +00:00
|
|
|
|
TimeFormat::ISOFormat(ref iso) => iso.format_zoned(time, zone),
|
2017-07-05 23:21:38 +00:00
|
|
|
|
TimeFormat::LongISO => long_zoned(time, zone),
|
|
|
|
|
TimeFormat::FullISO => full_zoned(time, zone),
|
2017-07-05 22:27:48 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2017-07-03 07:45:14 +00:00
|
|
|
|
#[derive(Debug, Clone)]
|
2017-07-05 22:27:48 +00:00
|
|
|
|
pub struct DefaultFormat {
|
2017-07-03 07:45:14 +00:00
|
|
|
|
|
|
|
|
|
/// The year of the current time. This gets used to determine which date
|
|
|
|
|
/// format to use.
|
|
|
|
|
pub current_year: i64,
|
|
|
|
|
|
|
|
|
|
/// Localisation rules for formatting timestamps.
|
|
|
|
|
pub locale: locale::Time,
|
|
|
|
|
|
|
|
|
|
/// Date format for printing out timestamps that are in the current year.
|
|
|
|
|
pub date_and_time: DateFormat<'static>,
|
|
|
|
|
|
|
|
|
|
/// Date format for printing out timestamps that *aren’t*.
|
|
|
|
|
pub date_and_year: DateFormat<'static>,
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-05 22:27:48 +00:00
|
|
|
|
impl DefaultFormat {
|
|
|
|
|
pub fn new() -> DefaultFormat {
|
|
|
|
|
use unicode_width::UnicodeWidthStr;
|
|
|
|
|
|
|
|
|
|
let locale = locale::Time::load_user_locale()
|
|
|
|
|
.unwrap_or_else(|_| locale::Time::english());
|
|
|
|
|
|
|
|
|
|
let current_year = LocalDateTime::now().year();
|
|
|
|
|
|
|
|
|
|
// Some locales use a three-character wide month name (Jan to Dec);
|
2017-08-06 01:59:46 +00:00
|
|
|
|
// 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.
|
|
|
|
|
let mut maximum_month_width = 0;
|
|
|
|
|
for i in 0..11 {
|
|
|
|
|
let current_month_width = UnicodeWidthStr::width(&*locale.short_month_name(i));
|
|
|
|
|
maximum_month_width = cmp::max(maximum_month_width, current_month_width);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let date_and_time = match maximum_month_width {
|
|
|
|
|
4 => DateFormat::parse("{2>:D} {4<:M} {2>:h}:{02>:m}").unwrap(),
|
|
|
|
|
5 => DateFormat::parse("{2>:D} {5<:M} {2>:h}:{02>:m}").unwrap(),
|
2017-07-05 22:27:48 +00:00
|
|
|
|
_ => DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap(),
|
|
|
|
|
};
|
|
|
|
|
|
2017-08-06 01:59:46 +00:00
|
|
|
|
let date_and_year = match maximum_month_width {
|
|
|
|
|
4 => DateFormat::parse("{2>:D} {4<:M} {5>:Y}").unwrap(),
|
|
|
|
|
5 => DateFormat::parse("{2>:D} {5<:M} {5>:Y}").unwrap(),
|
2017-07-05 22:27:48 +00:00
|
|
|
|
_ => DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
DefaultFormat { current_year, locale, date_and_time, date_and_year }
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-03 07:45:14 +00:00
|
|
|
|
fn is_recent(&self, date: LocalDateTime) -> bool {
|
|
|
|
|
date.year() == self.current_year
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(trivial_numeric_casts)]
|
2017-07-05 22:27:48 +00:00
|
|
|
|
fn format_local(&self, time: Time) -> String {
|
2017-07-05 22:07:27 +00:00
|
|
|
|
let date = LocalDateTime::at(time.seconds as i64);
|
2017-07-03 07:45:14 +00:00
|
|
|
|
|
|
|
|
|
if self.is_recent(date) {
|
|
|
|
|
self.date_and_time.format(&date, &self.locale)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
self.date_and_year.format(&date, &self.locale)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(trivial_numeric_casts)]
|
2017-07-05 22:27:48 +00:00
|
|
|
|
fn format_zoned(&self, time: Time, zone: &TimeZone) -> String {
|
2017-07-05 22:07:27 +00:00
|
|
|
|
let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64));
|
2017-07-03 07:45:14 +00:00
|
|
|
|
|
|
|
|
|
if self.is_recent(date) {
|
|
|
|
|
self.date_and_time.format(&date, &self.locale)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
self.date_and_year.format(&date, &self.locale)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-07-05 23:01:45 +00:00
|
|
|
|
|
|
|
|
|
|
2017-07-05 23:21:38 +00:00
|
|
|
|
#[allow(trivial_numeric_casts)]
|
|
|
|
|
fn long_local(time: Time) -> String {
|
|
|
|
|
let date = LocalDateTime::at(time.seconds as i64);
|
|
|
|
|
format!("{:04}-{:02}-{:02} {:02}:{:02}",
|
|
|
|
|
date.year(), date.month() as usize, date.day(),
|
|
|
|
|
date.hour(), date.minute())
|
|
|
|
|
}
|
2017-07-05 23:01:45 +00:00
|
|
|
|
|
2017-07-05 23:21:38 +00:00
|
|
|
|
#[allow(trivial_numeric_casts)]
|
|
|
|
|
fn long_zoned(time: Time, zone: &TimeZone) -> String {
|
|
|
|
|
let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64));
|
|
|
|
|
format!("{:04}-{:02}-{:02} {:02}:{:02}",
|
|
|
|
|
date.year(), date.month() as usize, date.day(),
|
|
|
|
|
date.hour(), date.minute())
|
|
|
|
|
}
|
2017-07-05 23:01:45 +00:00
|
|
|
|
|
2017-07-05 23:21:38 +00:00
|
|
|
|
|
|
|
|
|
#[allow(trivial_numeric_casts)]
|
|
|
|
|
fn full_local(time: Time) -> String {
|
|
|
|
|
let date = LocalDateTime::at(time.seconds as i64);
|
|
|
|
|
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}",
|
|
|
|
|
date.year(), date.month() as usize, date.day(),
|
|
|
|
|
date.hour(), date.minute(), date.second(), time.nanoseconds)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(trivial_numeric_casts)]
|
|
|
|
|
fn full_zoned(time: Time, zone: &TimeZone) -> String {
|
|
|
|
|
use datetime::Offset;
|
|
|
|
|
|
|
|
|
|
let local = LocalDateTime::at(time.seconds as i64);
|
|
|
|
|
let date = zone.to_zoned(local);
|
|
|
|
|
let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range");
|
|
|
|
|
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {:+03}{:02}",
|
|
|
|
|
date.year(), date.month() as usize, date.day(),
|
|
|
|
|
date.hour(), date.minute(), date.second(), time.nanoseconds,
|
|
|
|
|
offset.hours(), offset.minutes().abs())
|
2017-07-05 23:01:45 +00:00
|
|
|
|
}
|
2017-07-05 23:39:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct ISOFormat {
|
|
|
|
|
|
|
|
|
|
/// The year of the current time. This gets used to determine which date
|
|
|
|
|
/// format to use.
|
|
|
|
|
pub current_year: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ISOFormat {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
let current_year = LocalDateTime::now().year();
|
|
|
|
|
ISOFormat { current_year }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_recent(&self, date: LocalDateTime) -> bool {
|
|
|
|
|
date.year() == self.current_year
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(trivial_numeric_casts)]
|
|
|
|
|
fn format_local(&self, time: Time) -> String {
|
|
|
|
|
let date = LocalDateTime::at(time.seconds as i64);
|
|
|
|
|
|
|
|
|
|
if self.is_recent(date) {
|
|
|
|
|
format!("{:02}-{:02} {:02}:{:02}",
|
|
|
|
|
date.month() as usize, date.day(),
|
|
|
|
|
date.hour(), date.minute())
|
|
|
|
|
}
|
2017-08-06 10:36:00 +00:00
|
|
|
|
else {
|
|
|
|
|
format!("{:04}-{:02}-{:02}",
|
|
|
|
|
date.year(), date.month() as usize, date.day())
|
|
|
|
|
}
|
2017-07-05 23:39:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(trivial_numeric_casts)]
|
|
|
|
|
fn format_zoned(&self, time: Time, zone: &TimeZone) -> String {
|
|
|
|
|
let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64));
|
|
|
|
|
|
|
|
|
|
if self.is_recent(date) {
|
|
|
|
|
format!("{:02}-{:02} {:02}:{:02}",
|
|
|
|
|
date.month() as usize, date.day(),
|
|
|
|
|
date.hour(), date.minute())
|
|
|
|
|
}
|
2017-08-06 10:36:00 +00:00
|
|
|
|
else {
|
|
|
|
|
format!("{:04}-{:02}-{:02}",
|
|
|
|
|
date.year(), date.month() as usize, date.day())
|
|
|
|
|
}
|
2017-07-05 23:39:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|