From 210d25f2a03830da9aff61aa3b9a9a774e005131 Mon Sep 17 00:00:00 2001 From: Yutaro Ohno Date: Thu, 3 Mar 2022 22:16:04 +0900 Subject: [PATCH] Add `into duration` (#4683) * Add `into duration` command * Avoid using unwrap() * Use existing logic to parse duration strings --- .../src/conversions/into/duration.rs | 288 ++++++++++++++++++ crates/nu-command/src/conversions/into/mod.rs | 2 + crates/nu-command/src/default_context.rs | 1 + crates/nu-parser/src/lib.rs | 3 +- crates/nu-parser/src/parser.rs | 73 +++-- 5 files changed, 328 insertions(+), 39 deletions(-) create mode 100644 crates/nu-command/src/conversions/into/duration.rs diff --git a/crates/nu-command/src/conversions/into/duration.rs b/crates/nu-command/src/conversions/into/duration.rs new file mode 100644 index 000000000..5b6cdb211 --- /dev/null +++ b/crates/nu-command/src/conversions/into/duration.rs @@ -0,0 +1,288 @@ +use nu_engine::CallExt; +use nu_parser::parse_duration_bytes; +use nu_protocol::{ + ast::{Call, CellPath, Expr}, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Unit, Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "into duration" + } + + fn signature(&self) -> Signature { + Signature::build("into duration") + .rest( + "rest", + SyntaxShape::CellPath, + "column paths to convert to duration (for table input)", + ) + .category(Category::Conversions) + } + + fn usage(&self) -> &str { + "Convert value to duration" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + into_duration(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + let span = Span::test_data(); + vec![ + Example { + description: "Convert string to duration in table", + example: "echo [[value]; ['1sec'] ['2min'] ['3hr'] ['4day'] ['5wk']] | into duration value", + result: Some(Value::List { + vals: vec![ + Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::Duration { + val: 1000 * 1000 * 1000, + span, + }], + span, + }, + Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::Duration { + val: 2 * 60 * 1000 * 1000 * 1000, + span, + }], + span, + }, + Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::Duration { + val: 3 * 60 * 60 * 1000 * 1000 * 1000, + span, + }], + span, + }, + Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::Duration { + val: 4 * 24 * 60 * 60 * 1000 * 1000 * 1000, + span, + }], + span, + }, + Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::Duration { + val: 5 * 7 * 24 * 60 * 60 * 1000 * 1000 * 1000, + span, + }], + span, + }, + ], + span, + }), + }, + Example { + description: "Convert string to duration", + example: "'7min' | into duration", + result: Some(Value::Duration { + val: 7 * 60 * 1000 * 1000 * 1000, + span, + }), + }, + ] + } +} + +fn into_duration( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = + ret.update_cell_path(&path.members, Box::new(move |old| action(old, head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn string_to_duration(s: &str, span: Span) -> Result { + if let Some(expression) = parse_duration_bytes(s.as_bytes(), span) { + if let Expr::ValueWithUnit(value, unit) = expression.expr { + if let Expr::Int(x) = value.expr { + match 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 * 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), + _ => {} + } + } + } + } + + Err(ShellError::CantConvert( + "duration".to_string(), + "string".to_string(), + span, + )) +} + +fn action(input: &Value, span: Span) -> Value { + match input { + Value::Duration { .. } => input.clone(), + Value::String { val, .. } => match string_to_duration(val, span) { + Ok(val) => Value::Duration { val, span }, + Err(error) => Value::Error { error }, + }, + _ => Value::Error { + error: ShellError::UnsupportedInput( + "'into duration' does not support this input".into(), + span, + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn turns_ns_to_duration() { + let span = Span::test_data(); + let word = Value::test_string("3ns"); + let expected = Value::Duration { val: 3, span }; + + let actual = action(&word, span); + assert_eq!(actual, expected); + } + + #[test] + fn turns_us_to_duration() { + let span = Span::test_data(); + 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_ms_to_duration() { + let span = Span::test_data(); + 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::test_data(); + let word = Value::test_string("1sec"); + let expected = Value::Duration { + val: 1 * 1000 * 1000 * 1000, + span, + }; + + let actual = action(&word, span); + assert_eq!(actual, expected); + } + + #[test] + fn turns_min_to_duration() { + let span = Span::test_data(); + 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::test_data(); + 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::test_data(); + 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::test_data(); + 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); + } +} diff --git a/crates/nu-command/src/conversions/into/mod.rs b/crates/nu-command/src/conversions/into/mod.rs index cdb100345..dc1281494 100644 --- a/crates/nu-command/src/conversions/into/mod.rs +++ b/crates/nu-command/src/conversions/into/mod.rs @@ -3,6 +3,7 @@ mod bool; mod command; mod datetime; mod decimal; +mod duration; mod filesize; mod int; mod string; @@ -13,5 +14,6 @@ pub use binary::SubCommand as IntoBinary; pub use command::Into; pub use datetime::SubCommand as IntoDatetime; pub use decimal::SubCommand as IntoDecimal; +pub use duration::SubCommand as IntoDuration; pub use int::SubCommand as IntoInt; pub use string::SubCommand as IntoString; diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index a71b8247c..9c6e9489a 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -279,6 +279,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { IntoBinary, IntoDatetime, IntoDecimal, + IntoDuration, IntoFilesize, IntoInt, IntoString, diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index 245a17abc..89e3f60d1 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -14,7 +14,8 @@ pub use lex::{lex, Token, TokenContents}; pub use lite_parse::{lite_parse, LiteBlock}; pub use parser::{ - is_math_expression_like, parse, parse_block, parse_external_call, trim_quotes, Import, + is_math_expression_like, parse, parse_block, parse_duration_bytes, parse_external_call, + trim_quotes, Import, }; #[cfg(feature = "plugin")] diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index e9849b4c1..27265569e 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1881,6 +1881,22 @@ pub fn parse_duration( ) -> (Expression, Option) { trace!("parsing: duration"); + let bytes = working_set.get_span_contents(span); + + match parse_duration_bytes(bytes, span) { + Some(expression) => (expression, None), + None => ( + garbage(span), + Some(ParseError::Mismatch( + "duration".into(), + "non-duration unit".into(), + span, + )), + ), + } +} + +pub fn parse_duration_bytes(bytes: &[u8], span: Span) -> Option { fn parse_decimal_str_to_number(decimal: &str) -> Option { let string_to_parse = format!("0.{}", decimal); if let Ok(x) = string_to_parse.parse::() { @@ -1889,17 +1905,8 @@ pub fn parse_duration( None } - let bytes = working_set.get_span_contents(span); - if bytes.is_empty() || (!bytes[0].is_ascii_digit() && bytes[0] != b'-') { - return ( - garbage(span), - Some(ParseError::Mismatch( - "duration".into(), - "non-duration unit".into(), - span, - )), - ); + return None; } let token = String::from_utf8_lossy(bytes).to_string(); @@ -1948,37 +1955,27 @@ pub fn parse_duration( let lhs_span = Span::new(span.start, span.start + lhs.len()); let unit_span = Span::new(span.start + lhs.len(), span.end); - return ( - Expression { - expr: Expr::ValueWithUnit( - Box::new(Expression { - expr: Expr::Int(x), - span: lhs_span, - ty: Type::Number, - custom_completion: None, - }), - Spanned { - item: unit_to_use, - span: unit_span, - }, - ), - span, - ty: Type::Duration, - custom_completion: None, - }, - None, - ); + return Some(Expression { + expr: Expr::ValueWithUnit( + Box::new(Expression { + expr: Expr::Int(x), + span: lhs_span, + ty: Type::Number, + custom_completion: None, + }), + Spanned { + item: unit_to_use, + span: unit_span, + }, + ), + span, + ty: Type::Duration, + custom_completion: None, + }); } } - ( - garbage(span), - Some(ParseError::Mismatch( - "duration".into(), - "non-duration unit".into(), - span, - )), - ) + None } /// Parse a unit type, eg '10kb'