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;
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum TimeFormat {
DefaultFormat,
ISOFormat,
LongISO,
FullISO,
Relative,
}
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))
.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 = {
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();
}