From ff673ba0ba151c01b71e58a9d0ba4cfaaeb35329 Mon Sep 17 00:00:00 2001 From: onthebridgetonowhere <71919805+onthebridgetonowhere@users.noreply.github.com> Date: Thu, 2 Dec 2021 18:54:47 +0100 Subject: [PATCH] Add the support of str to-int to the into int command (#389) --- crates/nu-command/src/conversions/into/int.rs | 181 ++++++++++++++++-- 1 file changed, 160 insertions(+), 21 deletions(-) diff --git a/crates/nu-command/src/conversions/into/int.rs b/crates/nu-command/src/conversions/into/int.rs index e6185dabbd..c7dba5590e 100644 --- a/crates/nu-command/src/conversions/into/int.rs +++ b/crates/nu-command/src/conversions/into/int.rs @@ -5,6 +5,11 @@ use nu_protocol::{ Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, }; +struct Arguments { + radix: Option, + column_paths: Vec, +} + #[derive(Clone)] pub struct SubCommand; @@ -15,6 +20,7 @@ impl Command for SubCommand { fn signature(&self) -> Signature { Signature::build("into int") + .named("radix", SyntaxShape::Number, "radix of integer", Some('r')) .rest( "rest", SyntaxShape::CellPath, @@ -75,6 +81,16 @@ impl Command for SubCommand { span: Span::unknown(), }), }, + Example { + description: "Convert to integer from binary", + example: "'1101' | into int -r 2", + result: Some(Value::test_int(13)), + }, + Example { + description: "Convert to integer from hex", + example: "'FF' | into int -r 16", + result: Some(Value::test_int(255)), + }, ] } } @@ -86,17 +102,36 @@ fn into_int( input: PipelineData, ) -> Result { let head = call.head; - let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + let options = Arguments { + radix: call.get_flag(engine_state, stack, "radix")?, + column_paths: call.rest(engine_state, stack, 0)?, + }; + + let radix: u32 = match options.radix { + Some(Value::Int { val, .. }) => val as u32, + Some(_) => 10, + None => 10, + }; + + if !(2..=36).contains(&radix) { + return Err(ShellError::UnsupportedInput( + "Radix must lie in the range [2, 36]".to_string(), + options.radix.unwrap().span().unwrap(), + )); + } input.map( move |v| { - if column_paths.is_empty() { - action(&v, head) + if options.column_paths.is_empty() { + action(&v, head, radix) } 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))); + for path in &options.column_paths { + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, head, radix)), + ); if let Err(error) = r { return Value::Error { error }; } @@ -109,18 +144,30 @@ fn into_int( ) } -pub fn action(input: &Value, span: Span) -> Value { +pub fn action(input: &Value, span: Span, radix: u32) -> Value { match input { - Value::Int { .. } => input.clone(), + Value::Int { val: _, .. } => { + if radix == 10 { + input.clone() + } else { + convert_int(input, span, radix) + } + } Value::Filesize { val, .. } => Value::Int { val: *val, span }, Value::Float { val, .. } => Value::Int { val: *val as i64, span, }, - Value::String { val, .. } => match int_from_string(val, span) { - Ok(val) => Value::Int { val, span }, - Err(error) => Value::Error { error }, - }, + Value::String { val, .. } => { + if radix == 10 { + match int_from_string(val, span) { + Ok(val) => Value::Int { val, span }, + Err(error) => Value::Error { error }, + } + } else { + convert_int(input, span, radix) + } + } Value::Bool { val, .. } => { if *val { Value::Int { val: 1, span } @@ -134,23 +181,83 @@ pub fn action(input: &Value, span: Span) -> Value { } } +fn convert_int(input: &Value, head: Span, radix: u32) -> Value { + let i = match input { + Value::Int { val, .. } => val.to_string(), + Value::String { val, .. } => { + if val.starts_with("0x") || val.starts_with("0b") { + match int_from_string(&val.to_string(), head) { + Ok(x) => return Value::Int { val: x, span: head }, + Err(e) => return Value::Error { error: e }, + } + } + val.to_string() + } + _ => { + return Value::Error { + error: ShellError::UnsupportedInput( + "only strings or integers are supported".to_string(), + head, + ), + } + } + }; + match i64::from_str_radix(&i, radix) { + Ok(n) => Value::Int { val: n, span: head }, + Err(reason) => Value::Error { + error: ShellError::CantConvert("".to_string(), reason.to_string(), head), + }, + } +} + fn int_from_string(a_string: &str, span: Span) -> Result { - match a_string.parse::() { - Ok(n) => Ok(n), - Err(_) => match a_string.parse::() { - Ok(f) => Ok(f as i64), - _ => Err(ShellError::CantConvert( - "into int".into(), - "string".into(), - span, - )), + let trimmed = a_string.trim(); + match trimmed { + b if b.starts_with("0b") => { + let num = match i64::from_str_radix(b.trim_start_matches("0b"), 2) { + Ok(n) => n, + Err(reason) => { + return Err(ShellError::CantConvert( + "could not parse as integer".to_string(), + reason.to_string(), + span, + )) + } + }; + Ok(num) + } + h if h.starts_with("0x") => { + let num = match i64::from_str_radix(h.trim_start_matches("0x"), 16) { + Ok(n) => n, + Err(reason) => { + return Err(ShellError::CantConvert( + "could not parse as int".to_string(), + reason.to_string(), + span, + )) + } + }; + Ok(num) + } + _ => match a_string.parse::() { + Ok(n) => Ok(n), + Err(_) => match a_string.parse::() { + Ok(f) => Ok(f as i64), + _ => Err(ShellError::CantConvert( + "into int".to_string(), + "string".to_string(), + span, + )), + }, }, } } #[cfg(test)] mod test { + use super::Value; use super::*; + use nu_protocol::Type::Error; #[test] fn test_examples() { @@ -158,4 +265,36 @@ mod test { test_examples(SubCommand {}) } + + #[test] + fn turns_to_integer() { + let word = Value::test_string("10"); + let expected = Value::test_int(10); + + let actual = action(&word, Span::unknown(), 10); + assert_eq!(actual, expected); + } + + #[test] + fn turns_binary_to_integer() { + let s = Value::test_string("0b101"); + let actual = action(&s, Span::unknown(), 10); + assert_eq!(actual, Value::test_int(5)); + } + + #[test] + fn turns_hex_to_integer() { + let s = Value::test_string("0xFF"); + let actual = action(&s, Span::unknown(), 16); + assert_eq!(actual, Value::test_int(255)); + } + + #[test] + fn communicates_parsing_error_given_an_invalid_integerlike_string() { + let integer_str = Value::test_string("36anra"); + + let actual = action(&integer_str, Span::unknown(), 10); + + assert_eq!(actual.get_type(), Error) + } }