Login

Currency Object

Author:
Rupe
Posted:
May 25, 2009
Language:
Python
Version:
1.0
Score:
0 (after 0 ratings)

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

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 3 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

Rupe (on May 28, 2009):

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.

#

Rupe (on June 3, 2009):

Now with rounding

#

Rupe (on July 14, 2009):

Now more gracefully handles using the object before adding the settings variables.

#

Rupe (on July 14, 2009):

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.