1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 19:47:45 +00:00

Merge pull request #7631 from drinkcat/format-hex-default

uucore: format: Fix hexadecimal default format print
This commit is contained in:
Sylvestre Ledru 2025-04-04 23:44:35 +02:00 committed by GitHub
commit 295628a2e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 149 additions and 52 deletions

View file

@ -177,7 +177,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
variant: FloatVariant::Decimal, variant: FloatVariant::Decimal,
width: padding, width: padding,
alignment: num_format::NumberAlignment::RightZero, alignment: num_format::NumberAlignment::RightZero,
precision, precision: Some(precision),
..Default::default() ..Default::default()
}, },
// format without precision: hexadecimal floats // format without precision: hexadecimal floats

View file

@ -220,7 +220,10 @@ pub struct Float {
pub width: usize, pub width: usize,
pub positive_sign: PositiveSign, pub positive_sign: PositiveSign,
pub alignment: NumberAlignment, pub alignment: NumberAlignment,
pub precision: usize, // For float, the default precision depends on the format, usually 6,
// but something architecture-specific for %a. Set this to None to
// use the default.
pub precision: Option<usize>,
} }
impl Default for Float { impl Default for Float {
@ -232,7 +235,7 @@ impl Default for Float {
width: 0, width: 0,
positive_sign: PositiveSign::None, positive_sign: PositiveSign::None,
alignment: NumberAlignment::Left, alignment: NumberAlignment::Left,
precision: 6, precision: None,
} }
} }
} }
@ -309,8 +312,8 @@ impl Formatter<&ExtendedBigDecimal> for Float {
}; };
let precision = match precision { let precision = match precision {
Some(CanAsterisk::Fixed(x)) => x, Some(CanAsterisk::Fixed(x)) => Some(x),
None => 6, // Default float precision (C standard) None => None,
Some(CanAsterisk::Asterisk) => return Err(FormatError::WrongSpecType), Some(CanAsterisk::Asterisk) => return Err(FormatError::WrongSpecType),
}; };
@ -354,8 +357,13 @@ fn format_float_non_finite(e: &ExtendedBigDecimal, case: Case) -> String {
s s
} }
fn format_float_decimal(bd: &BigDecimal, precision: usize, force_decimal: ForceDecimal) -> String { fn format_float_decimal(
bd: &BigDecimal,
precision: Option<usize>,
force_decimal: ForceDecimal,
) -> String {
debug_assert!(!bd.is_negative()); debug_assert!(!bd.is_negative());
let precision = precision.unwrap_or(6); // Default %f precision (C standard)
if precision == 0 { if precision == 0 {
let (bi, scale) = bd.as_bigint_and_scale(); let (bi, scale) = bd.as_bigint_and_scale();
if scale == 0 && force_decimal != ForceDecimal::Yes { if scale == 0 && force_decimal != ForceDecimal::Yes {
@ -370,11 +378,12 @@ fn format_float_decimal(bd: &BigDecimal, precision: usize, force_decimal: ForceD
fn format_float_scientific( fn format_float_scientific(
bd: &BigDecimal, bd: &BigDecimal,
precision: usize, precision: Option<usize>,
case: Case, case: Case,
force_decimal: ForceDecimal, force_decimal: ForceDecimal,
) -> String { ) -> String {
debug_assert!(!bd.is_negative()); debug_assert!(!bd.is_negative());
let precision = precision.unwrap_or(6); // Default %e precision (C standard)
let exp_char = match case { let exp_char = match case {
Case::Lowercase => 'e', Case::Lowercase => 'e',
Case::Uppercase => 'E', Case::Uppercase => 'E',
@ -415,11 +424,12 @@ fn format_float_scientific(
fn format_float_shortest( fn format_float_shortest(
bd: &BigDecimal, bd: &BigDecimal,
precision: usize, precision: Option<usize>,
case: Case, case: Case,
force_decimal: ForceDecimal, force_decimal: ForceDecimal,
) -> String { ) -> String {
debug_assert!(!bd.is_negative()); debug_assert!(!bd.is_negative());
let precision = precision.unwrap_or(6); // Default %g precision (C standard)
// Note: Precision here is how many digits should be displayed in total, // Note: Precision here is how many digits should be displayed in total,
// instead of how many digits in the fractional part. // instead of how many digits in the fractional part.
@ -497,22 +507,39 @@ fn format_float_shortest(
fn format_float_hexadecimal( fn format_float_hexadecimal(
bd: &BigDecimal, bd: &BigDecimal,
precision: usize, precision: Option<usize>,
case: Case, case: Case,
force_decimal: ForceDecimal, force_decimal: ForceDecimal,
) -> String { ) -> String {
debug_assert!(!bd.is_negative()); debug_assert!(!bd.is_negative());
// Default precision for %a is supposed to be sufficient to represent the
// exact value. This is platform specific, GNU coreutils uses a `long double`,
// which can be equivalent to a f64, f128, or an x86(-64) specific "f80".
// We have arbitrary precision in base 10, so we can't always represent
// the value exactly (e.g. 0.1 is c.ccccc...).
//
// Note that this is the maximum precision, trailing 0's are trimmed when
// printing.
//
// Emulate x86(-64) behavior, where 64 bits at _most_ are printed in total,
// that's 16 hex digits, including 1 before the decimal point (so 15 after).
//
// TODO: Make this configurable? e.g. arm64 value would be 28 (f128),
// arm value 13 (f64).
let max_precision = precision.unwrap_or(15);
let exp_char = match case { let (prefix, exp_char) = match case {
Case::Lowercase => 'p', Case::Lowercase => ("0x", 'p'),
Case::Uppercase => 'P', Case::Uppercase => ("0X", 'P'),
}; };
if BigDecimal::zero().eq(bd) { if BigDecimal::zero().eq(bd) {
return if force_decimal == ForceDecimal::Yes && precision == 0 { // To print 0, we don't ever need any digits after the decimal point, so default to
// that if precision is not specified.
return if force_decimal == ForceDecimal::Yes && precision.unwrap_or(0) == 0 {
format!("0x0.{exp_char}+0") format!("0x0.{exp_char}+0")
} else { } else {
format!("0x{:.*}{exp_char}+0", precision, 0.0) format!("0x{:.*}{exp_char}+0", precision.unwrap_or(0), 0.0)
}; };
} }
@ -547,7 +574,8 @@ fn format_float_hexadecimal(
// Then, dividing by 5^-exp10 loses at most -exp10*3 binary digits // Then, dividing by 5^-exp10 loses at most -exp10*3 binary digits
// (since 5^-exp10 < 8^-exp10), so we add that, and another bit for // (since 5^-exp10 < 8^-exp10), so we add that, and another bit for
// rounding. // rounding.
let margin = ((precision + 1) as i64 * 4 - frac10.bits() as i64).max(0) + -exp10 * 3 + 1; let margin =
((max_precision + 1) as i64 * 4 - frac10.bits() as i64).max(0) + -exp10 * 3 + 1;
// frac10 * 10^exp10 = frac10 * 2^margin * 10^exp10 * 2^-margin = // frac10 * 10^exp10 = frac10 * 2^margin * 10^exp10 * 2^-margin =
// (frac10 * 2^margin * 5^exp10) * 2^exp10 * 2^-margin = // (frac10 * 2^margin * 5^exp10) * 2^exp10 * 2^-margin =
@ -562,7 +590,7 @@ fn format_float_hexadecimal(
// so the value will always be between 0x8 and 0xf. // so the value will always be between 0x8 and 0xf.
// TODO: Make this configurable? e.g. arm64 only displays 1 digit. // TODO: Make this configurable? e.g. arm64 only displays 1 digit.
const BEFORE_BITS: usize = 4; const BEFORE_BITS: usize = 4;
let wanted_bits = (BEFORE_BITS + precision * 4) as u64; let wanted_bits = (BEFORE_BITS + max_precision * 4) as u64;
let bits = frac2.bits(); let bits = frac2.bits();
exp2 += bits as i64 - wanted_bits as i64; exp2 += bits as i64 - wanted_bits as i64;
@ -592,16 +620,37 @@ fn format_float_hexadecimal(
digits.make_ascii_uppercase(); digits.make_ascii_uppercase();
} }
let (first_digit, remaining_digits) = digits.split_at(1); let (first_digit, remaining_digits) = digits.split_at(1);
let exponent = exp2 + (4 * precision) as i64; let exponent = exp2 + (4 * max_precision) as i64;
let dot = let mut remaining_digits = remaining_digits.to_string();
if !remaining_digits.is_empty() || (precision == 0 && ForceDecimal::Yes == force_decimal) { if precision.is_none() {
// Trim trailing zeros
strip_fractional_zeroes(&mut remaining_digits);
}
let dot = if !remaining_digits.is_empty()
|| (precision.unwrap_or(0) == 0 && ForceDecimal::Yes == force_decimal)
{
"." "."
} else { } else {
"" ""
}; };
format!("0x{first_digit}{dot}{remaining_digits}{exp_char}{exponent:+}") format!("{prefix}{first_digit}{dot}{remaining_digits}{exp_char}{exponent:+}")
}
fn strip_fractional_zeroes(s: &mut String) {
let mut trim_to = s.len();
for (pos, c) in s.char_indices().rev() {
if pos + c.len_utf8() == trim_to {
if c == '0' {
trim_to = pos;
} else {
break;
}
}
}
s.truncate(trim_to);
} }
fn strip_fractional_zeroes_and_dot(s: &mut String) { fn strip_fractional_zeroes_and_dot(s: &mut String) {
@ -700,7 +749,8 @@ mod test {
#[test] #[test]
fn decimal_float() { fn decimal_float() {
use super::format_float_decimal; use super::format_float_decimal;
let f = |x| format_float_decimal(&BigDecimal::from_f64(x).unwrap(), 6, ForceDecimal::No); let f =
|x| format_float_decimal(&BigDecimal::from_f64(x).unwrap(), Some(6), ForceDecimal::No);
assert_eq!(f(0.0), "0.000000"); assert_eq!(f(0.0), "0.000000");
assert_eq!(f(1.0), "1.000000"); assert_eq!(f(1.0), "1.000000");
assert_eq!(f(100.0), "100.000000"); assert_eq!(f(100.0), "100.000000");
@ -711,11 +761,23 @@ mod test {
assert_eq!(f(1.999_999_5), "1.999999"); assert_eq!(f(1.999_999_5), "1.999999");
assert_eq!(f(1.999_999_6), "2.000000"); assert_eq!(f(1.999_999_6), "2.000000");
let f = |x| format_float_decimal(&BigDecimal::from_f64(x).unwrap(), 0, ForceDecimal::Yes); let f = |x| {
format_float_decimal(
&BigDecimal::from_f64(x).unwrap(),
Some(0),
ForceDecimal::Yes,
)
};
assert_eq!(f(100.0), "100."); assert_eq!(f(100.0), "100.");
// Test arbitrary precision: long inputs that would not fit in a f64, print 24 digits after decimal point. // Test arbitrary precision: long inputs that would not fit in a f64, print 24 digits after decimal point.
let f = |x| format_float_decimal(&BigDecimal::from_str(x).unwrap(), 24, ForceDecimal::No); let f = |x| {
format_float_decimal(
&BigDecimal::from_str(x).unwrap(),
Some(24),
ForceDecimal::No,
)
};
assert_eq!(f("0.12345678901234567890"), "0.123456789012345678900000"); assert_eq!(f("0.12345678901234567890"), "0.123456789012345678900000");
assert_eq!( assert_eq!(
f("1234567890.12345678901234567890"), f("1234567890.12345678901234567890"),
@ -727,7 +789,11 @@ mod test {
fn decimal_float_zero() { fn decimal_float_zero() {
use super::format_float_decimal; use super::format_float_decimal;
let f = |digits, scale| { let f = |digits, scale| {
format_float_decimal(&BigDecimal::from_bigint(digits, scale), 6, ForceDecimal::No) format_float_decimal(
&BigDecimal::from_bigint(digits, scale),
Some(6),
ForceDecimal::No,
)
}; };
assert_eq!(f(0.into(), 0), "0.000000"); assert_eq!(f(0.into(), 0), "0.000000");
assert_eq!(f(0.into(), -10), "0.000000"); assert_eq!(f(0.into(), -10), "0.000000");
@ -740,7 +806,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_scientific( format_float_scientific(
&BigDecimal::from_f64(x).unwrap(), &BigDecimal::from_f64(x).unwrap(),
6, None,
Case::Lowercase, Case::Lowercase,
ForceDecimal::No, ForceDecimal::No,
) )
@ -756,7 +822,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_scientific( format_float_scientific(
&BigDecimal::from_f64(x).unwrap(), &BigDecimal::from_f64(x).unwrap(),
6, Some(6),
Case::Uppercase, Case::Uppercase,
ForceDecimal::No, ForceDecimal::No,
) )
@ -768,7 +834,7 @@ mod test {
let f = |digits, scale| { let f = |digits, scale| {
format_float_scientific( format_float_scientific(
&BigDecimal::from_bigint(digits, scale), &BigDecimal::from_bigint(digits, scale),
6, Some(6),
Case::Lowercase, Case::Lowercase,
ForceDecimal::No, ForceDecimal::No,
) )
@ -785,7 +851,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_scientific( format_float_scientific(
&BigDecimal::from_f64(x).unwrap(), &BigDecimal::from_f64(x).unwrap(),
0, Some(0),
Case::Lowercase, Case::Lowercase,
ForceDecimal::No, ForceDecimal::No,
) )
@ -801,7 +867,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_scientific( format_float_scientific(
&BigDecimal::from_f64(x).unwrap(), &BigDecimal::from_f64(x).unwrap(),
0, Some(0),
Case::Lowercase, Case::Lowercase,
ForceDecimal::Yes, ForceDecimal::Yes,
) )
@ -821,7 +887,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_shortest( format_float_shortest(
&BigDecimal::from_f64(x).unwrap(), &BigDecimal::from_f64(x).unwrap(),
6, None,
Case::Lowercase, Case::Lowercase,
ForceDecimal::No, ForceDecimal::No,
) )
@ -843,7 +909,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_shortest( format_float_shortest(
&BigDecimal::from_f64(x).unwrap(), &BigDecimal::from_f64(x).unwrap(),
6, None,
Case::Lowercase, Case::Lowercase,
ForceDecimal::Yes, ForceDecimal::Yes,
) )
@ -865,7 +931,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_shortest( format_float_shortest(
&BigDecimal::from_f64(x).unwrap(), &BigDecimal::from_f64(x).unwrap(),
0, Some(0),
Case::Lowercase, Case::Lowercase,
ForceDecimal::No, ForceDecimal::No,
) )
@ -884,7 +950,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_shortest( format_float_shortest(
&BigDecimal::from_f64(x).unwrap(), &BigDecimal::from_f64(x).unwrap(),
0, Some(0),
Case::Lowercase, Case::Lowercase,
ForceDecimal::Yes, ForceDecimal::Yes,
) )
@ -910,7 +976,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_hexadecimal( format_float_hexadecimal(
&BigDecimal::from_str(x).unwrap(), &BigDecimal::from_str(x).unwrap(),
6, Some(6),
Case::Lowercase, Case::Lowercase,
ForceDecimal::No, ForceDecimal::No,
) )
@ -925,7 +991,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_hexadecimal( format_float_hexadecimal(
&BigDecimal::from_str(x).unwrap(), &BigDecimal::from_str(x).unwrap(),
0, Some(0),
Case::Lowercase, Case::Lowercase,
ForceDecimal::No, ForceDecimal::No,
) )
@ -937,7 +1003,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_hexadecimal( format_float_hexadecimal(
&BigDecimal::from_str(x).unwrap(), &BigDecimal::from_str(x).unwrap(),
0, Some(0),
Case::Lowercase, Case::Lowercase,
ForceDecimal::Yes, ForceDecimal::Yes,
) )
@ -946,22 +1012,53 @@ mod test {
assert_eq!(f("0.125"), "0x8.p-6"); assert_eq!(f("0.125"), "0x8.p-6");
assert_eq!(f("256.0"), "0x8.p+5"); assert_eq!(f("256.0"), "0x8.p+5");
// Default precision, maximum 13 digits (x86-64 behavior)
let f = |x| { let f = |x| {
format_float_hexadecimal( format_float_hexadecimal(
&BigDecimal::from_str(x).unwrap(), &BigDecimal::from_str(x).unwrap(),
6, None,
Case::Lowercase,
ForceDecimal::No,
)
};
assert_eq!(f("0"), "0x0p+0");
assert_eq!(f("0.00001"), "0xa.7c5ac471b478423p-20");
assert_eq!(f("0.125"), "0x8p-6");
assert_eq!(f("4.25"), "0x8.8p-1");
assert_eq!(f("17.203125"), "0x8.9ap+1");
assert_eq!(f("256.0"), "0x8p+5");
assert_eq!(f("1000.01"), "0xf.a00a3d70a3d70a4p+6");
assert_eq!(f("65536.0"), "0x8p+13");
let f = |x| {
format_float_hexadecimal(
&BigDecimal::from_str(x).unwrap(),
None,
Case::Lowercase,
ForceDecimal::Yes,
)
};
assert_eq!(f("0"), "0x0.p+0");
assert_eq!(f("0.125"), "0x8.p-6");
assert_eq!(f("4.25"), "0x8.8p-1");
assert_eq!(f("256.0"), "0x8.p+5");
let f = |x| {
format_float_hexadecimal(
&BigDecimal::from_str(x).unwrap(),
Some(6),
Case::Uppercase, Case::Uppercase,
ForceDecimal::No, ForceDecimal::No,
) )
}; };
assert_eq!(f("0.00001"), "0xA.7C5AC4P-20"); assert_eq!(f("0.00001"), "0XA.7C5AC4P-20");
assert_eq!(f("0.125"), "0x8.000000P-6"); assert_eq!(f("0.125"), "0X8.000000P-6");
// Test "0e10"/"0e-10". From cppreference.com: "If the value is 0, the exponent is also 0." // Test "0e10"/"0e-10". From cppreference.com: "If the value is 0, the exponent is also 0."
let f = |digits, scale| { let f = |digits, scale| {
format_float_hexadecimal( format_float_hexadecimal(
&BigDecimal::from_bigint(digits, scale), &BigDecimal::from_bigint(digits, scale),
6, Some(6),
Case::Lowercase, Case::Lowercase,
ForceDecimal::No, ForceDecimal::No,
) )
@ -991,7 +1088,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_shortest( format_float_shortest(
&BigDecimal::from_f64(x).unwrap(), &BigDecimal::from_f64(x).unwrap(),
6, None,
Case::Lowercase, Case::Lowercase,
ForceDecimal::No, ForceDecimal::No,
) )
@ -1009,7 +1106,7 @@ mod test {
let f = |x| { let f = |x| {
format_float_shortest( format_float_shortest(
&BigDecimal::from_f64(x).unwrap(), &BigDecimal::from_f64(x).unwrap(),
6, None,
Case::Lowercase, Case::Lowercase,
ForceDecimal::No, ForceDecimal::No,
) )
@ -1168,7 +1265,7 @@ mod test {
assert_eq!(f("%e", &(-123.0).into()), "-1.230000e+02"); assert_eq!(f("%e", &(-123.0).into()), "-1.230000e+02");
assert_eq!(f("%#09.e", &(-100.0).into()), "-001.e+02"); assert_eq!(f("%#09.e", &(-100.0).into()), "-001.e+02");
assert_eq!(f("%# 9.E", &100.0.into()), " 1.E+02"); assert_eq!(f("%# 9.E", &100.0.into()), " 1.E+02");
assert_eq!(f("% 12.2A", &(-100.0).into()), " -0xC.80P+3"); assert_eq!(f("% 12.2A", &(-100.0).into()), " -0XC.80P+3");
} }
#[test] #[test]

View file

@ -432,11 +432,13 @@ impl Spec {
precision, precision,
} => { } => {
let width = resolve_asterisk(*width, &mut args).unwrap_or(0); let width = resolve_asterisk(*width, &mut args).unwrap_or(0);
let precision = resolve_asterisk(*precision, &mut args).unwrap_or(6); let precision = resolve_asterisk(*precision, &mut args);
let f: ExtendedBigDecimal = args.get_extended_big_decimal(); let f: ExtendedBigDecimal = args.get_extended_big_decimal();
if precision as u64 > i32::MAX as u64 { if precision.is_some_and(|p| p as u64 > i32::MAX as u64) {
return Err(FormatError::InvalidPrecision(precision.to_string())); return Err(FormatError::InvalidPrecision(
precision.unwrap().to_string(),
));
} }
num_format::Float { num_format::Float {

View file

@ -390,7 +390,6 @@ fn sub_num_sci_negative() {
.stdout_only("-1234 is -1.234000e+03"); .stdout_only("-1234 is -1.234000e+03");
} }
#[cfg_attr(not(feature = "test_unimplemented"), ignore)]
#[test] #[test]
fn sub_num_hex_float_lower() { fn sub_num_hex_float_lower() {
new_ucmd!() new_ucmd!()
@ -399,7 +398,6 @@ fn sub_num_hex_float_lower() {
.stdout_only("0xep-4"); .stdout_only("0xep-4");
} }
#[cfg_attr(not(feature = "test_unimplemented"), ignore)]
#[test] #[test]
fn sub_num_hex_float_upper() { fn sub_num_hex_float_upper() {
new_ucmd!() new_ucmd!()