Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python ROUND_05UP

I've got a system that handles account & COD transactions. When a client is paying via COD, I need to round up or round down the cents.

Eg.
Total = 64.42 - rounds to: 64.40
Total = 64.57 - rounds to 64.60

I'm trying to figure out the best way to do this in python and also store the difference in a decimal field in a database.

I'm using DJANGO. I was trying to use the ROUND_05UP part of decimal but I'm not sure how to do it. I wrote a custom filter to display it, but that's not even working:

@register.filter
def currency(value, arg='en_US', symbol=True):

    saved = '.'.join([x for x in locale.getlocale() if x]) or (None, None)
    given = arg and ('.' in arg and str(arg) or str(arg) + '.UTF-8')

    # Workaround for Python bug 1699853 and other possibly related bugs.
    if '.' in saved and saved.split('.')[1].lower() in ('utf', 'utf8'):
        saved = saved.split('.')[0] + '.UTF-8'

    if saved == (None, None) and given == '':
        given = 'en_US.UTF-8'

    try:
        locale.setlocale(locale.LC_ALL, given)

        return locale.currency(value or 0, symbol, True)

    except (TypeError, locale.Error):
        return ''

    finally:
        locale.setlocale(locale.LC_ALL, saved)


@register.filter
def currency_cash(value):
    value = decimal.Decimal(value)
    return currency(value.quantize(Decimal(10) ** -2, rounding=decimal.ROUND_05UP))

Does anyone have a function written that does this?

UPDATE: The reason for this in australia we have no coins less then 5 cents. So 45.55 would stay as 45.55.

Cheers, Ben

like image 276
Ben Kilah Avatar asked May 20 '26 21:05

Ben Kilah


2 Answers

This type of rounding you are talking about is called Swedish rounding:

  • 1 cent rounds down to 0
  • 2 cents round down to 0
  • 3 cents round up to 5 cents
  • 4 cents round up to 5 cents

and so on.

from decimal import Decimal as D, ROUND_HALF_EVEN  # or ROUND_HALF_UP

def round_to_5_cents(d):
    """
    Round a Decimal value to the nearest multiple of 0.05,
    """
    return (d*2).quantize(D('0.1'), ROUND_HALF_EVEN)/D(2)

for x in range(80, 120):
    d = D(x) * D('0.01')
    print "{0} rounds to {1}".format(d, round_to_5_cents(d))

Sample output:

0.80 rounds to 0.8
0.81 rounds to 0.8
0.82 rounds to 0.8
0.83 rounds to 0.85
0.84 rounds to 0.85
0.85 rounds to 0.85
0.86 rounds to 0.85
0.87 rounds to 0.85
0.88 rounds to 0.9
0.89 rounds to 0.9
0.90 rounds to 0.9
0.91 rounds to 0.9
0.92 rounds to 0.9
0.93 rounds to 0.95
0.94 rounds to 0.95
0.95 rounds to 0.95
0.96 rounds to 0.95
0.97 rounds to 0.95
0.98 rounds to 1.0
0.99 rounds to 1.0
1.00 rounds to 1.0
1.01 rounds to 1.0
1.02 rounds to 1.0
1.03 rounds to 1.05
1.04 rounds to 1.05
1.05 rounds to 1.05
1.06 rounds to 1.05
1.07 rounds to 1.05
1.08 rounds to 1.1
1.09 rounds to 1.1
1.10 rounds to 1.1
1.11 rounds to 1.1
1.12 rounds to 1.1
1.13 rounds to 1.15
1.14 rounds to 1.15
1.15 rounds to 1.15
1.16 rounds to 1.15
1.17 rounds to 1.15
1.18 rounds to 1.2
1.19 rounds to 1.2

@Mark Dickingson's answer does a different thing. It leads to larger accumulated rounding error.

like image 79
Antony Hatchkins Avatar answered May 22 '26 11:05

Antony Hatchkins


If I understand correctly, the incoming amount is already an exact integer number of cents, and you just want to round to the nearest multiple of 10 cents, but leaving values that already end in 5 as they are. Is that correct? Here's a simple solution using the remainder_near method.

from decimal import Decimal

def round_to_10_cents(x):
    """
    Round a Decimal value to the nearest multiple of 0.10,
    unless already an exact multiple of 0.05.

    """
    remainder = x.remainder_near(Decimal('0.10'))
    if abs(remainder) == Decimal('0.05'):
        return x
    else:
        return x - remainder


# Test code.
for x in range(80, 120):
    y = Decimal(x) / Decimal('1E2')
    print "{0} rounds to {1}".format(y, round_to_10_cents(y))

Sample output:

0.80 rounds to 0.80
0.81 rounds to 0.80
0.82 rounds to 0.80
0.83 rounds to 0.80
0.84 rounds to 0.80
0.85 rounds to 0.85
0.86 rounds to 0.90
0.87 rounds to 0.90
0.88 rounds to 0.90
0.89 rounds to 0.90
0.90 rounds to 0.90
0.91 rounds to 0.90
0.92 rounds to 0.90
0.93 rounds to 0.90
0.94 rounds to 0.90
0.95 rounds to 0.95
0.96 rounds to 1.00
0.97 rounds to 1.00
0.98 rounds to 1.00
0.99 rounds to 1.00
1.00 rounds to 1.00
1.01 rounds to 1.00
1.02 rounds to 1.00
1.03 rounds to 1.00
1.04 rounds to 1.00
1.05 rounds to 1.05
1.06 rounds to 1.10
1.07 rounds to 1.10
1.08 rounds to 1.10
1.09 rounds to 1.10
1.10 rounds to 1.10
1.11 rounds to 1.10
1.12 rounds to 1.10
1.13 rounds to 1.10
1.14 rounds to 1.10
1.15 rounds to 1.15
1.16 rounds to 1.20
1.17 rounds to 1.20
1.18 rounds to 1.20
1.19 rounds to 1.20
like image 35
Mark Dickinson Avatar answered May 22 '26 11:05

Mark Dickinson