Skip to content

numfmt: honor --round for an explicit --format precision of 0 - #14392

Open
Socialpranker wants to merge 1 commit into
uutils:mainfrom
Socialpranker:numfmt-round-explicit-precision
Open

numfmt: honor --round for an explicit --format precision of 0#14392
Socialpranker wants to merge 1 commit into
uutils:mainfrom
Socialpranker:numfmt-round-explicit-precision

Conversation

@Socialpranker

Copy link
Copy Markdown
Contributor

numfmt --to=<scale> --format='%.0f' ignored --round and always rounded
the scaled value to nearest:

$ numfmt --to=si --format=%.0f 1234567     # GNU
2M
$ numfmt --to=si --format=%.0f 1234567     # uutils, before
1M

$ numfmt --to=si --format=%.0f --round=down 1934567    # GNU
1M
$ numfmt --to=si --format=%.0f --round=down 1934567    # uutils, before
2M

--help documents both halves of the contract: "Optional precision (%.1f)
will override the input determined precision", and --round=METHOD with
from-zero as the default. With an explicit .0, 1234567 / 10^6 = 1.234567 must be rounded to zero decimals by the selected method, giving
2M by default and 1M under --round=down.

The fix

consider_suffix picked its rounding by precision > 0, so a precision
of 0 fell into the div_round branch. That branch is for the default
presentation, where a value below the next base keeps one fractional digit
(1.3M): it rounds 10 * v and divides by ten. The result, 1.3, then
reached format_float(i2, 0), which rounds to nearest — hence 1M from
1.23, and 2M from 1.93 even with --round=down.

The function now takes is_precision_specified (already computed in
format_string and threaded to transform_to) and uses
round_with_precision whenever the precision was given, .0 included.
The implicit-precision path is untouched: consider_suffix returns early
for --to=none, which is the only case where an implicit precision can be
non-zero, so the div_round branch still governs every unspecified-format
invocation.

How the GNU behavior was established

By running the installed GNU coreutils 9.11 binary (Homebrew, gnumfmt)
as a black box and diffing its output against uutils across a matrix of
--to x --round x --format precisions. I did not read GNU coreutils
source.

Testing

  • New test test_format_precision_zero_with_to_scale_honors_round_method
    in tests/by-util/test_numfmt.rs: 7 cases covering the default
    (from-zero) plus up, down, nearest, towards-zero, a negative
    value, and --to=iec. Mutation-checked: reverting the one-line
    condition in format.rs makes it fail.
  • cargo test --features numfmt --test tests test_numfmt: 192 passed, 0
    failed (191 before, plus the new one).
  • cargo clippy -p uu_numfmt --all-targets -- -D warnings: clean.
  • cargo fmt --check: clean.
  • Differential A/B against GNU coreutils 9.11 over 216 numfmt
    invocations (--to/--from/--padding/--format/--round/--suffix/
    --invalid/--field/--grouping): mismatches 37 -> 36. The one
    remaining --format mismatch is a separate defect (zero-padding width),
    and the rest are pre-existing diagnostics wording differences. No case
    regressed.

Disclosure

Prepared with AI assistance (Claude Opus 5, via Claude Code), per the AI
policy in CONTRIBUTING.md. Every GNU behavior quoted above came from
running the installed binary, not from reading GPL source. All testing was
run locally.

With --to=<scale>, a "%.0f" precision took the div_round path, which keeps
one fractional digit below the next base and then rounds that to nearest,
so 1234567 printed 1M instead of GNU's 2M under the default from-zero
rounding. An explicitly given precision now rounds the scaled value with
the selected method.
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/uniq/uniq-perf. tests/uniq/uniq-perf is passing on 'main'. Maybe you have to rebase?
Skip an intermittent issue tests/tail/tail-n0f (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/rm/isatty (passes in this run but fails in the 'main' branch)
Note: The gnu test tests/tail/pipe-f is now being skipped but was previously passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant