From 5478ec44bb4889627cffdac34e482d3e64641798 Mon Sep 17 00:00:00 2001 From: Bahex Date: Thu, 26 Jun 2025 23:15:19 +0300 Subject: [PATCH] `to `: preserve round float numbers' type (#16016) - fixes #16011 # Description `Display` implementation for `f64` omits the decimal part for round numbers, and by using it we did the same. This affected: - conversions to delimited formats: `csv`, `tsv` - textual formats: `html`, `md`, `text` - pretty printed `json` (`--raw` was unaffected) - how single float values are displayed in the REPL > [!TIP] > This PR fixes our existing json pretty printing implementation. > We can likely switch to using serde_json's impl using its PrettyFormatter which allows arbitrary indent strings. # User-Facing Changes - Round trips through `csv`, `tsv`, and `json` preserve the type of round floats. - It's always clear whether a number is an integer or a float in the REPL ```nushell 4 / 2 # => 2 # before: is this an int or a float? 4 / 2 # => 2.0 # after: clearly a float ``` # Tests + Formatting Adjusted tests for the new behavior. - :green_circle: toolkit fmt - :green_circle: toolkit clippy - :green_circle: toolkit test - :green_circle: toolkit test stdlib # After Submitting N/A --------- Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com> --- Cargo.lock | 3 + crates/nu-command/src/formats/to/text.rs | 3 +- .../tests/commands/database/query_db.rs | 4 +- crates/nu-command/tests/commands/histogram.rs | 2 +- crates/nu-command/tests/commands/math/avg.rs | 2 +- crates/nu-command/tests/commands/math/log.rs | 2 +- crates/nu-command/tests/commands/math/mod.rs | 4 +- .../nu-command/tests/commands/math/round.rs | 4 +- crates/nu-command/tests/commands/math/sqrt.rs | 4 +- .../tests/commands/math/variance.rs | 2 +- crates/nu-command/tests/commands/mut_.rs | 4 +- .../tests/format_conversions/csv.rs | 11 + .../tests/format_conversions/json.rs | 11 + .../tests/format_conversions/tsv.rs | 11 + crates/nu-json/Cargo.toml | 3 + crates/nu-json/src/ser.rs | 6 +- crates/nu-json/tests/main.rs | 236 +++++------------- crates/nu-protocol/src/value/format.rs | 16 -- crates/nu-protocol/src/value/mod.rs | 5 +- crates/nu-protocol/src/value/range.rs | 6 +- crates/nu-utils/src/float.rs | 21 ++ crates/nu-utils/src/lib.rs | 2 + crates/nuon/src/to.rs | 3 +- tests/assets/nu_json/comments_result.hjson | 2 +- tests/assets/nu_json/comments_result.json | 2 +- tests/assets/nu_json/kan_result.json | 4 +- tests/assets/nu_json/pass1_result.json | 10 +- tests/const_/mod.rs | 2 +- tests/plugins/stream.rs | 2 +- tests/repl/test_custom_commands.rs | 2 +- tests/repl/test_engine.rs | 4 +- tests/shell/pipeline/commands/internal.rs | 4 +- 32 files changed, 169 insertions(+), 228 deletions(-) delete mode 100644 crates/nu-protocol/src/value/format.rs create mode 100644 crates/nu-utils/src/float.rs diff --git a/Cargo.lock b/Cargo.lock index 8b715c5ba5..40cd6134ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3868,7 +3868,10 @@ dependencies = [ "linked-hash-map", "nu-path", "nu-test-support", + "nu-utils", "num-traits", + "pretty_assertions", + "rstest", "serde", "serde_json", ] diff --git a/crates/nu-command/src/formats/to/text.rs b/crates/nu-command/src/formats/to/text.rs index ad15377188..16a0d6f146 100644 --- a/crates/nu-command/src/formats/to/text.rs +++ b/crates/nu-command/src/formats/to/text.rs @@ -2,6 +2,7 @@ use chrono::Datelike; use chrono_humanize::HumanTime; use nu_engine::command_prelude::*; use nu_protocol::{ByteStream, PipelineMetadata, format_duration, shell_error::io::IoError}; +use nu_utils::ObviousFloat; use std::io::Write; const LINE_ENDING: &str = if cfg!(target_os = "windows") { @@ -164,7 +165,7 @@ fn local_into_string( match value { Value::Bool { val, .. } => val.to_string(), Value::Int { val, .. } => val.to_string(), - Value::Float { val, .. } => val.to_string(), + Value::Float { val, .. } => ObviousFloat(val).to_string(), Value::Filesize { val, .. } => val.to_string(), Value::Duration { val, .. } => format_duration(val), Value::Date { val, .. } => { diff --git a/crates/nu-command/tests/commands/database/query_db.rs b/crates/nu-command/tests/commands/database/query_db.rs index b74c11e37e..89ca161fd3 100644 --- a/crates/nu-command/tests/commands/database/query_db.rs +++ b/crates/nu-command/tests/commands/database/query_db.rs @@ -68,7 +68,7 @@ fn ordered_params() { assert_eq!( results.out, - "nimurod-20-6-1-2024-03-23 00:15:24-03:00-72400000-1000000--[104, 101, 108, 108, 111]_\ + "nimurod-20-6.0-1-2024-03-23 00:15:24-03:00-72400000-1000000--[104, 101, 108, 108, 111]_\ string-int-float-int-string-int-int-nothing-binary" ); }); @@ -108,7 +108,7 @@ fn named_params() { assert_eq!( results.out, - "nimurod-20-6-1-2024-03-23 00:15:24-03:00-72400000-1000000--[104, 101, 108, 108, 111]_\ + "nimurod-20-6.0-1-2024-03-23 00:15:24-03:00-72400000-1000000--[104, 101, 108, 108, 111]_\ string-int-float-int-string-int-int-nothing-binary" ); }); diff --git a/crates/nu-command/tests/commands/histogram.rs b/crates/nu-command/tests/commands/histogram.rs index 36e3c84109..1a82d16d10 100644 --- a/crates/nu-command/tests/commands/histogram.rs +++ b/crates/nu-command/tests/commands/histogram.rs @@ -80,7 +80,7 @@ fn count() { " )); - let bit_json = r#"[ { "bit": 1, "count": 3, "quantile": 0.5, "percentage": "50.00%" }, { "bit": 0, "count": 6, "quantile": 1, "percentage": "100.00%" }]"#; + let bit_json = r#"[ { "bit": 1, "count": 3, "quantile": 0.5, "percentage": "50.00%" }, { "bit": 0, "count": 6, "quantile": 1.0, "percentage": "100.00%" }]"#; assert_eq!(actual.out, bit_json); } diff --git a/crates/nu-command/tests/commands/math/avg.rs b/crates/nu-command/tests/commands/math/avg.rs index ec9a124b7b..8720dd6ead 100644 --- a/crates/nu-command/tests/commands/math/avg.rs +++ b/crates/nu-command/tests/commands/math/avg.rs @@ -38,5 +38,5 @@ fn cannot_average_infinite_range() { #[test] fn const_avg() { let actual = nu!("const AVG = [1 3 5] | math avg; $AVG"); - assert_eq!(actual.out, "3"); + assert_eq!(actual.out, "3.0"); } diff --git a/crates/nu-command/tests/commands/math/log.rs b/crates/nu-command/tests/commands/math/log.rs index 23d6abd7cb..a070b0c090 100644 --- a/crates/nu-command/tests/commands/math/log.rs +++ b/crates/nu-command/tests/commands/math/log.rs @@ -3,7 +3,7 @@ use nu_test_support::nu; #[test] fn const_log() { let actual = nu!("const LOG = 16 | math log 2; $LOG"); - assert_eq!(actual.out, "4"); + assert_eq!(actual.out, "4.0"); } #[test] diff --git a/crates/nu-command/tests/commands/math/mod.rs b/crates/nu-command/tests/commands/math/mod.rs index f500f9dc02..fc59f3bfae 100644 --- a/crates/nu-command/tests/commands/math/mod.rs +++ b/crates/nu-command/tests/commands/math/mod.rs @@ -101,7 +101,7 @@ fn division_of_ints() { "# )); - assert_eq!(actual.out, "2"); + assert_eq!(actual.out, "2.0"); } #[test] @@ -189,7 +189,7 @@ fn floor_division_of_floats() { "# )); - assert_eq!(actual.out, "-2"); + assert_eq!(actual.out, "-2.0"); } #[test] diff --git a/crates/nu-command/tests/commands/math/round.rs b/crates/nu-command/tests/commands/math/round.rs index 70b7567354..9f694de079 100644 --- a/crates/nu-command/tests/commands/math/round.rs +++ b/crates/nu-command/tests/commands/math/round.rs @@ -18,14 +18,14 @@ fn can_round_very_large_numbers_with_precision() { fn can_round_integer_with_negative_precision() { let actual = nu!("123 | math round --precision -1"); - assert_eq!(actual.out, "120") + assert_eq!(actual.out, "120.0") } #[test] fn can_round_float_with_negative_precision() { let actual = nu!("123.3 | math round --precision -1"); - assert_eq!(actual.out, "120") + assert_eq!(actual.out, "120.0") } #[test] diff --git a/crates/nu-command/tests/commands/math/sqrt.rs b/crates/nu-command/tests/commands/math/sqrt.rs index 3da12fab7c..71d3edf958 100644 --- a/crates/nu-command/tests/commands/math/sqrt.rs +++ b/crates/nu-command/tests/commands/math/sqrt.rs @@ -18,13 +18,13 @@ fn can_sqrt_irrational() { fn can_sqrt_perfect_square() { let actual = nu!("echo 4 | math sqrt"); - assert_eq!(actual.out, "2"); + assert_eq!(actual.out, "2.0"); } #[test] fn const_sqrt() { let actual = nu!("const SQRT = 4 | math sqrt; $SQRT"); - assert_eq!(actual.out, "2"); + assert_eq!(actual.out, "2.0"); } #[test] diff --git a/crates/nu-command/tests/commands/math/variance.rs b/crates/nu-command/tests/commands/math/variance.rs index b31d75e148..508d5d8fa2 100644 --- a/crates/nu-command/tests/commands/math/variance.rs +++ b/crates/nu-command/tests/commands/math/variance.rs @@ -3,7 +3,7 @@ use nu_test_support::nu; #[test] fn const_variance() { let actual = nu!("const VAR = [1 2 3 4 5] | math variance; $VAR"); - assert_eq!(actual.out, "2"); + assert_eq!(actual.out, "2.0"); } #[test] diff --git a/crates/nu-command/tests/commands/mut_.rs b/crates/nu-command/tests/commands/mut_.rs index 5007948ec5..dca545e17a 100644 --- a/crates/nu-command/tests/commands/mut_.rs +++ b/crates/nu-command/tests/commands/mut_.rs @@ -69,7 +69,7 @@ fn mut_multiply_assign() { fn mut_divide_assign() { let actual = nu!("mut y = 8; $y /= 2; $y"); - assert_eq!(actual.out, "4"); + assert_eq!(actual.out, "4.0"); } #[test] @@ -104,7 +104,7 @@ fn mut_path_upsert_list() { fn mut_path_operator_assign() { let actual = nu!("mut a = {b:1}; $a.b += 3; $a.b -= 2; $a.b *= 10; $a.b /= 4; $a.b"); - assert_eq!(actual.out, "5"); + assert_eq!(actual.out, "5.0"); } #[test] diff --git a/crates/nu-command/tests/format_conversions/csv.rs b/crates/nu-command/tests/format_conversions/csv.rs index c098bc9f20..abdc31dc11 100644 --- a/crates/nu-command/tests/format_conversions/csv.rs +++ b/crates/nu-command/tests/format_conversions/csv.rs @@ -78,6 +78,17 @@ fn table_to_csv_text_skipping_headers_after_conversion() { }) } +#[test] +fn table_to_csv_float_doesnt_become_int() { + let actual = nu!(pipeline( + r#" + [[a]; [1.0]] | to csv | from csv | get 0.a | describe + "# + )); + + assert_eq!(actual.out, "float") +} + #[test] fn infers_types() { Playground::setup("filter_from_csv_test_1", |dirs, sandbox| { diff --git a/crates/nu-command/tests/format_conversions/json.rs b/crates/nu-command/tests/format_conversions/json.rs index 9b6c0453d7..413b6f32c6 100644 --- a/crates/nu-command/tests/format_conversions/json.rs +++ b/crates/nu-command/tests/format_conversions/json.rs @@ -17,6 +17,17 @@ fn table_to_json_text_and_from_json_text_back_into_table() { assert_eq!(actual.out, "markup"); } +#[test] +fn table_to_json_float_doesnt_become_int() { + let actual = nu!(pipeline( + r#" + [[a]; [1.0]] | to json | from json | get 0.a | describe + "# + )); + + assert_eq!(actual.out, "float") +} + #[test] fn from_json_text_to_table() { Playground::setup("filter_from_json_test_1", |dirs, sandbox| { diff --git a/crates/nu-command/tests/format_conversions/tsv.rs b/crates/nu-command/tests/format_conversions/tsv.rs index 31740ef8d9..4a6a09f1f8 100644 --- a/crates/nu-command/tests/format_conversions/tsv.rs +++ b/crates/nu-command/tests/format_conversions/tsv.rs @@ -78,6 +78,17 @@ fn table_to_tsv_text_skipping_headers_after_conversion() { }) } +#[test] +fn table_to_tsv_float_doesnt_become_int() { + let actual = nu!(pipeline( + r#" + [[a]; [1.0]] | to tsv | from tsv | get 0.a | describe + "# + )); + + assert_eq!(actual.out, "float") +} + #[test] fn from_tsv_text_to_table() { Playground::setup("filter_from_tsv_test_1", |dirs, sandbox| { diff --git a/crates/nu-json/Cargo.toml b/crates/nu-json/Cargo.toml index 7a0a7fd427..100257a47d 100644 --- a/crates/nu-json/Cargo.toml +++ b/crates/nu-json/Cargo.toml @@ -24,12 +24,15 @@ linked-hash-map = { version = "0.5", optional = true } num-traits = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false } [dev-dependencies] nu-test-support = { path = "../nu-test-support", version = "0.105.2" } nu-path = { path = "../nu-path", version = "0.105.2" } serde_json = "1.0" fancy-regex = "0.14.0" +pretty_assertions = { workspace = true } +rstest = { workspace = true, default-features = false } [lints] workspace = true diff --git a/crates/nu-json/src/ser.rs b/crates/nu-json/src/ser.rs index 85a00d4516..b6a3c922d2 100644 --- a/crates/nu-json/src/ser.rs +++ b/crates/nu-json/src/ser.rs @@ -6,6 +6,8 @@ use std::fmt::{Display, LowerExp}; use std::io; use std::num::FpCategory; +use nu_utils::ObviousFloat; + use super::error::{Error, ErrorCode, Result}; use serde::ser; @@ -868,7 +870,7 @@ where { match value.classify() { FpCategory::Nan | FpCategory::Infinite => wr.write_all(b"null")?, - _ => wr.write_all(fmt_small(value).as_bytes())?, + _ => wr.write_all(fmt_small(ObviousFloat(value as f64)).as_bytes())?, } Ok(()) @@ -880,7 +882,7 @@ where { match value.classify() { FpCategory::Nan | FpCategory::Infinite => wr.write_all(b"null")?, - _ => wr.write_all(fmt_small(value).as_bytes())?, + _ => wr.write_all(fmt_small(ObviousFloat(value)).as_bytes())?, } Ok(()) diff --git a/crates/nu-json/tests/main.rs b/crates/nu-json/tests/main.rs index 73c68f3d8e..4393b4284a 100644 --- a/crates/nu-json/tests/main.rs +++ b/crates/nu-json/tests/main.rs @@ -1,201 +1,97 @@ -use fancy_regex::Regex; use nu_json::Value; +use pretty_assertions::assert_eq; +use rstest::rstest; use std::fs; use std::io; use std::path::{Path, PathBuf}; -fn txt(text: &str) -> String { - let out = String::from_utf8_lossy(text.as_bytes()); +fn txt(text: String) -> String { + let out = text; #[cfg(windows)] { - out.replace("\r\n", "").replace('\n', "") + out.replace("\r\n", "\n") } #[cfg(not(windows))] { - out.to_string() + out } } -fn hjson_expectations() -> PathBuf { - nu_test_support::fs::assets().join("nu_json").into() +// This test will fail if/when `nu_test_support::fs::assets()`'s return value changes. +#[rstest] +fn assert_rstest_finds_assets(#[files("../../tests/assets/nu_json")] rstest_supplied: PathBuf) { + // rstest::files runs paths through `fs::canonicalize`, which: + // > On Windows, this converts the path to use extended length path syntax + // So we make sure to canonicalize both paths. + assert_eq!( + fs::canonicalize(rstest_supplied).unwrap(), + fs::canonicalize(nu_test_support::fs::assets().join("nu_json")).unwrap() + ); } -fn get_test_content(name: &str) -> io::Result { - let expectations = hjson_expectations(); - - let mut p = format!("{}/{}_test.hjson", expectations.display(), name); - - if !Path::new(&p).exists() { - p = format!("{}/{}_test.json", expectations.display(), name); - } - - fs::read_to_string(&p) +#[rstest] +fn test_hjson_fails(#[files("../../tests/assets/nu_json/fail*_test.*")] file: PathBuf) { + let contents = fs::read_to_string(file).unwrap(); + let data: nu_json::Result = nu_json::from_str(&contents); + assert!(data.is_err()); } -fn get_result_content(name: &str) -> io::Result<(String, String)> { - let expectations = hjson_expectations(); +#[rstest] +fn test_hjson( + #[files("../../tests/assets/nu_json/*_test.*")] + #[exclude("fail*")] + test_file: PathBuf, +) -> Result<(), Box> { + let name = test_file + .file_stem() + .and_then(|x| x.to_str()) + .and_then(|x| x.strip_suffix("_test")) + .unwrap(); - let p1 = format!("{}/{}_result.json", expectations.display(), name); - let p2 = format!("{}/{}_result.hjson", expectations.display(), name); + let data: Value = nu_json::from_str(fs::read_to_string(&test_file)?.as_str())?; - Ok((fs::read_to_string(p1)?, fs::read_to_string(p2)?)) + let r_json = get_content(get_result_path(&test_file, "json").as_deref().unwrap())?; + // let r_hjson = get_content(get_result_path(&test_file, "hjson").as_deref().unwrap())?; + let r_hjson = r_json.as_str(); + + let actual_json = serde_json::to_string_pretty(&data).map(get_fix(name))?; + let actual_hjson = nu_json::to_string(&data).map(txt)?; + + assert_eq!(r_json, actual_json); + assert_eq!(r_hjson, actual_hjson); + + Ok(()) } -macro_rules! run_test { - // {{ is a workaround for rust stable - ($v: ident, $list: expr, $fix: expr) => {{ - let name = stringify!($v); - $list.push(format!("{}_test", name)); - println!("- running {}", name); - let should_fail = name.starts_with("fail"); - let test_content = get_test_content(name).unwrap(); - let data: nu_json::Result = nu_json::from_str(&test_content); - assert!(should_fail == data.is_err()); +fn get_result_path(test_file: &Path, ext: &str) -> Option { + let name = test_file + .file_stem() + .and_then(|x| x.to_str()) + .and_then(|x| x.strip_suffix("_test"))?; - if !should_fail { - let udata = data.unwrap(); - let (rjson, rhjson) = get_result_content(name).unwrap(); - let rjson = txt(&rjson); - let rhjson = txt(&rhjson); - let actual_hjson = nu_json::to_string(&udata).unwrap(); - let actual_hjson = txt(&actual_hjson); - let actual_json = $fix(serde_json::to_string_pretty(&udata).unwrap()); - let actual_json = txt(&actual_json); - // nu_json::to_string now outputs json instead of hjson! - if rjson != actual_hjson { - println!( - "{:?}\n---hjson expected\n{}\n---hjson actual\n{}\n---\n", - name, rhjson, actual_hjson - ); - } - if rjson != actual_json { - println!( - "{:?}\n---json expected\n{}\n---json actual\n{}\n---\n", - name, rjson, actual_json - ); - } - assert!(rjson == actual_hjson && rjson == actual_json); - } - }}; + Some(test_file.with_file_name(format!("{name}_result.{ext}"))) +} + +fn get_content(file: &Path) -> io::Result { + fs::read_to_string(file).map(txt) } // add fixes where rust's json differs from javascript +fn get_fix(s: &str) -> fn(String) -> String { + fn remove_negative_zero(json: String) -> String { + json.replace(" -0,", " 0,") + } -fn std_fix(json: String) -> String { - // serde_json serializes integers with a superfluous .0 suffix - let re = Regex::new(r"(?m)(?P\d)\.0(?P,?)$").unwrap(); - re.replace_all(&json, "$d$s").to_string() -} + fn positive_exp_add_sign(json: String) -> String { + json.replace("1.23456789e34", "1.23456789e+34") + .replace("2.3456789012e76", "2.3456789012e+76") + } -fn fix_kan(json: String) -> String { - std_fix(json).replace(" -0,", " 0,") -} - -fn fix_pass1(json: String) -> String { - std_fix(json) - .replace("1.23456789e34", "1.23456789e+34") - .replace("2.3456789012e76", "2.3456789012e+76") -} - -#[test] -fn test_hjson() { - let mut done: Vec = Vec::new(); - - println!(); - run_test!(charset, done, std_fix); - run_test!(comments, done, std_fix); - run_test!(empty, done, std_fix); - run_test!(failCharset1, done, std_fix); - run_test!(failJSON02, done, std_fix); - run_test!(failJSON05, done, std_fix); - run_test!(failJSON06, done, std_fix); - run_test!(failJSON07, done, std_fix); - run_test!(failJSON08, done, std_fix); - run_test!(failJSON10, done, std_fix); - run_test!(failJSON11, done, std_fix); - run_test!(failJSON12, done, std_fix); - run_test!(failJSON13, done, std_fix); - run_test!(failJSON14, done, std_fix); - run_test!(failJSON15, done, std_fix); - run_test!(failJSON16, done, std_fix); - run_test!(failJSON17, done, std_fix); - run_test!(failJSON19, done, std_fix); - run_test!(failJSON20, done, std_fix); - run_test!(failJSON21, done, std_fix); - run_test!(failJSON22, done, std_fix); - run_test!(failJSON23, done, std_fix); - run_test!(failJSON24, done, std_fix); - run_test!(failJSON26, done, std_fix); - run_test!(failJSON28, done, std_fix); - run_test!(failJSON29, done, std_fix); - run_test!(failJSON30, done, std_fix); - run_test!(failJSON31, done, std_fix); - run_test!(failJSON32, done, std_fix); - run_test!(failJSON33, done, std_fix); - run_test!(failJSON34, done, std_fix); - run_test!(failKey1, done, std_fix); - run_test!(failKey2, done, std_fix); - run_test!(failKey3, done, std_fix); - run_test!(failKey4, done, std_fix); - run_test!(failMLStr1, done, std_fix); - run_test!(failObj1, done, std_fix); - run_test!(failObj2, done, std_fix); - run_test!(failObj3, done, std_fix); - run_test!(failStr1a, done, std_fix); - run_test!(failStr1b, done, std_fix); - run_test!(failStr1c, done, std_fix); - run_test!(failStr1d, done, std_fix); - run_test!(failStr2a, done, std_fix); - run_test!(failStr2b, done, std_fix); - run_test!(failStr2c, done, std_fix); - run_test!(failStr2d, done, std_fix); - run_test!(failStr3a, done, std_fix); - run_test!(failStr3b, done, std_fix); - run_test!(failStr3c, done, std_fix); - run_test!(failStr3d, done, std_fix); - run_test!(failStr4a, done, std_fix); - run_test!(failStr4b, done, std_fix); - run_test!(failStr4c, done, std_fix); - run_test!(failStr4d, done, std_fix); - run_test!(failStr5a, done, std_fix); - run_test!(failStr5b, done, std_fix); - run_test!(failStr5c, done, std_fix); - run_test!(failStr5d, done, std_fix); - run_test!(failStr6a, done, std_fix); - run_test!(failStr6b, done, std_fix); - run_test!(failStr6c, done, std_fix); - run_test!(failStr6d, done, std_fix); - run_test!(kan, done, fix_kan); - run_test!(keys, done, std_fix); - run_test!(oa, done, std_fix); - run_test!(pass1, done, fix_pass1); - run_test!(pass2, done, std_fix); - run_test!(pass3, done, std_fix); - run_test!(pass4, done, std_fix); - run_test!(passSingle, done, std_fix); - run_test!(root, done, std_fix); - run_test!(stringify1, done, std_fix); - run_test!(strings, done, std_fix); - run_test!(trail, done, std_fix); - - // check if we include all assets - let paths = fs::read_dir(hjson_expectations()).unwrap(); - - let all = paths - .map(|item| String::from(item.unwrap().path().file_stem().unwrap().to_str().unwrap())) - .filter(|x| x.contains("_test")); - - let missing = all - .into_iter() - .filter(|x| !done.iter().any(|y| x == y)) - .collect::>(); - - if !missing.is_empty() { - for item in missing { - println!("missing: {}", item); - } - panic!(); + match s { + "kan" => remove_negative_zero, + "pass1" => positive_exp_add_sign, + _ => std::convert::identity, } } diff --git a/crates/nu-protocol/src/value/format.rs b/crates/nu-protocol/src/value/format.rs deleted file mode 100644 index a4f07ffdf6..0000000000 --- a/crates/nu-protocol/src/value/format.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::fmt::Display; - -/// A f64 wrapper that formats whole numbers with a decimal point. -pub struct ObviousFloat(pub f64); - -impl Display for ObviousFloat { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let val = self.0; - // This serialises these as 'nan', 'inf' and '-inf', respectively. - if val.round() == val && val.is_finite() { - write!(f, "{}.0", val) - } else { - write!(f, "{}", val) - } - } -} diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index b92a527cea..5ff4b8a2d1 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -8,7 +8,6 @@ mod range; #[cfg(test)] mod test_derive; -pub mod format; pub mod record; pub use custom_value::CustomValue; pub use duration::*; @@ -29,7 +28,7 @@ use chrono::{DateTime, Datelike, Duration, FixedOffset, Local, Locale, TimeZone} use chrono_humanize::HumanTime; use fancy_regex::Regex; use nu_utils::{ - SharedCow, contains_emoji, + ObviousFloat, SharedCow, contains_emoji, locale::{LOCALE_OVERRIDE_ENV_VAR, get_system_locale_string}, }; use serde::{Deserialize, Serialize}; @@ -939,7 +938,7 @@ impl Value { match self { Value::Bool { val, .. } => val.to_string(), Value::Int { val, .. } => val.to_string(), - Value::Float { val, .. } => val.to_string(), + Value::Float { val, .. } => ObviousFloat(*val).to_string(), Value::Filesize { val, .. } => config.filesize.format(*val).to_string(), Value::Duration { val, .. } => format_duration(*val), Value::Date { val, .. } => match &config.datetime_format.normal { diff --git a/crates/nu-protocol/src/value/range.rs b/crates/nu-protocol/src/value/range.rs index 8b001b4b2d..ef80bbb48b 100644 --- a/crates/nu-protocol/src/value/range.rs +++ b/crates/nu-protocol/src/value/range.rs @@ -307,10 +307,8 @@ mod int_range { } mod float_range { - use crate::{ - IntRange, Range, ShellError, Signals, Span, Value, ast::RangeInclusion, - format::ObviousFloat, - }; + use crate::{IntRange, Range, ShellError, Signals, Span, Value, ast::RangeInclusion}; + use nu_utils::ObviousFloat; use serde::{Deserialize, Serialize}; use std::{cmp::Ordering, fmt::Display, ops::Bound}; diff --git a/crates/nu-utils/src/float.rs b/crates/nu-utils/src/float.rs new file mode 100644 index 0000000000..a1183fdb1e --- /dev/null +++ b/crates/nu-utils/src/float.rs @@ -0,0 +1,21 @@ +use std::fmt::{Display, LowerExp}; + +/// A f64 wrapper that formats whole numbers with a decimal point. +pub struct ObviousFloat(pub f64); + +impl Display for ObviousFloat { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let val = self.0; + if val.fract() == 0.0 { + write!(f, "{val:.1}") + } else { + Display::fmt(&val, f) + } + } +} + +impl LowerExp for ObviousFloat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + LowerExp::fmt(&self.0, f) + } +} diff --git a/crates/nu-utils/src/lib.rs b/crates/nu-utils/src/lib.rs index 117b6fc28b..ff52f557a1 100644 --- a/crates/nu-utils/src/lib.rs +++ b/crates/nu-utils/src/lib.rs @@ -4,6 +4,7 @@ mod deansi; pub mod emoji; pub mod filesystem; pub mod flatten_json; +pub mod float; pub mod locale; mod quoting; mod shared_cow; @@ -22,6 +23,7 @@ pub use deansi::{ }; pub use emoji::contains_emoji; pub use flatten_json::JsonFlattener; +pub use float::ObviousFloat; pub use quoting::{escape_quote_string, needs_quoting}; pub use shared_cow::SharedCow; diff --git a/crates/nuon/src/to.rs b/crates/nuon/src/to.rs index e95e291cb3..c17cbe7379 100644 --- a/crates/nuon/src/to.rs +++ b/crates/nuon/src/to.rs @@ -1,8 +1,7 @@ use core::fmt::Write; use nu_engine::get_columns; -use nu_protocol::format::ObviousFloat; use nu_protocol::{Range, ShellError, Span, Value, engine::EngineState}; -use nu_utils::{escape_quote_string, needs_quoting}; +use nu_utils::{ObviousFloat, escape_quote_string, needs_quoting}; /// control the way Nushell [`Value`] is converted to NUON data pub enum ToStyle { diff --git a/tests/assets/nu_json/comments_result.hjson b/tests/assets/nu_json/comments_result.hjson index a99ce23dba..1060d06364 100644 --- a/tests/assets/nu_json/comments_result.hjson +++ b/tests/assets/nu_json/comments_result.hjson @@ -9,7 +9,7 @@ rem2: "// test" rem3: "/* test */" num1: 0 - num2: 0 + num2: 0.0 num3: 2 true1: true true2: true diff --git a/tests/assets/nu_json/comments_result.json b/tests/assets/nu_json/comments_result.json index e247803e65..ca4076deb8 100644 --- a/tests/assets/nu_json/comments_result.json +++ b/tests/assets/nu_json/comments_result.json @@ -9,7 +9,7 @@ "rem2": "// test", "rem3": "/* test */", "num1": 0, - "num2": 0, + "num2": 0.0, "num3": 2, "true1": true, "true2": true, diff --git a/tests/assets/nu_json/kan_result.json b/tests/assets/nu_json/kan_result.json index babb9d4e06..20ca7de305 100644 --- a/tests/assets/nu_json/kan_result.json +++ b/tests/assets/nu_json/kan_result.json @@ -7,8 +7,8 @@ 42.1, -5, -5.1, - 1701, - -1701, + 1701.0, + -1701.0, 12.345, -12.345 ], diff --git a/tests/assets/nu_json/pass1_result.json b/tests/assets/nu_json/pass1_result.json index 69b354d05e..165c6ff716 100644 --- a/tests/assets/nu_json/pass1_result.json +++ b/tests/assets/nu_json/pass1_result.json @@ -65,11 +65,11 @@ 98.6, 99.44, 1066, - 10, - 1, + 10.0, + 1.0, 0.1, - 1, - 2, - 2, + 1.0, + 2.0, + 2.0, "rosebud" ] \ No newline at end of file diff --git a/tests/const_/mod.rs b/tests/const_/mod.rs index b969c2c6f1..76dc712489 100644 --- a/tests/const_/mod.rs +++ b/tests/const_/mod.rs @@ -147,7 +147,7 @@ fn const_unary_operator(#[case] inp: &[&str], #[case] expect: &str) { #[rstest] #[case(&["const x = 1 + 2", "$x"], "3")] #[case(&["const x = 1 * 2", "$x"], "2")] -#[case(&["const x = 4 / 2", "$x"], "2")] +#[case(&["const x = 4 / 2", "$x"], "2.0")] #[case(&["const x = 4 mod 3", "$x"], "1")] #[case(&["const x = 5.0 / 2.0", "$x"], "2.5")] #[case(&[r#"const x = "a" + "b" "#, "$x"], "ab")] diff --git a/tests/plugins/stream.rs b/tests/plugins/stream.rs index b8771580f7..a37132d40e 100644 --- a/tests/plugins/stream.rs +++ b/tests/plugins/stream.rs @@ -103,7 +103,7 @@ fn sum_accepts_stream_of_float() { "seq 1 5 | into float | example sum" ); - assert_eq!(actual.out, "15"); + assert_eq!(actual.out, "15.0"); } #[test] diff --git a/tests/repl/test_custom_commands.rs b/tests/repl/test_custom_commands.rs index 281c496f54..d6cd718cf1 100644 --- a/tests/repl/test_custom_commands.rs +++ b/tests/repl/test_custom_commands.rs @@ -306,5 +306,5 @@ fn dont_allow_implicit_casting_between_glob_and_string() -> TestResult { #[test] fn allow_pass_negative_float() -> TestResult { run_test("def spam [val: float] { $val }; spam -1.4", "-1.4")?; - run_test("def spam [val: float] { $val }; spam -2", "-2") + run_test("def spam [val: float] { $val }; spam -2", "-2.0") } diff --git a/tests/repl/test_engine.rs b/tests/repl/test_engine.rs index 7c90bb4bb6..c7fca8b2e8 100644 --- a/tests/repl/test_engine.rs +++ b/tests/repl/test_engine.rs @@ -187,12 +187,12 @@ fn proper_variable_captures_with_nesting() -> TestResult { #[test] fn divide_duration() -> TestResult { - run_test(r#"4ms / 4ms"#, "1") + run_test(r#"4ms / 4ms"#, "1.0") } #[test] fn divide_filesize() -> TestResult { - run_test(r#"4mb / 4mb"#, "1") + run_test(r#"4mb / 4mb"#, "1.0") } #[test] diff --git a/tests/shell/pipeline/commands/internal.rs b/tests/shell/pipeline/commands/internal.rs index 2ecf221863..8b05f71ac6 100644 --- a/tests/shell/pipeline/commands/internal.rs +++ b/tests/shell/pipeline/commands/internal.rs @@ -759,7 +759,7 @@ fn range_with_mixed_types() { echo 1..10.5 | math sum "); - assert_eq!(actual.out, "55"); + assert_eq!(actual.out, "55.0"); } #[test] @@ -802,7 +802,7 @@ fn exclusive_range_with_mixed_types() { echo 1..<10.5 | math sum "); - assert_eq!(actual.out, "55"); + assert_eq!(actual.out, "55.0"); } #[test]