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