From 070067b75e333c701fc57afdce598bb382f05909 Mon Sep 17 00:00:00 2001 From: Stefan Stanciulescu Date: Tue, 2 Nov 2021 20:39:16 +0100 Subject: [PATCH 1/5] Add into string command --- crates/nu-command/Cargo.toml | 4 + crates/nu-command/src/conversions/into/mod.rs | 2 + .../nu-command/src/conversions/into/string.rs | 377 ++++++++++++++++++ crates/nu-command/src/default_context.rs | 1 + 4 files changed, 384 insertions(+) create mode 100644 crates/nu-command/src/conversions/into/string.rs diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 99d13d33c3..b822e6084e 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -22,6 +22,10 @@ unicode-segmentation = "1.8.0" glob = "0.3.0" thiserror = "1.0.29" sysinfo = "0.20.4" +bigdecimal = { package = "bigdecimal-rs", version = "0.2.1", features = ["serde"] } +num-bigint = { version="0.3.1", features=["serde"] } +num-format = { version="0.4.0", features=["with-num-bigint"] } +num-traits = "0.2.14" chrono = { version = "0.4.19", features = ["serde"] } chrono-humanize = "0.2.1" chrono-tz = "0.6.0" diff --git a/crates/nu-command/src/conversions/into/mod.rs b/crates/nu-command/src/conversions/into/mod.rs index 7563ca614a..d9fb8cb253 100644 --- a/crates/nu-command/src/conversions/into/mod.rs +++ b/crates/nu-command/src/conversions/into/mod.rs @@ -2,8 +2,10 @@ mod binary; mod command; mod filesize; mod int; +mod string; pub use self::filesize::SubCommand as IntoFilesize; pub use binary::SubCommand as IntoBinary; pub use command::Into; pub use int::SubCommand as IntoInt; +pub use string::SubCommand as IntoString; diff --git a/crates/nu-command/src/conversions/into/string.rs b/crates/nu-command/src/conversions/into/string.rs new file mode 100644 index 0000000000..a74b630e2a --- /dev/null +++ b/crates/nu-command/src/conversions/into/string.rs @@ -0,0 +1,377 @@ +use nu_protocol::Value::Filesize; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +use bigdecimal::{BigDecimal, FromPrimitive}; + +use nu_engine::CallExt; + +use num_bigint::{BigInt, BigUint, ToBigInt}; +use num_format::Locale; +use num_traits::{Pow, Signed}; +use std::iter; +// TODO num_format::SystemLocale once platform-specific dependencies are stable (see Cargo.toml) + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "into string" + } + + fn signature(&self) -> Signature { + Signature::build("into string") + // FIXME - need to support column paths + // .rest( + // "rest", + // SyntaxShape::ColumnPaths(), + // "column paths to convert to string (for table input)", + // ) + .named( + "decimals", + SyntaxShape::Int, + "decimal digits to which to round", + Some('d'), + ) + } + + fn usage(&self) -> &str { + "Convert value to string" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + string_helper(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert decimal to string and round to nearest integer", + example: "1.7 | into string -d 0", + result: Some(Value::String { + val: "2".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert decimal to string", + example: "1.7 | into string -d 1", + result: Some(Value::String { + val: "1.7".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert decimal to string and limit to 2 decimals", + example: "1.734 | into string -d 2", + result: Some(Value::String { + val: "1.73".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "try to convert decimal to string and provide negative decimal points", + example: "1.734 | into string -d -2", + result: None, + // FIXME + // result: Some(Value::Error { + // error: ShellError::UnsupportedInput( + // String::from("Cannot accept negative integers for decimals arguments"), + // Span::unknown(), + // ), + // }), + }, + Example { + description: "convert decimal to string", + example: "4.3 | into string", + result: Some(Value::String { + val: "4.3".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert string to string", + example: "'1234' | into string", + result: Some(Value::String { + val: "1234".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert boolean to string", + example: "$true | into string", + result: Some(Value::String { + val: "true".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert date to string", + example: "date now | into string", + result: None, + }, + Example { + description: "convert filepath to string", + example: "ls Cargo.toml | get name | into string", + result: None, + }, + Example { + description: "convert filesize to string", + example: "ls Cargo.toml | get size | into string", + result: None, + }, + ] + } +} + +fn string_helper( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let decimals = call.has_flag("decimals"); + let head = call.head; + let decimals_value: Option = call.get_flag(engine_state, stack, "decimals")?; + + if decimals && decimals_value.is_some() && decimals_value.unwrap().is_negative() { + return Err(ShellError::UnsupportedInput( + "Cannot accept negative integers for decimals arguments".to_string(), + head, + )); + } + + input.map( + move |v| action(v, head, decimals, decimals_value, false), + engine_state.ctrlc.clone(), + ) +} + +pub fn action( + input: Value, + head: Span, + decimals: bool, + digits: Option, + group_digits: bool, +) -> Value { + match input { + Value::Int { val, span } => { + let res = if group_digits { + format_int(val) // int.to_formatted_string(*locale) + } else { + val.to_string() + }; + + Value::String { + val: res, + span: head, + } + } + Value::Float { val, span } => { + if decimals { + let dec = BigDecimal::from_f64(val); + let decimal_value = digits.unwrap() as u64; + match dec { + Some(x) => Value::String { + val: format_decimal(x, Some(decimal_value), group_digits), + span, + }, + None => Value::Error { + error: ShellError::CantConvert( + String::from(format!("cannot convert {}to BigDecimal", val)), + head, + ), + }, + } + } else { + Value::String { + val: val.to_string(), + span: head, + } + } + } + // We do not seem to have BigInt at the moment as a Value Type + // Value::BigInt { val, span } => { + // let res = if group_digits { + // format_bigint(val) // int.to_formatted_string(*locale) + // } else { + // int.to_string() + // }; + + // Value::String { + // val: res, + // span: head, + // } + // .into_pipeline_data() + // } + Value::Bool { val, span } => Value::String { + val: val.to_string(), + span: head, + }, + + Value::Date { val, span } => Value::String { + val: val.format("%c").to_string(), + span: head, + }, + + Value::String { val, span } => Value::String { val, span: head }, + + // FIXME - we do not have a FilePath type anymore. Do we need to support this? + // Value::FilePath(a_filepath) => a_filepath.as_path().display().to_string(), + Value::Filesize { val, span } => { + // let byte_string = InlineShape::format_bytes(*val, None); + // Ok(Value::String { + // val: byte_string.1, + // span, + // } + Value::String { + val: input.into_string(), + span: head, + } + } + Value::Nothing { span } => Value::String { + val: "nothing".to_string(), + span: head, + }, + Value::Record { cols, vals, span } => Value::Error { + error: ShellError::UnsupportedInput( + "Cannot convert Record into string".to_string(), + head, + ), + }, + + _ => Value::Error { + error: ShellError::CantConvert( + String::from(" into string. Probably this type is not supported yet"), + head, + ), + }, + } +} +fn format_int(int: i64) -> String { + int.to_string() + + // TODO once platform-specific dependencies are stable (see Cargo.toml) + // #[cfg(windows)] + // { + // int.to_formatted_string(&Locale::en) + // } + // #[cfg(not(windows))] + // { + // match SystemLocale::default() { + // Ok(locale) => int.to_formatted_string(&locale), + // Err(_) => int.to_formatted_string(&Locale::en), + // } + // } +} + +fn format_bigint(int: &BigInt) -> String { + int.to_string() + + // TODO once platform-specific dependencies are stable (see Cargo.toml) + // #[cfg(windows)] + // { + // int.to_formatted_string(&Locale::en) + // } + // #[cfg(not(windows))] + // { + // match SystemLocale::default() { + // Ok(locale) => int.to_formatted_string(&locale), + // Err(_) => int.to_formatted_string(&Locale::en), + // } + // } +} + +fn format_decimal(mut decimal: BigDecimal, digits: Option, group_digits: bool) -> String { + if let Some(n) = digits { + decimal = round_decimal(&decimal, n) + } + + if decimal.is_integer() && (digits.is_none() || digits == Some(0)) { + let int = decimal.as_bigint_and_exponent().0; + // .expect("integer BigDecimal should convert to BigInt"); + return if group_digits { + int.to_string() + } else { + format_bigint(&int) + }; + } + + let (int, exp) = decimal.as_bigint_and_exponent(); + let factor = BigInt::from(10).pow(BigUint::from(exp as u64)); // exp > 0 for non-int decimal + let int_part = &int / &factor; + let dec_part = (&int % &factor) + .abs() + .to_biguint() + .expect("BigInt::abs should always produce positive signed BigInt and thus BigUInt") + .to_str_radix(10); + + let dec_str = if let Some(n) = digits { + dec_part + .chars() + .chain(iter::repeat('0')) + .take(n as usize) + .collect() + } else { + String::from(dec_part.trim_end_matches('0')) + }; + + let format_default_loc = |int_part: BigInt| { + let loc = Locale::en; + //TODO: when num_format is available for recent bigint, replace this with the locale-based format + let (int_str, sep) = (int_part.to_string(), String::from(loc.decimal())); + + format!("{}{}{}", int_str, sep, dec_str) + }; + + format_default_loc(int_part) + + // TODO once platform-specific dependencies are stable (see Cargo.toml) + // #[cfg(windows)] + // { + // format_default_loc(int_part) + // } + // #[cfg(not(windows))] + // { + // match SystemLocale::default() { + // Ok(sys_loc) => { + // let int_str = int_part.to_formatted_string(&sys_loc); + // let sep = String::from(sys_loc.decimal()); + // format!("{}{}{}", int_str, sep, dec_str) + // } + // Err(_) => format_default_loc(int_part), + // } + // } +} + +fn round_decimal(decimal: &BigDecimal, mut digits: u64) -> BigDecimal { + let mut mag = decimal.clone(); + while mag >= BigDecimal::from(1) { + mag = mag / 10; + digits += 1; + } + + decimal.with_prec(digits) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 3d41f27355..708c581fea 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -53,6 +53,7 @@ pub fn create_default_context() -> EngineState { IntoBinary, IntoFilesize, IntoInt, + IntoString, Last, Length, Let, From 78cc3452df44fb8c80131bf60476bb34ac5d62ec Mon Sep 17 00:00:00 2001 From: Stefan Stanciulescu Date: Tue, 2 Nov 2021 20:51:03 +0100 Subject: [PATCH 2/5] Fix clippy warnings for into string command --- .../nu-command/src/conversions/into/string.rs | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/crates/nu-command/src/conversions/into/string.rs b/crates/nu-command/src/conversions/into/string.rs index a74b630e2a..1f526726a8 100644 --- a/crates/nu-command/src/conversions/into/string.rs +++ b/crates/nu-command/src/conversions/into/string.rs @@ -1,15 +1,12 @@ -use nu_protocol::Value::Filesize; +use nu_engine::CallExt; use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, - Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, + Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, }; use bigdecimal::{BigDecimal, FromPrimitive}; - -use nu_engine::CallExt; - -use num_bigint::{BigInt, BigUint, ToBigInt}; +use num_bigint::{BigInt, BigUint}; use num_format::Locale; use num_traits::{Pow, Signed}; use std::iter; @@ -165,7 +162,7 @@ pub fn action( group_digits: bool, ) -> Value { match input { - Value::Int { val, span } => { + Value::Int { val, span: _ } => { let res = if group_digits { format_int(val) // int.to_formatted_string(*locale) } else { @@ -177,18 +174,18 @@ pub fn action( span: head, } } - Value::Float { val, span } => { + Value::Float { val, span: _ } => { if decimals { let dec = BigDecimal::from_f64(val); let decimal_value = digits.unwrap() as u64; match dec { Some(x) => Value::String { val: format_decimal(x, Some(decimal_value), group_digits), - span, + span: head, }, None => Value::Error { error: ShellError::CantConvert( - String::from(format!("cannot convert {}to BigDecimal", val)), + format!("cannot convert {} to BigDecimal", val), head, ), }, @@ -214,36 +211,33 @@ pub fn action( // } // .into_pipeline_data() // } - Value::Bool { val, span } => Value::String { + Value::Bool { val, span: _ } => Value::String { val: val.to_string(), span: head, }, - Value::Date { val, span } => Value::String { + Value::Date { val, span: _ } => Value::String { val: val.format("%c").to_string(), span: head, }, - Value::String { val, span } => Value::String { val, span: head }, + Value::String { val, span: _ } => Value::String { val, span: head }, // FIXME - we do not have a FilePath type anymore. Do we need to support this? // Value::FilePath(a_filepath) => a_filepath.as_path().display().to_string(), - Value::Filesize { val, span } => { - // let byte_string = InlineShape::format_bytes(*val, None); - // Ok(Value::String { - // val: byte_string.1, - // span, - // } - Value::String { - val: input.into_string(), - span: head, - } - } - Value::Nothing { span } => Value::String { + Value::Filesize { val: _, span: _ } => Value::String { + val: input.into_string(), + span: head, + }, + Value::Nothing { span: _ } => Value::String { val: "nothing".to_string(), span: head, }, - Value::Record { cols, vals, span } => Value::Error { + Value::Record { + cols: _, + vals: _, + span: _, + } => Value::Error { error: ShellError::UnsupportedInput( "Cannot convert Record into string".to_string(), head, From bf6c3e53a07527ae60b896f160d0bc5fa93312b3 Mon Sep 17 00:00:00 2001 From: Stefan Stanciulescu Date: Wed, 3 Nov 2021 08:38:31 +0100 Subject: [PATCH 3/5] Remove BigDecimal and use i64/f64 instead --- .../nu-command/src/conversions/into/string.rs | 159 ++---------------- 1 file changed, 18 insertions(+), 141 deletions(-) diff --git a/crates/nu-command/src/conversions/into/string.rs b/crates/nu-command/src/conversions/into/string.rs index 1f526726a8..8392218c1a 100644 --- a/crates/nu-command/src/conversions/into/string.rs +++ b/crates/nu-command/src/conversions/into/string.rs @@ -5,11 +5,6 @@ use nu_protocol::{ Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, }; -use bigdecimal::{BigDecimal, FromPrimitive}; -use num_bigint::{BigInt, BigUint}; -use num_format::Locale; -use num_traits::{Pow, Signed}; -use std::iter; // TODO num_format::SystemLocale once platform-specific dependencies are stable (see Cargo.toml) #[derive(Clone)] @@ -156,82 +151,54 @@ fn string_helper( pub fn action( input: Value, - head: Span, + span: Span, decimals: bool, digits: Option, group_digits: bool, ) -> Value { match input { - Value::Int { val, span: _ } => { + Value::Int { val, .. } => { let res = if group_digits { format_int(val) // int.to_formatted_string(*locale) } else { val.to_string() }; - Value::String { - val: res, - span: head, - } + Value::String { val: res, span } } - Value::Float { val, span: _ } => { + Value::Float { val, .. } => { if decimals { - let dec = BigDecimal::from_f64(val); - let decimal_value = digits.unwrap() as u64; - match dec { - Some(x) => Value::String { - val: format_decimal(x, Some(decimal_value), group_digits), - span: head, - }, - None => Value::Error { - error: ShellError::CantConvert( - format!("cannot convert {} to BigDecimal", val), - head, - ), - }, + let decimal_value = digits.unwrap() as usize; + Value::String { + val: format!("{:.*}", decimal_value, val), + span, } } else { Value::String { val: val.to_string(), - span: head, + span, } } } - // We do not seem to have BigInt at the moment as a Value Type - // Value::BigInt { val, span } => { - // let res = if group_digits { - // format_bigint(val) // int.to_formatted_string(*locale) - // } else { - // int.to_string() - // }; - - // Value::String { - // val: res, - // span: head, - // } - // .into_pipeline_data() - // } - Value::Bool { val, span: _ } => Value::String { + Value::Bool { val, .. } => Value::String { val: val.to_string(), - span: head, + span, }, - - Value::Date { val, span: _ } => Value::String { + Value::Date { val, .. } => Value::String { val: val.format("%c").to_string(), - span: head, + span, }, - - Value::String { val, span: _ } => Value::String { val, span: head }, + Value::String { val, .. } => Value::String { val, span }, // FIXME - we do not have a FilePath type anymore. Do we need to support this? // Value::FilePath(a_filepath) => a_filepath.as_path().display().to_string(), - Value::Filesize { val: _, span: _ } => Value::String { + Value::Filesize { val: _, .. } => Value::String { val: input.into_string(), - span: head, + span, }, - Value::Nothing { span: _ } => Value::String { + Value::Nothing { .. } => Value::String { val: "nothing".to_string(), - span: head, + span, }, Value::Record { cols: _, @@ -243,7 +210,6 @@ pub fn action( head, ), }, - _ => Value::Error { error: ShellError::CantConvert( String::from(" into string. Probably this type is not supported yet"), @@ -269,95 +235,6 @@ fn format_int(int: i64) -> String { // } } -fn format_bigint(int: &BigInt) -> String { - int.to_string() - - // TODO once platform-specific dependencies are stable (see Cargo.toml) - // #[cfg(windows)] - // { - // int.to_formatted_string(&Locale::en) - // } - // #[cfg(not(windows))] - // { - // match SystemLocale::default() { - // Ok(locale) => int.to_formatted_string(&locale), - // Err(_) => int.to_formatted_string(&Locale::en), - // } - // } -} - -fn format_decimal(mut decimal: BigDecimal, digits: Option, group_digits: bool) -> String { - if let Some(n) = digits { - decimal = round_decimal(&decimal, n) - } - - if decimal.is_integer() && (digits.is_none() || digits == Some(0)) { - let int = decimal.as_bigint_and_exponent().0; - // .expect("integer BigDecimal should convert to BigInt"); - return if group_digits { - int.to_string() - } else { - format_bigint(&int) - }; - } - - let (int, exp) = decimal.as_bigint_and_exponent(); - let factor = BigInt::from(10).pow(BigUint::from(exp as u64)); // exp > 0 for non-int decimal - let int_part = &int / &factor; - let dec_part = (&int % &factor) - .abs() - .to_biguint() - .expect("BigInt::abs should always produce positive signed BigInt and thus BigUInt") - .to_str_radix(10); - - let dec_str = if let Some(n) = digits { - dec_part - .chars() - .chain(iter::repeat('0')) - .take(n as usize) - .collect() - } else { - String::from(dec_part.trim_end_matches('0')) - }; - - let format_default_loc = |int_part: BigInt| { - let loc = Locale::en; - //TODO: when num_format is available for recent bigint, replace this with the locale-based format - let (int_str, sep) = (int_part.to_string(), String::from(loc.decimal())); - - format!("{}{}{}", int_str, sep, dec_str) - }; - - format_default_loc(int_part) - - // TODO once platform-specific dependencies are stable (see Cargo.toml) - // #[cfg(windows)] - // { - // format_default_loc(int_part) - // } - // #[cfg(not(windows))] - // { - // match SystemLocale::default() { - // Ok(sys_loc) => { - // let int_str = int_part.to_formatted_string(&sys_loc); - // let sep = String::from(sys_loc.decimal()); - // format!("{}{}{}", int_str, sep, dec_str) - // } - // Err(_) => format_default_loc(int_part), - // } - // } -} - -fn round_decimal(decimal: &BigDecimal, mut digits: u64) -> BigDecimal { - let mut mag = decimal.clone(); - while mag >= BigDecimal::from(1) { - mag = mag / 10; - digits += 1; - } - - decimal.with_prec(digits) -} - #[cfg(test)] mod test { use super::*; From 6906de7c4893b60d8b4b5463f6a4520613b2294f Mon Sep 17 00:00:00 2001 From: Stefan Stanciulescu Date: Wed, 3 Nov 2021 08:48:13 +0100 Subject: [PATCH 4/5] Ooops fix the wrong naming --- crates/nu-command/src/conversions/into/string.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/nu-command/src/conversions/into/string.rs b/crates/nu-command/src/conversions/into/string.rs index 8392218c1a..31e9fa9c04 100644 --- a/crates/nu-command/src/conversions/into/string.rs +++ b/crates/nu-command/src/conversions/into/string.rs @@ -207,13 +207,13 @@ pub fn action( } => Value::Error { error: ShellError::UnsupportedInput( "Cannot convert Record into string".to_string(), - head, + span, ), }, _ => Value::Error { error: ShellError::CantConvert( String::from(" into string. Probably this type is not supported yet"), - head, + span, ), }, } From 20f3b8b27418ca1c3b1803e9352345e40ec8b171 Mon Sep 17 00:00:00 2001 From: Stefan Stanciulescu Date: Wed, 3 Nov 2021 10:41:01 +0100 Subject: [PATCH 5/5] Remove unnecessary crate imports --- crates/nu-command/Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index b822e6084e..99d13d33c3 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -22,10 +22,6 @@ unicode-segmentation = "1.8.0" glob = "0.3.0" thiserror = "1.0.29" sysinfo = "0.20.4" -bigdecimal = { package = "bigdecimal-rs", version = "0.2.1", features = ["serde"] } -num-bigint = { version="0.3.1", features=["serde"] } -num-format = { version="0.4.0", features=["with-num-bigint"] } -num-traits = "0.2.14" chrono = { version = "0.4.19", features = ["serde"] } chrono-humanize = "0.2.1" chrono-tz = "0.6.0"