diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index a6416595b1e..89bfe40358c 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -654,10 +654,7 @@ fn try_format_exact_int_without_suffix_scaling( let scaled = integer / to_unit; if is_too_large_to_format(scaled, precision) { - let value_sci = format_gnu_scientific(scaled as f64); - return Some(Err(format!( - "value/precision too large to be printed: '{value_sci}/{precision}' (consider using --to)" - ))); + return Some(Err(too_large_to_print(scaled as f64, precision))); } Some(Ok(if precision == 0 { @@ -671,20 +668,40 @@ fn try_format_exact_int_without_suffix_scaling( })) } -fn format_gnu_scientific(v: f64) -> String { - // 6 significant figures with trimmed trailing zeros and signed exponent +/// GNU names the precision only when there is one. +fn too_large_to_print(value: f64, precision: usize) -> String { + let value_sci = format_gnu_general(value); + if precision == 0 { + format!("value too large to be printed: '{value_sci}' (consider using --to)") + } else { + format!( + "value/precision too large to be printed: '{value_sci}/{precision}' (consider using --to)" + ) + } +} + +fn format_gnu_general(v: f64) -> String { + // like C's %g: 6 significant figures, trailing zeros trimmed, and an + // exponent of at least two digits only outside 1e-4..1e6 let s = format!("{v:.5e}"); let Some(e_pos) = s.find('e') else { return s; }; let (mantissa, rest) = s.split_at(e_pos); - let exp = &rest[1..]; - let mantissa = mantissa.trim_end_matches('0').trim_end_matches('.'); - if exp.starts_with('-') { - format!("{mantissa}e{exp}") - } else { - format!("{mantissa}e+{exp}") + let exp: i32 = rest[1..].parse().unwrap_or(0); + if (-4..6).contains(&exp) { + let fixed = format!("{v:.*}", (5 - exp) as usize); + if !fixed.contains('.') { + return fixed; + } + return fixed + .trim_end_matches('0') + .trim_end_matches('.') + .to_string(); } + let mantissa = mantissa.trim_end_matches('0').trim_end_matches('.'); + let sign = if exp < 0 { '-' } else { '+' }; + format!("{mantissa}e{sign}{:02}", exp.abs()) } /// Format `value` with `precision` decimals, exactly as `{:.precision$}` would. @@ -725,6 +742,13 @@ fn transform_to( let s = s.to_f64(); let i2 = s / (opts.to_unit as f64); + // a value read with a suffix, like 1Z, is a float and skipped the check above + if opts.to == Unit::None + && i2.is_finite() + && i2.abs().max(1.0) * 10_f64.powi(precision.min(19) as i32) >= 1e19 + { + return Err(too_large_to_print(i2, precision)); + } let (i2, s) = consider_suffix(i2, opts.to, round_method, precision, is_precision_specified)?; let dec_sep = locale_decimal_separator(); let localize = |s: String| -> String { diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 036da1c09cc..d80349c641b 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -66,7 +66,6 @@ fn test_invalid_arg() { // numfmt: --format width accounting diverges from GNU for multi-byte --suffix #[case::multibyte_suffix_issue11937(&["--suffix=€", "--format=%10.2f", "692"], " 692.00€\n")] #[case::float_precision_greater_than_16bits(&["--to=iec", "--format=%.65536f", "1"], "1\n")] -#[case::float_precision_greater_than_16bits_without_scaling(&["--format=%.65536f", "1.5"], "2\n")] #[case::float_precision_greater_than_16bits_with_suffix(&["--to=si", "--format=%.65536f", "1000000"], "1M\n")] // https://github.com/uutils/coreutils/issues/13272 // argument following `--header` must not be interpreted as the option's value @@ -1487,13 +1486,51 @@ fn test_si_format_precision_no_cap() { .stdout_is("1.23457M\n"); } +#[test] +fn test_value_too_large_without_precision() { + new_ucmd!() + .arg("10000000000000000000") + .fails_with_code(2) + .stderr_only("numfmt: value too large to be printed: '1e+19' (consider using --to)\n"); + new_ucmd!() + .args(&["--format=%.20f", "1"]) + .fails_with_code(2) + .stderr_only( + "numfmt: value/precision too large to be printed: '1/20' (consider using --to)\n", + ); + new_ucmd!() + .args(&["--format=%.65536f", "1.5"]) + .fails_with_code(2) + .stderr_only( + "numfmt: value/precision too large to be printed: '1.5/65536' (consider using --to)\n", + ); +} + +#[test] +fn test_value_with_suffix_too_large_to_print() { + new_ucmd!() + .args(&["--from=si", "1Z"]) + .fails_with_code(2) + .stderr_only("numfmt: value too large to be printed: '1e+21' (consider using --to)\n"); + new_ucmd!() + .args(&["--from=si", "--format=%.1f", "1Z"]) + .fails_with_code(2) + .stderr_only( + "numfmt: value/precision too large to be printed: '1e+21/1' (consider using --to)\n", + ); + new_ucmd!() + .args(&["--from=si", "9E"]) + .succeeds() + .stdout_only("9000000000000000000\n"); +} + #[test] fn test_format_precision_too_large_on_zero() { new_ucmd!() .args(&["--format=%.40f", "0"]) .fails_with_code(2) .stderr_only( - "numfmt: value/precision too large to be printed: '0e+0/40' (consider using --to)\n", + "numfmt: value/precision too large to be printed: '0/40' (consider using --to)\n", ); new_ucmd!() .args(&["--format=%.18f", "0"])