mirror of
https://github.com/nushell/nushell.git
synced 2025-04-10 22:18:17 +02:00
# Description Continuing from #12568, this PR further reduces the size of `Expr` from 64 to 40 bytes. It also reduces `Expression` from 128 to 96 bytes and `Type` from 32 to 24 bytes. This was accomplished by: - for `Expr` with multiple fields (e.g., `Expr::Thing(A, B, C)`), merging the fields into new AST struct types and then boxing this struct (e.g. `Expr::Thing(Box<ABC>)`). - replacing `Vec<T>` with `Box<[T]>` in multiple places. `Expr`s and `Expression`s should rarely be mutated, if at all, so this optimization makes sense. By reducing the size of these types, I didn't notice a large performance improvement (at least compared to #12568). But this PR does reduce the memory usage of nushell. My config is somewhat light so I only noticed a difference of 1.4MiB (38.9MiB vs 37.5MiB). --------- Co-authored-by: Stefan Holderbach <sholderbach@users.noreply.github.com>
313 lines
10 KiB
Rust
313 lines
10 KiB
Rust
use nu_engine::command_prelude::*;
|
|
use nu_parser::{parse_unit_value, DURATION_UNIT_GROUPS};
|
|
use nu_protocol::{ast::Expr, Unit};
|
|
|
|
const NS_PER_SEC: i64 = 1_000_000_000;
|
|
#[derive(Clone)]
|
|
pub struct SubCommand;
|
|
|
|
impl Command for SubCommand {
|
|
fn name(&self) -> &str {
|
|
"into duration"
|
|
}
|
|
|
|
fn signature(&self) -> Signature {
|
|
Signature::build("into duration")
|
|
.input_output_types(vec![
|
|
(Type::Int, Type::Duration),
|
|
(Type::String, Type::Duration),
|
|
(Type::Duration, Type::Duration),
|
|
(Type::table(), Type::table()),
|
|
//todo: record<hour,minute,sign> | into duration -> Duration
|
|
//(Type::record(), Type::record()),
|
|
])
|
|
//.allow_variants_without_examples(true)
|
|
.named(
|
|
"unit",
|
|
SyntaxShape::String,
|
|
"Unit to convert number into (will have an effect only with integer input)",
|
|
Some('u'),
|
|
)
|
|
.rest(
|
|
"rest",
|
|
SyntaxShape::CellPath,
|
|
"For a data structure input, convert data at the given cell paths.",
|
|
)
|
|
.category(Category::Conversions)
|
|
}
|
|
|
|
fn usage(&self) -> &str {
|
|
"Convert value to duration."
|
|
}
|
|
|
|
fn extra_usage(&self) -> &str {
|
|
"Max duration value is i64::MAX nanoseconds; max duration time unit is wk (weeks)."
|
|
}
|
|
|
|
fn search_terms(&self) -> Vec<&str> {
|
|
vec!["convert", "time", "period"]
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
into_duration(engine_state, stack, call, input)
|
|
}
|
|
|
|
fn examples(&self) -> Vec<Example> {
|
|
vec![
|
|
Example {
|
|
description: "Convert duration string to duration value",
|
|
example: "'7min' | into duration",
|
|
result: Some(Value::test_duration(7 * 60 * NS_PER_SEC)),
|
|
},
|
|
Example {
|
|
description: "Convert compound duration string to duration value",
|
|
example: "'1day 2hr 3min 4sec' | into duration",
|
|
result: Some(Value::test_duration(
|
|
(((((/* 1 * */24) + 2) * 60) + 3) * 60 + 4) * NS_PER_SEC,
|
|
)),
|
|
},
|
|
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::test_list(vec![
|
|
Value::test_record(record! {
|
|
"value" => Value::test_duration(NS_PER_SEC),
|
|
}),
|
|
Value::test_record(record! {
|
|
"value" => Value::test_duration(2 * 60 * NS_PER_SEC),
|
|
}),
|
|
Value::test_record(record! {
|
|
"value" => Value::test_duration(3 * 60 * 60 * NS_PER_SEC),
|
|
}),
|
|
Value::test_record(record! {
|
|
"value" => Value::test_duration(4 * 24 * 60 * 60 * NS_PER_SEC),
|
|
}),
|
|
Value::test_record(record! {
|
|
"value" => Value::test_duration(5 * 7 * 24 * 60 * 60 * NS_PER_SEC),
|
|
}),
|
|
])),
|
|
},
|
|
Example {
|
|
description: "Convert duration to duration",
|
|
example: "420sec | into duration",
|
|
result: Some(Value::test_duration(7 * 60 * NS_PER_SEC)),
|
|
},
|
|
Example {
|
|
description: "Convert a number of ns to duration",
|
|
example: "1_234_567 | into duration",
|
|
result: Some(Value::test_duration(1_234_567)),
|
|
},
|
|
Example {
|
|
description: "Convert a number of an arbitrary unit to duration",
|
|
example: "1_234 | into duration --unit ms",
|
|
result: Some(Value::test_duration(1_234 * 1_000_000)),
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
fn into_duration(
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let span = match input.span() {
|
|
Some(t) => t,
|
|
None => call.head,
|
|
};
|
|
let column_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
|
|
|
|
let unit = match call.get_flag::<String>(engine_state, stack, "unit")? {
|
|
Some(sep) => {
|
|
if ["ns", "us", "µs", "ms", "sec", "min", "hr", "day", "wk"]
|
|
.iter()
|
|
.any(|d| d == &sep)
|
|
{
|
|
sep
|
|
} else {
|
|
return Err(ShellError::CantConvertToDuration {
|
|
details: sep,
|
|
dst_span: span,
|
|
src_span: span,
|
|
help: Some(
|
|
"supported units are ns, us/µs, ms, sec, min, hr, day, and wk".to_string(),
|
|
),
|
|
});
|
|
}
|
|
}
|
|
None => "ns".to_string(),
|
|
};
|
|
|
|
input.map(
|
|
move |v| {
|
|
if column_paths.is_empty() {
|
|
action(&v, &unit.clone(), span)
|
|
} else {
|
|
let unitclone = &unit.clone();
|
|
let mut ret = v;
|
|
for path in &column_paths {
|
|
let r = ret.update_cell_path(
|
|
&path.members,
|
|
Box::new(move |old| action(old, unitclone, span)),
|
|
);
|
|
if let Err(error) = r {
|
|
return Value::error(error, span);
|
|
}
|
|
}
|
|
|
|
ret
|
|
}
|
|
},
|
|
engine_state.ctrlc.clone(),
|
|
)
|
|
}
|
|
|
|
// 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(
|
|
s.as_bytes(),
|
|
span,
|
|
DURATION_UNIT_GROUPS,
|
|
Type::Duration,
|
|
|x| x,
|
|
) {
|
|
if let Expr::ValueWithUnit(value) = expression.expr {
|
|
if let Expr::Int(x) = value.expr.expr {
|
|
match value.unit.item {
|
|
Unit::Nanosecond => return Ok(x),
|
|
Unit::Microsecond => return Ok(x * 1000),
|
|
Unit::Millisecond => return Ok(x * 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),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(ShellError::CantConvertToDuration {
|
|
details: s.to_string(),
|
|
dst_span: span,
|
|
src_span: span,
|
|
help: Some("supported units are ns, us/µs, ms, sec, min, hr, day, and wk".to_string()),
|
|
})
|
|
}
|
|
|
|
fn action(input: &Value, unit: &str, span: Span) -> Value {
|
|
let value_span = input.span();
|
|
match input {
|
|
Value::Duration { .. } => input.clone(),
|
|
Value::String { val, .. } => match compound_to_duration(val, value_span) {
|
|
Ok(val) => Value::duration(val, span),
|
|
Err(error) => Value::error(error, span),
|
|
},
|
|
Value::Int { val, .. } => {
|
|
let ns = match unit {
|
|
"ns" => 1,
|
|
"us" | "µs" => 1_000,
|
|
"ms" => 1_000_000,
|
|
"sec" => NS_PER_SEC,
|
|
"min" => NS_PER_SEC * 60,
|
|
"hr" => NS_PER_SEC * 60 * 60,
|
|
"day" => NS_PER_SEC * 60 * 60 * 24,
|
|
"wk" => NS_PER_SEC * 60 * 60 * 24 * 7,
|
|
_ => 0,
|
|
};
|
|
Value::duration(*val * ns, span)
|
|
}
|
|
// Propagate errors by explicitly matching them before the final case.
|
|
Value::Error { .. } => input.clone(),
|
|
other => Value::error(
|
|
ShellError::OnlySupportsThisInputType {
|
|
exp_input_type: "string or duration".into(),
|
|
wrong_type: other.get_type().to_string(),
|
|
dst_span: span,
|
|
src_span: other.span(),
|
|
},
|
|
span,
|
|
),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use rstest::rstest;
|
|
|
|
#[test]
|
|
fn test_examples() {
|
|
use crate::test_examples;
|
|
|
|
test_examples(SubCommand {})
|
|
}
|
|
|
|
const NS_PER_SEC: i64 = 1_000_000_000;
|
|
|
|
#[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", 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
|
|
|
|
fn turns_string_to_duration(#[case] phrase: &str, #[case] expected_duration_val: i64) {
|
|
let actual = action(
|
|
&Value::test_string(phrase),
|
|
"ns",
|
|
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:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|