Skip to content

Commit 8cf4da0

Browse files
committed
uucore: format: Fix hexadecimal default format print
The default hex format, on x86(-64) prints 15 digits after the decimal point, _but_ also trims trailing zeros, so it's not just a simple default precision and a little bit of extra handling is required. Also add a bunch of tests. Fixes #7364.
1 parent 3f24796 commit 8cf4da0

File tree

2 files changed

+72
-16
lines changed

2 files changed

+72
-16
lines changed

src/uucore/src/lib/features/format/num_format.rs

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -524,23 +524,28 @@ fn format_float_hexadecimal(
524524
// We have arbitrary precision in base 10, so we can't always represent
525525
// the value exactly (e.g. 0.1 is c.ccccc...).
526526
//
527-
// Emulate x86(-64) behavior, where 64 bits are printed in total, that's
528-
// 16 hex digits, including 1 before the decimal point (so 15 after).
527+
// Note that this is the maximum precision, trailing 0's are trimmed when
528+
// printing.
529+
//
530+
// Emulate x86(-64) behavior, where 64 bits at _most_ are printed in total,
531+
// that's 16 hex digits, including 1 before the decimal point (so 15 after).
529532
//
530533
// TODO: Make this configurable? e.g. arm64 value would be 28 (f128),
531534
// arm value 13 (f64).
532-
let precision = precision.unwrap_or(15);
535+
let max_precision = precision.unwrap_or(15);
533536

534537
let (prefix, exp_char) = match case {
535538
Case::Lowercase => ("0x", 'p'),
536539
Case::Uppercase => ("0X", 'P'),
537540
};
538541

539542
if BigDecimal::zero().eq(bd) {
540-
return if force_decimal == ForceDecimal::Yes && precision == 0 {
543+
// To print 0, we don't ever need any digits after the decimal point, so default to
544+
// that if precision is not specified.
545+
return if force_decimal == ForceDecimal::Yes && precision.unwrap_or(0) == 0 {
541546
format!("0x0.{exp_char}+0")
542547
} else {
543-
format!("0x{:.*}{exp_char}+0", precision, 0.0)
548+
format!("0x{:.*}{exp_char}+0", precision.unwrap_or(0), 0.0)
544549
};
545550
}
546551

@@ -575,7 +580,8 @@ fn format_float_hexadecimal(
575580
// Then, dividing by 5^-exp10 loses at most -exp10*3 binary digits
576581
// (since 5^-exp10 < 8^-exp10), so we add that, and another bit for
577582
// rounding.
578-
let margin = ((precision + 1) as i64 * 4 - frac10.bits() as i64).max(0) + -exp10 * 3 + 1;
583+
let margin =
584+
((max_precision + 1) as i64 * 4 - frac10.bits() as i64).max(0) + -exp10 * 3 + 1;
579585

580586
// frac10 * 10^exp10 = frac10 * 2^margin * 10^exp10 * 2^-margin =
581587
// (frac10 * 2^margin * 5^exp10) * 2^exp10 * 2^-margin =
@@ -590,7 +596,7 @@ fn format_float_hexadecimal(
590596
// so the value will always be between 0x8 and 0xf.
591597
// TODO: Make this configurable? e.g. arm64 only displays 1 digit.
592598
const BEFORE_BITS: usize = 4;
593-
let wanted_bits = (BEFORE_BITS + precision * 4) as u64;
599+
let wanted_bits = (BEFORE_BITS + max_precision * 4) as u64;
594600
let bits = frac2.bits();
595601

596602
exp2 += bits as i64 - wanted_bits as i64;
@@ -620,18 +626,39 @@ fn format_float_hexadecimal(
620626
digits.make_ascii_uppercase();
621627
}
622628
let (first_digit, remaining_digits) = digits.split_at(1);
623-
let exponent = exp2 + (4 * precision) as i64;
629+
let exponent = exp2 + (4 * max_precision) as i64;
624630

625-
let dot =
626-
if !remaining_digits.is_empty() || (precision == 0 && ForceDecimal::Yes == force_decimal) {
627-
"."
628-
} else {
629-
""
630-
};
631+
let mut remaining_digits = remaining_digits.to_string();
632+
if precision.is_none() {
633+
// Trim trailing zeros
634+
strip_fractional_zeroes(&mut remaining_digits);
635+
}
636+
637+
let dot = if !remaining_digits.is_empty()
638+
|| (precision.unwrap_or(0) == 0 && ForceDecimal::Yes == force_decimal)
639+
{
640+
"."
641+
} else {
642+
""
643+
};
631644

632645
format!("{prefix}{first_digit}{dot}{remaining_digits}{exp_char}{exponent:+}")
633646
}
634647

648+
fn strip_fractional_zeroes(s: &mut String) {
649+
let mut trim_to = s.len();
650+
for (pos, c) in s.char_indices().rev() {
651+
if pos + c.len_utf8() == trim_to {
652+
if c == '0' {
653+
trim_to = pos;
654+
} else {
655+
break;
656+
}
657+
}
658+
}
659+
s.truncate(trim_to);
660+
}
661+
635662
fn strip_fractional_zeroes_and_dot(s: &mut String) {
636663
let mut trim_to = s.len();
637664
for (pos, c) in s.char_indices().rev() {
@@ -995,6 +1022,37 @@ mod test {
9951022
assert_eq!(f("0.125"), "0x8.p-6");
9961023
assert_eq!(f("256.0"), "0x8.p+5");
9971024

1025+
// Default precision, maximum 13 digits (x86-64 behavior)
1026+
let f = |x| {
1027+
format_float_hexadecimal(
1028+
&BigDecimal::from_str(x).unwrap(),
1029+
None,
1030+
Case::Lowercase,
1031+
ForceDecimal::No,
1032+
)
1033+
};
1034+
assert_eq!(f("0"), "0x0p+0");
1035+
assert_eq!(f("0.00001"), "0xa.7c5ac471b478423p-20");
1036+
assert_eq!(f("0.125"), "0x8p-6");
1037+
assert_eq!(f("4.25"), "0x8.8p-1");
1038+
assert_eq!(f("17.203125"), "0x8.9ap+1");
1039+
assert_eq!(f("256.0"), "0x8p+5");
1040+
assert_eq!(f("1000.01"), "0xf.a00a3d70a3d70a4p+6");
1041+
assert_eq!(f("65536.0"), "0x8p+13");
1042+
1043+
let f = |x| {
1044+
format_float_hexadecimal(
1045+
&BigDecimal::from_str(x).unwrap(),
1046+
None,
1047+
Case::Lowercase,
1048+
ForceDecimal::Yes,
1049+
)
1050+
};
1051+
assert_eq!(f("0"), "0x0.p+0");
1052+
assert_eq!(f("0.125"), "0x8.p-6");
1053+
assert_eq!(f("4.25"), "0x8.8p-1");
1054+
assert_eq!(f("256.0"), "0x8.p+5");
1055+
9981056
let f = |x| {
9991057
format_float_hexadecimal(
10001058
&BigDecimal::from_str(x).unwrap(),

tests/by-util/test_printf.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,6 @@ fn sub_num_sci_negative() {
390390
.stdout_only("-1234 is -1.234000e+03");
391391
}
392392

393-
#[cfg_attr(not(feature = "test_unimplemented"), ignore)]
394393
#[test]
395394
fn sub_num_hex_float_lower() {
396395
new_ucmd!()
@@ -399,7 +398,6 @@ fn sub_num_hex_float_lower() {
399398
.stdout_only("0xep-4");
400399
}
401400

402-
#[cfg_attr(not(feature = "test_unimplemented"), ignore)]
403401
#[test]
404402
fn sub_num_hex_float_upper() {
405403
new_ucmd!()

0 commit comments

Comments
 (0)