1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-29 03:57:44 +00:00

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.
This commit is contained in:
Nicolas Boichat 2025-03-11 16:26:42 +01:00 committed by Sylvestre Ledru
parent 7f0e5eb473
commit f0e9b8621f

View file

@ -265,7 +265,7 @@ impl Formatter<&ExtendedBigDecimal> for Float {
format_float_scientific(&bd, self.precision, self.case, self.force_decimal) format_float_scientific(&bd, self.precision, self.case, self.force_decimal)
} }
FloatVariant::Shortest => { FloatVariant::Shortest => {
format_float_shortest(x, self.precision, self.case, self.force_decimal) format_float_shortest(&bd, self.precision, self.case, self.force_decimal)
} }
FloatVariant::Hexadecimal => { FloatVariant::Hexadecimal => {
format_float_hexadecimal(x, self.precision, self.case, self.force_decimal) format_float_hexadecimal(x, self.precision, self.case, self.force_decimal)
@ -403,50 +403,50 @@ fn format_float_scientific(
} }
fn format_float_shortest( fn format_float_shortest(
f: f64, bd: &BigDecimal,
precision: usize, precision: usize,
case: Case, case: Case,
force_decimal: ForceDecimal, force_decimal: ForceDecimal,
) -> String { ) -> String {
debug_assert!(!f.is_sign_negative()); debug_assert!(!bd.is_negative());
// Precision here is about how many digits should be displayed
// instead of how many digits for the fractional part, this means that if
// we pass this to rust's format string, it's always gonna be one less.
let precision = precision.saturating_sub(1);
if f == 0.0 { // Note: Precision here is how many digits should be displayed in total,
// instead of how many digits in the fractional part.
// Precision 0 is equivalent to precision 1.
let precision = precision.max(1);
if BigDecimal::zero().eq(bd) {
return match (force_decimal, precision) { return match (force_decimal, precision) {
(ForceDecimal::Yes, 0) => "0.".into(), (ForceDecimal::Yes, 1) => "0.".into(),
(ForceDecimal::Yes, _) => { (ForceDecimal::Yes, _) => {
format!("{:.*}", precision, 0.0) format!("{:.*}", precision - 1, 0.0)
} }
(ForceDecimal::No, _) => "0".into(), (ForceDecimal::No, _) => "0".into(),
}; };
} }
// Retrieve the exponent. Note that log10 is undefined for negative numbers. // Round bd to precision digits (including the leading digit)
// To avoid NaN or zero (due to i32 conversion), use the absolute value of f. // We call `with_prec` twice as it will produce an extra digit if rounding overflows
let mut exponent = f.abs().log10().floor() as i32; // (e.g. 9995.with_prec(3) => 1000 * 10^1, but we want 100 * 10^2).
if f != 0.0 && exponent < -4 || exponent > precision as i32 { let bd_round = bd.with_prec(precision as u64).with_prec(precision as u64);
// Convert to the form XXX * 10^-p (XXX is precision digit long)
let (frac, e) = bd_round.as_bigint_and_exponent();
let digits = frac.to_str_radix(10);
// If we end up with scientific formatting, we would convert XXX to X.XX:
// that divides by 10^(precision-1), so add that to the exponent.
let exponent = -e + precision as i64 - 1;
if exponent < -4 || exponent >= precision as i64 {
// Scientific-ish notation (with a few differences) // Scientific-ish notation (with a few differences)
let mut normalized = f / 10.0_f64.powi(exponent);
// If the normalized value will be rounded to a value greater than 10 // Scale down "XXX" to "X.XX"
// we need to correct. let (first_digit, remaining_digits) = digits.split_at(1);
if (normalized * 10_f64.powi(precision as i32)).round() / 10_f64.powi(precision as i32)
>= 10.0
{
normalized /= 10.0;
exponent += 1;
}
let additional_dot = if precision == 0 && ForceDecimal::Yes == force_decimal { // Always add the dot, we might trim it later.
"." let mut normalized = format!("{first_digit}.{remaining_digits}");
} else {
""
};
let mut normalized = format!("{normalized:.precision$}");
if force_decimal == ForceDecimal::No { if force_decimal == ForceDecimal::No {
strip_fractional_zeroes_and_dot(&mut normalized); strip_fractional_zeroes_and_dot(&mut normalized);
@ -457,18 +457,23 @@ fn format_float_shortest(
Case::Uppercase => 'E', Case::Uppercase => 'E',
}; };
format!("{normalized}{additional_dot}{exp_char}{exponent:+03}") format!("{normalized}{exp_char}{exponent:+03}")
} else { } else {
// Decimal-ish notation with a few differences: // Decimal-ish notation with a few differences:
// - The precision works differently and specifies the total number // - The precision works differently and specifies the total number
// of digits instead of the digits in the fractional part. // of digits instead of the digits in the fractional part.
// - If we don't force the decimal, `.` and trailing `0` in the fractional part // - If we don't force the decimal, `.` and trailing `0` in the fractional part
// are trimmed. // are trimmed.
let decimal_places = (precision as i32 - exponent) as usize; let mut formatted = if exponent < 0 {
let mut formatted = if decimal_places == 0 && force_decimal == ForceDecimal::Yes { // Small number, prepend some "0.00" string
format!("{f:.0}.") let zeros = "0".repeat(-exponent as usize - 1);
format!("0.{zeros}{digits}")
} else { } else {
format!("{f:.decimal_places$}") // exponent >= 0, slot in a dot at the right spot
let (first_digits, remaining_digits) = digits.split_at(exponent as usize + 1);
// Always add `.` even if it's trailing, we might trim it later
format!("{first_digits}.{remaining_digits}")
}; };
if force_decimal == ForceDecimal::No { if force_decimal == ForceDecimal::No {
@ -692,8 +697,17 @@ mod test {
#[test] #[test]
fn shortest_float() { fn shortest_float() {
use super::format_float_shortest; use super::format_float_shortest;
let f = |x| format_float_shortest(x, 6, Case::Lowercase, ForceDecimal::No); let f = |x| {
format_float_shortest(
&BigDecimal::from_f64(x).unwrap(),
6,
Case::Lowercase,
ForceDecimal::No,
)
};
assert_eq!(f(0.0), "0"); assert_eq!(f(0.0), "0");
assert_eq!(f(0.00001), "1e-05");
assert_eq!(f(0.0001), "0.0001");
assert_eq!(f(1.0), "1"); assert_eq!(f(1.0), "1");
assert_eq!(f(100.0), "100"); assert_eq!(f(100.0), "100");
assert_eq!(f(123_456.789), "123457"); assert_eq!(f(123_456.789), "123457");
@ -705,8 +719,17 @@ mod test {
#[test] #[test]
fn shortest_float_force_decimal() { fn shortest_float_force_decimal() {
use super::format_float_shortest; use super::format_float_shortest;
let f = |x| format_float_shortest(x, 6, Case::Lowercase, ForceDecimal::Yes); let f = |x| {
format_float_shortest(
&BigDecimal::from_f64(x).unwrap(),
6,
Case::Lowercase,
ForceDecimal::Yes,
)
};
assert_eq!(f(0.0), "0.00000"); assert_eq!(f(0.0), "0.00000");
assert_eq!(f(0.00001), "1.00000e-05");
assert_eq!(f(0.0001), "0.000100000");
assert_eq!(f(1.0), "1.00000"); assert_eq!(f(1.0), "1.00000");
assert_eq!(f(100.0), "100.000"); assert_eq!(f(100.0), "100.000");
assert_eq!(f(123_456.789), "123457."); assert_eq!(f(123_456.789), "123457.");
@ -718,18 +741,38 @@ mod test {
#[test] #[test]
fn shortest_float_force_decimal_zero_precision() { fn shortest_float_force_decimal_zero_precision() {
use super::format_float_shortest; use super::format_float_shortest;
let f = |x| format_float_shortest(x, 0, Case::Lowercase, ForceDecimal::No); let f = |x| {
format_float_shortest(
&BigDecimal::from_f64(x).unwrap(),
0,
Case::Lowercase,
ForceDecimal::No,
)
};
assert_eq!(f(0.0), "0"); assert_eq!(f(0.0), "0");
assert_eq!(f(0.00001), "1e-05");
assert_eq!(f(0.0001), "0.0001");
assert_eq!(f(1.0), "1"); assert_eq!(f(1.0), "1");
assert_eq!(f(10.0), "1e+01");
assert_eq!(f(100.0), "1e+02"); assert_eq!(f(100.0), "1e+02");
assert_eq!(f(123_456.789), "1e+05"); assert_eq!(f(123_456.789), "1e+05");
assert_eq!(f(12.345_678_9), "1e+01"); assert_eq!(f(12.345_678_9), "1e+01");
assert_eq!(f(1_000_000.0), "1e+06"); assert_eq!(f(1_000_000.0), "1e+06");
assert_eq!(f(99_999_999.0), "1e+08"); assert_eq!(f(99_999_999.0), "1e+08");
let f = |x| format_float_shortest(x, 0, Case::Lowercase, ForceDecimal::Yes); let f = |x| {
format_float_shortest(
&BigDecimal::from_f64(x).unwrap(),
0,
Case::Lowercase,
ForceDecimal::Yes,
)
};
assert_eq!(f(0.0), "0."); assert_eq!(f(0.0), "0.");
assert_eq!(f(0.00001), "1.e-05");
assert_eq!(f(0.0001), "0.0001");
assert_eq!(f(1.0), "1."); assert_eq!(f(1.0), "1.");
assert_eq!(f(10.0), "1.e+01");
assert_eq!(f(100.0), "1.e+02"); assert_eq!(f(100.0), "1.e+02");
assert_eq!(f(123_456.789), "1.e+05"); assert_eq!(f(123_456.789), "1.e+05");
assert_eq!(f(12.345_678_9), "1.e+01"); assert_eq!(f(12.345_678_9), "1.e+01");
@ -773,7 +816,14 @@ mod test {
#[test] #[test]
fn shortest_float_abs_value_less_than_one() { fn shortest_float_abs_value_less_than_one() {
use super::format_float_shortest; use super::format_float_shortest;
let f = |x| format_float_shortest(x, 6, Case::Lowercase, ForceDecimal::No); let f = |x| {
format_float_shortest(
&BigDecimal::from_f64(x).unwrap(),
6,
Case::Lowercase,
ForceDecimal::No,
)
};
assert_eq!(f(0.1171875), "0.117188"); assert_eq!(f(0.1171875), "0.117188");
assert_eq!(f(0.01171875), "0.0117188"); assert_eq!(f(0.01171875), "0.0117188");
assert_eq!(f(0.001171875), "0.00117187"); assert_eq!(f(0.001171875), "0.00117187");
@ -784,7 +834,14 @@ mod test {
#[test] #[test]
fn shortest_float_switch_decimal_scientific() { fn shortest_float_switch_decimal_scientific() {
use super::format_float_shortest; use super::format_float_shortest;
let f = |x| format_float_shortest(x, 6, Case::Lowercase, ForceDecimal::No); let f = |x| {
format_float_shortest(
&BigDecimal::from_f64(x).unwrap(),
6,
Case::Lowercase,
ForceDecimal::No,
)
};
assert_eq!(f(0.001), "0.001"); assert_eq!(f(0.001), "0.001");
assert_eq!(f(0.0001), "0.0001"); assert_eq!(f(0.0001), "0.0001");
assert_eq!(f(0.00001), "1e-05"); assert_eq!(f(0.00001), "1e-05");