mirror of
https://github.com/nushell/nushell.git
synced 2025-07-02 23:51:49 +02:00
to <format>
: 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. - 🟢 toolkit fmt - 🟢 toolkit clippy - 🟢 toolkit test - 🟢 toolkit test stdlib # After Submitting N/A --------- Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com>
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -3868,7 +3868,10 @@ dependencies = [
|
|||||||
"linked-hash-map",
|
"linked-hash-map",
|
||||||
"nu-path",
|
"nu-path",
|
||||||
"nu-test-support",
|
"nu-test-support",
|
||||||
|
"nu-utils",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"pretty_assertions",
|
||||||
|
"rstest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
@ -2,6 +2,7 @@ use chrono::Datelike;
|
|||||||
use chrono_humanize::HumanTime;
|
use chrono_humanize::HumanTime;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::{ByteStream, PipelineMetadata, format_duration, shell_error::io::IoError};
|
use nu_protocol::{ByteStream, PipelineMetadata, format_duration, shell_error::io::IoError};
|
||||||
|
use nu_utils::ObviousFloat;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
const LINE_ENDING: &str = if cfg!(target_os = "windows") {
|
const LINE_ENDING: &str = if cfg!(target_os = "windows") {
|
||||||
@ -164,7 +165,7 @@ fn local_into_string(
|
|||||||
match value {
|
match value {
|
||||||
Value::Bool { val, .. } => val.to_string(),
|
Value::Bool { val, .. } => val.to_string(),
|
||||||
Value::Int { 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::Filesize { val, .. } => val.to_string(),
|
||||||
Value::Duration { val, .. } => format_duration(val),
|
Value::Duration { val, .. } => format_duration(val),
|
||||||
Value::Date { val, .. } => {
|
Value::Date { val, .. } => {
|
||||||
|
@ -68,7 +68,7 @@ fn ordered_params() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
results.out,
|
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"
|
string-int-float-int-string-int-int-nothing-binary"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -108,7 +108,7 @@ fn named_params() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
results.out,
|
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"
|
string-int-float-int-string-int-int-nothing-binary"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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);
|
assert_eq!(actual.out, bit_json);
|
||||||
}
|
}
|
||||||
|
@ -38,5 +38,5 @@ fn cannot_average_infinite_range() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn const_avg() {
|
fn const_avg() {
|
||||||
let actual = nu!("const AVG = [1 3 5] | math avg; $AVG");
|
let actual = nu!("const AVG = [1 3 5] | math avg; $AVG");
|
||||||
assert_eq!(actual.out, "3");
|
assert_eq!(actual.out, "3.0");
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use nu_test_support::nu;
|
|||||||
#[test]
|
#[test]
|
||||||
fn const_log() {
|
fn const_log() {
|
||||||
let actual = nu!("const LOG = 16 | math log 2; $LOG");
|
let actual = nu!("const LOG = 16 | math log 2; $LOG");
|
||||||
assert_eq!(actual.out, "4");
|
assert_eq!(actual.out, "4.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -101,7 +101,7 @@ fn division_of_ints() {
|
|||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(actual.out, "2");
|
assert_eq!(actual.out, "2.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -189,7 +189,7 @@ fn floor_division_of_floats() {
|
|||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(actual.out, "-2");
|
assert_eq!(actual.out, "-2.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -18,14 +18,14 @@ fn can_round_very_large_numbers_with_precision() {
|
|||||||
fn can_round_integer_with_negative_precision() {
|
fn can_round_integer_with_negative_precision() {
|
||||||
let actual = nu!("123 | math round --precision -1");
|
let actual = nu!("123 | math round --precision -1");
|
||||||
|
|
||||||
assert_eq!(actual.out, "120")
|
assert_eq!(actual.out, "120.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_round_float_with_negative_precision() {
|
fn can_round_float_with_negative_precision() {
|
||||||
let actual = nu!("123.3 | math round --precision -1");
|
let actual = nu!("123.3 | math round --precision -1");
|
||||||
|
|
||||||
assert_eq!(actual.out, "120")
|
assert_eq!(actual.out, "120.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -18,13 +18,13 @@ fn can_sqrt_irrational() {
|
|||||||
fn can_sqrt_perfect_square() {
|
fn can_sqrt_perfect_square() {
|
||||||
let actual = nu!("echo 4 | math sqrt");
|
let actual = nu!("echo 4 | math sqrt");
|
||||||
|
|
||||||
assert_eq!(actual.out, "2");
|
assert_eq!(actual.out, "2.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn const_sqrt() {
|
fn const_sqrt() {
|
||||||
let actual = nu!("const SQRT = 4 | math sqrt; $SQRT");
|
let actual = nu!("const SQRT = 4 | math sqrt; $SQRT");
|
||||||
assert_eq!(actual.out, "2");
|
assert_eq!(actual.out, "2.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -3,7 +3,7 @@ use nu_test_support::nu;
|
|||||||
#[test]
|
#[test]
|
||||||
fn const_variance() {
|
fn const_variance() {
|
||||||
let actual = nu!("const VAR = [1 2 3 4 5] | math variance; $VAR");
|
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]
|
#[test]
|
||||||
|
@ -69,7 +69,7 @@ fn mut_multiply_assign() {
|
|||||||
fn mut_divide_assign() {
|
fn mut_divide_assign() {
|
||||||
let actual = nu!("mut y = 8; $y /= 2; $y");
|
let actual = nu!("mut y = 8; $y /= 2; $y");
|
||||||
|
|
||||||
assert_eq!(actual.out, "4");
|
assert_eq!(actual.out, "4.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -104,7 +104,7 @@ fn mut_path_upsert_list() {
|
|||||||
fn mut_path_operator_assign() {
|
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");
|
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]
|
#[test]
|
||||||
|
@ -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]
|
#[test]
|
||||||
fn infers_types() {
|
fn infers_types() {
|
||||||
Playground::setup("filter_from_csv_test_1", |dirs, sandbox| {
|
Playground::setup("filter_from_csv_test_1", |dirs, sandbox| {
|
||||||
|
@ -17,6 +17,17 @@ fn table_to_json_text_and_from_json_text_back_into_table() {
|
|||||||
assert_eq!(actual.out, "markup");
|
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]
|
#[test]
|
||||||
fn from_json_text_to_table() {
|
fn from_json_text_to_table() {
|
||||||
Playground::setup("filter_from_json_test_1", |dirs, sandbox| {
|
Playground::setup("filter_from_json_test_1", |dirs, sandbox| {
|
||||||
|
@ -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]
|
#[test]
|
||||||
fn from_tsv_text_to_table() {
|
fn from_tsv_text_to_table() {
|
||||||
Playground::setup("filter_from_tsv_test_1", |dirs, sandbox| {
|
Playground::setup("filter_from_tsv_test_1", |dirs, sandbox| {
|
||||||
|
@ -24,12 +24,15 @@ linked-hash-map = { version = "0.5", optional = true }
|
|||||||
num-traits = { workspace = true }
|
num-traits = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.105.2" }
|
nu-test-support = { path = "../nu-test-support", version = "0.105.2" }
|
||||||
nu-path = { path = "../nu-path", version = "0.105.2" }
|
nu-path = { path = "../nu-path", version = "0.105.2" }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
fancy-regex = "0.14.0"
|
fancy-regex = "0.14.0"
|
||||||
|
pretty_assertions = { workspace = true }
|
||||||
|
rstest = { workspace = true, default-features = false }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
@ -6,6 +6,8 @@ use std::fmt::{Display, LowerExp};
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::num::FpCategory;
|
use std::num::FpCategory;
|
||||||
|
|
||||||
|
use nu_utils::ObviousFloat;
|
||||||
|
|
||||||
use super::error::{Error, ErrorCode, Result};
|
use super::error::{Error, ErrorCode, Result};
|
||||||
use serde::ser;
|
use serde::ser;
|
||||||
|
|
||||||
@ -868,7 +870,7 @@ where
|
|||||||
{
|
{
|
||||||
match value.classify() {
|
match value.classify() {
|
||||||
FpCategory::Nan | FpCategory::Infinite => wr.write_all(b"null")?,
|
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(())
|
Ok(())
|
||||||
@ -880,7 +882,7 @@ where
|
|||||||
{
|
{
|
||||||
match value.classify() {
|
match value.classify() {
|
||||||
FpCategory::Nan | FpCategory::Infinite => wr.write_all(b"null")?,
|
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(())
|
Ok(())
|
||||||
|
@ -1,201 +1,97 @@
|
|||||||
use fancy_regex::Regex;
|
|
||||||
use nu_json::Value;
|
use nu_json::Value;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use rstest::rstest;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
fn txt(text: &str) -> String {
|
fn txt(text: String) -> String {
|
||||||
let out = String::from_utf8_lossy(text.as_bytes());
|
let out = text;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
out.replace("\r\n", "").replace('\n', "")
|
out.replace("\r\n", "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
out.to_string()
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hjson_expectations() -> PathBuf {
|
// This test will fail if/when `nu_test_support::fs::assets()`'s return value changes.
|
||||||
nu_test_support::fs::assets().join("nu_json").into()
|
#[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<String> {
|
#[rstest]
|
||||||
let expectations = hjson_expectations();
|
fn test_hjson_fails(#[files("../../tests/assets/nu_json/fail*_test.*")] file: PathBuf) {
|
||||||
|
let contents = fs::read_to_string(file).unwrap();
|
||||||
let mut p = format!("{}/{}_test.hjson", expectations.display(), name);
|
let data: nu_json::Result<Value> = nu_json::from_str(&contents);
|
||||||
|
assert!(data.is_err());
|
||||||
if !Path::new(&p).exists() {
|
|
||||||
p = format!("{}/{}_test.json", expectations.display(), name);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::read_to_string(&p)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_result_content(name: &str) -> io::Result<(String, String)> {
|
#[rstest]
|
||||||
let expectations = hjson_expectations();
|
fn test_hjson(
|
||||||
|
#[files("../../tests/assets/nu_json/*_test.*")]
|
||||||
|
#[exclude("fail*")]
|
||||||
|
test_file: PathBuf,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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 data: Value = nu_json::from_str(fs::read_to_string(&test_file)?.as_str())?;
|
||||||
let p2 = format!("{}/{}_result.hjson", expectations.display(), name);
|
|
||||||
|
|
||||||
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 {
|
fn get_result_path(test_file: &Path, ext: &str) -> Option<PathBuf> {
|
||||||
// {{ is a workaround for rust stable
|
let name = test_file
|
||||||
($v: ident, $list: expr, $fix: expr) => {{
|
.file_stem()
|
||||||
let name = stringify!($v);
|
.and_then(|x| x.to_str())
|
||||||
$list.push(format!("{}_test", name));
|
.and_then(|x| x.strip_suffix("_test"))?;
|
||||||
println!("- running {}", name);
|
|
||||||
let should_fail = name.starts_with("fail");
|
|
||||||
let test_content = get_test_content(name).unwrap();
|
|
||||||
let data: nu_json::Result<Value> = nu_json::from_str(&test_content);
|
|
||||||
assert!(should_fail == data.is_err());
|
|
||||||
|
|
||||||
if !should_fail {
|
Some(test_file.with_file_name(format!("{name}_result.{ext}")))
|
||||||
let udata = data.unwrap();
|
}
|
||||||
let (rjson, rhjson) = get_result_content(name).unwrap();
|
|
||||||
let rjson = txt(&rjson);
|
fn get_content(file: &Path) -> io::Result<String> {
|
||||||
let rhjson = txt(&rhjson);
|
fs::read_to_string(file).map(txt)
|
||||||
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);
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add fixes where rust's json differs from javascript
|
// 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 {
|
fn positive_exp_add_sign(json: String) -> String {
|
||||||
// serde_json serializes integers with a superfluous .0 suffix
|
json.replace("1.23456789e34", "1.23456789e+34")
|
||||||
let re = Regex::new(r"(?m)(?P<d>\d)\.0(?P<s>,?)$").unwrap();
|
.replace("2.3456789012e76", "2.3456789012e+76")
|
||||||
re.replace_all(&json, "$d$s").to_string()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn fix_kan(json: String) -> String {
|
match s {
|
||||||
std_fix(json).replace(" -0,", " 0,")
|
"kan" => remove_negative_zero,
|
||||||
}
|
"pass1" => positive_exp_add_sign,
|
||||||
|
_ => std::convert::identity,
|
||||||
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<String> = 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::<Vec<String>>();
|
|
||||||
|
|
||||||
if !missing.is_empty() {
|
|
||||||
for item in missing {
|
|
||||||
println!("missing: {}", item);
|
|
||||||
}
|
|
||||||
panic!();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,7 +8,6 @@ mod range;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_derive;
|
mod test_derive;
|
||||||
|
|
||||||
pub mod format;
|
|
||||||
pub mod record;
|
pub mod record;
|
||||||
pub use custom_value::CustomValue;
|
pub use custom_value::CustomValue;
|
||||||
pub use duration::*;
|
pub use duration::*;
|
||||||
@ -29,7 +28,7 @@ use chrono::{DateTime, Datelike, Duration, FixedOffset, Local, Locale, TimeZone}
|
|||||||
use chrono_humanize::HumanTime;
|
use chrono_humanize::HumanTime;
|
||||||
use fancy_regex::Regex;
|
use fancy_regex::Regex;
|
||||||
use nu_utils::{
|
use nu_utils::{
|
||||||
SharedCow, contains_emoji,
|
ObviousFloat, SharedCow, contains_emoji,
|
||||||
locale::{LOCALE_OVERRIDE_ENV_VAR, get_system_locale_string},
|
locale::{LOCALE_OVERRIDE_ENV_VAR, get_system_locale_string},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -939,7 +938,7 @@ impl Value {
|
|||||||
match self {
|
match self {
|
||||||
Value::Bool { val, .. } => val.to_string(),
|
Value::Bool { val, .. } => val.to_string(),
|
||||||
Value::Int { 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::Filesize { val, .. } => config.filesize.format(*val).to_string(),
|
||||||
Value::Duration { val, .. } => format_duration(*val),
|
Value::Duration { val, .. } => format_duration(*val),
|
||||||
Value::Date { val, .. } => match &config.datetime_format.normal {
|
Value::Date { val, .. } => match &config.datetime_format.normal {
|
||||||
|
@ -307,10 +307,8 @@ mod int_range {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod float_range {
|
mod float_range {
|
||||||
use crate::{
|
use crate::{IntRange, Range, ShellError, Signals, Span, Value, ast::RangeInclusion};
|
||||||
IntRange, Range, ShellError, Signals, Span, Value, ast::RangeInclusion,
|
use nu_utils::ObviousFloat;
|
||||||
format::ObviousFloat,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{cmp::Ordering, fmt::Display, ops::Bound};
|
use std::{cmp::Ordering, fmt::Display, ops::Bound};
|
||||||
|
|
||||||
|
21
crates/nu-utils/src/float.rs
Normal file
21
crates/nu-utils/src/float.rs
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ mod deansi;
|
|||||||
pub mod emoji;
|
pub mod emoji;
|
||||||
pub mod filesystem;
|
pub mod filesystem;
|
||||||
pub mod flatten_json;
|
pub mod flatten_json;
|
||||||
|
pub mod float;
|
||||||
pub mod locale;
|
pub mod locale;
|
||||||
mod quoting;
|
mod quoting;
|
||||||
mod shared_cow;
|
mod shared_cow;
|
||||||
@ -22,6 +23,7 @@ pub use deansi::{
|
|||||||
};
|
};
|
||||||
pub use emoji::contains_emoji;
|
pub use emoji::contains_emoji;
|
||||||
pub use flatten_json::JsonFlattener;
|
pub use flatten_json::JsonFlattener;
|
||||||
|
pub use float::ObviousFloat;
|
||||||
pub use quoting::{escape_quote_string, needs_quoting};
|
pub use quoting::{escape_quote_string, needs_quoting};
|
||||||
pub use shared_cow::SharedCow;
|
pub use shared_cow::SharedCow;
|
||||||
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
use core::fmt::Write;
|
use core::fmt::Write;
|
||||||
use nu_engine::get_columns;
|
use nu_engine::get_columns;
|
||||||
use nu_protocol::format::ObviousFloat;
|
|
||||||
use nu_protocol::{Range, ShellError, Span, Value, engine::EngineState};
|
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
|
/// control the way Nushell [`Value`] is converted to NUON data
|
||||||
pub enum ToStyle {
|
pub enum ToStyle {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
rem2: "// test"
|
rem2: "// test"
|
||||||
rem3: "/* test */"
|
rem3: "/* test */"
|
||||||
num1: 0
|
num1: 0
|
||||||
num2: 0
|
num2: 0.0
|
||||||
num3: 2
|
num3: 2
|
||||||
true1: true
|
true1: true
|
||||||
true2: true
|
true2: true
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"rem2": "// test",
|
"rem2": "// test",
|
||||||
"rem3": "/* test */",
|
"rem3": "/* test */",
|
||||||
"num1": 0,
|
"num1": 0,
|
||||||
"num2": 0,
|
"num2": 0.0,
|
||||||
"num3": 2,
|
"num3": 2,
|
||||||
"true1": true,
|
"true1": true,
|
||||||
"true2": true,
|
"true2": true,
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
42.1,
|
42.1,
|
||||||
-5,
|
-5,
|
||||||
-5.1,
|
-5.1,
|
||||||
1701,
|
1701.0,
|
||||||
-1701,
|
-1701.0,
|
||||||
12.345,
|
12.345,
|
||||||
-12.345
|
-12.345
|
||||||
],
|
],
|
||||||
|
@ -65,11 +65,11 @@
|
|||||||
98.6,
|
98.6,
|
||||||
99.44,
|
99.44,
|
||||||
1066,
|
1066,
|
||||||
10,
|
10.0,
|
||||||
1,
|
1.0,
|
||||||
0.1,
|
0.1,
|
||||||
1,
|
1.0,
|
||||||
2,
|
2.0,
|
||||||
2,
|
2.0,
|
||||||
"rosebud"
|
"rosebud"
|
||||||
]
|
]
|
@ -147,7 +147,7 @@ fn const_unary_operator(#[case] inp: &[&str], #[case] expect: &str) {
|
|||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(&["const x = 1 + 2", "$x"], "3")]
|
#[case(&["const x = 1 + 2", "$x"], "3")]
|
||||||
#[case(&["const x = 1 * 2", "$x"], "2")]
|
#[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 = 4 mod 3", "$x"], "1")]
|
||||||
#[case(&["const x = 5.0 / 2.0", "$x"], "2.5")]
|
#[case(&["const x = 5.0 / 2.0", "$x"], "2.5")]
|
||||||
#[case(&[r#"const x = "a" + "b" "#, "$x"], "ab")]
|
#[case(&[r#"const x = "a" + "b" "#, "$x"], "ab")]
|
||||||
|
@ -103,7 +103,7 @@ fn sum_accepts_stream_of_float() {
|
|||||||
"seq 1 5 | into float | example sum"
|
"seq 1 5 | into float | example sum"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(actual.out, "15");
|
assert_eq!(actual.out, "15.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -306,5 +306,5 @@ fn dont_allow_implicit_casting_between_glob_and_string() -> TestResult {
|
|||||||
#[test]
|
#[test]
|
||||||
fn allow_pass_negative_float() -> TestResult {
|
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 -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")
|
||||||
}
|
}
|
||||||
|
@ -187,12 +187,12 @@ fn proper_variable_captures_with_nesting() -> TestResult {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn divide_duration() -> TestResult {
|
fn divide_duration() -> TestResult {
|
||||||
run_test(r#"4ms / 4ms"#, "1")
|
run_test(r#"4ms / 4ms"#, "1.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn divide_filesize() -> TestResult {
|
fn divide_filesize() -> TestResult {
|
||||||
run_test(r#"4mb / 4mb"#, "1")
|
run_test(r#"4mb / 4mb"#, "1.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -759,7 +759,7 @@ fn range_with_mixed_types() {
|
|||||||
echo 1..10.5 | math sum
|
echo 1..10.5 | math sum
|
||||||
");
|
");
|
||||||
|
|
||||||
assert_eq!(actual.out, "55");
|
assert_eq!(actual.out, "55.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -802,7 +802,7 @@ fn exclusive_range_with_mixed_types() {
|
|||||||
echo 1..<10.5 | math sum
|
echo 1..<10.5 | math sum
|
||||||
");
|
");
|
||||||
|
|
||||||
assert_eq!(actual.out, "55");
|
assert_eq!(actual.out, "55.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
Reference in New Issue
Block a user