From d856ac92f4e42ca38df2a55dab98079beb75882f Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Tue, 26 Jul 2022 08:05:37 -0500 Subject: [PATCH] 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 --- .../src/conversions/into/duration.rs | 7 + crates/nu-command/src/formats/from/nuon.rs | 53 +++- crates/nu-command/tests/commands/math/mod.rs | 21 +- crates/nu-engine/src/eval.rs | 60 ++++- crates/nu-parser/src/parser.rs | 7 + crates/nu-protocol/src/value/mod.rs | 234 +++++++++++++++--- crates/nu-protocol/src/value/unit.rs | 7 + 7 files changed, 339 insertions(+), 50 deletions(-) diff --git a/crates/nu-command/src/conversions/into/duration.rs b/crates/nu-command/src/conversions/into/duration.rs index 1c5ef6e6a..ff2419c1e 100644 --- a/crates/nu-command/src/conversions/into/duration.rs +++ b/crates/nu-command/src/conversions/into/duration.rs @@ -28,6 +28,10 @@ impl Command for SubCommand { "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> { vec!["convert", "time", "period"] } @@ -149,6 +153,9 @@ fn string_to_duration(s: &str, span: Span, value_span: Span) -> Result return Ok(x * 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::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 _ => {} } } diff --git a/crates/nu-command/src/formats/from/nuon.rs b/crates/nu-command/src/formats/from/nuon.rs index e45ee5884..1f26de136 100644 --- a/crates/nu-command/src/formats/from/nuon.rs +++ b/crates/nu-command/src/formats/from/nuon.rs @@ -442,6 +442,14 @@ fn convert_to_value( val: size * 1000 * 1000 * 1000 * 1000 * 1000, 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 { val: size * 1024, @@ -463,6 +471,14 @@ fn convert_to_value( val: size * 1024 * 1024 * 1024 * 1024 * 1024, 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::Microsecond => Ok(Value::Duration { @@ -489,8 +505,8 @@ fn convert_to_value( Some(val) => Ok(Value::Duration { val, span }), None => Err(ShellError::OutsideSpannedLabeledError( original_text.to_string(), - "duration too large".into(), - "duration too large".into(), + "day duration too large".into(), + "day duration too large".into(), expr.span, )), }, @@ -499,11 +515,40 @@ fn convert_to_value( Some(val) => Ok(Value::Duration { val, span }), None => Err(ShellError::OutsideSpannedLabeledError( original_text.to_string(), - "duration too large".into(), - "duration too large".into(), + "week duration too large".into(), + "week duration too large".into(), 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( diff --git a/crates/nu-command/tests/commands/math/mod.rs b/crates/nu-command/tests/commands/math/mod.rs index 362701822..f7136139a 100644 --- a/crates/nu-command/tests/commands/math/mod.rs +++ b/crates/nu-command/tests/commands/math/mod.rs @@ -291,7 +291,7 @@ fn duration_math() { "# )); - assert_eq!(actual.out, "8day"); + assert_eq!(actual.out, "1wk 1day"); } #[test] @@ -315,7 +315,7 @@ fn duration_math_with_nanoseconds() { "# )); - assert_eq!(actual.out, "7day 10ns"); + assert_eq!(actual.out, "1wk 10ns"); } #[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] diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index b20d646be..81971b44c 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -1452,6 +1452,14 @@ fn compute(size: i64, unit: Unit, span: Span) -> Value { val: size * 1000 * 1000 * 1000 * 1000 * 1000, 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 { val: size * 1024, @@ -1473,6 +1481,14 @@ fn compute(size: i64, unit: Unit, span: Span) -> Value { val: size * 1024 * 1024 * 1024 * 1024 * 1024, 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::Microsecond => Value::Duration { @@ -1499,8 +1515,8 @@ fn compute(size: i64, unit: Unit, span: Span) -> Value { Some(val) => Value::Duration { val, span }, None => Value::Error { error: ShellError::GenericError( - "duration too large".into(), - "duration too large".into(), + "day duration too large".into(), + "day duration too large".into(), Some(span), None, Vec::new(), @@ -1511,8 +1527,44 @@ fn compute(size: i64, unit: Unit, span: Span) -> Value { Some(val) => Value::Duration { val, span }, None => Value::Error { error: ShellError::GenericError( - "duration too large".into(), - "duration too large".into(), + "week 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), None, Vec::new(), diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index d48d4083d..577b649e5 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -2170,6 +2170,9 @@ pub fn parse_duration_bytes(bytes: &[u8], span: Span) -> Option { (Unit::Hour, "HR", Some((Unit::Minute, 60))), (Unit::Day, "DAY", Some((Unit::Minute, 1440))), (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)) { let mut lhs = token; @@ -2264,11 +2267,15 @@ pub fn parse_filesize( (Unit::Gigabyte, "GB", Some((Unit::Megabyte, 1000))), (Unit::Terabyte, "TB", Some((Unit::Gigabyte, 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::Mebibyte, "MIB", Some((Unit::Kibibyte, 1024))), (Unit::Gibibyte, "GIB", Some((Unit::Mebibyte, 1024))), (Unit::Tebibyte, "TIB", Some((Unit::Gibibyte, 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), ]; if let Some(unit) = unit_groups.iter().find(|&x| upper.ends_with(x.1)) { diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 628f65744..ad48c8d2e 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -5,32 +5,32 @@ mod range; mod stream; 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 chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, Duration, FixedOffset}; use chrono_humanize::HumanTime; +pub use custom_value::CustomValue; pub use from_value::FromValue; use indexmap::map::IndexMap; use num_format::{Locale, ToFormattedString}; pub use range::*; use regex::Regex; 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::*; use sys_locale::get_locale; 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. // NOTE: Please do not reorder these enum cases without thinking through the // impact on the PartialOrd implementation and the global sort order @@ -2525,54 +2525,210 @@ impl From>> 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 { + // 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 { (1, duration) } else { (-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 { - output_prep.push(format!("{}day", days)); + /// Split this a duration into number of whole years and the remainder + fn split_years(duration: Duration) -> (Option, 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 { - output_prep.push(format!("{}hr", hours)); + /// Split this a duration into number of whole months and the remainder + fn split_months(duration: Duration) -> (Option, 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 { - output_prep.push(format!("{}min", mins)); - } - // output 0sec for zero duration - if duration == 0 || secs != 0 { - output_prep.push(format!("{}sec", secs)); + /// Split this a duration into number of whole weeks and the remainder + fn split_weeks(duration: Duration) -> (Option, Duration) { + let weeks = duration.num_weeks(); + let remainder = duration - Duration::weeks(weeks); + normalize_split(weeks, remainder) } - if millis != 0 { - output_prep.push(format!("{}ms", millis)); + /// Split this a duration into number of whole days and the remainder + fn split_days(duration: Duration) -> (Option, Duration) { + let days = duration.num_days(); + let remainder = duration - Duration::days(days); + normalize_split(days, remainder) } - if micros != 0 { - output_prep.push(format!("{}us", micros)); + /// Split this a duration into number of whole hours and the remainder + fn split_hours(duration: Duration) -> (Option, Duration) { + let hours = duration.num_hours(); + let remainder = duration - Duration::hours(hours); + normalize_split(hours, remainder) } - if nanos != 0 { - output_prep.push(format!("{}ns", nanos)); + /// Split this a duration into number of whole minutes and the remainder + fn split_minutes(duration: Duration) -> (Option, 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, 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, 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, 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, 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>, + remainder: Duration, + ) -> (Option, 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::>(); + + // if let Some(last) = last { + // text.push(format!("and {}", last)); + // } + format!( "{}{}", if sign == -1 { "-" } else { "" }, - output_prep.join(" ") + text.join(" ").trim() ) } diff --git a/crates/nu-protocol/src/value/unit.rs b/crates/nu-protocol/src/value/unit.rs index e11f1cf6f..54a899b03 100644 --- a/crates/nu-protocol/src/value/unit.rs +++ b/crates/nu-protocol/src/value/unit.rs @@ -9,6 +9,8 @@ pub enum Unit { Gigabyte, Terabyte, Petabyte, + Exabyte, + Zettabyte, // Filesize units: ISO/IEC 80000 Kibibyte, @@ -16,6 +18,8 @@ pub enum Unit { Gibibyte, Tebibyte, Pebibyte, + Exbibyte, + Zebibyte, // Duration units Nanosecond, @@ -26,4 +30,7 @@ pub enum Unit { Hour, Day, Week, + Month, + Year, + Decade, }