Recipe3.13.Formatting Decimals as Currency


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='$', sep=',', dp='.', pos='', neg='-',              overall=10): ...

If you 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.



Python Cookbook
Python Cookbook
ISBN: 0596007973
EAN: 2147483647
Year: 2004
Pages: 420

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net