Skip to content

Commit

Permalink
uucore:format:fix floating-point rounding
Browse files Browse the repository at this point in the history
Handle a special case for floating-point numbers when |f| < 1.0.

In this case, the total number of decimal places should include both the
padding zeros which are not subject to precision rounding and the
significant digits, rounded according to the precision parameter.
  • Loading branch information
alexs-sh committed Dec 31, 2024
1 parent ed9e80e commit dfd1621
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 1 deletion.
37 changes: 36 additions & 1 deletion src/uucore/src/lib/features/format/num_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,27 @@ fn format_float_shortest(
// of digits instead of the digits in the fractional part.
// - If we don't force the decimal, `.` and trailing `0` in the fractional part
// are trimmed.
let decimal_places = (precision as i32 - exponent) as usize;
let decimal_places = if f.abs() >= 1.0 {
(precision as i32 - exponent) as usize
} else {
// This is a special case for |f| < 1.0.
//
// `decimal_places` is calculated as the sum of the padding zeros on the left
// and the number of significant digits. The precision parameter applies only
// to the significant digits.
//
// For example, the number 0.01171875 with a precision of 6 should be interpreted as
// 2 padding zeros and 7 significant digits (1171875). Applying the precision parameter
// to 1171875 results in the number 117188.
//
// So, the final result is 0.0117188.

// Get the number of leading zeros. Note:
// - log10 is undefined for negative numbers, so abs() is required
// - |f| < 1.0, which makes log10 negative, and the result must be inverted.
let num_of_padding_zeros = -f.abs().log10().floor() as usize;
precision + num_of_padding_zeros
};
let mut formatted = if decimal_places == 0 && force_decimal == ForceDecimal::Yes {
format!("{f:.0}.")
} else {
Expand Down Expand Up @@ -665,4 +685,19 @@ mod test {
assert_eq!(&f("1000.02030"), "1000.0203");
assert_eq!(&f("1000.00000"), "1000");
}

#[test]
fn shortest_float_abs_value_less_than_one() {
use super::format_float_shortest;
let f = |x| format_float_shortest(x, 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");
assert_eq!(f(-0.001171875), "-0.00117187");
assert_eq!(f(-0.001171875001), "-0.00117188");
assert_eq!(f(0.001171875001), "0.00117188");
}
}
34 changes: 34 additions & 0 deletions tests/by-util/test_printf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,3 +916,37 @@ fn float_flag_position_space_padding() {
.succeeds()
.stdout_only(" +1.0");
}

#[test]
fn float_abs_value_less_than_one() {
new_ucmd!()
.args(&["%g", "0.1171875"])
.succeeds()
.stdout_only("0.117188");

// The original value from #7031 issue
new_ucmd!()
.args(&["%g", "-0.1171875"])
.succeeds()
.stdout_only("-0.117188");

new_ucmd!()
.args(&["%g", "0.01171875"])
.succeeds()
.stdout_only("0.0117188");

new_ucmd!()
.args(&["%g", "-0.01171875"])
.succeeds()
.stdout_only("-0.0117188");

new_ucmd!()
.args(&["%g", "0.001171875001"])
.succeeds()
.stdout_only("0.00117188");

new_ucmd!()
.args(&["%g", "-0.001171875001"])
.succeeds()
.stdout_only("-0.00117188");
}

0 comments on commit dfd1621

Please sign in to comment.