This object stitches together the Babel number formating and the Decimal object, with a little of my own hand rolled validation for parsing.
Note the comment at the end of the code. It contains two lines to add to your settings.py.
CURRENCY_LANGUAGE_CODE = 'pt_BR'
CURRENCY_CODE = '' # If one exists like 'USD', 'EUR'
UPDATE 06-03-2009: Now with rounding
UPDATE 07-14-2009: Now with - More graceful handling of missing settings variables - Support for negatives (small oversight) - More flexible format strings - Thorough doctest tests
UPDATE 07-30-2009: Added the parse_string argument to the __new__()
method. This fixes a bug when importing data using manage.py loaddata
. I have not yet updated the tests to reflect the change.
The rest of the series: Currency Widget, Currency Form Field, Currency DB Field, Admin Integration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | # Currency.py
"""
Currency = Decimal + Babel
>>> Currency()
Decimal("0.00")
"""
from decimal import *
from babel.numbers import format_decimal, format_currency, parse_decimal, parse_number, get_decimal_symbol, get_group_symbol, get_currency_symbol, NumberFormatError
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
default_error_messages = {
'decimal_symbol': _(u'Ensure that there is only one decimal symbol (%s).'),
'invalid_format': _(u'Invalid currency format. Please use the format 9%s999%s00')
}
TWOPLACES = Decimal(10) ** -2
def _getSymbols(value):
retVal = ''
for x in value:
if x < u'0' or x > u'9':
retVal += x
return retVal
def _getCodes():
l_currency_language_code = 'en_US'
l_currency_code = 'USD'
try:
l_currency_language_code = settings.CURRENCY_LANGUAGE_CODE
l_currency_code = ''
except AttributeError:
pass
try:
l_currency_code = settings.CURRENCY_CODE
except AttributeError:
pass
return (l_currency_language_code, l_currency_code)
def parse_value(value):
"""
Accepts a string value and attempts to parce it as a currency value.
Returns the extracted numeric value converted to a string
"""
l_currency_language_code, l_currency_code = _getCodes()
curSym = get_currency_symbol(l_currency_code, l_currency_language_code)
grpSym = get_group_symbol(locale=l_currency_language_code.lower())
decSym = get_decimal_symbol(locale=l_currency_language_code.lower())
# Convert the Official characters into what comes from the keyboard.
# This section may need to grow over time.
# - Character 160 is a non-breaking space, which is different from a typed space
if ord(grpSym) == 160:
value = value.replace(u' ', grpSym)
allSym = _getSymbols(value)
invalidSym = allSym.replace(curSym, '').replace(grpSym, '').replace(decSym, '').replace(u'-', '')
value = value.replace(curSym, '')
if allSym.count(decSym) > 1:
raise NumberFormatError(default_error_messages['decimal_symbol'] % decSym)
elif (allSym.count(decSym) == 1 and allSym[-1] != decSym) or len(invalidSym) > 0:
raise NumberFormatError(default_error_messages['invalid_format'] % (grpSym, decSym))
elif value.count(decSym) == 1:
value = parse_decimal(value, locale=l_currency_language_code.lower())
else:
value = parse_number(value, locale=l_currency_language_code.lower())
# The value is converted into a string because the parse functions return floats
return str(value)
class Currency(Decimal):
"""
A Currency data type that extends the Decimal type and integrates the Bable libraries.
Accepts any numeric value or formated currency string as input.
Testing different numeric inputs and rounding
>>> Currency(.1)
Decimal("0.10")
>>> Currency(1)
Decimal("1.00")
>>> Currency(.015)
Decimal("0.02")
>>> Currency(.014)
Decimal("0.01")
Testing string input and format validation using en_US currency format
>>> import os
>>> os.environ['DJANGO_SETTINGS_MODULE'] = 'django.conf.global_settings'
>>> Currency("1")
Decimal("1.00")
>>> Currency("1,234.00")
Decimal("1234.00")
>>> Currency("1,234.0.0")
Traceback (most recent call last):
...
NumberFormatError: Ensure that there is only one decimal symbol (.).
>>> Currency("1,2,34.0")
Decimal("1234.00")
>>> Currency("1,234.00").format()
u'1,234.00'
>>> Currency("1,234.00").format_pretty()
u'$1,234.00'
>>> Currency("-1,234.00").format_pretty()
u'$-1,234.00'
>>> Currency("1 234.00")
Traceback (most recent call last):
...
NumberFormatError: Invalid currency format. Please use the format 9,999.00
>>> Currency("1.234,00")
Traceback (most recent call last):
...
NumberFormatError: Invalid currency format. Please use the format 9,999.00
>>> Currency("1.234")
Decimal("1.23")
>>> Currency("$1,234.00")
Decimal("1234.00")
>>> Currency("$1,234.00", format="#,##0").format()
u'1,234'
>>> Currency("$-1,234.00", format_pretty=u"#,##0 \xa4").format_pretty()
u'-1,234 $'
Testing string input and format validation using pt_BR currency format
>>> from django.conf import settings
>>> settings.CURRENCY_LANGUAGE_CODE = 'pt_BR'
>>> Currency("1 234.00")
Traceback (most recent call last):
...
NumberFormatError: Invalid currency format. Please use the format 9.999,00
>>> Currency("1.234")
Decimal("1234.00")
>>> Currency("1.234").format()
u'1.234,00'
"""
def __new__(cls, value="0", format=None, format_pretty=None, parse_string=False, context=None):
"""
Create a new Currency object
value: Can be any number (integer, decimal, or float) or a properly formated string
format: The format to use in the format() method
format_pretty: The format used in the format_pretty() method
parse_string: *IMPORTANT* Set this to True if you are passing a string formatted in a currency
other than the standard decimal format of #,###.##
context: How to handle a malformed string value
"""
if value != "0" and isinstance(value, basestring) and parse_string:
value = parse_value(value)
elif isinstance(value, float):
value = str(value)
if format:
cls._format = format
else:
cls._format = '#,##0.00;-#'
if format_pretty:
cls._formatPretty = format_pretty
else:
cls._formatPretty = u'\xa4#,##0.00;\xa4-#'
ld_rounded = Decimal(value).quantize(TWOPLACES, ROUND_HALF_UP)
return super(Currency, cls).__new__(cls, value=ld_rounded, context=context)
def format(self):
l_currency_language_code, l_currency_code = _getCodes()
return format_decimal(self, format=self._format, locale=l_currency_language_code)
def format_pretty(self):
l_currency_language_code, l_currency_code = _getCodes()
return format_currency(self, l_currency_code, format=self._formatPretty, locale=l_currency_language_code)
def _test():
import doctest
doctest.testmod()
if __name__ == "__main__":
_test()
# Additions to Setting.py
#CURRENCY_LANGUAGE_CODE = 'pt_BR'
#CURRENCY_CODE = '' # If one exists like 'USD', 'EUR'
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 3 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
Fixed a bug that I "fixed" (created) when posting. Lesson: Don't fix what isn't broke! I'll learn it some day.
The parce function now properly converts the floats returned from the Babel parce functions to strings before in turn turning them into Decimals.
#
Now with rounding
#
Now more gracefully handles using the object before adding the settings variables.
#
Now with: - Support for negatives (small oversight) - More flexible format strings (and working currency (pretty) formatting) - Thorough doctest tests (where I found and fixed the for mentioned issues)
#
Please login first before commenting.