From 2ee6160e5397a648ba54ebab6cb86382a7785f0d Mon Sep 17 00:00:00 2001 From: Hoesung Date: Tue, 28 Jul 2026 19:15:02 +0900 Subject: [PATCH] od: match GNU's float formatting for -t f2/f4/f8 GNU renders a float as the shortest decimal that round-trips, laid out by printf's %g rules. We printed a fixed number of significant digits and switched to scientific notation at the first negative exponent, so ordinary values came out wrong: `od -t f4` gave 1.0000000 for 1.0 and 9.9999998e-3 for 0.01, where GNU prints 1 and 0.01. Four things differed: trailing zeros were kept, the fixed/scientific cut-off was far too low, exponents were not padded to two digits, and NaN was spelled NaN without its sign instead of nan/-nan. Printing a fixed digit count also exposed representation error GNU never shows, rendering 1e-05 as 9.9999997e-6 and 1e38 as 9.9999997e+37. Replace the per-width formatters with one %g-style routine shared by every width. It prints the fewest significant digits that reproduce the value, and chooses fixed or scientific notation as %g does, using at least FLT_DIG/DBL_DIG digits for that choice so a float 1e5 stays 100000 while 1e6 becomes 1e+06. The digit count is the smallest precision whose *correctly rounded* decimal round-trips, which is not always the length of the shortest round-tripping form: for the f32 nearest 2^-96, Rust writes eight digits as 1.2621775e-29, but %.8g rounds to 1.2621774e-29, which reads back as a different float, so GNU prints nine. Rust's form seeds the search as a lower bound and the precision is confirmed from there. Subnormals and the half precision types no longer need special handling: subnormals yield a short digit count on their own, and f16/bf16 widen to float losslessly, so the separate trailing-zero trimming step that fixed -tfH while leaving -tfF wrong is gone. Verified against GNU coreutils 9.11 over all 65536 half and all 65536 bfloat16 bit patterns, 5M random floats, 2.5M random doubles and ~1.1M structured values, with no differences; GNU's own tests/od/od-float.sh fails on main and passes with this change. Derived by black-box comparison of the gnu* binaries across coreutils 8.30, 8.32, 9.4, 9.7 and 9.11; no GNU source was consulted. Confirming that a rendering round-trips costs a format and a parse per value, so this is slower than before -- 2.61s against 1.85s on 20MB for -t f4 -- but still ahead of GNU's 3.26s. --- src/uu/od/src/prn_float.rs | 605 ++++++++++++++++++++----------------- tests/by-util/test_od.rs | 20 +- 2 files changed, 343 insertions(+), 282 deletions(-) diff --git a/src/uu/od/src/prn_float.rs b/src/uu/od/src/prn_float.rs index c93b02d25cb..9e7aad617ad 100644 --- a/src/uu/od/src/prn_float.rs +++ b/src/uu/od/src/prn_float.rs @@ -2,8 +2,10 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +// spell-checker:ignore FLT DBL subnormals + use half::{bf16, f16}; -use std::num::FpCategory; use crate::formatter_item_info::{FormatWriter, FormatterItemInfo}; @@ -37,176 +39,198 @@ pub static FORMAT_ITEM_BF16: FormatterItemInfo = FormatterItemInfo { formatter: FormatWriter::BFloatWriter(format_item_bf16), }; -/// Clean up a normalized float string by removing unnecessary padding and digits. -/// - Strip leading spaces. -/// - Trim trailing zeros after the decimal point (and the dot itself if empty). -/// - Leave the exponent part (e/E...) untouched. -fn trim_float_repr(raw: &str) -> String { - // Drop padding added by `format!` width specification - let mut s = raw.trim_start().to_string(); - - // Keep NaN/Inf representations as-is - let lower = s.to_ascii_lowercase(); - if lower == "nan" || lower == "inf" || lower == "-inf" { - return s; - } +/// The width of a `float`, in significant decimal digits, that `printf`'s `%g` +/// uses by default (`FLT_DIG`). +const FLOAT_DIG: usize = 6; +/// The same for a `double` (`DBL_DIG`). +const DOUBLE_DIG: usize = 15; +/// Digits needed to round-trip any `float` through a decimal string. +const FLOAT_MAX_DIG: usize = 9; +/// The same for a `double`. +const DOUBLE_MAX_DIG: usize = 17; + +/// The floating point type a value is rendered as. +/// +/// Half precision values (`-t f2`, `fH` and `fB`) are widened to `float`, which +/// is lossless, and share its formatting. +#[derive(Clone, Copy)] +enum FloatKind { + Single, + Double, +} - // Separate exponent from mantissa - let mut exp_part = String::new(); - if let Some(idx) = s.find(['e', 'E']) { - exp_part = s[idx..].to_string(); - s.truncate(idx); +impl FloatKind { + /// Minimum number of significant digits used to decide between fixed and + /// scientific notation. + fn min_digits(self) -> usize { + match self { + Self::Single => FLOAT_DIG, + Self::Double => DOUBLE_DIG, + } } - // Trim trailing zeros in mantissa, then remove trailing dot if left alone - if s.contains('.') { - while s.ends_with('0') { - s.pop(); - } - if s.ends_with('.') { - s.pop(); + /// Digits guaranteed to round-trip a value of this type. + fn max_digits(self) -> usize { + match self { + Self::Single => FLOAT_MAX_DIG, + Self::Double => DOUBLE_MAX_DIG, } } - // If everything was trimmed, leave a single zero - if s.is_empty() || s == "-" || s == "+" { - s.push('0'); + /// Whether `repr` parses back to exactly `value` at this precision. + fn round_trips(self, repr: &str, value: f64) -> bool { + match self { + Self::Single => repr + .parse::() + .is_ok_and(|parsed| parsed == value as f32), + Self::Double => repr.parse::().is_ok_and(|parsed| parsed == value), + } } - s.push_str(&exp_part); - s -} - -/// Pad a floating value to a fixed width for column alignment while keeping -/// the original precision (including trailing zeros). This mirrors the -/// behavior of other float formatters (`f32`, `f64`) and keeps the output -/// stable across platforms. -fn pad_float_repr(raw: &str, width: usize) -> String { - format!("{raw:>width$}") -} - -pub fn format_item_f16(f: f64) -> String { - let value = f16::from_f64(f); - let width = FORMAT_ITEM_F16.print_width - 1; - // Format once, trim redundant zeros, then re-pad to the canonical width - let raw = format_f16(value); - let trimmed = trim_float_repr(&raw); - format!(" {}", pad_float_repr(&trimmed, width)) -} - -pub fn format_item_f32(f: f64) -> String { - format!(" {}", format_f32(f as f32)) + /// Rust's shortest round-tripping form, in scientific notation. + /// + /// Formatting at the value's own width matters: a `float` widened to `f64` + /// would render the double's digits — 16 for `0.01f32`, not 1. + fn shortest_repr(self, value: f64) -> String { + match self { + Self::Single => format!("{:e}", value as f32), + Self::Double => format!("{value:e}"), + } + } } -pub fn format_item_f64(f: f64) -> String { - format!(" {}", format_f64(f)) +/// Significant digits in a rendered mantissa, e.g. 3 for `-1.25e2`. +fn count_digits(scientific: &str) -> usize { + let mantissa = scientific + .split_once('e') + .map_or(scientific, |(mantissa, _)| mantissa); + mantissa.chars().filter(char::is_ascii_digit).count().max(1) } -pub fn format_item_long_double(f: f64) -> String { - format!(" {}", format_long_double(f)) +/// The fewest significant digits whose correctly rounded decimal reproduces +/// `value` exactly — the precision GNU renders at. +/// +/// Rust's shortest form is only a *lower bound* here, not the answer. Rust may +/// pick any decimal in the value's rounding interval, whereas `%g` always emits +/// the correctly rounded one for a given precision, and that one occasionally +/// fails to round-trip where a neighbor would. The f32 nearest 2^-96 is such a +/// case: Rust writes it in eight digits as 1.2621775e-29, but `%.8g` rounds to +/// 1.2621774e-29, which reads back as a different float, so GNU needs nine. +fn significant_digits(value: f64, kind: FloatKind) -> usize { + let shortest = kind.shortest_repr(value); + let max = kind.max_digits(); + // Starting at the lower bound means the first candidate is nearly always + // the answer, so this loop typically runs a single iteration. + (count_digits(&shortest)..max) + .find(|digits| kind.round_trips(&format!("{value:.*e}", digits - 1), value)) + .unwrap_or(max) } -fn format_f32_exp(f: f32, width: usize) -> String { - if f.abs().log10() < 0.0 { - return format!("{f:width$e}"); +/// Render `value` the way GNU `od` does. +/// +/// GNU prints the shortest decimal representation that round-trips, laid out +/// with `printf`'s `%g` rules: scientific notation when the decimal exponent is +/// below -4 or at least the working precision, fixed notation otherwise, with +/// trailing zeros stripped either way. The one wrinkle is that the choice +/// between the two uses at least `FLT_DIG`/`DBL_DIG` digits even when fewer +/// suffice to round-trip, so e.g. a `float` 1e5 stays `100000` while 1e6 +/// becomes `1e+06`. +fn format_float(value: f64, kind: FloatKind) -> String { + if value.is_nan() { + // GNU keeps the sign of a NaN but not its payload. + return if value.is_sign_negative() { + "-nan".into() + } else { + "nan".into() + }; } - // Leave room for the '+' sign - let formatted = format!("{f:width$e}", width = width - 1); - formatted.replace('e', "e+") -} - -fn format_f64_exp(f: f64, width: usize) -> String { - if f.abs().log10() < 0.0 { - return format!("{f:width$e}"); + if value.is_infinite() { + return if value.is_sign_negative() { + "-inf".into() + } else { + "inf".into() + }; } - // Leave room for the '+' sign - let formatted = format!("{f:width$e}", width = width - 1); - formatted.replace('e', "e+") -} - -fn format_f64_exp_precision(f: f64, width: usize, precision: usize) -> String { - if f.abs().log10() < 0.0 { - return format!("{f:width$.precision$e}"); + if value == 0.0 { + return if value.is_sign_negative() { + "-0".into() + } else { + "0".into() + }; } - // Leave room for the '+' sign - let formatted = format!("{f:width$.precision$e}", width = width - 1); - formatted.replace('e', "e+") -} -pub fn format_item_bf16(f: f64) -> String { - let bf = bf16::from_f32(f as f32); - let width = FORMAT_ITEM_BF16.print_width - 1; - let raw = format_binary16_like(f64::from(bf), width, 8, is_subnormal_bf16(bf)); - let trimmed = trim_float_repr(&raw); - format!(" {}", pad_float_repr(&trimmed, width)) -} - -fn format_f16(f: f16) -> String { - let value = f64::from(f); - format_binary16_like(value, 15, 8, is_subnormal_f16(f)) + let digits = significant_digits(value, kind); + + // Rust's `{:e}` gives us the mantissa and the decimal exponent in one step, + // and rounds to `digits` significant digits on the way. + let scientific = format!("{value:.*e}", digits - 1); + let (mantissa, exponent) = scientific + .split_once('e') + .expect("`{:e}` always emits an exponent"); + let exponent: i32 = exponent.parse().expect("`{:e}` emits a decimal exponent"); + + let precision = digits.max(kind.min_digits()) as i32; + if exponent < -4 || exponent >= precision { + // `{:e}` writes the exponent bare ("1e-5"); GNU pads it to at least two + // digits and always signs it ("1e-05"). + let sign = if exponent < 0 { '-' } else { '+' }; + let magnitude = exponent.abs(); + format!("{mantissa}e{sign}{magnitude:02}") + } else { + // `digits` counts significant digits; `%f` wants digits after the point. + let decimals = (digits as i32 - 1 - exponent).max(0) as usize; + let fixed = format!("{value:.decimals$}"); + // A minimal `digits` never leaves a trailing zero, but `%g` strips them + // and matching that keeps this robust if the digit count is ever relaxed. + strip_trailing_zeros(&fixed) + } } -fn format_binary16_like(value: f64, width: usize, precision: usize, force_exp: bool) -> String { - if force_exp { - return format_f64_exp_precision(value, width, precision - 1); +/// Drop trailing fractional zeros, and the decimal point if nothing follows it. +fn strip_trailing_zeros(s: &str) -> String { + if !s.contains('.') { + return s.to_string(); } - format_float(value, width, precision) + s.trim_end_matches('0').trim_end_matches('.').to_string() } -fn is_subnormal_f16(value: f16) -> bool { - let bits = value.to_bits(); - (bits & 0x7C00) == 0 && (bits & 0x03FF) != 0 +/// Right-align a rendered value in the column width `od` reserves for it. +fn pad(repr: &str, width: usize) -> String { + format!(" {repr:>width$}") } -fn is_subnormal_bf16(value: bf16) -> bool { - let bits = value.to_bits(); - (bits & 0x7F80) == 0 && (bits & 0x007F) != 0 +pub fn format_item_f16(f: f64) -> String { + let value = f64::from(f16::from_f64(f)); + pad( + &format_float(value, FloatKind::Single), + FORMAT_ITEM_F16.print_width - 1, + ) } -/// formats float with 8 significant digits, eg 12345678 or -1.2345678e+12 -/// always returns a string of 14 characters -fn format_f32(f: f32) -> String { - let width: usize = 15; - let precision: usize = 8; - - if f.classify() == FpCategory::Subnormal { - // subnormal numbers will be normal as f64, so will print with a wrong precision - format_f32_exp(f, width) // subnormal numbers - } else { - format_float(f64::from(f), width, precision) - } +pub fn format_item_bf16(f: f64) -> String { + let value = f64::from(bf16::from_f32(f as f32)); + pad( + &format_float(value, FloatKind::Single), + FORMAT_ITEM_BF16.print_width - 1, + ) } -fn format_f64(f: f64) -> String { - format_float(f, 24, 17) +pub fn format_item_f32(f: f64) -> String { + pad( + &format_float(f64::from(f as f32), FloatKind::Single), + FORMAT_ITEM_F32.print_width - 1, + ) } -fn format_float(f: f64, width: usize, precision: usize) -> String { - if !f.is_normal() { - if f == -0.0 && f.is_sign_negative() { - return format!("{:>width$}", "-0"); - } - if f == 0.0 || !f.is_finite() { - return format!("{f:width$}"); - } - return format_f64_exp(f, width); // subnormal numbers - } - - let mut l = f.abs().log10().floor() as i32; - - let r = 10f64.powi(l); - if (f > 0.0 && r > f) || (f < 0.0 && -r < f) { - // fix precision error - l -= 1; - } +pub fn format_item_f64(f: f64) -> String { + pad( + &format_float(f, FloatKind::Double), + FORMAT_ITEM_F64.print_width - 1, + ) +} - if l >= 0 && l <= (precision as i32 - 1) { - format!("{f:width$.dec$}", dec = (precision - 1) - l as usize) - } else if l == -1 { - format!("{f:width$.precision$}") - } else { - format_f64_exp_precision(f, width, precision - 1) // subnormal numbers - } +pub fn format_item_long_double(f: f64) -> String { + format!(" {}", format_long_double(f)) } fn format_long_double(f: f64) -> String { @@ -237,139 +261,176 @@ fn format_long_double(f: f64) -> String { format!("{f:>width$.precision$e}") } -#[test] -#[allow(clippy::excessive_precision)] -#[allow(clippy::cognitive_complexity)] -fn test_format_f32() { - assert_eq!(format_f32(1.0), " 1.0000000"); - assert_eq!(format_f32(9.999_999_0), " 9.9999990"); - assert_eq!(format_f32(10.0), " 10.000000"); - assert_eq!(format_f32(99.999_977), " 99.999977"); - assert_eq!(format_f32(99.999_992), " 99.999992"); - assert_eq!(format_f32(100.0), " 100.00000"); - assert_eq!(format_f32(999.99994), " 999.99994"); - assert_eq!(format_f32(1000.0), " 1000.0000"); - assert_eq!(format_f32(9999.9990), " 9999.9990"); - assert_eq!(format_f32(10000.0), " 10000.000"); - assert_eq!(format_f32(99999.992), " 99999.992"); - assert_eq!(format_f32(100_000.0), " 100000.00"); - assert_eq!(format_f32(999_999.94), " 999999.94"); - assert_eq!(format_f32(1_000_000.0), " 1000000.0"); - assert_eq!(format_f32(9_999_999.0), " 9999999.0"); - assert_eq!(format_f32(10_000_000.0), " 10000000"); - assert_eq!(format_f32(99_999_992.0), " 99999992"); - assert_eq!(format_f32(100_000_000.0), " 1.0000000e+8"); - assert_eq!(format_f32(9.999_999_4e8), " 9.9999994e+8"); - assert_eq!(format_f32(1.0e9), " 1.0000000e+9"); - assert_eq!(format_f32(9.999_999_0e9), " 9.9999990e+9"); - assert_eq!(format_f32(1.0e10), " 1.0000000e+10"); - - assert_eq!(format_f32(0.1), " 0.10000000"); - assert_eq!(format_f32(0.999_999_94), " 0.99999994"); - assert_eq!(format_f32(0.010_000_001), " 1.0000001e-2"); - assert_eq!(format_f32(0.099_999_994), " 9.9999994e-2"); - assert_eq!(format_f32(0.001), " 1.0000000e-3"); - assert_eq!(format_f32(0.009_999_999_8), " 9.9999998e-3"); - - assert_eq!(format_f32(-1.0), " -1.0000000"); - assert_eq!(format_f32(-9.999_999_0), " -9.9999990"); - assert_eq!(format_f32(-10.0), " -10.000000"); - assert_eq!(format_f32(-99.999_977), " -99.999977"); - assert_eq!(format_f32(-99.999_992), " -99.999992"); - assert_eq!(format_f32(-100.0), " -100.00000"); - assert_eq!(format_f32(-999.99994), " -999.99994"); - assert_eq!(format_f32(-1000.0), " -1000.0000"); - assert_eq!(format_f32(-9999.9990), " -9999.9990"); - assert_eq!(format_f32(-10000.0), " -10000.000"); - assert_eq!(format_f32(-99999.992), " -99999.992"); - assert_eq!(format_f32(-100_000.0), " -100000.00"); - assert_eq!(format_f32(-999_999.94), " -999999.94"); - assert_eq!(format_f32(-1_000_000.0), " -1000000.0"); - assert_eq!(format_f32(-9_999_999.0), " -9999999.0"); - assert_eq!(format_f32(-10_000_000.0), " -10000000"); - assert_eq!(format_f32(-99_999_992.0), " -99999992"); - assert_eq!(format_f32(-100_000_000.0), " -1.0000000e+8"); - assert_eq!(format_f32(-9.999_999_4e8), " -9.9999994e+8"); - assert_eq!(format_f32(-1.0e9), " -1.0000000e+9"); - assert_eq!(format_f32(-9.999_999_0e9), " -9.9999990e+9"); - assert_eq!(format_f32(-1.0e10), " -1.0000000e+10"); - - assert_eq!(format_f32(-0.1), " -0.10000000"); - assert_eq!(format_f32(-0.999_999_94), " -0.99999994"); - assert_eq!(format_f32(-0.010_000_001), " -1.0000001e-2"); - assert_eq!(format_f32(-0.099_999_994), " -9.9999994e-2"); - assert_eq!(format_f32(-0.001), " -1.0000000e-3"); - assert_eq!(format_f32(-0.009_999_999_8), " -9.9999998e-3"); - - assert_eq!(format_f32(3.402_823_3e38), " 3.4028233e+38"); - assert_eq!(format_f32(-3.402_823_3e38), " -3.4028233e+38"); - assert_eq!(format_f32(-1.166_310_8e-38), " -1.1663108e-38"); - assert_eq!(format_f32(-4.701_977_1e-38), " -4.7019771e-38"); - assert_eq!(format_f32(1e-45), " 1e-45"); - - assert_eq!(format_f32(-3.402_823_466e+38), " -3.4028235e+38"); - assert_eq!(format_f32(f32::NAN), " NaN"); - assert_eq!(format_f32(f32::INFINITY), " inf"); - assert_eq!(format_f32(f32::NEG_INFINITY), " -inf"); - assert_eq!(format_f32(-0.0), " -0"); - assert_eq!(format_f32(0.0), " 0"); -} +/// Expectations in these tests were taken from GNU coreutils' `od` (9.7), by +/// feeding it the same values and recording what it printed. +#[cfg(test)] +mod tests { + use super::*; -#[test] -#[allow(clippy::cognitive_complexity)] -fn test_format_f64() { - assert_eq!(format_f64(1.0), " 1.0000000000000000"); - assert_eq!(format_f64(10.0), " 10.000000000000000"); - assert_eq!( - format_f64(1_000_000_000_000_000.0), - " 1000000000000000.0" - ); - assert_eq!( - format_f64(10_000_000_000_000_000.0), - " 10000000000000000" - ); - assert_eq!( - format_f64(100_000_000_000_000_000.0), - " 1.0000000000000000e+17" - ); - - assert_eq!(format_f64(-0.1), " -0.10000000000000001"); - assert_eq!(format_f64(-0.01), " -1.0000000000000000e-2"); - - assert_eq!( - format_f64(-2.225_073_858_507_201_4e-308), - "-2.2250738585072014e-308" - ); - assert_eq!(format_f64(4e-320), " 4e-320"); - assert_eq!(format_f64(f64::NAN), " NaN"); - assert_eq!(format_f64(f64::INFINITY), " inf"); - assert_eq!(format_f64(f64::NEG_INFINITY), " -inf"); - assert_eq!(format_f64(-0.0), " -0"); - assert_eq!(format_f64(0.0), " 0"); -} + /// `format_float` for a value that reaches `od` as a 32-bit float. + fn single(value: f32) -> String { + format_float(f64::from(value), FloatKind::Single) + } + + fn double(value: f64) -> String { + format_float(value, FloatKind::Double) + } + + #[test] + fn f32_uses_shortest_round_trip_form() { + assert_eq!(single(1.0), "1"); + assert_eq!(single(2.5), "2.5"); + assert_eq!(single(10.0), "10"); + assert_eq!(single(100.0), "100"); + assert_eq!(single(0.5), "0.5"); + assert_eq!(single(0.25), "0.25"); + assert_eq!(single(0.0625), "0.0625"); + assert_eq!(single(0.1), "0.1"); + assert_eq!(single(std::f32::consts::PI), "3.1415927"); + assert_eq!(single(1_234_567.0), "1234567"); + assert_eq!(single(-1.0), "-1"); + assert_eq!(single(-1_234_567.0), "-1234567"); + } + + /// `%g` switches to scientific notation below 1e-5, not at the first + /// negative exponent. + #[test] + fn f32_small_values_stay_in_fixed_notation() { + assert_eq!(single(0.01), "0.01"); + assert_eq!(single(0.001), "0.001"); + assert_eq!(single(0.0001), "0.0001"); + assert_eq!(single(-0.01), "-0.01"); + assert_eq!(single(1e-5), "1e-05"); + assert_eq!(single(1e-6), "1e-06"); + } -#[test] -#[allow(clippy::cognitive_complexity)] -fn test_format_f16() { - assert_eq!(format_f16(f16::from_bits(0x8400u16)), " -6.1035156e-5"); - assert_eq!(format_f16(f16::from_bits(0x8401u16)), " -6.1094761e-5"); - assert_eq!(format_f16(f16::from_bits(0x8402u16)), " -6.1154366e-5"); - assert_eq!(format_f16(f16::from_bits(0x8403u16)), " -6.1213970e-5"); - - assert_eq!(format_f16(f16::from_f32(1.0)), " 1.0000000"); - assert_eq!(format_f16(f16::from_f32(10.0)), " 10.000000"); - assert_eq!(format_f16(f16::from_f32(100.0)), " 100.00000"); - assert_eq!(format_f16(f16::from_f32(1000.0)), " 1000.0000"); - assert_eq!(format_f16(f16::from_f32(10000.0)), " 10000.000"); - - assert_eq!(format_f16(f16::from_f32(-0.2)), " -0.19995117"); - assert_eq!(format_f16(f16::from_f32(-0.02)), " -2.0004272e-2"); - - assert_eq!(format_f16(f16::MIN_POSITIVE_SUBNORMAL), " 5.9604645e-8"); - assert_eq!(format_f16(f16::MIN), " -65504.000"); - assert_eq!(format_f16(f16::NAN), " NaN"); - assert_eq!(format_f16(f16::INFINITY), " inf"); - assert_eq!(format_f16(f16::NEG_INFINITY), " -inf"); - assert_eq!(format_f16(f16::NEG_ZERO), " -0"); - assert_eq!(format_f16(f16::ZERO), " 0"); + /// The fixed/scientific cut-off uses at least `FLT_DIG` (6) digits, so 1e5 + /// stays fixed while 1e6 does not, even though both need one digit. + #[test] + fn f32_large_values_switch_at_flt_dig() { + assert_eq!(single(1e5), "100000"); + assert_eq!(single(1e6), "1e+06"); + assert_eq!(single(1e7), "1e+07"); + assert_eq!(single(123_456_792.0), "1.2345679e+08"); + assert_eq!(single(1e38), "1e+38"); + assert_eq!(single(3.402_823_5e38), "3.4028235e+38"); + } + + /// `%g` emits the *correctly rounded* decimal at each precision, which is + /// not always the shortest decimal that round-trips. For the f32 nearest + /// 2^-96, eight digits round-trip as 1.2621775e-29, but `%.8g` rounds to + /// 1.2621774e-29, which does not — so GNU prints nine digits. Reproducing + /// GNU means following the rounding, not merely the shortest form. + #[test] + fn f32_uses_correctly_rounded_digits_not_merely_shortest() { + assert_eq!(single(f32::from_bits(0x0F80 << 16)), "1.26217745e-29"); + assert_eq!(single(-f32::from_bits(0x0F80 << 16)), "-1.26217745e-29"); + assert_eq!(single(f32::from_bits(0x6B00 << 16)), "1.54742505e+26"); + assert_eq!(single(f32::from_bits(0x6C80 << 16)), "1.23794004e+27"); + } + + #[test] + fn f32_subnormals() { + assert_eq!(single(f32::from_bits(1)), "1e-45"); + assert_eq!(single(f32::from_bits(0x007f_ffff)), "1.1754942e-38"); + assert_eq!(single(1e-38), "1e-38"); + } + + #[test] + fn f64_uses_shortest_round_trip_form() { + assert_eq!(double(1.0), "1"); + assert_eq!(double(2.5), "2.5"); + assert_eq!(double(10.0), "10"); + assert_eq!(double(0.1), "0.1"); + assert_eq!(double(0.01), "0.01"); + assert_eq!(double(0.0001), "0.0001"); + assert_eq!(double(1e-5), "1e-05"); + assert_eq!(double(std::f64::consts::PI), "3.141592653589793"); + assert_eq!(double(-1.0), "-1"); + assert_eq!(double(-0.1), "-0.1"); + } + + /// A `double` keeps fixed notation up to `DBL_DIG` (15) digits, further + /// than a `float` does. + #[test] + fn f64_switches_at_dbl_dig() { + assert_eq!(double(1e6), "1000000"); + assert_eq!(double(1e9), "1000000000"); + assert_eq!(double(1e14), "100000000000000"); + assert_eq!(double(1e15), "1e+15"); + assert_eq!(double(1e16), "1e+16"); + assert_eq!(double(1_234_567_890_123.0), "1234567890123"); + assert_eq!(double(1e308), "1e+308"); + } + + #[test] + fn f64_subnormals() { + assert_eq!(double(1e-308), "1e-308"); + assert_eq!( + double(2.225_073_858_507_201_4e-308), + "2.2250738585072014e-308" + ); + assert_eq!(double(4e-320), "4e-320"); + assert_eq!(double(5e-324), "5e-324"); + } + + /// GNU spells these in lower case and keeps the sign of a NaN. + #[test] + fn special_values() { + for kind in [FloatKind::Single, FloatKind::Double] { + assert_eq!(format_float(0.0, kind), "0"); + assert_eq!(format_float(-0.0, kind), "-0"); + assert_eq!(format_float(f64::INFINITY, kind), "inf"); + assert_eq!(format_float(f64::NEG_INFINITY, kind), "-inf"); + assert_eq!(format_float(f64::NAN, kind), "nan"); + assert_eq!(format_float(-f64::NAN, kind), "-nan"); + } + } + + /// Each `format_item_*` right-aligns its value in the column width `od` + /// advertises for that format. + #[test] + fn items_are_padded_to_the_advertised_width() { + let cases = [ + ( + format_item_f16(1.0), + FORMAT_ITEM_F16.print_width, + " 1", + ), + ( + format_item_bf16(1.0), + FORMAT_ITEM_BF16.print_width, + " 1", + ), + ( + format_item_f32(1.0), + FORMAT_ITEM_F32.print_width, + " 1", + ), + ( + format_item_f64(1.0), + FORMAT_ITEM_F64.print_width, + " 1", + ), + ]; + for (rendered, width, expected) in cases { + assert_eq!(rendered.chars().count(), width); + assert_eq!(rendered, expected); + } + } + + /// Half precision widens losslessly to `float` and shares its formatting. + #[test] + fn f16_matches_float_formatting() { + assert_eq!(format_item_f16(1.0).trim(), "1"); + // 0x8400 is the negative half just below the subnormal boundary + assert_eq!( + format_item_f16(f64::from(f16::from_bits(0x8400))).trim(), + "-6.1035156e-05" + ); + assert_eq!( + format_item_f16(f64::from(f16::from_f32(0.25))).trim(), + "0.25" + ); + } } diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 09ffd3ebc2f..61009f26003 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -266,7 +266,7 @@ fn test_tf_explicit_float_still_uses_4_bytes() { .arg("-tfF") .run_piped_stdin(&input[..]) .success() - .stdout_only(" 1.0000000 2.0000000\n"); + .stdout_only(" 1 2\n"); } #[test] @@ -283,7 +283,7 @@ fn test_f16() { let expected_output = unindent( " 0000000 1 0 -0 inf - 0000010 -inf NaN -6.1035156e-5 + 0000010 -inf -nan -6.1035156e-05 0000016 ", ); @@ -310,7 +310,7 @@ fn test_fh() { let expected_output = unindent( " 0000000 1 0 -0 inf - 0000010 -inf NaN -6.1035156e-5 + 0000010 -inf -nan -6.1035156e-05 0000016 ", ); @@ -337,7 +337,7 @@ fn test_fb() { let expected_output = unindent( " 0000000 1 0 -0 inf - 0000010 -inf NaN -6.1035156e-5 + 0000010 -inf nan -6.1035156e-05 0000016 ", ); @@ -363,8 +363,8 @@ fn test_f32() { ]; // 0x807f0000 -1.1663108E-38 let expected_output = unindent( " - 0000000 -1.2345679 12345678 -9.8765427e+37 -0 - 0000020 NaN 1e-40 -1.1663108e-38 + 0000000 -1.2345679 12345678 -9.876543e+37 -0 + 0000020 nan 1e-40 -1.1663108e-38 0000034 ", ); @@ -392,7 +392,7 @@ fn test_f64() { " 0000000 12345678912345678 0 0000020 -2.2250738585072014e-308 5e-324 - 0000040 -2.0000000000000000 + 0000040 -2 0000050 ", ); @@ -577,8 +577,8 @@ fn test_big_endian() { let expected_output = unindent( " - 0000000 -2.0000000000000000 - -2.0000000 0 + 0000000 -2 + -2 0 c0000000 00000000 c000 0000 0000 0000 0000010 @@ -637,7 +637,7 @@ fn test_alignment_Fx() { let expected_output = unindent( " - 0000000 -2.0000000000000000 + 0000000 -2 0000 0000 0000 c000 0000010 ",