expand durations to include month, year, decade (#6123)

* expand durations to include month, year, decade

* remove commented out fn

* oops, found more debug comments

* tweaked tests for the new way, borrowed heavily from chrono-humanize-rs

* clippy

* grammar
This commit is contained in:
Darren Schroeder 2022-07-26 08:05:37 -05:00 committed by GitHub
parent f5856b0914
commit d856ac92f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 339 additions and 50 deletions

View File

@ -28,6 +28,10 @@ impl Command for SubCommand {
"Convert value to duration" "Convert value to duration"
} }
fn extra_usage(&self) -> &str {
"into duration does not take leap years into account and every month is calculated with 30 days"
}
fn search_terms(&self) -> Vec<&str> { fn search_terms(&self) -> Vec<&str> {
vec!["convert", "time", "period"] vec!["convert", "time", "period"]
} }
@ -149,6 +153,9 @@ fn string_to_duration(s: &str, span: Span, value_span: Span) -> Result<i64, Shel
Unit::Hour => return Ok(x * 60 * 60 * 1000 * 1000 * 1000), Unit::Hour => return Ok(x * 60 * 60 * 1000 * 1000 * 1000),
Unit::Day => return Ok(x * 24 * 60 * 60 * 1000 * 1000 * 1000), Unit::Day => return Ok(x * 24 * 60 * 60 * 1000 * 1000 * 1000),
Unit::Week => return Ok(x * 7 * 24 * 60 * 60 * 1000 * 1000 * 1000), Unit::Week => return Ok(x * 7 * 24 * 60 * 60 * 1000 * 1000 * 1000),
Unit::Month => return Ok(x * 30 * 24 * 60 * 60 * 1000 * 1000 * 1000), //30 days to a month
Unit::Year => return Ok(x * 365 * 24 * 60 * 60 * 1000 * 1000 * 1000), //365 days to a year
Unit::Decade => return Ok(x * 10 * 365 * 24 * 60 * 60 * 1000 * 1000 * 1000), //365 days to a year
_ => {} _ => {}
} }
} }

View File

@ -442,6 +442,14 @@ fn convert_to_value(
val: size * 1000 * 1000 * 1000 * 1000 * 1000, val: size * 1000 * 1000 * 1000 * 1000 * 1000,
span, span,
}), }),
Unit::Exabyte => Ok(Value::Filesize {
val: size * 1000 * 1000 * 1000 * 1000 * 1000 * 1000,
span,
}),
Unit::Zettabyte => Ok(Value::Filesize {
val: size * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000,
span,
}),
Unit::Kibibyte => Ok(Value::Filesize { Unit::Kibibyte => Ok(Value::Filesize {
val: size * 1024, val: size * 1024,
@ -463,6 +471,14 @@ fn convert_to_value(
val: size * 1024 * 1024 * 1024 * 1024 * 1024, val: size * 1024 * 1024 * 1024 * 1024 * 1024,
span, span,
}), }),
Unit::Exbibyte => Ok(Value::Filesize {
val: size * 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
span,
}),
Unit::Zebibyte => Ok(Value::Filesize {
val: size * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
span,
}),
Unit::Nanosecond => Ok(Value::Duration { val: size, span }), Unit::Nanosecond => Ok(Value::Duration { val: size, span }),
Unit::Microsecond => Ok(Value::Duration { Unit::Microsecond => Ok(Value::Duration {
@ -489,8 +505,8 @@ fn convert_to_value(
Some(val) => Ok(Value::Duration { val, span }), Some(val) => Ok(Value::Duration { val, span }),
None => Err(ShellError::OutsideSpannedLabeledError( None => Err(ShellError::OutsideSpannedLabeledError(
original_text.to_string(), original_text.to_string(),
"duration too large".into(), "day duration too large".into(),
"duration too large".into(), "day duration too large".into(),
expr.span, expr.span,
)), )),
}, },
@ -499,11 +515,40 @@ fn convert_to_value(
Some(val) => Ok(Value::Duration { val, span }), Some(val) => Ok(Value::Duration { val, span }),
None => Err(ShellError::OutsideSpannedLabeledError( None => Err(ShellError::OutsideSpannedLabeledError(
original_text.to_string(), original_text.to_string(),
"duration too large".into(), "week duration too large".into(),
"duration too large".into(), "week duration too large".into(),
expr.span, expr.span,
)), )),
}, },
Unit::Month => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24 * 30) {
Some(val) => Ok(Value::Duration { val, span }),
None => Err(ShellError::OutsideSpannedLabeledError(
original_text.to_string(),
"month duration too large".into(),
"month duration too large".into(),
expr.span,
)),
},
Unit::Year => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24 * 365) {
Some(val) => Ok(Value::Duration { val, span }),
None => Err(ShellError::OutsideSpannedLabeledError(
original_text.to_string(),
"year duration too large".into(),
"year duration too large".into(),
expr.span,
)),
},
Unit::Decade => {
match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24 * 365 * 10) {
Some(val) => Ok(Value::Duration { val, span }),
None => Err(ShellError::OutsideSpannedLabeledError(
original_text.to_string(),
"decade duration too large".into(),
"decade duration too large".into(),
expr.span,
)),
}
}
} }
} }
Expr::Var(..) => Err(ShellError::OutsideSpannedLabeledError( Expr::Var(..) => Err(ShellError::OutsideSpannedLabeledError(

View File

@ -291,7 +291,7 @@ fn duration_math() {
"# "#
)); ));
assert_eq!(actual.out, "8day"); assert_eq!(actual.out, "1wk 1day");
} }
#[test] #[test]
@ -315,7 +315,7 @@ fn duration_math_with_nanoseconds() {
"# "#
)); ));
assert_eq!(actual.out, "7day 10ns"); assert_eq!(actual.out, "1wk 10ns");
} }
#[test] #[test]
@ -327,7 +327,22 @@ fn duration_decimal_math_with_nanoseconds() {
"# "#
)); ));
assert_eq!(actual.out, "10day 10ns"); assert_eq!(actual.out, "1wk 3day 10ns");
}
#[test]
fn duration_decimal_math_with_all_units() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
5dec + 3yr + 2month + 1wk + 3day + 8hr + 10min + 16sec + 121ms + 11us + 12ns
"#
));
assert_eq!(
actual.out,
"53yr 2month 1wk 3day 8hr 10min 16sec 121ms 11µs 12ns"
);
} }
#[test] #[test]

View File

@ -1452,6 +1452,14 @@ fn compute(size: i64, unit: Unit, span: Span) -> Value {
val: size * 1000 * 1000 * 1000 * 1000 * 1000, val: size * 1000 * 1000 * 1000 * 1000 * 1000,
span, span,
}, },
Unit::Exabyte => Value::Filesize {
val: size * 1000 * 1000 * 1000 * 1000 * 1000 * 1000,
span,
},
Unit::Zettabyte => Value::Filesize {
val: size * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000,
span,
},
Unit::Kibibyte => Value::Filesize { Unit::Kibibyte => Value::Filesize {
val: size * 1024, val: size * 1024,
@ -1473,6 +1481,14 @@ fn compute(size: i64, unit: Unit, span: Span) -> Value {
val: size * 1024 * 1024 * 1024 * 1024 * 1024, val: size * 1024 * 1024 * 1024 * 1024 * 1024,
span, span,
}, },
Unit::Exbibyte => Value::Filesize {
val: size * 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
span,
},
Unit::Zebibyte => Value::Filesize {
val: size * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
span,
},
Unit::Nanosecond => Value::Duration { val: size, span }, Unit::Nanosecond => Value::Duration { val: size, span },
Unit::Microsecond => Value::Duration { Unit::Microsecond => Value::Duration {
@ -1499,8 +1515,8 @@ fn compute(size: i64, unit: Unit, span: Span) -> Value {
Some(val) => Value::Duration { val, span }, Some(val) => Value::Duration { val, span },
None => Value::Error { None => Value::Error {
error: ShellError::GenericError( error: ShellError::GenericError(
"duration too large".into(), "day duration too large".into(),
"duration too large".into(), "day duration too large".into(),
Some(span), Some(span),
None, None,
Vec::new(), Vec::new(),
@ -1511,8 +1527,44 @@ fn compute(size: i64, unit: Unit, span: Span) -> Value {
Some(val) => Value::Duration { val, span }, Some(val) => Value::Duration { val, span },
None => Value::Error { None => Value::Error {
error: ShellError::GenericError( error: ShellError::GenericError(
"duration too large".into(), "week duration too large".into(),
"duration too large".into(), "week duration too large".into(),
Some(span),
None,
Vec::new(),
),
},
},
Unit::Month => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24 * 30) {
Some(val) => Value::Duration { val, span },
None => Value::Error {
error: ShellError::GenericError(
"month duration too large".into(),
"month duration too large".into(),
Some(span),
None,
Vec::new(),
),
},
},
Unit::Year => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24 * 365) {
Some(val) => Value::Duration { val, span },
None => Value::Error {
error: ShellError::GenericError(
"year duration too large".into(),
"year duration too large".into(),
Some(span),
None,
Vec::new(),
),
},
},
Unit::Decade => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24 * 365 * 10) {
Some(val) => Value::Duration { val, span },
None => Value::Error {
error: ShellError::GenericError(
"decade duration too large".into(),
"decade duration too large".into(),
Some(span), Some(span),
None, None,
Vec::new(), Vec::new(),

View File

@ -2170,6 +2170,9 @@ pub fn parse_duration_bytes(bytes: &[u8], span: Span) -> Option<Expression> {
(Unit::Hour, "HR", Some((Unit::Minute, 60))), (Unit::Hour, "HR", Some((Unit::Minute, 60))),
(Unit::Day, "DAY", Some((Unit::Minute, 1440))), (Unit::Day, "DAY", Some((Unit::Minute, 1440))),
(Unit::Week, "WK", Some((Unit::Day, 7))), (Unit::Week, "WK", Some((Unit::Day, 7))),
(Unit::Month, "MONTH", Some((Unit::Day, 30))), //30 day month
(Unit::Year, "YR", Some((Unit::Day, 365))), //365 day year
(Unit::Decade, "DEC", Some((Unit::Year, 10))), //365 day years
]; ];
if let Some(unit) = unit_groups.iter().find(|&x| upper.ends_with(x.1)) { if let Some(unit) = unit_groups.iter().find(|&x| upper.ends_with(x.1)) {
let mut lhs = token; let mut lhs = token;
@ -2264,11 +2267,15 @@ pub fn parse_filesize(
(Unit::Gigabyte, "GB", Some((Unit::Megabyte, 1000))), (Unit::Gigabyte, "GB", Some((Unit::Megabyte, 1000))),
(Unit::Terabyte, "TB", Some((Unit::Gigabyte, 1000))), (Unit::Terabyte, "TB", Some((Unit::Gigabyte, 1000))),
(Unit::Petabyte, "PB", Some((Unit::Terabyte, 1000))), (Unit::Petabyte, "PB", Some((Unit::Terabyte, 1000))),
(Unit::Exabyte, "EB", Some((Unit::Petabyte, 1000))),
(Unit::Zettabyte, "ZB", Some((Unit::Exabyte, 1000))),
(Unit::Kibibyte, "KIB", Some((Unit::Byte, 1024))), (Unit::Kibibyte, "KIB", Some((Unit::Byte, 1024))),
(Unit::Mebibyte, "MIB", Some((Unit::Kibibyte, 1024))), (Unit::Mebibyte, "MIB", Some((Unit::Kibibyte, 1024))),
(Unit::Gibibyte, "GIB", Some((Unit::Mebibyte, 1024))), (Unit::Gibibyte, "GIB", Some((Unit::Mebibyte, 1024))),
(Unit::Tebibyte, "TIB", Some((Unit::Gibibyte, 1024))), (Unit::Tebibyte, "TIB", Some((Unit::Gibibyte, 1024))),
(Unit::Pebibyte, "PIB", Some((Unit::Tebibyte, 1024))), (Unit::Pebibyte, "PIB", Some((Unit::Tebibyte, 1024))),
(Unit::Exbibyte, "EIB", Some((Unit::Pebibyte, 1024))),
(Unit::Zebibyte, "ZIB", Some((Unit::Exbibyte, 1024))),
(Unit::Byte, "B", None), (Unit::Byte, "B", None),
]; ];
if let Some(unit) = unit_groups.iter().find(|&x| upper.ends_with(x.1)) { if let Some(unit) = unit_groups.iter().find(|&x| upper.ends_with(x.1)) {

View File

@ -5,32 +5,32 @@ mod range;
mod stream; mod stream;
mod unit; mod unit;
use crate::ast::Operator;
use crate::ast::{CellPath, PathMember};
use crate::ShellError;
use crate::{did_you_mean, BlockId, Config, Span, Spanned, Type, VarId};
use byte_unit::ByteUnit; use byte_unit::ByteUnit;
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, Duration, FixedOffset};
use chrono_humanize::HumanTime; use chrono_humanize::HumanTime;
pub use custom_value::CustomValue;
pub use from_value::FromValue; pub use from_value::FromValue;
use indexmap::map::IndexMap; use indexmap::map::IndexMap;
use num_format::{Locale, ToFormattedString}; use num_format::{Locale, ToFormattedString};
pub use range::*; pub use range::*;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
collections::HashMap,
fmt::{Display, Formatter, Result as FmtResult},
iter,
path::PathBuf,
{cmp::Ordering, fmt::Debug},
};
pub use stream::*; pub use stream::*;
use sys_locale::get_locale; use sys_locale::get_locale;
pub use unit::*; pub use unit::*;
use std::collections::HashMap;
use std::path::PathBuf;
use std::{cmp::Ordering, fmt::Debug};
use crate::ast::{CellPath, PathMember};
use crate::{did_you_mean, BlockId, Config, Span, Spanned, Type, VarId};
use crate::ast::Operator;
pub use custom_value::CustomValue;
use std::iter;
use crate::ShellError;
/// Core structured values that pass through the pipeline in Nushell. /// Core structured values that pass through the pipeline in Nushell.
// NOTE: Please do not reorder these enum cases without thinking through the // NOTE: Please do not reorder these enum cases without thinking through the
// impact on the PartialOrd implementation and the global sort order // impact on the PartialOrd implementation and the global sort order
@ -2525,54 +2525,210 @@ impl From<Spanned<IndexMap<String, Value>>> for Value {
} }
} }
/// Format a duration in nanoseconds into a string /// Is the given year a leap year?
#[allow(clippy::nonminimal_bool)]
pub fn is_leap_year(year: i32) -> bool {
(year % 4 == 0) && (year % 100 != 0 || (year % 100 == 0 && year % 400 == 0))
}
#[derive(Clone, Copy)]
enum TimePeriod {
Nanos(i64),
Micros(i64),
Millis(i64),
Seconds(i64),
Minutes(i64),
Hours(i64),
Days(i64),
Weeks(i64),
Months(i64),
Years(i64),
}
impl TimePeriod {
fn to_text(self) -> Cow<'static, str> {
match self {
Self::Nanos(n) => format!("{}ns", n).into(),
Self::Micros(n) => format!("{}µs", n).into(),
Self::Millis(n) => format!("{}ms", n).into(),
Self::Seconds(n) => format!("{}sec", n).into(),
Self::Minutes(n) => format!("{}min", n).into(),
Self::Hours(n) => format!("{}hr", n).into(),
Self::Days(n) => format!("{}day", n).into(),
Self::Weeks(n) => format!("{}wk", n).into(),
Self::Months(n) => format!("{}month", n).into(),
Self::Years(n) => format!("{}yr", n).into(),
}
}
}
impl Display for TimePeriod {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", self.to_text())
}
}
pub fn format_duration(duration: i64) -> String { pub fn format_duration(duration: i64) -> String {
// Attribution: most of this is taken from chrono-humanize-rs. Thanks!
// https://gitlab.com/imp/chrono-humanize-rs/-/blob/master/src/humantime.rs
const DAYS_IN_YEAR: i64 = 365;
const DAYS_IN_MONTH: i64 = 30;
let (sign, duration) = if duration >= 0 { let (sign, duration) = if duration >= 0 {
(1, duration) (1, duration)
} else { } else {
(-1, -duration) (-1, -duration)
}; };
let (micros, nanos): (i64, i64) = (duration / 1000, duration % 1000);
let (millis, micros): (i64, i64) = (micros / 1000, micros % 1000);
let (secs, millis): (i64, i64) = (millis / 1000, millis % 1000);
let (mins, secs): (i64, i64) = (secs / 60, secs % 60);
let (hours, mins): (i64, i64) = (mins / 60, mins % 60);
let (days, hours): (i64, i64) = (hours / 24, hours % 24);
let mut output_prep = vec![]; let dur = Duration::nanoseconds(duration);
if days != 0 { /// Split this a duration into number of whole years and the remainder
output_prep.push(format!("{}day", days)); fn split_years(duration: Duration) -> (Option<i64>, Duration) {
let years = duration.num_days() / DAYS_IN_YEAR;
let remainder = duration - Duration::days(years * DAYS_IN_YEAR);
normalize_split(years, remainder)
} }
if hours != 0 { /// Split this a duration into number of whole months and the remainder
output_prep.push(format!("{}hr", hours)); fn split_months(duration: Duration) -> (Option<i64>, Duration) {
let months = duration.num_days() / DAYS_IN_MONTH;
let remainder = duration - Duration::days(months * DAYS_IN_MONTH);
normalize_split(months, remainder)
} }
if mins != 0 { /// Split this a duration into number of whole weeks and the remainder
output_prep.push(format!("{}min", mins)); fn split_weeks(duration: Duration) -> (Option<i64>, Duration) {
} let weeks = duration.num_weeks();
// output 0sec for zero duration let remainder = duration - Duration::weeks(weeks);
if duration == 0 || secs != 0 { normalize_split(weeks, remainder)
output_prep.push(format!("{}sec", secs));
} }
if millis != 0 { /// Split this a duration into number of whole days and the remainder
output_prep.push(format!("{}ms", millis)); fn split_days(duration: Duration) -> (Option<i64>, Duration) {
let days = duration.num_days();
let remainder = duration - Duration::days(days);
normalize_split(days, remainder)
} }
if micros != 0 { /// Split this a duration into number of whole hours and the remainder
output_prep.push(format!("{}us", micros)); fn split_hours(duration: Duration) -> (Option<i64>, Duration) {
let hours = duration.num_hours();
let remainder = duration - Duration::hours(hours);
normalize_split(hours, remainder)
} }
if nanos != 0 { /// Split this a duration into number of whole minutes and the remainder
output_prep.push(format!("{}ns", nanos)); fn split_minutes(duration: Duration) -> (Option<i64>, Duration) {
let minutes = duration.num_minutes();
let remainder = duration - Duration::minutes(minutes);
normalize_split(minutes, remainder)
} }
/// Split this a duration into number of whole seconds and the remainder
fn split_seconds(duration: Duration) -> (Option<i64>, Duration) {
let seconds = duration.num_seconds();
let remainder = duration - Duration::seconds(seconds);
normalize_split(seconds, remainder)
}
/// Split this a duration into number of whole milliseconds and the remainder
fn split_milliseconds(duration: Duration) -> (Option<i64>, Duration) {
let millis = duration.num_milliseconds();
let remainder = duration - Duration::milliseconds(millis);
normalize_split(millis, remainder)
}
/// Split this a duration into number of whole seconds and the remainder
fn split_microseconds(duration: Duration) -> (Option<i64>, Duration) {
let micros = duration.num_microseconds().unwrap_or_default();
let remainder = duration - Duration::microseconds(micros);
normalize_split(micros, remainder)
}
/// Split this a duration into number of whole seconds and the remainder
fn split_nanoseconds(duration: Duration) -> (Option<i64>, Duration) {
let nanos = duration.num_nanoseconds().unwrap_or_default();
let remainder = duration - Duration::nanoseconds(nanos);
normalize_split(nanos, remainder)
}
fn normalize_split(
wholes: impl Into<Option<i64>>,
remainder: Duration,
) -> (Option<i64>, Duration) {
let wholes = wholes.into().map(i64::abs).filter(|x| *x > 0);
(wholes, remainder)
}
let mut periods = vec![];
let (years, remainder) = split_years(dur);
if let Some(years) = years {
periods.push(TimePeriod::Years(years));
}
let (months, remainder) = split_months(remainder);
if let Some(months) = months {
periods.push(TimePeriod::Months(months));
}
let (weeks, remainder) = split_weeks(remainder);
if let Some(weeks) = weeks {
periods.push(TimePeriod::Weeks(weeks));
}
let (days, remainder) = split_days(remainder);
if let Some(days) = days {
periods.push(TimePeriod::Days(days));
}
let (hours, remainder) = split_hours(remainder);
if let Some(hours) = hours {
periods.push(TimePeriod::Hours(hours));
}
let (minutes, remainder) = split_minutes(remainder);
if let Some(minutes) = minutes {
periods.push(TimePeriod::Minutes(minutes));
}
let (seconds, remainder) = split_seconds(remainder);
if let Some(seconds) = seconds {
periods.push(TimePeriod::Seconds(seconds));
}
let (millis, remainder) = split_milliseconds(remainder);
if let Some(millis) = millis {
periods.push(TimePeriod::Millis(millis));
}
let (micros, remainder) = split_microseconds(remainder);
if let Some(micros) = micros {
periods.push(TimePeriod::Micros(micros));
}
let (nanos, _remainder) = split_nanoseconds(remainder);
if let Some(nanos) = nanos {
periods.push(TimePeriod::Nanos(nanos));
}
if periods.is_empty() {
periods.push(TimePeriod::Seconds(0));
}
// let last = periods.pop().map(|last| last.to_text().to_string());
let text = periods
.into_iter()
.map(|p| p.to_text().to_string())
.collect::<Vec<String>>();
// if let Some(last) = last {
// text.push(format!("and {}", last));
// }
format!( format!(
"{}{}", "{}{}",
if sign == -1 { "-" } else { "" }, if sign == -1 { "-" } else { "" },
output_prep.join(" ") text.join(" ").trim()
) )
} }

View File

@ -9,6 +9,8 @@ pub enum Unit {
Gigabyte, Gigabyte,
Terabyte, Terabyte,
Petabyte, Petabyte,
Exabyte,
Zettabyte,
// Filesize units: ISO/IEC 80000 // Filesize units: ISO/IEC 80000
Kibibyte, Kibibyte,
@ -16,6 +18,8 @@ pub enum Unit {
Gibibyte, Gibibyte,
Tebibyte, Tebibyte,
Pebibyte, Pebibyte,
Exbibyte,
Zebibyte,
// Duration units // Duration units
Nanosecond, Nanosecond,
@ -26,4 +30,7 @@ pub enum Unit {
Hour, Hour,
Day, Day,
Week, Week,
Month,
Year,
Decade,
} }