From 43d7c71a6836062b72cb1f2d8f24ef10c5bf17dd Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Wed, 7 Feb 2024 14:19:17 -0500 Subject: [PATCH] Fix integer rounding The [docs](https://docs.python.org/3/library/functions.html#round) specify that calling `round` with a negative precision removes significant digits, so that `round(12345, -2) == 12300`. The implementation was simply returning the original integer. Additionally, `round(a, b)` is implemented as `(a / 10^b) * 10^b`, using half-even rounding during the division. --- Lib/test/test_long.py | 2 -- vm/src/builtins/int.rs | 43 +++++++++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_long.py b/Lib/test/test_long.py index a25eff5b0..6bbb44a69 100644 --- a/Lib/test/test_long.py +++ b/Lib/test/test_long.py @@ -1141,8 +1141,6 @@ class LongTest(unittest.TestCase): self.assertEqual((a ^ 63).bit_count(), 7) self.assertEqual(((a - 1) ^ 510).bit_count(), exp - 8) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_round(self): # check round-half-even algorithm. For round to nearest ten; # rounding map is invariant under adding multiples of 20 diff --git a/vm/src/builtins/int.rs b/vm/src/builtins/int.rs index 9b25a504f..9d5463a46 100644 --- a/vm/src/builtins/int.rs +++ b/vm/src/builtins/int.rs @@ -502,19 +502,40 @@ impl PyInt { #[pymethod(magic)] fn round( zelf: PyRef, - precision: OptionalArg, + ndigits: OptionalArg, vm: &VirtualMachine, ) -> PyResult> { - match precision { - OptionalArg::Missing => (), - OptionalArg::Present(ref value) => { - // Only accept int type ndigits - let _ndigits = value.payload_if_subclass::(vm).ok_or_else(|| { - vm.new_type_error(format!( - "'{}' object cannot be interpreted as an integer", - value.class().name() - )) - })?; + if let OptionalArg::Present(ndigits) = ndigits { + let ndigits = ndigits.as_bigint(); + // round(12345, -2) == 12300 + // If precision >= 0, then any integer is already rounded correctly + if let Some(ndigits) = ndigits.neg().to_u32() { + if ndigits > 0 { + // Work with positive integers and negate at the end if necessary + let sign = if zelf.value.is_negative() { + BigInt::from(-1) + } else { + BigInt::from(1) + }; + let value = zelf.value.abs(); + + // Divide and multiply by the power of 10 to get the approximate answer + let pow10 = BigInt::from(10).pow(ndigits); + let quotient = &value / &pow10; + let rounded = "ient * &pow10; + + // Malachite division uses floor rounding, Python uses half-even + let remainder = &value - &rounded; + let halfpow10 = &pow10 / BigInt::from(2); + let correction = + if remainder > halfpow10 || (remainder == halfpow10 && quotient.is_odd()) { + pow10 + } else { + BigInt::from(0) + }; + let rounded = (rounded + correction) * sign; + return Ok(vm.ctx.new_int(rounded)); + } } } Ok(zelf)