a money object (was: I had a thought ...)

Sam Penrose sam at ddmweb.com
Thu May 31 12:29:12 EDT 2001


Here is a money object which mostly works. I had planned to put off
posting this until I had worked out the remaining issues and
written unittest.py-derived tests for it. Since the topic keeps coming
up I will offer it up to the tender mercies of the newsgroup.

Known issues:
1)  There is some repeated logic in __repr__ and printLegibly which
suggests some weaknesses in either __repr__ or in __init__'s call to
str().

2) It gets very confused if you attempt to multiply by a negative
number; haven't decided how to handle that. I suspect there are other
lurking arithmetic issues. Martin Fowler notes at
<http://www.martinfowler.com/ap2/quantity.html> that if you divide
$100.00 into three parts, you should get not ($33.33)*3 but ($33.33,
$33.33, and $33.34). This is not something you can shoehorn into
__div__.

3) The Continental subclass is probably misguided; decimalSeparator
presumably needs to remain '.' for all Python-compatible computers, you
would only want to use ',' in printLegibly.

4) printLegibly is just ugly.

There will be others; do your worst!

A note on motivation: my initial spur was MySQLdb.py's habit of
returning MySQL's decimal type as a string. I used UserList as a model,
so arithmetic methods (__add__, etc.) were a string of if-elif type
checking. Reading some of Alex Martelli's Python CookBook recipes, I
noticed a couple of anti-type checking comments and asked him about
them. His recommendation to use type coercion--convert whatever you are
adding/subtracting to an instance of self before performing the
operation--made the code simpler, clearer, and more robust.

class Currency:
    """Take a representation of decimal money, default '0.00', as:
    
          1) an integer or string containing an integer, representing
          monetary 'wholes' (e.g., dollars, francs) 
          2) a float or string containing a float, representing
          wholes.parts (e.g., dollars and cents, francs and centimes).
          
    and birth an object which 'knows' that it is money, meaning that you
    can do arithmetic on it without getting weird results due to computer
    handling of floating point values and that it will always print to
    the right number of decimal places.
    Examples:
         price = Currency(99.9)
         price = Currency('99.9')
         price = Currency('99.90')
         price = Currency('99.900')
    ...all create an equivalent object, representing the sum 99.90 in
    any currency whose smallest unit is .01 of a 'whole.'
         print price and str(price)
    print 99.90 and return '99.90', respectively.
         Currency(99), Currency('.75'), Currency()
    return objects equivalent to 99.00, 0.75, and 0.00, respectively.

    To manipulate a currency commonly represented to a different number
    of decimal places, subclass and override decimalPlaces accordingly."""
    decimalPlaces = 2
    decimalOffset = -decimalPlaces
    unitToFloatRatio = 10 ** decimalPlaces
    floatToUnitRatio = 1.0 / unitToFloatRatio
    decimalSeparator = '.'
    wholeSeparator = ','
    currencySymbol = ''
    def __init__(self, amount='0.00'):
        amount = float(str(amount)) #str() to handle Currency objects
        self.units = long(round(amount*self.unitToFloatRatio))

    def __repr__(self):
        """Return a decimal representation which makes sense to both
        computers (can be used to construct a new Currency instance,
        inserted into an SQL decimal field, etc.) and people."""
        if self.units >= 10: unitString = str(self.units)
        else: unitString = '0' + str(self.units)
        self.whole = unitString[:self.decimalOffset]
        self.fraction = unitString[self.decimalOffset:]
        return self.decimalSeparator.join([self.whole, self.fraction])

    def __add__(self, other):
        sum = self.__class__(other)
        sum.units += self.units
        return sum

    def __radd__(self, other):
        sum = self.__class__(other)
        sum.units += self.units
        return sum
    
    def __sub__(self, other):
        sum = self.__class__(other)
        difference = (self.units - sum.units) * self.floatToUnitRatio
        return self.__class__(difference)
    
    def __rsub__(self, other):
        sum = self.__class__(other)
        difference = (sum.units - self.units) * self.floatToUnitRatio
        return self.__class__(difference)
    
    def __cmp__(self, other):
        sum = self.__class__(other)
        difference = self.units - sum.units
        if difference == 0: return 0
        elif difference > 0: return 1
        elif difference < 0: return -1
    
    def __mul__(self, n):
        product = (self.units * n) * self.floatToUnitRatio
        return self.__class__(product)
    
    def __rmul__(self, n):
        product = (n * self.units) * self.floatToUnitRatio
        return self.__class__(product)
    
    def __div__(self, n):
        product = (self.units / n) * self.floatToUnitRatio
        return self.__class__(product)

    def printLegibly(self):
        """Return a string optimized for human eyes, giving up
        machine-readability."""
        if self.units >= 10: unitString = str(self.units)
        else: unitString = '0' + str(self.units)
        whole = unitString[:self.decimalOffset]
        fraction = unitString[self.decimalOffset:]
        length = len(whole)
        if length < 4: # if self < '1000', we're done.
            return self.currencySymbol + str(self)
        
        divisions = length / 3
        steps = [i * -1 for i in range(3, length, 3)]
        steps.reverse()
        for step in steps:
            whole = self.wholeSeparator.join([whole[:step], whole[step:]])
        return self.currencySymbol + \
                  self.decimalSeparator.join([whole, fraction])

class Dollars(Currency):
    currencySymbol = '$'
    
class Continental(Currency):
    decimalSeparator = ','
    wholeSeparator = '.'




More information about the Python-list mailing list