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'
|
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)
#