mirror of
https://github.com/nushell/nushell
synced 2024-12-26 04:53:09 +00:00
Fix duration type to not report months or years (#9632)
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> This PR should close #8036, #9028 (in the negative) and #9118. Fix for #9118 is a bit pedantic. As reported, the issue is: ``` > 2023-05-07T04:08:45+12:00 - 2019-05-10T09:59:12+12:00 3yr 12month 2day 18hr 9min 33sec ``` with this PR, you now get: ``` > 2023-05-07T04:08:45+12:00 - 2019-05-10T09:59:12+12:00 208wk 1day 18hr 9min 33sec ``` Which is strictly correct, but could still fairly be called "weird date arithmetic". # Description * [x] Abide by constraint that Value::Duration remains a number of nanoseconds with no additional fields. * [x] `to_string()` only displays weeks .. nanoseconds. Duration doesn't have base date to compute months or years from. * [x] `duration | into record` likewise only has fields for weeks .. nanoseconds. * [x] `string | into duration` now accepts compound form of duration to_string() (e.g '2day 3hr`, not just '2day') * [x] `duration | into string` now works (and produces the same representation as to_string(), which may be compound). # User-Facing Changes ## duration -> string -> duration Now you can "round trip" an arbitrary duration value: convert it to a string that may include multiple time units (a "compound" value), then convert that string back into a duration. This required changes to `string | into duration` and the addition of `duration | into string'. ``` > 2day + 3hr 2day 3hr # the "to_string()" representation (in this case, a compound value) > 2day + 3hr | into string 2day 3hr # string value > 2day + 3hr | into string | into duration 2day 3hr # round-trip duration -> string -> duration ``` Note that `to nuon` and `from nuon` already round-tripped durations, but use a different string representation. ## potentially breaking changes * string rendering of a duration no longer has 'yr' or 'month' phrases. * record from `duration | into record` no longer has 'year' or 'month' fields. The excess duration is all lumped into the `week` field, which is the largest time unit you can convert to without knowing the datetime from which the duration was calculated. Scripts that depended on month or year time units on output will need to be changed. ### Examples ``` > 365day 52wk 1day ## Used to be: ## 1yr > 365day | into record ╭──────┬────╮ │ week │ 52 │ │ day │ 1 │ │ sign │ + │ ╰──────┴────╯ ## used to be: ##╭──────┬───╮ ##│ year │ 1 │ ##│ sign │ + │ ##╰──────┴───╯ > (365day + 4wk + 5day + 6hr + 7min + 8sec + 9ms + 10us + 11ns) 56wk 6day 6hr 7min 8sec 9ms 10µs 11ns ## used to be: ## 1yr 1month 3day 6hr 7min 8sec 9ms 10µs 11ns ## which looks reasonable, but was actually only correct in 75% of the years and 25% of the months in the last 4 years. > (365day + 4wk + 5day + 6hr + 7min + 8sec + 9ms + 10us + 11ns) | into record ╭─────────────┬────╮ │ week │ 56 │ │ day │ 6 │ │ hour │ 6 │ │ minute │ 7 │ │ second │ 8 │ │ millisecond │ 9 │ │ microsecond │ 10 │ │ nanosecond │ 11 │ │ sign │ + │ ╰─────────────┴────╯ ``` Strictly speaking, these changes could break an existing user script. Losing years and months as time units is arguably a regression in behavior. Also, the corrected duration calculation could break an existing script that was calibrated using the old algorithm. # Tests + Formatting ``` > toolkit check pr ``` - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib` # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. --> --------- Co-authored-by: Bob Hyman <bobhy@localhost.localdomain>
This commit is contained in:
parent
839010b89d
commit
570175f95d
8 changed files with 174 additions and 194 deletions
52
Cargo.lock
generated
52
Cargo.lock
generated
|
@ -1382,6 +1382,12 @@ version = "0.3.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-timer"
|
||||||
|
version = "3.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.28"
|
version = "0.3.28"
|
||||||
|
@ -2524,7 +2530,7 @@ dependencies = [
|
||||||
"openssl",
|
"openssl",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"reedline",
|
"reedline",
|
||||||
"rstest",
|
"rstest 0.17.0",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
|
@ -2569,7 +2575,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"reedline",
|
"reedline",
|
||||||
"rstest",
|
"rstest 0.17.0",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
@ -2727,7 +2733,7 @@ dependencies = [
|
||||||
"rayon",
|
"rayon",
|
||||||
"regex",
|
"regex",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
"rstest",
|
"rstest 0.17.0",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"same-file",
|
"same-file",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -2812,7 +2818,7 @@ dependencies = [
|
||||||
"nu-path",
|
"nu-path",
|
||||||
"nu-plugin",
|
"nu-plugin",
|
||||||
"nu-protocol",
|
"nu-protocol",
|
||||||
"rstest",
|
"rstest 0.17.0",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2860,6 +2866,7 @@ dependencies = [
|
||||||
"nu-test-support",
|
"nu-test-support",
|
||||||
"nu-utils",
|
"nu-utils",
|
||||||
"num-format",
|
"num-format",
|
||||||
|
"rstest 0.18.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum 0.25.0",
|
"strum 0.25.0",
|
||||||
|
@ -4176,6 +4183,12 @@ version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
|
checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "relative-path"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4bf2521270932c3c7bed1a59151222bd7643c79310f2916f01925e1e16255698"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rmp"
|
name = "rmp"
|
||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
|
@ -4213,7 +4226,19 @@ version = "0.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962"
|
checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962"
|
||||||
dependencies = [
|
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",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -4231,6 +4256,23 @@ dependencies = [
|
||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "rusqlite"
|
name = "rusqlite"
|
||||||
version = "0.29.0"
|
version = "0.29.0"
|
||||||
|
|
|
@ -6,6 +6,7 @@ use nu_protocol::{
|
||||||
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Unit, Value,
|
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Unit, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NS_PER_SEC: i64 = 1_000_000_000;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SubCommand;
|
pub struct SubCommand;
|
||||||
|
|
||||||
|
@ -20,9 +21,10 @@ impl Command for SubCommand {
|
||||||
(Type::String, Type::Duration),
|
(Type::String, Type::Duration),
|
||||||
(Type::Duration, Type::Duration),
|
(Type::Duration, Type::Duration),
|
||||||
(Type::Table(vec![]), Type::Table(vec![])),
|
(Type::Table(vec![]), Type::Table(vec![])),
|
||||||
(Type::Record(vec![]), Type::Record(vec![])),
|
//todo: record<hour,minute,sign> | into duration -> Duration
|
||||||
|
//(Type::Record(vec![]), Type::Record(vec![])),
|
||||||
])
|
])
|
||||||
.allow_variants_without_examples(true)
|
//.allow_variants_without_examples(true)
|
||||||
.rest(
|
.rest(
|
||||||
"rest",
|
"rest",
|
||||||
SyntaxShape::CellPath,
|
SyntaxShape::CellPath,
|
||||||
|
@ -36,7 +38,7 @@ impl Command for SubCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extra_usage(&self) -> &str {
|
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> {
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
@ -57,7 +59,23 @@ impl Command for SubCommand {
|
||||||
let span = Span::test_data();
|
let span = Span::test_data();
|
||||||
vec![
|
vec![
|
||||||
Example {
|
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:
|
example:
|
||||||
"[[value]; ['1sec'] ['2min'] ['3hr'] ['4day'] ['5wk']] | into duration value",
|
"[[value]; ['1sec'] ['2min'] ['3hr'] ['4day'] ['5wk']] | into duration value",
|
||||||
result: Some(Value::List {
|
result: Some(Value::List {
|
||||||
|
@ -65,7 +83,7 @@ impl Command for SubCommand {
|
||||||
Value::Record {
|
Value::Record {
|
||||||
cols: vec!["value".to_string()],
|
cols: vec!["value".to_string()],
|
||||||
vals: vec![Value::Duration {
|
vals: vec![Value::Duration {
|
||||||
val: 1000 * 1000 * 1000,
|
val: NS_PER_SEC,
|
||||||
span,
|
span,
|
||||||
}],
|
}],
|
||||||
span,
|
span,
|
||||||
|
@ -73,7 +91,7 @@ impl Command for SubCommand {
|
||||||
Value::Record {
|
Value::Record {
|
||||||
cols: vec!["value".to_string()],
|
cols: vec!["value".to_string()],
|
||||||
vals: vec![Value::Duration {
|
vals: vec![Value::Duration {
|
||||||
val: 2 * 60 * 1000 * 1000 * 1000,
|
val: 2 * 60 * NS_PER_SEC,
|
||||||
span,
|
span,
|
||||||
}],
|
}],
|
||||||
span,
|
span,
|
||||||
|
@ -81,7 +99,7 @@ impl Command for SubCommand {
|
||||||
Value::Record {
|
Value::Record {
|
||||||
cols: vec!["value".to_string()],
|
cols: vec!["value".to_string()],
|
||||||
vals: vec![Value::Duration {
|
vals: vec![Value::Duration {
|
||||||
val: 3 * 60 * 60 * 1000 * 1000 * 1000,
|
val: 3 * 60 * 60 * NS_PER_SEC,
|
||||||
span,
|
span,
|
||||||
}],
|
}],
|
||||||
span,
|
span,
|
||||||
|
@ -89,7 +107,7 @@ impl Command for SubCommand {
|
||||||
Value::Record {
|
Value::Record {
|
||||||
cols: vec!["value".to_string()],
|
cols: vec!["value".to_string()],
|
||||||
vals: vec![Value::Duration {
|
vals: vec![Value::Duration {
|
||||||
val: 4 * 24 * 60 * 60 * 1000 * 1000 * 1000,
|
val: 4 * 24 * 60 * 60 * NS_PER_SEC,
|
||||||
span,
|
span,
|
||||||
}],
|
}],
|
||||||
span,
|
span,
|
||||||
|
@ -97,7 +115,7 @@ impl Command for SubCommand {
|
||||||
Value::Record {
|
Value::Record {
|
||||||
cols: vec!["value".to_string()],
|
cols: vec!["value".to_string()],
|
||||||
vals: vec![Value::Duration {
|
vals: vec![Value::Duration {
|
||||||
val: 5 * 7 * 24 * 60 * 60 * 1000 * 1000 * 1000,
|
val: 5 * 7 * 24 * 60 * 60 * NS_PER_SEC,
|
||||||
span,
|
span,
|
||||||
}],
|
}],
|
||||||
span,
|
span,
|
||||||
|
@ -106,19 +124,11 @@ impl Command for SubCommand {
|
||||||
span,
|
span,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
Example {
|
|
||||||
description: "Convert string to duration",
|
|
||||||
example: "'7min' | into duration",
|
|
||||||
result: Some(Value::Duration {
|
|
||||||
val: 7 * 60 * 1000 * 1000 * 1000,
|
|
||||||
span,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Example {
|
Example {
|
||||||
description: "Convert duration to duration",
|
description: "Convert duration to duration",
|
||||||
example: "420sec | into duration",
|
example: "420sec | into duration",
|
||||||
result: Some(Value::Duration {
|
result: Some(Value::Duration {
|
||||||
val: 7 * 60 * 1000 * 1000 * 1000,
|
val: 7 * 60 * NS_PER_SEC,
|
||||||
span,
|
span,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -161,7 +171,32 @@ fn into_duration(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn string_to_duration(s: &str, span: Span, value_span: Span) -> Result<i64, ShellError> {
|
// 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<Item = (&str, Span)> {
|
||||||
|
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<i64, ShellError> {
|
||||||
|
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<i64, ShellError> {
|
||||||
if let Some(Ok(expression)) = parse_unit_value(
|
if let Some(Ok(expression)) = parse_unit_value(
|
||||||
s.as_bytes(),
|
s.as_bytes(),
|
||||||
span,
|
span,
|
||||||
|
@ -175,11 +210,11 @@ fn string_to_duration(s: &str, span: Span, value_span: Span) -> Result<i64, Shel
|
||||||
Unit::Nanosecond => return Ok(x),
|
Unit::Nanosecond => return Ok(x),
|
||||||
Unit::Microsecond => return Ok(x * 1000),
|
Unit::Microsecond => return Ok(x * 1000),
|
||||||
Unit::Millisecond => return Ok(x * 1000 * 1000),
|
Unit::Millisecond => return Ok(x * 1000 * 1000),
|
||||||
Unit::Second => return Ok(x * 1000 * 1000 * 1000),
|
Unit::Second => return Ok(x * NS_PER_SEC),
|
||||||
Unit::Minute => return Ok(x * 60 * 1000 * 1000 * 1000),
|
Unit::Minute => return Ok(x * 60 * NS_PER_SEC),
|
||||||
Unit::Hour => return Ok(x * 60 * 60 * 1000 * 1000 * 1000),
|
Unit::Hour => return Ok(x * 60 * 60 * NS_PER_SEC),
|
||||||
Unit::Day => return Ok(x * 24 * 60 * 60 * 1000 * 1000 * 1000),
|
Unit::Day => return Ok(x * 24 * 60 * 60 * NS_PER_SEC),
|
||||||
Unit::Week => return Ok(x * 7 * 24 * 60 * 60 * 1000 * 1000 * 1000),
|
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<i64, Shel
|
||||||
Err(ShellError::CantConvertToDuration {
|
Err(ShellError::CantConvertToDuration {
|
||||||
details: s.to_string(),
|
details: s.to_string(),
|
||||||
dst_span: span,
|
dst_span: span,
|
||||||
src_span: value_span,
|
src_span: span,
|
||||||
help: Some(
|
help: Some("supported units are ns, us/µs, ms, sec, min, hr, day, and wk".to_string()),
|
||||||
"supported units are ns, us/µs, ms, sec, min, hr, day, wk, month, yr, and dec"
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +235,7 @@ fn action(input: &Value, span: Span) -> Value {
|
||||||
Value::String {
|
Value::String {
|
||||||
val,
|
val,
|
||||||
span: value_span,
|
span: value_span,
|
||||||
} => match string_to_duration(val, span, *value_span) {
|
} => match compound_to_duration(val, *value_span) {
|
||||||
Ok(val) => Value::Duration { val, span },
|
Ok(val) => Value::Duration { val, span },
|
||||||
Err(error) => Value::Error {
|
Err(error) => Value::Error {
|
||||||
error: Box::new(error),
|
error: Box::new(error),
|
||||||
|
@ -225,6 +257,7 @@ fn action(input: &Value, span: Span) -> Value {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_examples() {
|
fn test_examples() {
|
||||||
|
@ -233,130 +266,33 @@ mod test {
|
||||||
test_examples(SubCommand {})
|
test_examples(SubCommand {})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
const NS_PER_SEC: i64 = 1_000_000_000;
|
||||||
fn turns_ns_to_duration() {
|
|
||||||
let span = Span::new(0, 2);
|
|
||||||
let word = Value::test_string("3ns");
|
|
||||||
let expected = Value::Duration { val: 3, span };
|
|
||||||
|
|
||||||
let actual = action(&word, span);
|
#[rstest]
|
||||||
assert_eq!(actual, expected);
|
#[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_string_to_duration(#[case] phrase: &str, #[case] expected_duration_val: i64) {
|
||||||
fn turns_us_to_duration() {
|
let actual = action(&Value::test_string(phrase), Span::new(0, phrase.len()));
|
||||||
let span = Span::new(0, 2);
|
match actual {
|
||||||
let word = Value::test_string("4us");
|
Value::Duration {
|
||||||
let expected = Value::Duration {
|
val: observed_val, ..
|
||||||
val: 4 * 1000,
|
} => {
|
||||||
span,
|
assert_eq!(expected_duration_val, observed_val, "expected != observed")
|
||||||
};
|
}
|
||||||
|
other => {
|
||||||
let actual = action(&word, span);
|
panic!("Expected Value::Duration, observed {other:?}");
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,21 +83,21 @@ impl Command for SubCommand {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "convert duration to record",
|
description: "convert duration to record (weeks max)",
|
||||||
example: "-500day | into record",
|
example: "(-500day - 4hr - 5sec) | into record",
|
||||||
result: Some(Value::Record {
|
result: Some(Value::Record {
|
||||||
cols: vec![
|
cols: vec![
|
||||||
"year".into(),
|
|
||||||
"month".into(),
|
|
||||||
"week".into(),
|
"week".into(),
|
||||||
"day".into(),
|
"day".into(),
|
||||||
|
"hour".into(),
|
||||||
|
"second".into(),
|
||||||
"sign".into(),
|
"sign".into(),
|
||||||
],
|
],
|
||||||
vals: vec![
|
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: 4, span },
|
||||||
Value::Int { val: 2, span },
|
Value::Int { val: 5, span },
|
||||||
Value::Int { val: 1, span },
|
|
||||||
Value::String {
|
Value::String {
|
||||||
val: "-".into(),
|
val: "-".into(),
|
||||||
span,
|
span,
|
||||||
|
@ -261,8 +261,6 @@ fn parse_duration_into_record(duration: i64, span: Span) -> Value {
|
||||||
"hr" => "hour".into(),
|
"hr" => "hour".into(),
|
||||||
"day" => "day".into(),
|
"day" => "day".into(),
|
||||||
"wk" => "week".into(),
|
"wk" => "week".into(),
|
||||||
"month" => "month".into(),
|
|
||||||
"yr" => "year".into(),
|
|
||||||
_ => "unknown".into(),
|
_ => "unknown".into(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ impl Command for SubCommand {
|
||||||
(Type::Bool, Type::String),
|
(Type::Bool, Type::String),
|
||||||
(Type::Filesize, Type::String),
|
(Type::Filesize, Type::String),
|
||||||
(Type::Date, Type::String),
|
(Type::Date, Type::String),
|
||||||
|
(Type::Duration, Type::String),
|
||||||
(
|
(
|
||||||
Type::List(Box::new(Type::Any)),
|
Type::List(Box::new(Type::Any)),
|
||||||
Type::List(Box::new(Type::String)),
|
Type::List(Box::new(Type::String)),
|
||||||
|
@ -145,6 +146,11 @@ impl Command for SubCommand {
|
||||||
example: "1KiB | into string",
|
example: "1KiB | into string",
|
||||||
result: Some(Value::test_string("1,024 B")),
|
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),
|
val: input.into_string(", ", config),
|
||||||
span,
|
span,
|
||||||
},
|
},
|
||||||
|
Value::Duration { val: _, .. } => Value::String {
|
||||||
|
val: input.into_string("", config),
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
|
||||||
Value::Error { error } => Value::String {
|
Value::Error { error } => Value::String {
|
||||||
val: into_code(error).unwrap_or_default(),
|
val: into_code(error).unwrap_or_default(),
|
||||||
span,
|
span,
|
||||||
|
|
|
@ -2318,6 +2318,7 @@ pub const FILESIZE_UNIT_GROUPS: &[UnitGroup] = &[
|
||||||
|
|
||||||
pub const DURATION_UNIT_GROUPS: &[UnitGroup] = &[
|
pub const DURATION_UNIT_GROUPS: &[UnitGroup] = &[
|
||||||
(Unit::Nanosecond, "ns", None),
|
(Unit::Nanosecond, "ns", None),
|
||||||
|
// todo start adding aliases for duration units here
|
||||||
(Unit::Microsecond, "us", Some((Unit::Nanosecond, 1000))),
|
(Unit::Microsecond, "us", Some((Unit::Nanosecond, 1000))),
|
||||||
(
|
(
|
||||||
// µ Micro Sign
|
// µ Micro Sign
|
||||||
|
|
|
@ -36,3 +36,4 @@ serde_json = "1.0"
|
||||||
strum = "0.25"
|
strum = "0.25"
|
||||||
strum_macros = "0.25"
|
strum_macros = "0.25"
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.83.2" }
|
nu-test-support = { path = "../nu-test-support", version = "0.83.2" }
|
||||||
|
rstest = "*"
|
||||||
|
|
|
@ -3766,8 +3766,8 @@ pub fn format_duration(duration: i64) -> String {
|
||||||
pub fn format_duration_as_timeperiod(duration: i64) -> (i32, Vec<TimePeriod>) {
|
pub fn format_duration_as_timeperiod(duration: i64) -> (i32, Vec<TimePeriod>) {
|
||||||
// Attribution: most of this is taken from chrono-humanize-rs. Thanks!
|
// Attribution: most of this is taken from chrono-humanize-rs. Thanks!
|
||||||
// https://gitlab.com/imp/chrono-humanize-rs/-/blob/master/src/humantime.rs
|
// https://gitlab.com/imp/chrono-humanize-rs/-/blob/master/src/humantime.rs
|
||||||
const DAYS_IN_YEAR: i64 = 365;
|
// Current duration doesn't know a date it's based on, weeks is the max time unit it can normalize into.
|
||||||
const DAYS_IN_MONTH: i64 = 30;
|
// Don't guess or estimate how many years or months it might contain.
|
||||||
|
|
||||||
let (sign, duration) = if duration >= 0 {
|
let (sign, duration) = if duration >= 0 {
|
||||||
(1, duration)
|
(1, duration)
|
||||||
|
@ -3777,20 +3777,6 @@ pub fn format_duration_as_timeperiod(duration: i64) -> (i32, Vec<TimePeriod>) {
|
||||||
|
|
||||||
let dur = Duration::nanoseconds(duration);
|
let dur = Duration::nanoseconds(duration);
|
||||||
|
|
||||||
/// Split this a duration into number of whole years and the remainder
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Split this a duration into number of whole months and the remainder
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Split this a duration into number of whole weeks and the remainder
|
/// Split this a duration into number of whole weeks and the remainder
|
||||||
fn split_weeks(duration: Duration) -> (Option<i64>, Duration) {
|
fn split_weeks(duration: Duration) -> (Option<i64>, Duration) {
|
||||||
let weeks = duration.num_weeks();
|
let weeks = duration.num_weeks();
|
||||||
|
@ -3856,17 +3842,8 @@ pub fn format_duration_as_timeperiod(duration: i64) -> (i32, Vec<TimePeriod>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut periods = 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);
|
let (weeks, remainder) = split_weeks(dur);
|
||||||
if let Some(months) = months {
|
|
||||||
periods.push(TimePeriod::Months(months));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (weeks, remainder) = split_weeks(remainder);
|
|
||||||
if let Some(weeks) = weeks {
|
if let Some(weeks) = weeks {
|
||||||
periods.push(TimePeriod::Weeks(weeks));
|
periods.push(TimePeriod::Weeks(weeks));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use nu_protocol::{Span, Value};
|
use nu_protocol::{Config, Span, Value};
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_comparison_nothing() {
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue