4

I'm trying to round money numbers in Decimal to the nearest 0.05. Right now, I'm doing this:

def round_down(amount):
    amount *= 100
    amount = (amount - amount % 5) / Decimal(100)
    return Decimal(amount)

def round_up(amount):
    amount = int(math.ceil(float(100 * amount) / 5)) * 5 / Decimal(100)
    return Decimal(amount)

Is there any way I can do this more elegantly without dealing with floats using python Decimals (using quantize perhaps)?

gurch101
  • 2,064
  • 1
  • 18
  • 18
  • why not use the round in python? – Seek Addo Jun 15 '16 at 04:05
  • @SeekAddo. How would I round to the nearest 0.05? also I need to control rounding direction (ie I need both round up/round down). – gurch101 Jun 15 '16 at 04:22
  • def round_num(amount): print(round(amount,2)) round_num(23.045954) will give you 23.05, let me ask, how do you want the round down to be like @gurch101 – Seek Addo Jun 15 '16 at 04:33
  • @SeekAddo I'm pretty sure the above two functions work as I would expect. Just looking for a more elegant solution. – gurch101 Jun 15 '16 at 05:03

3 Answers3

12

With floats, simply use round(x * 2, 1) / 2. This doesn't give control over the rounding direction, though.

Using Decimal.quantize you also get complete control over the type and direction of rounding (Python 3.5.1):

>>> from decimal import Decimal, ROUND_UP

>>> x = Decimal("3.426")
>>> (x * 2).quantize(Decimal('.1'), rounding=ROUND_UP) / 2
Decimal('3.45')

>>> x = Decimal("3.456")
>>> (x * 2).quantize(Decimal('.1'), rounding=ROUND_UP) / 2
Decimal('3.5')
taleinat
  • 8,441
  • 1
  • 30
  • 44
1

A more generic solution for any rounding base.

from decimal import ROUND_DOWN
def round_decimal(decimal_number, base=1, rounding=ROUND_DOWN):
    """
    Round decimal number to the nearest base

    :param decimal_number: decimal number to round to the nearest base
    :type decimal_number: Decimal
    :param base: rounding base, e.g. 5, Decimal('0.05')
    :type base: int or Decimal
    :param rounding: Decimal rounding type
    :rtype: Decimal
    """
    return base * (decimal_number / base).quantize(1, rounding=rounding)

Examples:

>>> from decimal import Decimal, ROUND_UP

>>> round_decimal(Decimal('123.34'), base=5)
Decimal('120')
>>> round_decimal(Decimal('123.34'), base=6, rounding=ROUND_UP)
Decimal('126')
>>> round_decimal(Decimal('123.34'), base=Decimal('0.05'))
Decimal('123.30')
>>> round_decimal(Decimal('123.34'), base=Decimal('0.5'), rounding=ROUND_UP)
Decimal('123.5')
t4z
  • 11
  • 2
  • If I call `round_decimal(Decimal('123.36'), base=Decimal('0.09'))`, I get `Decimal('123.30')`. Shouldn't I get `Decimal('123.29')`? I may have missed something. – Guillaume Ansanay-Alex Apr 09 '21 at 14:29
  • With decimal bases it works as if you would first multiple the decimal_number and base by the least significant digit "multiplier" of the base. `base = 0.09` `decimal_number = 123.36` `multiplier = 100` then `base = 9` `decimal_numer = 12336` `12336 / 9 = 1370.666666666666667` `1370 * 9 = 12330` `12330 / 100 = 123.30` – t4z Apr 09 '21 at 15:45
0

First note this problem (unexpected rounding down) only sometimes occurs when the digit immediately inferior (to the left of) the digit you're rounding to has a 5.

i.e.

>>> round(1.0005,3)
1.0
>>> round(2.0005,3)
2.001
>>> round(3.0005,3)
3.001
>>> round(4.0005,3)
4.0
>>> round(1.005,2)
1.0
>>> round(5.005,2)
5.0
>>> round(6.005,2)
6.0
>>> round(7.005,2)
7.0
>>> round(3.005,2)
3.0
>>> round(8.005,2)
8.01

But there's an easy solution, I've found that seems to always work, and which doesn't rely upon the import of additional libraries. The solution is to add a 1e-X where X is the length of the number string you're trying to use round on plus 1.

>>> round(0.075,2)

0.07

>>> round(0.075+10**(-2*6),2)

0.08

Aha! So based on this we can make a handy wrapper function, which is standalone and does not need additional import calls...

def roundTraditional(val,digits):
   return round(val+10**(-len(str(val))-1))

Basically this adds a value guaranteed to be smaller than the least given digit of the string you're trying to use round on. By adding that small quantity it preserve's round's behavior in most cases, while now ensuring if the digit inferior to the one being rounded to is 5 it rounds up, and if it is 4 it rounds down.

The approach of using 10**(-len(val)-1) was deliberate, as it the largest small number you can add to force the shift, while also ensuring that the value you add never changes the rounding even if the decimal . is missing. I could use just 10**(-len(val)) with a condiditional if (val>1) to subtract 1 more... but it's simpler to just always subtract the 1 as that won't change much the applicable range of decimal numbers this workaround can properly handle. This approach will fail if your values reaches the limits of the type, this will fail, but for nearly the entire range of valid decimal values it should work.

You can also use the decimal library to accomplish this, but the wrapper I propose is simpler and may be preferred in some cases.


Edit: Thanks Blckknght for pointing out that the 5 fringe case occurs only for certain values here.

Community
  • 1
  • 1
Jason R. Mick
  • 5,177
  • 4
  • 40
  • 69