use clap::{command, Arg};
use fundu::TimeUnit::*;
use fundu::{
CustomDurationParserBuilder, CustomTimeUnit, Duration, Multiplier, SaturatingInto, TimeKeyword,
};
const GNU_TIME_UNITS: [CustomTimeUnit<'static>; 8] = [
CustomTimeUnit::with_default(Second, &["sec", "secs", "second", "seconds"]),
CustomTimeUnit::with_default(Minute, &["min", "mins", "minute", "minutes"]),
CustomTimeUnit::with_default(Hour, &["hour", "hours"]),
CustomTimeUnit::with_default(Day, &["day", "days"]),
CustomTimeUnit::with_default(Week, &["week", "weeks"]),
CustomTimeUnit::new(Week, &["fortnight", "fortnights"], Some(Multiplier(2, 0))),
CustomTimeUnit::with_default(Month, &["month", "months"]),
CustomTimeUnit::with_default(Year, &["year", "years"]),
];
const GNU_KEYWORDS: [TimeKeyword<'static>; 3] = [
TimeKeyword::new(Day, &["yesterday"], Some(Multiplier(-1, 0))),
TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0))),
TimeKeyword::new(Day, &["now", "today"], Some(Multiplier(0, 0))),
];
const DELIMITER: fn(u8) -> bool =
|byte| matches!(byte, b' ' | b'\t' | b'\n' | b'\x0c' | b'\r' | b'\x0b');
const PARSER_BUILDER: CustomDurationParserBuilder = CustomDurationParserBuilder::new()
.allow_ago()
.allow_time_unit_delimiter()
.allow_negative()
.disable_exponent()
.disable_fraction()
.disable_infinity()
.number_is_optional()
.inner_delimiter(DELIMITER)
.outer_delimiter(DELIMITER)
.parse_multiple(None);
fn make_plural(time: u64, singular: &str) -> String {
if time > 1 {
format!("{}s", singular)
} else {
singular.to_string()
}
}
fn make_human(duration: Duration) -> String {
const YEAR: u64 = Year.multiplier().0.unsigned_abs();
const MONTH: u64 = Month.multiplier().0.unsigned_abs();
const WEEK: u64 = Week.multiplier().0.unsigned_abs();
const DAY: u64 = Day.multiplier().0.unsigned_abs();
const HOUR: u64 = Hour.multiplier().0.unsigned_abs();
const MINUTE: u64 = Minute.multiplier().0.unsigned_abs();
if duration.is_zero() {
return "0sec".to_string();
}
let std_duration: std::time::Duration = duration.abs().saturating_into();
let mut result = Vec::with_capacity(10);
let mut secs = std_duration.as_secs();
if secs > 0 {
if secs >= YEAR {
let years = secs / YEAR;
result.push(format!("{}{}", years, make_plural(years, "year")));
secs %= YEAR;
}
if secs >= MONTH {
let months = secs / MONTH;
result.push(format!("{}{}", months, make_plural(months, "month")));
secs %= MONTH;
}
if secs >= WEEK {
let weeks = secs / WEEK;
result.push(format!("{}{}", weeks, make_plural(weeks, "week")));
secs %= WEEK;
}
if secs >= DAY {
let days = secs / DAY;
result.push(format!("{}{}", days, make_plural(days, "day")));
secs %= DAY;
}
if secs >= HOUR {
let hours = secs / HOUR;
result.push(format!("{}{}", hours, make_plural(hours, "hour")));
secs %= HOUR;
}
if secs >= MINUTE {
let minutes = secs / MINUTE;
result.push(format!("{}{}", minutes, make_plural(minutes, "min")));
secs %= MINUTE;
}
if secs >= 1 {
result.push(format!("{}{}", secs, make_plural(secs, "sec")));
}
}
if duration.is_negative() {
format!("-{}", &result.join(" -"))
} else {
result.join(" ")
}
}
fn main() {
let matches = command!()
.about(
"A gnu relative time parser as specified in `info '(coreutils) Relative items in \
date'`.",
)
.allow_negative_numbers(true)
.allow_hyphen_values(true)
.arg(
Arg::new("GNU_RELATIVE_TIME")
.action(clap::ArgAction::Set)
.help(
"A relative time as specified in `info '(coreutils) Relative items in date \
strings'`",
),
)
.get_matches();
let parser = PARSER_BUILDER
.time_units(&GNU_TIME_UNITS)
.keywords(&GNU_KEYWORDS)
.build();
let input: &String = matches
.get_one("GNU_RELATIVE_TIME")
.expect("One argument must be present");
match parser.parse(input.trim()) {
Ok(duration) => {
let std_duration: std::time::Duration = duration.abs().saturating_into();
println!("{:>8}: {}", "Original", input);
println!(
"{:>8}: {}",
"Seconds",
if duration.is_negative() {
format!("-{}", std_duration.as_secs())
} else {
std_duration.as_secs().to_string()
}
);
println!("{:>8}: {}", "Human", make_human(duration));
}
Err(error) => eprintln!("Failed to parse relative time '{}': {}", &input, error),
}
}