eza 0.10.1

A modern replacement for ls
//! Timestamp formatting.

use std::convert::TryInto;
use std::cmp::max;
use std::time::{SystemTime, UNIX_EPOCH, Duration};

use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece, Instant};
use datetime::fmt::DateFormat;

use lazy_static::lazy_static;
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.
///
/// Currently exa does not support *custom* styles, where the user enters a
/// format string in an environment variable or something. Just these four.
#[derive(PartialEq, Eq, Debug, Copy, 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,
}

// There are two different formatting functions because local and zoned
// timestamps are separate types.

impl TimeFormat {
    pub fn format_local(self, time: SystemTime) -> String {
        match self {
            Self::DefaultFormat  => default_local(time),
            Self::ISOFormat      => iso_local(time),
            Self::LongISO        => long_local(time),
            Self::FullISO        => full_local(time),
            Self::Relative       => relative(time),
        }
    }

    pub fn format_zoned(self, time: SystemTime, zone: &TimeZone) -> String {
        match self {
            Self::DefaultFormat  => default_zoned(time, zone),
            Self::ISOFormat      => iso_zoned(time, zone),
            Self::LongISO        => long_zoned(time, zone),
            Self::FullISO        => full_zoned(time, zone),
            Self::Relative       => relative(time),
        }
    }
}


#[allow(trivial_numeric_casts)]
fn default_local(time: SystemTime) -> String {
    let date = LocalDateTime::at(systemtime_epoch(time));
    let date_format = get_dateformat(&date);
    date_format.format(&date, &*LOCALE)
}

#[allow(trivial_numeric_casts)]
fn default_zoned(time: SystemTime, zone: &TimeZone) -> String {
    let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
    let date_format = get_dateformat(&date);
    date_format.format(&date, &*LOCALE)
}

fn get_dateformat(date: &LocalDateTime) -> &'static DateFormat<'static> {
    match (is_recent(date), *MAXIMUM_MONTH_WIDTH) {
        (true, 4)   => &FOUR_WIDE_DATE_TIME,
        (true, 5)   => &FIVE_WIDE_DATE_TIME,
        (true, _)   => &OTHER_WIDE_DATE_TIME,
        (false, 4)  => &FOUR_WIDE_DATE_YEAR,
        (false, 5)  => &FIVE_WIDE_DATE_YEAR,
        (false, _)  => &OTHER_WIDE_DATE_YEAR,
    }
}

#[allow(trivial_numeric_casts)]
fn long_local(time: SystemTime) -> String {
    let date = LocalDateTime::at(systemtime_epoch(time));
    format!("{:04}-{:02}-{:02} {:02}:{:02}",
            date.year(), date.month() as usize, date.day(),
            date.hour(), date.minute())
}

#[allow(trivial_numeric_casts)]
fn long_zoned(time: SystemTime, zone: &TimeZone) -> String {
    let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
    format!("{:04}-{:02}-{:02} {:02}:{:02}",
            date.year(), date.month() as usize, date.day(),
            date.hour(), date.minute())
}

#[allow(trivial_numeric_casts)]
fn relative(time: SystemTime) -> String {
    timeago::Formatter::new()
        .ago("")
        .convert(
            Duration::from_secs(
                max(0, Instant::now().seconds() - systemtime_epoch(time))
                // this .unwrap is safe since the call above can never result in a 
                // value < 0
                .try_into().unwrap()
            )
        )
}

#[allow(trivial_numeric_casts)]
fn full_local(time: SystemTime) -> String {
    let date = LocalDateTime::at(systemtime_epoch(time));
    format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}",
            date.year(), date.month() as usize, date.day(),
            date.hour(), date.minute(), date.second(), systemtime_nanos(time))
}

#[allow(trivial_numeric_casts)]
fn full_zoned(time: SystemTime, zone: &TimeZone) -> String {
    use datetime::Offset;

    let local = LocalDateTime::at(systemtime_epoch(time));
    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(), systemtime_nanos(time),
            offset.hours(), offset.minutes().abs())
}

#[allow(trivial_numeric_casts)]
fn iso_local(time: SystemTime) -> String {
    let date = LocalDateTime::at(systemtime_epoch(time));

    if is_recent(&date) {
        format!("{:02}-{:02} {:02}:{:02}",
                date.month() as usize, date.day(),
                date.hour(), date.minute())
    }
    else {
        format!("{:04}-{:02}-{:02}",
                date.year(), date.month() as usize, date.day())
    }
}

#[allow(trivial_numeric_casts)]
fn iso_zoned(time: SystemTime, zone: &TimeZone) -> String {
    let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));

    if is_recent(&date) {
        format!("{:02}-{:02} {:02}:{:02}",
                date.month() as usize, date.day(),
                date.hour(), date.minute())
    }
    else {
        format!("{:04}-{:02}-{:02}",
                date.year(), date.month() as usize, date.day())
    }
}


fn systemtime_epoch(time: SystemTime) -> i64 {
    time.duration_since(UNIX_EPOCH)
        .map(|t| t.as_secs() as i64)
        .unwrap_or_else(|e| {
            let diff = e.duration();
            let mut secs = diff.as_secs();
            if diff.subsec_nanos() > 0 {
                secs += 1;
            }
            -(secs as i64)
        })
}

fn systemtime_nanos(time: SystemTime) -> u32 {
    time.duration_since(UNIX_EPOCH)
        .map(|t| t.subsec_nanos())
        .unwrap_or_else(|e| {
            let nanos = e.duration().subsec_nanos();
            if nanos > 0 {
                1_000_000_000 - nanos
            } else {
                nanos
            }
        })
}

fn is_recent(date: &LocalDateTime) -> bool {
    date.year() == *CURRENT_YEAR
}


lazy_static! {

    static ref CURRENT_YEAR: i64 = LocalDateTime::now().year();

    static ref LOCALE: locale::Time = {
        locale::Time::load_user_locale()
               .unwrap_or_else(|_| locale::Time::english())
    };

    static ref MAXIMUM_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.
        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 = std::cmp::max(maximum_month_width, current_month_width);
        }
        maximum_month_width
    };

    static ref FOUR_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
        "{2>:D} {4<:M} {02>:h}:{02>:m}"
    ).unwrap();

    static ref FIVE_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
        "{2>:D} {5<:M} {02>:h}:{02>:m}"
    ).unwrap();

    static ref OTHER_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
        "{2>:D} {:M} {02>:h}:{02>:m}"
    ).unwrap();

    static ref FOUR_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse(
        "{2>:D} {4<:M} {5>:Y}"
    ).unwrap();

    static ref FIVE_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse(
        "{2>:D} {5<:M} {5>:Y}"
    ).unwrap();

    static ref OTHER_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse(
        "{2>:D} {:M} {5>:Y}"
    ).unwrap();
}