diff --git a/Cargo.lock b/Cargo.lock index 618dceb1d6..e4ab4e7bbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2319,6 +2319,7 @@ dependencies = [ name = "nu-plugin" version = "0.14.1" dependencies = [ + "bigdecimal", "indexmap", "nu-build", "nu-errors", @@ -2531,6 +2532,7 @@ dependencies = [ name = "nu_plugin_str" version = "0.14.1" dependencies = [ + "bigdecimal", "chrono", "nu-build", "nu-errors", diff --git a/crates/nu-cli/src/commands/autoview.rs b/crates/nu-cli/src/commands/autoview.rs index e44c7bcee1..ccf3e35f78 100644 --- a/crates/nu-cli/src/commands/autoview.rs +++ b/crates/nu-cli/src/commands/autoview.rs @@ -179,7 +179,16 @@ pub fn autoview(context: RunnableContext) -> Result { value: UntaggedValue::Primitive(Primitive::Decimal(n)), .. } => { - out!("{}", n); + // TODO: normalize decimal to remove trailing zeros. + // normalization will be available in next release of bigdecimal crate + let mut output = n.to_string(); + if output.contains('.') { + output = output.trim_end_matches('0').to_owned(); + } + if output.ends_with('.') { + output.push('0'); + } + out!("{}", output); } Value { value: UntaggedValue::Primitive(Primitive::Boolean(b)), diff --git a/crates/nu-plugin/Cargo.toml b/crates/nu-plugin/Cargo.toml index 22be81eaa0..1a134b6c15 100644 --- a/crates/nu-plugin/Cargo.toml +++ b/crates/nu-plugin/Cargo.toml @@ -19,6 +19,7 @@ indexmap = { version = "1.3.2", features = ["serde-1"] } serde = { version = "1.0.110", features = ["derive"] } num-bigint = { version = "0.2.6", features = ["serde"] } serde_json = "1.0.53" +bigdecimal = { version = "0.1.2", features = ["serde"] } [build-dependencies] nu-build = { version = "0.14.1", path = "../nu-build" } diff --git a/crates/nu-plugin/src/test_helpers.rs b/crates/nu-plugin/src/test_helpers.rs index addc13d54e..f64c01ef52 100644 --- a/crates/nu-plugin/src/test_helpers.rs +++ b/crates/nu-plugin/src/test_helpers.rs @@ -172,6 +172,7 @@ pub fn expect_return_value_at( } pub mod value { + use bigdecimal::BigDecimal; use nu_errors::ShellError; use nu_protocol::{Primitive, TaggedDictBuilder, UntaggedValue, Value}; use nu_source::Tag; @@ -186,6 +187,10 @@ pub mod value { UntaggedValue::Primitive(Primitive::Int(i.into())).into_untagged_value() } + pub fn decimal(f: impl Into) -> Value { + UntaggedValue::Primitive(Primitive::Decimal(f.into())).into_untagged_value() + } + pub fn string(input: impl Into) -> Value { UntaggedValue::string(input.into()).into_untagged_value() } diff --git a/crates/nu_plugin_str/Cargo.toml b/crates/nu_plugin_str/Cargo.toml index 306d19688e..c895484b4c 100644 --- a/crates/nu_plugin_str/Cargo.toml +++ b/crates/nu_plugin_str/Cargo.toml @@ -19,6 +19,7 @@ chrono = { version = "0.4.11", features = ["serde"] } regex = "1" num-bigint = "0.2.6" +bigdecimal = { version = "0.1.2", features = ["serde"] } [build-dependencies] nu-build = { version = "0.14.1", path = "../nu-build" } diff --git a/crates/nu_plugin_str/src/nu/mod.rs b/crates/nu_plugin_str/src/nu/mod.rs index 22ee89884b..efd54c75f7 100644 --- a/crates/nu_plugin_str/src/nu/mod.rs +++ b/crates/nu_plugin_str/src/nu/mod.rs @@ -19,6 +19,7 @@ impl Plugin for Str { .switch("downcase", "convert string to lowercase", Some('d')) .switch("upcase", "convert string to uppercase", Some('U')) .switch("to-int", "convert string to integer", Some('i')) + .switch("to-float", "convert string to float", Some('F')) .switch("trim", "trims the string", Some('t')) .named( "replace", @@ -66,6 +67,9 @@ impl Plugin for Str { if args.has("to-int") { self.for_to_int(); } + if args.has("to-float") { + self.for_to_float(); + } if args.has("substring") { if let Some(start_end) = args.get("substring") { match start_end { diff --git a/crates/nu_plugin_str/src/nu/tests.rs b/crates/nu_plugin_str/src/nu/tests.rs index 743f943611..62ab893575 100644 --- a/crates/nu_plugin_str/src/nu/tests.rs +++ b/crates/nu_plugin_str/src/nu/tests.rs @@ -3,7 +3,7 @@ mod integration { use crate::Str; use nu_errors::ShellError; use nu_plugin::test_helpers::value::{ - column_path, get_data, int, string, structured_sample_record, table, + column_path, decimal, get_data, int, string, structured_sample_record, table, unstructured_sample_record, }; use nu_plugin::test_helpers::{expect_return_value_at, plugin, CallStub}; @@ -91,6 +91,13 @@ mod integration { .setup(|plugin, _| plugin.expect_action(Action::ToInteger)); } + #[test] + fn picks_up_to_float_flag() { + plugin(&mut Str::new()) + .args(CallStub::new().with_long_flag("to-float").create()) + .setup(|plugin, _| plugin.expect_action(Action::ToFloat)); + } + #[test] fn picks_up_arguments_for_replace_flag() { let argument = String::from("replace_text"); @@ -259,6 +266,24 @@ mod integration { assert_eq!(get_data(actual, "Nu_birthday"), int(10)); Ok(()) } + #[test] + fn converts_the_input_to_float_using_the_field_passed_as_parameter() -> Result<(), ShellError> { + let run = plugin(&mut Str::new()) + .args( + CallStub::new() + .with_long_flag("to-float") + .with_parameter("PI")? + .create(), + ) + .input(structured_sample_record("PI", "3.1415")) + .setup(|_, _| {}) + .test(); + + let actual = expect_return_value_at(run, 0); + + assert_eq!(get_data(actual, "PI"), decimal(3.1415)); + Ok(()) + } #[test] fn replaces_the_input_using_the_field_passed_as_parameter() -> Result<(), ShellError> { diff --git a/crates/nu_plugin_str/src/strutils.rs b/crates/nu_plugin_str/src/strutils.rs index f208580605..da8f024ab0 100644 --- a/crates/nu_plugin_str/src/strutils.rs +++ b/crates/nu_plugin_str/src/strutils.rs @@ -1,5 +1,6 @@ extern crate chrono; +use bigdecimal::BigDecimal; use chrono::DateTime; use nu_errors::ShellError; use nu_protocol::{did_you_mean, ColumnPath, Primitive, ShellTypeName, UntaggedValue, Value}; @@ -7,6 +8,7 @@ use nu_source::{span_for_spanned_list, Tagged}; use nu_value_ext::ValueExt; use regex::Regex; use std::cmp; +use std::str::FromStr; #[derive(Debug, Eq, PartialEq)] pub enum Action { @@ -14,6 +16,7 @@ pub enum Action { Downcase, Upcase, ToInteger, + ToFloat, Substring(usize, usize), Replace(ReplaceAction), ToDateTime(String), @@ -93,6 +96,10 @@ impl Str { Err(_) => UntaggedValue::string(input), } } + Some(Action::ToFloat) => match BigDecimal::from_str(input.trim()) { + Ok(v) => UntaggedValue::decimal(v), + Err(_) => UntaggedValue::string(input), + }, Some(Action::ToDateTime(dt)) => match DateTime::parse_from_str(input, dt) { Ok(d) => UntaggedValue::date(d), Err(_) => UntaggedValue::string(input), @@ -119,6 +126,10 @@ impl Str { self.add_action(Action::ToInteger); } + pub fn for_to_float(&mut self) { + self.add_action(Action::ToFloat); + } + pub fn for_capitalize(&mut self) { self.add_action(Action::Capitalize); } @@ -177,7 +188,7 @@ impl Str { } pub fn usage() -> &'static str { - "Usage: str field [--capitalize|--downcase|--upcase|--to-int|--substring \"start,end\"|--replace|--find-replace [pattern replacement]|to-date-time|--trim]" + "Usage: str field [--capitalize|--downcase|--upcase|--to-int|--to-float|--substring \"start,end\"|--replace|--find-replace [pattern replacement]|to-date-time|--trim]" } pub fn strutils(&self, value: Value) -> Result { @@ -240,7 +251,7 @@ impl Str { pub mod tests { use super::ReplaceAction; use super::Str; - use nu_plugin::test_helpers::value::{int, string}; + use nu_plugin::test_helpers::value::{decimal, int, string}; #[test] fn trim() -> Result<(), Box> { @@ -282,6 +293,14 @@ pub mod tests { Ok(()) } + #[test] + fn converts_to_float() -> Result<(), Box> { + let mut strutils = Str::new(); + strutils.for_to_float(); + assert_eq!(strutils.apply("3.1415")?, decimal(3.1415).value); + Ok(()) + } + #[test] fn replaces() -> Result<(), Box> { let mut strutils = Str::new(); diff --git a/tests/plugins/core_str.rs b/tests/plugins/core_str.rs index 07b50c4dd1..22da0821f5 100644 --- a/tests/plugins/core_str.rs +++ b/tests/plugins/core_str.rs @@ -9,7 +9,7 @@ fn can_only_apply_one() { "open caco3_plastics.csv | first 1 | str origin --downcase --upcase" ); - assert!(actual.err.contains(r#"--capitalize|--downcase|--upcase|--to-int|--substring "start,end"|--replace|--find-replace [pattern replacement]|to-date-time|--trim]"#)); + assert!(actual.err.contains(r#"--capitalize|--downcase|--upcase|--to-int|--to-float|--substring "start,end"|--replace|--find-replace [pattern replacement]|to-date-time|--trim]"#)); } #[test] @@ -131,6 +131,21 @@ fn converts_to_int() { assert_eq!(actual.out, "1"); } +#[test] +fn converts_to_float() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo "3.1, 0.0415" + | split row "," + | str --to-float + | sum + "# + )); + + assert_eq!(actual.out, "3.1415"); +} + #[test] fn replaces() { Playground::setup("plugin_str_test_5", |dirs, sandbox| {