GeezerVison is showing github.com — Round float at the decimal level to match CPython's _Py_dg_dtoa · RustPython/RustPython@2310495
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: Lib/test/test_float.py -2Lines changed: 0 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -965,7 +965,6 @@ def test_overflow(self): | 965 | 965 | self.assertRaises(OverflowError, round, 1.6e308, -308) | 966 | 966 | self.assertRaises(OverflowError, round, -1.7e308, -308) | 967 | 967 |
| 968 | | - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 56294995342131.51 != 56294995342131.5 | 969 | 968 | @unittest.skipUnless(getattr(sys, 'float_repr_style', '') == 'short', | 970 | 969 | "applies only when using short float repr style") | 971 | 970 | def test_previous_round_bugs(self): | @@ -984,7 +983,6 @@ def test_previous_round_bugs(self): | 984 | 983 | self.assertEqual(round(85.0, -1), 80.0) | 985 | 984 | self.assertEqual(round(95.0, -1), 100.0) | 986 | 985 |
| 987 | | - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0.01 != 0.0 | 988 | 986 | @unittest.skipUnless(getattr(sys, 'float_repr_style', '') == 'short', | 989 | 987 | "applies only when using short float repr style") | 990 | 988 | def test_matches_float_format(self): | |
Collapse file Copy file name to clipboardExpand all lines: crates/common/src/float_ops.rs +43-57Lines changed: 43 additions & 57 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | use core::f64; | 2 | 2 | use malachite_bigint::{BigInt, ToBigInt}; | 3 | | -use num_traits::{Float, Signed, ToPrimitive, Zero}; | | 3 | +use num_traits::{Signed, ToPrimitive}; | 4 | 4 |
| 5 | 5 | pub const fn decompose_float(value: f64) -> (f64, i32) { | 6 | 6 | if 0.0 == value { | @@ -208,64 +208,50 @@ pub fn ulp(x: f64) -> f64 { | 208 | 208 | } | 209 | 209 |
| 210 | 210 | pub fn round_float_digits(x: f64, ndigits: i32) -> Option<f64> { | 211 | | - let float = if ndigits.is_zero() { | 212 | | - let fract = x.fract(); | 213 | | - if (fract.abs() - 0.5).abs() < f64::EPSILON { | 214 | | - if x.trunc() % 2.0 == 0.0 { | 215 | | - x - fract | 216 | | - } else { | 217 | | - x + fract | 218 | | - } | 219 | | - } else { | 220 | | - x.round() | 221 | | - } | 222 | | - } else { | 223 | | - const NDIGITS_MAX: i32 = | 224 | | - ((f64::MANTISSA_DIGITS as i32 - f64::MIN_EXP) as f64 * f64::consts::LOG10_2) as i32; | 225 | | - const NDIGITS_MIN: i32 = -(((f64::MAX_EXP + 1) as f64 * f64::consts::LOG10_2) as i32); | 226 | | - if ndigits > NDIGITS_MAX { | 227 | | - x | 228 | | - } else if ndigits < NDIGITS_MIN { | 229 | | - 0.0f64.copysign(x) | 230 | | - } else { | 231 | | - let (y, pow1, pow2) = if ndigits >= 0 { | 232 | | - // according to cpython: pow1 and pow2 are each safe from overflow, but | 233 | | - // pow1*pow2 ~= pow(10.0, ndigits) might overflow | 234 | | - let (pow1, pow2) = if ndigits > 22 { | 235 | | - (10.0.powf((ndigits - 22) as f64), 1e22) | 236 | | - } else { | 237 | | - (10.0.powf(ndigits as f64), 1.0) | 238 | | - }; | 239 | | - let y = (x * pow1) * pow2; | 240 | | - if !y.is_finite() { | 241 | | - return Some(x); | 242 | | - } | 243 | | - (y, pow1, Some(pow2)) | 244 | | - } else { | 245 | | - let pow1 = 10.0.powf((-ndigits) as f64); | 246 | | - (x / pow1, pow1, None) | 247 | | - }; | 248 | | - let z = y.round(); | 249 | | - #[allow(clippy::float_cmp)] | 250 | | - let z = if (y - z).abs() == 0.5 { | 251 | | - 2.0 * (y / 2.0).round() | 252 | | - } else { | 253 | | - z | 254 | | - }; | 255 | | - let z = if let Some(pow2) = pow2 { | 256 | | - // ndigits >= 0 | 257 | | - (z / pow2) / pow1 | 258 | | - } else { | 259 | | - z * pow1 | 260 | | - }; | | 211 | + // Mirror CPython's `float.__round__` (Objects/floatobject.c), which uses | | 212 | + // `_Py_dg_dtoa` to round at the decimal level. Multiplying by 10**ndigits | | 213 | + // and rounding at the IEEE 754 binary level diverges for values that | | 214 | + // aren't exactly representable: 2.675 stores as 2.67499..., which dtoa | | 215 | + // correctly rounds down to 2.67, but `(2.675 * 100.0).round() / 100.0` | | 216 | + // lands on 2.68 because the multiplication produces a phantom 267.5 tie. | | 217 | + // Rust's `{:.*}` float formatting uses dtoa-style algorithms and matches | | 218 | + // CPython's `_Py_dg_dtoa` byte-for-byte. | | 219 | + if !x.is_finite() { | | 220 | + return Some(x); | | 221 | + } | 261 | 222 |
| 262 | | - if !z.is_finite() { | 263 | | - // overflow | 264 | | - return None; | 265 | | - } | | 223 | + const NDIGITS_MAX: i32 = | | 224 | + ((f64::MANTISSA_DIGITS as i32 - f64::MIN_EXP) as f64 * f64::consts::LOG10_2) as i32; | | 225 | + const NDIGITS_MIN: i32 = -(((f64::MAX_EXP + 1) as f64 * f64::consts::LOG10_2) as i32); | | 226 | + | | 227 | + if ndigits > NDIGITS_MAX { | | 228 | + return Some(x); | | 229 | + } | | 230 | + if ndigits < NDIGITS_MIN { | | 231 | + return Some(0.0f64.copysign(x)); | | 232 | + } | 266 | 233 |
| | 234 | + let result: f64 = if ndigits >= 0 { | | 235 | + let s = format!("{:.*}", ndigits as usize, x); | | 236 | + s.parse().ok()? | | 237 | + } else { | | 238 | + // ndigits < 0: divide-then-round avoids the phantom-tie problem | | 239 | + // because dividing typical inputs by 10**|ndigits| produces genuine | | 240 | + // half-integer ties rather than synthesizing them. | | 241 | + let pow1 = 10.0f64.powi(-ndigits); | | 242 | + let y = x / pow1; | | 243 | + let z = y.round(); | | 244 | + #[allow(clippy::float_cmp)] | | 245 | + let z = if (y - z).abs() == 0.5 { | | 246 | + 2.0 * (y / 2.0).round() | | 247 | + } else { | 267 | 248 | z | 268 | | - } | | 249 | + }; | | 250 | + z * pow1 | 269 | 251 | }; | 270 | | - Some(float) | | 252 | + | | 253 | + if !result.is_finite() { | | 254 | + return None; | | 255 | + } | | 256 | + Some(result) | 271 | 257 | } |
Collapse file Copy file name to clipboardExpand all lines: extra_tests/snippets/builtin_round.py +50Lines changed: 50 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import math | | 2 | + | 1 | 3 | from testutils import assert_raises | 2 | 4 |
| 3 | 5 | assert round(1.2) == 1 | @@ -43,3 +45,51 @@ def __round__(self, ndigits=None): | 43 | 45 | assert round(X(), 1) == 1.1 | 44 | 46 | assert round(X(), None) == 1.1 | 45 | 47 | assert round(X()) == 1.1 | | 48 | + | | 49 | + | | 50 | +# Banker's rounding at the decimal level: values like 2.675 store as | | 51 | +# 2.67499... in IEEE 754, so a multiply-then-round implementation creates | | 52 | +# a phantom 267.5 tie that round-half-to-even snaps up to 2.68. CPython's | | 53 | +# `_Py_dg_dtoa` rounds at the decimal level and returns 2.67. | | 54 | +assert round(2.675, 2) == 2.67 | | 55 | +assert round(2.685, 2) == 2.69 | | 56 | +assert round(-2.675, 2) == -2.67 | | 57 | +assert round(0.05, 1) == 0.1 | | 58 | +assert round(0.15, 1) == 0.1 | | 59 | +assert round(0.35, 1) == 0.3 | | 60 | +assert round(0.45, 1) == 0.5 | | 61 | +assert round(0.65, 1) == 0.7 | | 62 | +assert round(0.85, 1) == 0.8 | | 63 | +assert round(0.95, 1) == 0.9 | | 64 | +assert round(0.645, 2) == 0.65 | | 65 | +assert round(0.665, 2) == 0.67 | | 66 | +assert round(0.685, 2) == 0.69 | | 67 | +assert round(0.695, 2) == 0.69 | | 68 | +assert round(1.685, 2) == 1.69 | | 69 | +assert round(3.745, 2) == 3.75 | | 70 | + | | 71 | +# Exact-halfway ties at integer level use banker's (round-half-to-even). | | 72 | +assert round(0.5, 0) == 0.0 | | 73 | +assert round(1.5, 0) == 2.0 | | 74 | +assert round(2.5, 0) == 2.0 | | 75 | + | | 76 | +# Negative ndigits uses divide-then-round; banker's still applies. | | 77 | +assert round(1235, -1) == 1240 | | 78 | +assert round(1245, -1) == 1240 | | 79 | +assert round(1234.5, -1) == 1230.0 | | 80 | +assert round(150, -2) == 200 | | 81 | +assert round(250, -2) == 200 | | 82 | + | | 83 | +# NaN and infinities round to themselves with no error when ndigits is given. | | 84 | +assert math.isnan(round(float("nan"), 2)) | | 85 | +assert round(float("inf"), 2) == float("inf") | | 86 | +assert round(float("-inf"), 2) == float("-inf") | | 87 | + | | 88 | +# Signed zero is preserved through rounding. | | 89 | +assert math.copysign(1.0, round(-0.0, 2)) == -1.0 | | 90 | +assert math.copysign(1.0, round(0.0, 2)) == 1.0 | | 91 | + | | 92 | +# Out-of-range ndigits short-circuits without overflow. | | 93 | +assert round(1.0, 1000) == 1.0 | | 94 | +assert round(1.0, -1000) == 0.0 | | 95 | +assert round(1.7976931348623157e308, 0) == 1.7976931348623157e308 |
|