Money database property for Google App Engine

Jonathan

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

2 Responses to “Money database property for Google App Engine”

  • eli Says:

    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)

  • Jonathan Says:

    Thanks for giving it a name. I didn’t know what it was called.

Leave a Reply