Money database property for Google App Engine
Calculating money is a tricky thing. Your calculations have to be ultra-precise, so no storing things as floats where $1.33 might actually be stored as 1.3299999999. That is no good for calculations… But equally it is no good storing in cents either: what is 133c /2?
Python has a Decimal datatype, but this requires serialisation to String. Nick Johnson from the Google App Engine team wrote a post about how to write a Decimal property for Google App Engine. Unfortunately this because this serialises to String, then when you do any sorting, then you get String sorting: 100 comes before 11, which comes before 20. Bummer. I played around with storing numbers with a bunch of leading zeroes ie: 0000000010. But that starts to feel a bit hacky.
So I wrote a Money database property that has 6 places of precision, and works as a normal Python numeric type. I haven’t implemented all of those methods, just the ones that I needed. I would be happy to take feedback though.
To use:
class Transaction(db.model): dateOccurred = db.DateProperty(auto_now_add=True) description = db.StringProperty(required=True) amount = MoneyProperty(required=True) t = Transaction(description="I got some money", amount=Money(10.34))
from google.appengine.ext import db class Money(object): multiple = 1000000.0 def __init__(self, val, multiply=True): if multiply: self._intVal = int(float(val) * self.multiple) else: self._intVal = int(val) def format(self, places=2): return "%.*f" % (places, float(self)) def __float__(self): return self._intVal / self.multiple def __repr__(self): return "%.06f" % (self._intVal / self.multiple) def __mul__(self, other): if type(other) == Money: return Money((self._intVal * other._intVal) / self.multiple, False) else: return Money(self._intVal * other, False) __rmul__ = __mul__ def __add__(self, other): if type(other) == Money: return Money(self._intVal + other._intVal, False) else: return Money(self._intVal + (other * self.multiple), False) __radd__ = __add__ def __cmp__(self, other): if other == None: return 1 elif other == "": return 1 elif type(other) != Money: return self._intVal - other*self.multiple return self._intVal - other._intVal def __sub__(self, other): return Money(self._intVal - other._intVal, False) def __rsub__(self, other): return Money(other._intVal - self._intVal, False) def __div__(self, other): if type(other) == Money: return Money((self._intVal * self.multiple) / other._intVal, False) else: return Money(self._intVal / other, False) def __rdiv__(self, other): if type(other) == Money: return Money((other._intVal * self.multiple) / self._intVal, False) else: return Money((other * self.multiple * self.multiple) / self._intVal, False) def __neg__(self): return Money(self._intVal * -1, False) class MoneyProperty(db.Property): data_type = Money def get_value_for_datastore(self, model_instance): value = super(MoneyProperty, self).get_value_for_datastore(model_instance) if value==None: return None elif isinstance(value, Money): return value._intVal else: return Money(value)._intVal def make_value_from_datastore(self, value): if value==None: return None else: return Money(value, False) def empty(self, value): return value == None def get_value_for_form(self, instance): value = super(MoneyProperty, self).get_value_for_form(instance) if not value: return None if isinstance(value, Money): return float(value) return value def make_value_from_form(self, value): if not value: return [] if isinstance(value, Money): return Money(value) return value
February 2nd, 2010 at 9:28 pm
Did you consider doing Fixed Point for this?
e.g. 100.24 would become 10024 or maybe 100240 in the db. You just decide how many digits to the right of the decimal you want to store… thus you avoid floating point errors. Then you just convert them when putting and getting.
http://en.wikipedia.org/wiki/Fixed-point_arithmetic
Though, I think this is what you are doing.. you are multiplying every number by 1,000,000 and storing that.. yes? If so.. this is fixed point. hooray! (forgive my sleepiness.. I’ve suggested you do something that you are already doing)
February 2nd, 2010 at 10:41 pm
Thanks for giving it a name. I didn’t know what it was called.