use super::locale::current_locale;
use crate::color::{ColoredString, Colors, Elem};
use crate::flags::{DateFlag, Flags};
use chrono::{DateTime, Duration, Local};
use chrono_humanize::HumanTime;
use std::fs::Metadata;
use std::panic;
use std::time::SystemTime;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Date {
Date(DateTime<Local>),
Invalid,
}
impl From<SystemTime> for Date {
fn from(systime: SystemTime) -> Self {
let res = panic::catch_unwind(|| systime.into());
res.map_or(Date::Invalid, Date::Date)
}
}
impl From<&Metadata> for Date {
fn from(meta: &Metadata) -> Self {
meta.modified()
.expect("failed to retrieve modified date")
.into()
}
}
impl Date {
pub fn render(&self, colors: &Colors, flags: &Flags) -> ColoredString {
let now = Local::now();
#[allow(deprecated)]
let elem = match self {
&Date::Date(modified) if modified > now - Duration::hours(1) => Elem::HourOld,
&Date::Date(modified) if modified > now - Duration::days(1) => Elem::DayOld,
&Date::Date(_) | Date::Invalid => Elem::Older,
};
colors.colorize(self.date_string(flags), &elem)
}
fn date_string(&self, flags: &Flags) -> String {
let locale = current_locale();
if let Date::Date(val) = self {
#[allow(deprecated)]
match &flags.date {
DateFlag::Date => val.format("%c").to_string(),
DateFlag::Locale => val.format_localized("%c", locale).to_string(),
DateFlag::Relative => HumanTime::from(*val - Local::now()).to_string(),
DateFlag::Iso => {
if *val > Local::now() - Duration::seconds(15_778_476) {
val.format("%m-%d %R").to_string()
} else {
val.format("%F").to_string()
}
}
DateFlag::Formatted(format) => val.format_localized(format, locale).to_string(),
}
} else {
String::from('-')
}
}
}
#[cfg(test)]
mod test {
use super::Date;
use crate::color::{Colors, ThemeOption};
use crate::flags::{DateFlag, Flags};
use crate::meta::locale::current_locale;
use chrono::{DateTime, Duration, Local};
use crossterm::style::{Color, Stylize};
use std::io;
use std::path::Path;
use std::process::{Command, ExitStatus};
use std::{env, fs};
#[cfg(unix)]
fn cross_platform_touch(path: &Path, date: &DateTime<Local>) -> io::Result<ExitStatus> {
Command::new("touch")
.arg("-t")
.arg(date.format("%Y%m%d%H%M.%S").to_string())
.arg(path)
.status()
}
#[cfg(windows)]
fn cross_platform_touch(path: &Path, date: &DateTime<Local>) -> io::Result<ExitStatus> {
use std::process::Stdio;
let copy_success = Command::new("cmd")
.arg("/C")
.arg("copy")
.arg("NUL")
.arg(path)
.stdout(Stdio::null()) .status()?
.success();
assert!(copy_success, "failed to create empty file");
Command::new("powershell")
.arg("-Command")
.arg(format!(
r#"$(Get-Item {}).lastwritetime=$(Get-Date "{}")"#,
path.display(),
date.to_rfc3339()
))
.status()
}
#[test]
fn test_an_hour_old_file_color() {
let mut file_path = env::temp_dir();
file_path.push("test_an_hour_old_file_color.tmp");
#[allow(deprecated)]
let creation_date = Local::now() - chrono::Duration::seconds(4);
let success = cross_platform_touch(&file_path, &creation_date)
.unwrap()
.success();
assert!(success, "failed to exec touch");
let colors = Colors::new(ThemeOption::Default);
let date = Date::from(&file_path.metadata().unwrap());
let flags = Flags::default();
assert_eq!(
creation_date
.format("%c")
.to_string()
.with(Color::AnsiValue(40)),
date.render(&colors, &flags)
);
fs::remove_file(file_path).unwrap();
}
#[test]
fn test_a_day_old_file_color() {
let mut file_path = env::temp_dir();
file_path.push("test_a_day_old_file_color.tmp");
#[allow(deprecated)]
let creation_date = Local::now() - chrono::Duration::hours(4);
let success = cross_platform_touch(&file_path, &creation_date)
.unwrap()
.success();
assert!(success, "failed to exec touch");
let colors = Colors::new(ThemeOption::Default);
let date = Date::from(&file_path.metadata().unwrap());
let flags = Flags::default();
assert_eq!(
creation_date
.format("%c")
.to_string()
.with(Color::AnsiValue(42)),
date.render(&colors, &flags)
);
fs::remove_file(file_path).unwrap();
}
#[test]
fn test_a_several_days_old_file_color() {
let mut file_path = env::temp_dir();
file_path.push("test_a_several_days_old_file_color.tmp");
#[allow(deprecated)]
let creation_date = Local::now() - chrono::Duration::days(2);
let success = cross_platform_touch(&file_path, &creation_date)
.unwrap()
.success();
assert!(success, "failed to exec touch");
let colors = Colors::new(ThemeOption::Default);
let date = Date::from(&file_path.metadata().unwrap());
let flags = Flags::default();
assert_eq!(
creation_date
.format("%c")
.to_string()
.with(Color::AnsiValue(36)),
date.render(&colors, &flags)
);
fs::remove_file(file_path).unwrap();
}
#[test]
fn test_with_relative_date() {
let mut file_path = env::temp_dir();
file_path.push("test_with_relative_date.tmp");
#[allow(deprecated)]
let creation_date = Local::now() - chrono::Duration::days(2);
let success = cross_platform_touch(&file_path, &creation_date)
.unwrap()
.success();
assert!(success, "failed to exec touch");
let colors = Colors::new(ThemeOption::Default);
let date = Date::from(&file_path.metadata().unwrap());
let flags = Flags {
date: DateFlag::Relative,
..Default::default()
};
assert_eq!(
"2 days ago".to_string().with(Color::AnsiValue(36)),
date.render(&colors, &flags)
);
fs::remove_file(file_path).unwrap();
}
#[test]
fn test_with_relative_date_now() {
let mut file_path = env::temp_dir();
file_path.push("test_with_relative_date_now.tmp");
let creation_date = Local::now();
let success = cross_platform_touch(&file_path, &creation_date)
.unwrap()
.success();
assert!(success, "failed to exec touch");
let colors = Colors::new(ThemeOption::Default);
let date = Date::from(&file_path.metadata().unwrap());
let flags = Flags {
date: DateFlag::Relative,
..Default::default()
};
assert_eq!(
"now".to_string().with(Color::AnsiValue(40)),
date.render(&colors, &flags)
);
fs::remove_file(file_path).unwrap();
}
#[test]
fn test_iso_format_now() {
let mut file_path = env::temp_dir();
file_path.push("test_iso_format_now.tmp");
let creation_date = Local::now();
let success = cross_platform_touch(&file_path, &creation_date)
.unwrap()
.success();
assert!(success, "failed to exec touch");
let colors = Colors::new(ThemeOption::Default);
let date = Date::from(&file_path.metadata().unwrap());
let flags = Flags {
date: DateFlag::Iso,
..Default::default()
};
assert_eq!(
creation_date
.format("%m-%d %R")
.to_string()
.with(Color::AnsiValue(40)),
date.render(&colors, &flags)
);
fs::remove_file(file_path).unwrap();
}
#[test]
fn test_iso_format_year_old() {
let mut file_path = env::temp_dir();
file_path.push("test_iso_format_year_old.tmp");
#[allow(deprecated)]
let creation_date = Local::now() - Duration::days(400);
let success = cross_platform_touch(&file_path, &creation_date)
.unwrap()
.success();
assert!(success, "failed to exec touch");
let colors = Colors::new(ThemeOption::Default);
let date = Date::from(&file_path.metadata().unwrap());
let flags = Flags {
date: DateFlag::Iso,
..Default::default()
};
assert_eq!(
creation_date
.format("%F")
.to_string()
.with(Color::AnsiValue(36)),
date.render(&colors, &flags)
);
fs::remove_file(file_path).unwrap();
}
#[test]
fn test_locale_format_now() {
let mut file_path = env::temp_dir();
file_path.push("test_locale_format_now.tmp");
let creation_date = Local::now();
let success = cross_platform_touch(&file_path, &creation_date)
.unwrap()
.success();
assert!(success, "failed to exec touch");
let colors = Colors::new(ThemeOption::Default);
let date = Date::from(&file_path.metadata().unwrap());
let flags = Flags {
date: DateFlag::Locale,
..Default::default()
};
assert_eq!(
creation_date
.format_localized("%c", current_locale())
.to_string()
.with(Color::AnsiValue(40)),
date.render(&colors, &flags)
);
fs::remove_file(file_path).unwrap();
}
#[test]
#[cfg(all(not(windows), target_arch = "x86_64"))]
fn test_bad_date() {
let end_time = std::time::SystemTime::UNIX_EPOCH
+ std::time::Duration::new(4437052 * 365 * 24 * 60 * 60, 0);
let colors = Colors::new(ThemeOption::Default);
let date = Date::from(end_time);
let flags = Flags {
date: DateFlag::Date,
..Default::default()
};
assert_eq!(
"-".to_string().with(Color::AnsiValue(36)),
date.render(&colors, &flags)
);
}
}