Pythonic way to ROUND_HALF_UP float with an arbitrary precision

Viewed 1561

First of all I would like to mention that this question is not a duplicate of:

Python Rounding Inconsistently

Python 3.x rounding behavior

I know about IEEE 754 and I know that:

The simple "always round 0.5 up" technique results in a slight bias toward the higher number. With large numbers of calculations, this can be significant. The Python 3.0 approach eliminates this issue.

I agree that ROUND_HALF_UP is inferior method to the one implemented by default in Python. Nevertheless there are people who do not know that and one needs to use that method if the specs require that. Easy way to make this work is:

def round_my(num, precission):
    exp  = 2*10**(-precission)
    temp = num * exp
    if temp%2 < 1:
        return int(temp - temp%2)/exp
    else:
        return int(temp - temp%2 + 2)/exp

But my consideration is that this is not Pythonic... According to the docs I should use something like:

def round_my(num, pricission):
    N_PLACES = Decimal(10) ** pricission       # same as Decimal('0.01')
    # Round to n places
    Decimal(num).quantize(N_PLACES)

The problem is that this would not pass all test cases:

class myRound(unittest.TestCase):
    def test_1(self):
        self.assertEqual(piotrSQL.round_my(1.53, -1), 1.5)
        self.assertEqual(piotrSQL.round_my(1.55, -1), 1.6)
        self.assertEqual(piotrSQL.round_my(1.63, -1), 1.6)
        self.assertEqual(piotrSQL.round_my(1.65, -1), 1.7)
        self.assertEqual(piotrSQL.round_my(1.53, -2), 1.53)
        self.assertEqual(piotrSQL.round_my(1.53, -3), 1.53)
        self.assertEqual(piotrSQL.round_my(1.53,  0), 2)
        self.assertEqual(piotrSQL.round_my(1.53,  1), 0)
        self.assertEqual(piotrSQL.round_my(15.3,  1), 20)
        self.assertEqual(piotrSQL.round_my(157.3,  2), 200)

Because of the nature of conversion between float and decimal and because quantize does not seem to be working for exponents like 10 or 100. Is there a Pythonic way to do this?

And I know that I could just add infinitesimally small number and round(num+10**(precission-20),-pricission) would work but this is so wrong that "the puppies would die"...

2 Answers

Here's a version that matches the behavior and API of the builtin round() function:

from decimal import Decimal, ROUND_HALF_UP

def round_half_up(number, ndigits=None):
    return_type = type(number)
    if ndigits is None:
        ndigits = 0
        return_type = int
    if not isinstance(ndigits, int):
        msg = f"'{type(ndigits).__name__}' object cannot be interpreted as an integer"
        raise TypeError(msg)
    quant_level = Decimal(f"10E{-ndigits}")
    return return_type(Decimal(number).quantize(quant_level, ROUND_HALF_UP))
Related