diff --git a/Cargo.lock b/Cargo.lock index 8bdd205af..92e485a06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1382,6 +1382,12 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.28" @@ -2524,7 +2530,7 @@ dependencies = [ "openssl", "pretty_assertions", "reedline", - "rstest", + "rstest 0.17.0", "serde_json", "serial_test", "signal-hook", @@ -2569,7 +2575,7 @@ dependencies = [ "once_cell", "percent-encoding", "reedline", - "rstest", + "rstest 0.17.0", "sysinfo", "unicode-segmentation", ] @@ -2727,7 +2733,7 @@ dependencies = [ "rayon", "regex", "roxmltree", - "rstest", + "rstest 0.17.0", "rusqlite", "same-file", "serde", @@ -2812,7 +2818,7 @@ dependencies = [ "nu-path", "nu-plugin", "nu-protocol", - "rstest", + "rstest 0.17.0", "serde_json", ] @@ -2860,6 +2866,7 @@ dependencies = [ "nu-test-support", "nu-utils", "num-format", + "rstest 0.18.1", "serde", "serde_json", "strum 0.25.0", @@ -4176,6 +4183,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" +[[package]] +name = "relative-path" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bf2521270932c3c7bed1a59151222bd7643c79310f2916f01925e1e16255698" + [[package]] name = "rmp" version = "0.8.11" @@ -4213,7 +4226,19 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962" dependencies = [ - "rstest_macros", + "rstest_macros 0.17.0", + "rustc_version", +] + +[[package]] +name = "rstest" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b96577ca10cb3eade7b337eb46520108a67ca2818a24d0b63f41fd62bc9651c" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros 0.18.1", "rustc_version", ] @@ -4231,6 +4256,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rstest_macros" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225e674cf31712b8bb15fdbca3ec0c1b9d825c5a24407ff2b7e005fb6a29ba03" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.23", + "unicode-ident", +] + [[package]] name = "rusqlite" version = "0.29.0" diff --git a/crates/nu-command/src/conversions/into/duration.rs b/crates/nu-command/src/conversions/into/duration.rs index 8c98ea6a5..ad0e721ed 100644 --- a/crates/nu-command/src/conversions/into/duration.rs +++ b/crates/nu-command/src/conversions/into/duration.rs @@ -6,6 +6,7 @@ use nu_protocol::{ Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Unit, Value, }; +const NS_PER_SEC: i64 = 1_000_000_000; #[derive(Clone)] pub struct SubCommand; @@ -20,9 +21,10 @@ impl Command for SubCommand { (Type::String, Type::Duration), (Type::Duration, Type::Duration), (Type::Table(vec![]), Type::Table(vec![])), - (Type::Record(vec![]), Type::Record(vec![])), + //todo: record | into duration -> Duration + //(Type::Record(vec![]), Type::Record(vec![])), ]) - .allow_variants_without_examples(true) + //.allow_variants_without_examples(true) .rest( "rest", SyntaxShape::CellPath, @@ -36,7 +38,7 @@ impl Command for SubCommand { } fn extra_usage(&self) -> &str { - "This command does not take leap years into account, and every month is assumed to have 30 days." + "Max duration value is i64::MAX nanoseconds; max duration time unit is wk (weeks)." } fn search_terms(&self) -> Vec<&str> { @@ -57,7 +59,23 @@ impl Command for SubCommand { let span = Span::test_data(); vec![ Example { - description: "Convert string to duration in table", + description: "Convert duration string to duration value", + example: "'7min' | into duration", + result: Some(Value::Duration { + val: 7 * 60 * NS_PER_SEC, + span, + }), + }, + Example { + description: "Convert compound duration string to duration value", + example: "'1day 2hr 3min 4sec' | into duration", + result: Some(Value::Duration { + val: (((((/* 1 * */24) + 2) * 60) + 3) * 60 + 4) * NS_PER_SEC, + span, + }), + }, + Example { + description: "Convert table of duration strings to table of duration values", example: "[[value]; ['1sec'] ['2min'] ['3hr'] ['4day'] ['5wk']] | into duration value", result: Some(Value::List { @@ -65,7 +83,7 @@ impl Command for SubCommand { Value::Record { cols: vec!["value".to_string()], vals: vec![Value::Duration { - val: 1000 * 1000 * 1000, + val: NS_PER_SEC, span, }], span, @@ -73,7 +91,7 @@ impl Command for SubCommand { Value::Record { cols: vec!["value".to_string()], vals: vec![Value::Duration { - val: 2 * 60 * 1000 * 1000 * 1000, + val: 2 * 60 * NS_PER_SEC, span, }], span, @@ -81,7 +99,7 @@ impl Command for SubCommand { Value::Record { cols: vec!["value".to_string()], vals: vec![Value::Duration { - val: 3 * 60 * 60 * 1000 * 1000 * 1000, + val: 3 * 60 * 60 * NS_PER_SEC, span, }], span, @@ -89,7 +107,7 @@ impl Command for SubCommand { Value::Record { cols: vec!["value".to_string()], vals: vec![Value::Duration { - val: 4 * 24 * 60 * 60 * 1000 * 1000 * 1000, + val: 4 * 24 * 60 * 60 * NS_PER_SEC, span, }], span, @@ -97,7 +115,7 @@ impl Command for SubCommand { Value::Record { cols: vec!["value".to_string()], vals: vec![Value::Duration { - val: 5 * 7 * 24 * 60 * 60 * 1000 * 1000 * 1000, + val: 5 * 7 * 24 * 60 * 60 * NS_PER_SEC, span, }], span, @@ -106,19 +124,11 @@ impl Command for SubCommand { span, }), }, - Example { - description: "Convert string to duration", - example: "'7min' | into duration", - result: Some(Value::Duration { - val: 7 * 60 * 1000 * 1000 * 1000, - span, - }), - }, Example { description: "Convert duration to duration", example: "420sec | into duration", result: Some(Value::Duration { - val: 7 * 60 * 1000 * 1000 * 1000, + val: 7 * 60 * NS_PER_SEC, span, }), }, @@ -161,7 +171,32 @@ fn into_duration( ) } -fn string_to_duration(s: &str, span: Span, value_span: Span) -> Result { +// convert string list of duration values to duration NS. +// technique for getting substrings and span based on: https://stackoverflow.com/a/67098851/2036651 +#[inline] +fn addr_of(s: &str) -> usize { + s.as_ptr() as usize +} + +fn split_whitespace_indices(s: &str, span: Span) -> impl Iterator { + s.split_whitespace().map(move |sub| { + let start_offset = span.start + addr_of(sub) - addr_of(s); + (sub, Span::new(start_offset, start_offset + sub.len())) + }) +} + +fn compound_to_duration(s: &str, span: Span) -> Result { + let mut duration_ns: i64 = 0; + + for (substring, substring_span) in split_whitespace_indices(s, span) { + let sub_ns = string_to_duration(substring, substring_span)?; + duration_ns += sub_ns; + } + + Ok(duration_ns) +} + +fn string_to_duration(s: &str, span: Span) -> Result { if let Some(Ok(expression)) = parse_unit_value( s.as_bytes(), span, @@ -175,11 +210,11 @@ fn string_to_duration(s: &str, span: Span, value_span: Span) -> Result return Ok(x), Unit::Microsecond => return Ok(x * 1000), Unit::Millisecond => return Ok(x * 1000 * 1000), - Unit::Second => return Ok(x * 1000 * 1000 * 1000), - Unit::Minute => return Ok(x * 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::Week => return Ok(x * 7 * 24 * 60 * 60 * 1000 * 1000 * 1000), + Unit::Second => return Ok(x * NS_PER_SEC), + Unit::Minute => return Ok(x * 60 * NS_PER_SEC), + Unit::Hour => return Ok(x * 60 * 60 * NS_PER_SEC), + Unit::Day => return Ok(x * 24 * 60 * 60 * NS_PER_SEC), + Unit::Week => return Ok(x * 7 * 24 * 60 * 60 * NS_PER_SEC), _ => {} } } @@ -189,11 +224,8 @@ fn string_to_duration(s: &str, span: Span, value_span: Span) -> Result Value { Value::String { val, span: value_span, - } => match string_to_duration(val, span, *value_span) { + } => match compound_to_duration(val, *value_span) { Ok(val) => Value::Duration { val, span }, Err(error) => Value::Error { error: Box::new(error), @@ -225,6 +257,7 @@ fn action(input: &Value, span: Span) -> Value { #[cfg(test)] mod test { use super::*; + use rstest::rstest; #[test] fn test_examples() { @@ -233,130 +266,33 @@ mod test { test_examples(SubCommand {}) } - #[test] - fn turns_ns_to_duration() { - let span = Span::new(0, 2); - let word = Value::test_string("3ns"); - let expected = Value::Duration { val: 3, span }; + const NS_PER_SEC: i64 = 1_000_000_000; - let actual = action(&word, span); - assert_eq!(actual, expected); - } + #[rstest] + #[case("3ns", 3)] + #[case("4us", 4*1000)] + #[case("4\u{00B5}s", 4*1000)] // micro sign + #[case("4\u{03BC}s", 4*1000)] // mu symbol + #[case("5ms", 5 * 1000 * 1000)] + #[case("1sec", 1 * NS_PER_SEC)] + #[case("7min", 7 * 60 * NS_PER_SEC)] + #[case("42hr", 42 * 60 * 60 * NS_PER_SEC)] + #[case("123day", 123 * 24 * 60 * 60 * NS_PER_SEC)] + #[case("3wk", 3 * 7 * 24 * 60 * 60 * NS_PER_SEC)] + #[case("86hr 26ns", 86 * 3600 * NS_PER_SEC + 26)] // compound duration string + #[case("14ns 3hr 17sec", 14 + 3 * 3600 * NS_PER_SEC + 17 * NS_PER_SEC)] // compound string with units in random order - #[test] - fn turns_us_to_duration() { - let span = Span::new(0, 2); - let word = Value::test_string("4us"); - let expected = Value::Duration { - val: 4 * 1000, - span, - }; - - let actual = action(&word, span); - assert_eq!(actual, expected); - } - - #[test] - fn turns_micro_sign_s_to_duration() { - let span = Span::new(0, 2); - let word = Value::test_string("4\u{00B5}s"); - let expected = Value::Duration { - val: 4 * 1000, - span, - }; - - let actual = action(&word, span); - assert_eq!(actual, expected); - } - - #[test] - fn turns_mu_s_to_duration() { - let span = Span::new(0, 2); - let word = Value::test_string("4\u{03BC}s"); - let expected = Value::Duration { - val: 4 * 1000, - span, - }; - - let actual = action(&word, span); - assert_eq!(actual, expected); - } - - #[test] - fn turns_ms_to_duration() { - let span = Span::new(0, 2); - let word = Value::test_string("5ms"); - let expected = Value::Duration { - val: 5 * 1000 * 1000, - span, - }; - - let actual = action(&word, span); - assert_eq!(actual, expected); - } - - #[test] - fn turns_sec_to_duration() { - let span = Span::new(0, 3); - let word = Value::test_string("1sec"); - let expected = Value::Duration { - val: 1000 * 1000 * 1000, - span, - }; - - let actual = action(&word, span); - assert_eq!(actual, expected); - } - - #[test] - fn turns_min_to_duration() { - let span = Span::new(0, 3); - let word = Value::test_string("7min"); - let expected = Value::Duration { - val: 7 * 60 * 1000 * 1000 * 1000, - span, - }; - - let actual = action(&word, span); - assert_eq!(actual, expected); - } - - #[test] - fn turns_hr_to_duration() { - let span = Span::new(0, 3); - let word = Value::test_string("42hr"); - let expected = Value::Duration { - val: 42 * 60 * 60 * 1000 * 1000 * 1000, - span, - }; - - let actual = action(&word, span); - assert_eq!(actual, expected); - } - - #[test] - fn turns_day_to_duration() { - let span = Span::new(0, 5); - let word = Value::test_string("123day"); - let expected = Value::Duration { - val: 123 * 24 * 60 * 60 * 1000 * 1000 * 1000, - span, - }; - - let actual = action(&word, span); - assert_eq!(actual, expected); - } - - #[test] - fn turns_wk_to_duration() { - let span = Span::new(0, 2); - let word = Value::test_string("3wk"); - let expected = Value::Duration { - val: 3 * 7 * 24 * 60 * 60 * 1000 * 1000 * 1000, - span, - }; - - let actual = action(&word, span); - assert_eq!(actual, expected); + fn turns_string_to_duration(#[case] phrase: &str, #[case] expected_duration_val: i64) { + let actual = action(&Value::test_string(phrase), Span::new(0, phrase.len())); + match actual { + Value::Duration { + val: observed_val, .. + } => { + assert_eq!(expected_duration_val, observed_val, "expected != observed") + } + other => { + panic!("Expected Value::Duration, observed {other:?}"); + } + } } } diff --git a/crates/nu-command/src/conversions/into/record.rs b/crates/nu-command/src/conversions/into/record.rs index 10be02dfd..4f6f5af14 100644 --- a/crates/nu-command/src/conversions/into/record.rs +++ b/crates/nu-command/src/conversions/into/record.rs @@ -83,21 +83,21 @@ impl Command for SubCommand { }), }, Example { - description: "convert duration to record", - example: "-500day | into record", + description: "convert duration to record (weeks max)", + example: "(-500day - 4hr - 5sec) | into record", result: Some(Value::Record { cols: vec![ - "year".into(), - "month".into(), "week".into(), "day".into(), + "hour".into(), + "second".into(), "sign".into(), ], vals: vec![ - Value::Int { val: 1, span }, + Value::Int { val: 71, span }, + Value::Int { val: 3, span }, Value::Int { val: 4, span }, - Value::Int { val: 2, span }, - Value::Int { val: 1, span }, + Value::Int { val: 5, span }, Value::String { val: "-".into(), span, @@ -261,8 +261,6 @@ fn parse_duration_into_record(duration: i64, span: Span) -> Value { "hr" => "hour".into(), "day" => "day".into(), "wk" => "week".into(), - "month" => "month".into(), - "yr" => "year".into(), _ => "unknown".into(), }); diff --git a/crates/nu-command/src/conversions/into/string.rs b/crates/nu-command/src/conversions/into/string.rs index f52d6a242..6249f3b1c 100644 --- a/crates/nu-command/src/conversions/into/string.rs +++ b/crates/nu-command/src/conversions/into/string.rs @@ -40,6 +40,7 @@ impl Command for SubCommand { (Type::Bool, Type::String), (Type::Filesize, Type::String), (Type::Date, Type::String), + (Type::Duration, Type::String), ( Type::List(Box::new(Type::Any)), Type::List(Box::new(Type::String)), @@ -145,6 +146,11 @@ impl Command for SubCommand { example: "1KiB | into string", result: Some(Value::test_string("1,024 B")), }, + Example { + description: "convert duration to string", + example: "9day | into string", + result: Some(Value::test_string("1wk 2day")), + }, ] } } @@ -239,6 +245,11 @@ fn action(input: &Value, args: &Arguments, span: Span) -> Value { val: input.into_string(", ", config), span, }, + Value::Duration { val: _, .. } => Value::String { + val: input.into_string("", config), + span, + }, + Value::Error { error } => Value::String { val: into_code(error).unwrap_or_default(), span, diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 89f33acf1..f88a93dfa 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -2318,6 +2318,7 @@ pub const FILESIZE_UNIT_GROUPS: &[UnitGroup] = &[ pub const DURATION_UNIT_GROUPS: &[UnitGroup] = &[ (Unit::Nanosecond, "ns", None), + // todo start adding aliases for duration units here (Unit::Microsecond, "us", Some((Unit::Nanosecond, 1000))), ( // µ Micro Sign diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index fd8225fe6..6e5dbc912 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -36,3 +36,4 @@ serde_json = "1.0" strum = "0.25" strum_macros = "0.25" nu-test-support = { path = "../nu-test-support", version = "0.83.2" } +rstest = "*" diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 81d517732..c6c6bb977 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -3766,8 +3766,8 @@ pub fn format_duration(duration: i64) -> String { pub fn format_duration_as_timeperiod(duration: i64) -> (i32, Vec) { // 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; + // Current duration doesn't know a date it's based on, weeks is the max time unit it can normalize into. + // Don't guess or estimate how many years or months it might contain. let (sign, duration) = if duration >= 0 { (1, duration) @@ -3777,20 +3777,6 @@ pub fn format_duration_as_timeperiod(duration: i64) -> (i32, Vec) { let dur = Duration::nanoseconds(duration); - /// 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) - } - - /// 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) - } - /// Split this a duration into number of whole weeks and the remainder fn split_weeks(duration: Duration) -> (Option, Duration) { let weeks = duration.num_weeks(); @@ -3856,17 +3842,8 @@ pub fn format_duration_as_timeperiod(duration: i64) -> (i32, Vec) { } 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); + let (weeks, remainder) = split_weeks(dur); if let Some(weeks) = weeks { periods.push(TimePeriod::Weeks(weeks)); } diff --git a/crates/nu-protocol/tests/test_value.rs b/crates/nu-protocol/tests/test_value.rs index be3f8e939..28faa4b1b 100644 --- a/crates/nu-protocol/tests/test_value.rs +++ b/crates/nu-protocol/tests/test_value.rs @@ -1,4 +1,5 @@ -use nu_protocol::{Span, Value}; +use nu_protocol::{Config, Span, Value}; +use rstest::rstest; #[test] fn test_comparison_nothing() { @@ -34,3 +35,16 @@ fn test_comparison_nothing() { )); } } + +#[rstest] +#[case(365 * 24 * 3600 * 1_000_000_000, "52wk 1day")] +#[case( ((((((((1 * 7) + 2) * 24 + 3) * 60 + 4) * 60) + 5) * 1000 + 6) * 1000 + 7) * 1000 + 8, +"1wk 2day 3hr 4min 5sec 6ms 7µs 8ns")] +fn test_duration_to_string(#[case] in_ns: i64, #[case] expected: &str) { + let dur = Value::test_duration(in_ns); + assert_eq!( + expected, + dur.into_string("", &Config::default()), + "expected != observed" + ); +}