Recipe 3.13. Formatting Decimals as Currency
Credit: Anna Martelli Ravenscroft, Alex Martelli, Raymond
Hettinger
Problem
You want to do some tax calculations and display the result in a
simple report as Euro currency.
Solution
Use the
new decimal module, along with a modified
moneyfmt function (the original, by Raymond
Hettinger, is part of the Python library reference section about
decimal):
import decimal
"" calculate Italian invoice taxes given a subtotal. ""
def italformat(value, places=2, curr='EUR',
sep='.', dp=',', pos='', neg='-',
overall=10):
"" Convert Decimal ``value'' to a money-formatted string.
places: required number of places after the decimal point
curr: optional currency symbol before the sign (may be blank)
sep: optional grouping separator (comma, period, or blank) every 3
dp: decimal point indicator (comma or period); only specify as
blank when places is zero
pos: optional sign for positive numbers: "+", space or blank
neg: optional sign for negative numbers: "-", "(", space or blank
overall: optional overall length of result, adds padding on the
left, between the currency symbol and digits
""
q = decimal.Decimal((0, (1,), -places)) # 2 places --> '0.01'
sign, digits, exp = value.quantize(q).as_tuple( )
result = [ ]
digits = map(str, digits)
append, next = result.append, digits.pop
for i in range(places):
if digits:
append(next( ))
else:
append('0')
append(dp)
i = 0
while digits:
append(next( ))
i += 1
if i == 3 and digits:
i = 0
append(sep)
while len(result) < overall:
append(' ')
append(curr)
if sign: append(neg)
else: append(pos)
result.reverse( )
return ''.join(result)
# get the subtotal for use in calculations
def getsubtotal(subtin=None):
if subtin == None:
subtin = input("Enter the subtotal: ")
subtotal = decimal.Decimal(str(subtin))
print "\n subtotal: ", italformat(subtotal)
return subtotal
# specific Italian tax law functions
def cnpcalc(subtotal):
contrib = subtotal * decimal.Decimal('.02')
print "+ contributo integrativo 2%: ", italformat(contrib, curr='')
return contrib
def vatcalc(subtotal, cnp):
vat = (subtotal+cnp) * decimal.Decimal('.20')
print "+ IVA 20%: ", italformat(vat, curr='')
return vat
def ritacalc(subtotal):
rit = subtotal * decimal.Decimal('.20')
print "-Ritenuta d'acconto 20%: ", italformat(rit, curr='')
return rit
def dototal(subtotal, cnp, iva=0, rit=0):
totl = (subtotal+cnp+iva)-rit
print " TOTALE: ", italformat(totl)
return totl
# overall calculations report
def invoicer(subtotal=None, context=None):
if context is None:
decimal.getcontext( ).rounding="ROUND_HALF_UP"
# Euro rounding rules
else:
decimal.setcontext(context) # set to context arg
subtot = getsubtotal(subtotal)
contrib = cnpcalc(subtot)
dototal(subtot, contrib, vatcalc(subtot, contrib), ritacalc(subtot))
if _ _name_ _=='_ _main_ _':
print "Welcome to the invoice calculator"
tests = [100, 1000.00, "10000", 555.55]
print "Euro context"
for test in tests:
invoicer(test)
print "default context"
for test in tests:
invoicer(test, context=decimal.DefaultContext)
Discussion
Italian tax calculations are somewhat complicated, more so than this
recipe demonstrates. This recipe applies only to invoicing customers
within Italy. I soon got tired of doing them by hand, so I wrote a
simple Python script to do the calculations for me.
I've currently refactored into the version shown in
this recipe, using the new decimal module, just on
the principle that money computations should never, but
never, be done with binary floats.How to best use the new decimal module for
monetary calculations was not immediately obvious. While the decimal
arithmetic is pretty straightforward, the options for displaying
results were less clear. The italformat function in
the recipe is based on Raymond Hettinger's
moneyfmt recipe, found in the
decimal module documentation available in the
Python 2.4 Library Reference. Some minor
modifications were helpful for my reporting purposes. The primary
addition was the overall parameter. This parameter
builds a decimal with a specific number of overall digits, with
whitespace padding between the currency symbol (if any) and the
digits. This eases alignment issues when the results are of a
standard, predictable length.Notice that I have coerced the subtotal input
subtin to be a string in subtotal
= decimal.Decimal(str(subtin)). This
makes it possible to feed floats (as well as integers or strings) to
getsubtotal without worrywithout this, a
float would raise an exception. If your program is likely to pass
tuples, refactor the code to handle that. In my case, a float was a
rather likely input to getsubtotal, but I
didn't have to worry about tuples.Of course, if you need to display using U.S. $, or need to use other
rounding rules, it's easy enough to modify things to
suit your needs. For example, to display U.S. currency, you could
change the curr, sep, and
dp arguments' default values as
follows:
def USformat(value, places=2, curr='$',If you
sep=',', dp='.', pos='', neg='-',
overall=10):
...
regularly have to use multiple currency formats, you may choose to
refactor the function so that it looks up the appropriate arguments
in a dictionary, or you may want to find other ways to pass the
appropriate arguments. In theory, the locale
module in the Python Standard Library should be the standard way to
let your code access locale-related preferences such as those
connected to money formatting, but in practice I've
never had much luck using locale (for this or any
other purpose), so that's one task that
I'll gladly leave as an exercise to the reader.Countries often have specific rules on rounding;
decimal uses ROUND_HALF_EVEN as
the default. However, the Euro rules specify
ROUND_HALF_UP. To use different rounding rules,
change the context, as shown in the recipe. The result of this change
may or may not be obvious, but one should be aware that it
can make a (small, but legally not negligible)
difference.You can also change the context more extensively, by creating and
setting your own context class instance. A change in context, whether
set by a simple getcontext attribution change, or
with a custom context class instance passed to
setcontext(mycontext), continues to apply
throughout the active thread, until you change it. If you are
considering using decimal in production code (or
even for your own home bookkeeping use), be sure to use the right
context (in particular, the correct rounding rules) for your
country's accounting practices.
See Also
Python 2.4's Library
Reference on decimal, particularly the
section on decimal.context and the
"recipes" at the end of that
section.