Round float at the decimal level to match CPython's _Py_dg_dtoa#7761
Conversation
CPython's `float.__round__` (Objects/floatobject.c) routes through
`_Py_dg_dtoa` and rounds at the decimal level. The previous
`round_float_digits` multiplied by 10**ndigits and rounded at the
IEEE 754 binary level, which diverges for values that aren't exactly
representable. For example, 2.675 stores as 2.67499...; dtoa correctly
rounds it down to 2.67, but `(2.675 * 100.0).round() / 100.0` lands on
2.68 because the multiplication produces a phantom 267.5 tie that
round-half-to-even snaps up.
Rust's `{:.*}` float formatting uses dtoa-style algorithms (Grisu3 +
Dragon4 fallback) and matches CPython's `_Py_dg_dtoa` byte-for-byte.
Replace the multiply-then-round step with `format!` + `parse` for
ndigits >= 0. The ndigits < 0 path is unchanged because dividing
typical inputs by 10**|ndigits| produces genuine ties rather than
synthesizing them.
Verified byte-identical with CPython 3.14.4 over a 108-case random
fuzz plus targeted half-tie probes. Unmasks
`test_float.RoundTestCase.test_matches_float_format` and
`test_previous_round_bugs`.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughRewrites ChangesFloat Rounding Implementation
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [x] test: cpython/Lib/test/test_float.py (TODO: 4) dependencies: dependent tests: (no tests depend on float) Legend:
|
ShaharNaveh
left a comment
There was a problem hiding this comment.
overall lgtm!
I had a minor nitpick
tysm for the patches:)
| let pow1 = 10.0f64.powi(-ndigits); | ||
| let y = x / pow1; | ||
| let z = y.round(); | ||
| #[allow(clippy::float_cmp)] |
There was a problem hiding this comment.
| #[allow(clippy::float_cmp)] | |
| #[expect(clippy::float_cmp, reason = "...")] |
Can you please change it to expect and add a reason to this cfg?
There was a problem hiding this comment.
good catch i'll keep this convention following prs too thanks !
Co-authored-by: ShaharNaveh <[email protected]>
thanks for the quick review !! |
Summary
CPython's
float.__round__(Objects/floatobject.c) routes through_Py_dg_dtoaand rounds at the decimal level. The previousround_float_digitsmultiplied by10**ndigitsand rounded at the IEEE 754 binary level, which diverges for values that aren't exactly representable.2.675stores as2.67499...; dtoa correctly rounds it down to2.67, but(2.675 * 100.0).round() / 100.0lands on2.68because the multiplication produces a phantom267.5tie that round-half-to-even snaps up.Rust's
{:.*}float formatting uses dtoa-style algorithms (Grisu3 + Dragon4 fallback) and matches CPython's_Py_dg_dtoabyte-for-byte. Replacing the multiply-then-round step withformat!("{:.*}", ndigits, x).parse()removes the entire phantom-tie class forndigits >= 0.For
ndigits < 0the existing divide-then-round path is preserved because dividing typical inputs by10**|ndigits|produces genuine half-integer ties rather than synthesizing them.Verification (CPython 3.14.4)
round(x)(different code path)Tests unmasked
Lib/test/test_float.py::RoundTestCase:test_matches_float_format— rounds againstfloat(format(x, '.{n}f'))across many values; this is exactly what the fix does internally.test_previous_round_bugs— explicit "round-half-even" cases (e.g.round(25.0, -1) == 20.0).Test runs
1573 tests pass, 0 regressions. All 188
extra_tests/snippets/*.pypass under the CI feature set (stdlib,importlib,stdio,encodings,sqlite,ssl-rustls,host_env).A regression block is added to
extra_tests/snippets/builtin_round.py.Summary by CodeRabbit
Bug Fixes
Tests