Skip to content

Commit 1de44d1

Browse files
committed
uucode: format: format_float_shortest: Take in &BigDecimal
Similar logic to scientific printing. Also add a few more tests around corner cases where we switch from decimal to scientific printing.
1 parent 1585aa3 commit 1de44d1

File tree

1 file changed

+98
-41
lines changed

1 file changed

+98
-41
lines changed

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

Lines changed: 98 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ impl Formatter<&ExtendedBigDecimal> for Float {
265265
format_float_scientific(&bd, self.precision, self.case, self.force_decimal)
266266
}
267267
FloatVariant::Shortest => {
268-
format_float_shortest(x, self.precision, self.case, self.force_decimal)
268+
format_float_shortest(&bd, self.precision, self.case, self.force_decimal)
269269
}
270270
FloatVariant::Hexadecimal => {
271271
format_float_hexadecimal(x, self.precision, self.case, self.force_decimal)
@@ -403,50 +403,50 @@ fn format_float_scientific(
403403
}
404404

405405
fn format_float_shortest(
406-
f: f64,
406+
bd: &BigDecimal,
407407
precision: usize,
408408
case: Case,
409409
force_decimal: ForceDecimal,
410410
) -> String {
411-
debug_assert!(!f.is_sign_negative());
412-
// Precision here is about how many digits should be displayed
413-
// instead of how many digits for the fractional part, this means that if
414-
// we pass this to rust's format string, it's always gonna be one less.
415-
let precision = precision.saturating_sub(1);
411+
debug_assert!(!bd.is_negative());
412+
413+
// Note: Precision here is how many digits should be displayed in total,
414+
// instead of how many digits in the fractional part.
416415

417-
if f == 0.0 {
416+
// Precision 0 is equivalent to precision 1.
417+
let precision = precision.max(1);
418+
419+
if BigDecimal::zero().eq(bd) {
418420
return match (force_decimal, precision) {
419-
(ForceDecimal::Yes, 0) => "0.".into(),
421+
(ForceDecimal::Yes, 1) => "0.".into(),
420422
(ForceDecimal::Yes, _) => {
421-
format!("{:.*}", precision, 0.0)
423+
format!("{:.*}", precision - 1, 0.0)
422424
}
423425
(ForceDecimal::No, _) => "0".into(),
424426
};
425427
}
426428

427-
// Retrieve the exponent. Note that log10 is undefined for negative numbers.
428-
// To avoid NaN or zero (due to i32 conversion), use the absolute value of f.
429-
let mut exponent = f.abs().log10().floor() as i32;
430-
if f != 0.0 && exponent < -4 || exponent > precision as i32 {
431-
// Scientific-ish notation (with a few differences)
432-
let mut normalized = f / 10.0_f64.powi(exponent);
429+
// Round bd to precision digits (including the leading digit)
430+
// We call `with_prec` twice as it will produce an extra digit if rounding overflows
431+
// (e.g. 9995.with_prec(3) => 1000 * 10^1, but we want 100 * 10^2).
432+
let bd_round = bd.with_prec(precision as u64).with_prec(precision as u64);
433433

434-
// If the normalized value will be rounded to a value greater than 10
435-
// we need to correct.
436-
if (normalized * 10_f64.powi(precision as i32)).round() / 10_f64.powi(precision as i32)
437-
>= 10.0
438-
{
439-
normalized /= 10.0;
440-
exponent += 1;
441-
}
434+
// Convert to the form XXX * 10^-p (XXX is precision digit long)
435+
let (frac, e) = bd_round.as_bigint_and_exponent();
442436

443-
let additional_dot = if precision == 0 && ForceDecimal::Yes == force_decimal {
444-
"."
445-
} else {
446-
""
447-
};
437+
let digits = frac.to_str_radix(10);
438+
// If we end up with scientific formatting, we would convert XXX to X.XX:
439+
// that divides by 10^(precision-1), so add that to the exponent.
440+
let exponent = -e + precision as i64 - 1;
448441

449-
let mut normalized = format!("{normalized:.precision$}");
442+
if exponent < -4 || exponent >= precision as i64 {
443+
// Scientific-ish notation (with a few differences)
444+
445+
// Scale down "XXX" to "X.XX"
446+
let (first_digit, remaining_digits) = digits.split_at(1);
447+
448+
// Always add the dot, we might trim it later.
449+
let mut normalized = format!("{first_digit}.{remaining_digits}");
450450

451451
if force_decimal == ForceDecimal::No {
452452
strip_fractional_zeroes_and_dot(&mut normalized);
@@ -457,18 +457,23 @@ fn format_float_shortest(
457457
Case::Uppercase => 'E',
458458
};
459459

460-
format!("{normalized}{additional_dot}{exp_char}{exponent:+03}")
460+
format!("{normalized}{exp_char}{exponent:+03}")
461461
} else {
462462
// Decimal-ish notation with a few differences:
463463
// - The precision works differently and specifies the total number
464464
// of digits instead of the digits in the fractional part.
465465
// - If we don't force the decimal, `.` and trailing `0` in the fractional part
466466
// are trimmed.
467-
let decimal_places = (precision as i32 - exponent) as usize;
468-
let mut formatted = if decimal_places == 0 && force_decimal == ForceDecimal::Yes {
469-
format!("{f:.0}.")
467+
let mut formatted = if exponent < 0 {
468+
// Small number, prepend some "0.00" string
469+
let zeros = "0".repeat(-exponent as usize - 1);
470+
format!("0.{zeros}{digits}")
470471
} else {
471-
format!("{f:.decimal_places$}")
472+
// exponent >= 0, slot in a dot at the right spot
473+
let (first_digits, remaining_digits) = digits.split_at(exponent as usize + 1);
474+
475+
// Always add `.` even if it's trailing, we might trim it later
476+
format!("{first_digits}.{remaining_digits}")
472477
};
473478

474479
if force_decimal == ForceDecimal::No {
@@ -692,8 +697,17 @@ mod test {
692697
#[test]
693698
fn shortest_float() {
694699
use super::format_float_shortest;
695-
let f = |x| format_float_shortest(x, 6, Case::Lowercase, ForceDecimal::No);
700+
let f = |x| {
701+
format_float_shortest(
702+
&BigDecimal::from_f64(x).unwrap(),
703+
6,
704+
Case::Lowercase,
705+
ForceDecimal::No,
706+
)
707+
};
696708
assert_eq!(f(0.0), "0");
709+
assert_eq!(f(0.00001), "1e-05");
710+
assert_eq!(f(0.0001), "0.0001");
697711
assert_eq!(f(1.0), "1");
698712
assert_eq!(f(100.0), "100");
699713
assert_eq!(f(123_456.789), "123457");
@@ -705,8 +719,17 @@ mod test {
705719
#[test]
706720
fn shortest_float_force_decimal() {
707721
use super::format_float_shortest;
708-
let f = |x| format_float_shortest(x, 6, Case::Lowercase, ForceDecimal::Yes);
722+
let f = |x| {
723+
format_float_shortest(
724+
&BigDecimal::from_f64(x).unwrap(),
725+
6,
726+
Case::Lowercase,
727+
ForceDecimal::Yes,
728+
)
729+
};
709730
assert_eq!(f(0.0), "0.00000");
731+
assert_eq!(f(0.00001), "1.00000e-05");
732+
assert_eq!(f(0.0001), "0.000100000");
710733
assert_eq!(f(1.0), "1.00000");
711734
assert_eq!(f(100.0), "100.000");
712735
assert_eq!(f(123_456.789), "123457.");
@@ -718,18 +741,38 @@ mod test {
718741
#[test]
719742
fn shortest_float_force_decimal_zero_precision() {
720743
use super::format_float_shortest;
721-
let f = |x| format_float_shortest(x, 0, Case::Lowercase, ForceDecimal::No);
744+
let f = |x| {
745+
format_float_shortest(
746+
&BigDecimal::from_f64(x).unwrap(),
747+
0,
748+
Case::Lowercase,
749+
ForceDecimal::No,
750+
)
751+
};
722752
assert_eq!(f(0.0), "0");
753+
assert_eq!(f(0.00001), "1e-05");
754+
assert_eq!(f(0.0001), "0.0001");
723755
assert_eq!(f(1.0), "1");
756+
assert_eq!(f(10.0), "1e+01");
724757
assert_eq!(f(100.0), "1e+02");
725758
assert_eq!(f(123_456.789), "1e+05");
726759
assert_eq!(f(12.345_678_9), "1e+01");
727760
assert_eq!(f(1_000_000.0), "1e+06");
728761
assert_eq!(f(99_999_999.0), "1e+08");
729762

730-
let f = |x| format_float_shortest(x, 0, Case::Lowercase, ForceDecimal::Yes);
763+
let f = |x| {
764+
format_float_shortest(
765+
&BigDecimal::from_f64(x).unwrap(),
766+
0,
767+
Case::Lowercase,
768+
ForceDecimal::Yes,
769+
)
770+
};
731771
assert_eq!(f(0.0), "0.");
772+
assert_eq!(f(0.00001), "1.e-05");
773+
assert_eq!(f(0.0001), "0.0001");
732774
assert_eq!(f(1.0), "1.");
775+
assert_eq!(f(10.0), "1.e+01");
733776
assert_eq!(f(100.0), "1.e+02");
734777
assert_eq!(f(123_456.789), "1.e+05");
735778
assert_eq!(f(12.345_678_9), "1.e+01");
@@ -773,7 +816,14 @@ mod test {
773816
#[test]
774817
fn shortest_float_abs_value_less_than_one() {
775818
use super::format_float_shortest;
776-
let f = |x| format_float_shortest(x, 6, Case::Lowercase, ForceDecimal::No);
819+
let f = |x| {
820+
format_float_shortest(
821+
&BigDecimal::from_f64(x).unwrap(),
822+
6,
823+
Case::Lowercase,
824+
ForceDecimal::No,
825+
)
826+
};
777827
assert_eq!(f(0.1171875), "0.117188");
778828
assert_eq!(f(0.01171875), "0.0117188");
779829
assert_eq!(f(0.001171875), "0.00117187");
@@ -784,7 +834,14 @@ mod test {
784834
#[test]
785835
fn shortest_float_switch_decimal_scientific() {
786836
use super::format_float_shortest;
787-
let f = |x| format_float_shortest(x, 6, Case::Lowercase, ForceDecimal::No);
837+
let f = |x| {
838+
format_float_shortest(
839+
&BigDecimal::from_f64(x).unwrap(),
840+
6,
841+
Case::Lowercase,
842+
ForceDecimal::No,
843+
)
844+
};
788845
assert_eq!(f(0.001), "0.001");
789846
assert_eq!(f(0.0001), "0.0001");
790847
assert_eq!(f(0.00001), "1e-05");

0 commit comments

Comments
 (0)