Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pythonic way to ROUND_HALF_UP float with an arbitrary precision

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"...

like image 215
Piotr Siejda Avatar asked Oct 22 '25 03:10

Piotr Siejda


1 Answers

As you said that doesn't work if you try to quantize with numbers greater than 1:

>>> Decimal('1.5').quantize(Decimal('10'))
Decimal('2')
>>> Decimal('1.5').quantize(Decimal('100'))
Decimal('2')

But you can simply divide, quantize and multiply:

from decimal import Decimal, ROUND_HALF_UP

def round_my(num, precision):
    N_PLACES = Decimal(10) ** precision
    # Round to n places
    return (Decimal(num) / N_PLACES).quantize(1, ROUND_HALF_UP) * N_PLACES

However that only passes the tests if you input Decimal and compare to Decimal:

assert round_my('1.53', -1) == Decimal('1.5')
assert round_my('1.55', -1) == Decimal('1.6')
assert round_my('1.63', -1) == Decimal('1.6')
assert round_my('1.65', -1) == Decimal('1.7')
assert round_my('1.53', -2) == Decimal('1.53')
assert round_my('1.53', -3) == Decimal('1.53')
assert round_my('1.53',  0) == Decimal('2')
assert round_my('1.53',  1) == Decimal('0')
assert round_my('15.3',  1) == Decimal('20')
assert round_my('157.3',  2) == Decimal('200')

As noted in the comments it's possible to use scientific notation decimals as "working" quantize arguments, which simplifies the function:

def round_my(num, precision):
    quant_level = Decimal('1e{}'.format(precision))
    return Decimal(num).quantize(quant_level, ROUND_HALF_UP) 

This also passes the test cases mentioned above.

like image 194
MSeifert Avatar answered Oct 23 '25 16:10

MSeifert



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!