1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
|
// Copyright (c) 2023 Joining7943 <joining@posteo.de>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//! A gnu relative time parser as specified here
//! `https://www.gnu.org/software/coreutils/manual/html_node/Relative-items-in-date-strings.html`.
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))),
];
// Whitespace as defined in the POSIX locale (and used by gnu). In contrast to rust's definition of
// whitespace the POSIX definition includes the VERTICAL TAB:
// SPACE, HORIZONTAL TAB, LINE FEED, FORM FEED, CARRIAGE RETURN, VERTICAL TAB
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(DELIMITER)
.allow_delimiter(DELIMITER)
.allow_negative()
.disable_exponent()
.disable_fraction()
.disable_infinity()
.number_is_optional()
.parse_multiple(DELIMITER, None);
fn make_plural(time: u64, singular: &str) -> String {
if time > 1 {
format!("{}s", singular)
} else {
singular.to_string()
}
}
/// Create a human readable string like `100years 2hours 1min 40secs` from a `Duration`
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),
}
}
|