From: Evan Broder Date: Fri, 18 Jul 2008 07:51:47 +0000 (+0000) Subject: Switch from using Decimal to using a new Currency class X-Git-Url: https://asedeno.scripts.mit.edu/gitweb/?a=commitdiff_plain;h=3b864c81e804769f2be45b38c319895d1735aef9;p=bluechips.git Switch from using Decimal to using a new Currency class * Include the SmartSubclass metaclass for making an object that is itself but acts like something else * Derive a Currency class from that * Use it EVERYWHERE! --- diff --git a/bluechips/controllers/status.py b/bluechips/controllers/status.py index 5d92d2f..f3373dd 100644 --- a/bluechips/controllers/status.py +++ b/bluechips/controllers/status.py @@ -10,7 +10,8 @@ from bluechips.lib.totals import * import sqlalchemy from datetime import date, timedelta -from decimal import Decimal + +from bluechips.model.types import Currency from pylons import request @@ -49,7 +50,7 @@ class StatusController(BaseController): return render('/status/index.mako') def _total(self, where): - return (meta.Session.execute(sqlalchemy.sql.select([ + return Currency(meta.Session.execute(sqlalchemy.sql.select([ sqlalchemy.func.sum(model.expenditures.c.amount).\ label('total')]).\ - where(where)).scalar() or Decimal("0.00")) / 100 + where(where)).scalar() or 0) diff --git a/bluechips/lib/helpers.py b/bluechips/lib/helpers.py index 4bb3576..1b8771b 100644 --- a/bluechips/lib/helpers.py +++ b/bluechips/lib/helpers.py @@ -14,7 +14,4 @@ from decimal import Decimal def bluechips(): return 'BlueChips' -def round_currency(value): - return Decimal(value).quantize(Decimal('0.01')) - flash = _Flash() diff --git a/bluechips/lib/subclass.py b/bluechips/lib/subclass.py new file mode 100644 index 0000000..3511d8e --- /dev/null +++ b/bluechips/lib/subclass.py @@ -0,0 +1,22 @@ +""" +Create subclasses that call out to their "superclass" for all methods +but return the "subclass's" type +""" + +def wrapper(c, func): + return (lambda self,*args: c(getattr(self.value, func)(*map(self.value.__class__, args)))) + +class SmartSubclass(object): + def __init__(self, superclass, exclude=[]): + self.superclass = superclass + self.exclude = exclude + def __call__(self, name, bases, dict): + c = type(name, bases, dict) + for func in dir(self.superclass): + if func not in dir(c) and \ + callable(getattr(self.superclass, func)) and \ + func not in self.exclude: + setattr(c, func, wrapper(c, func)) + return c + +__all__ = ['SmartSubclass'] diff --git a/bluechips/lib/totals.py b/bluechips/lib/totals.py index 3f18a32..53392c8 100644 --- a/bluechips/lib/totals.py +++ b/bluechips/lib/totals.py @@ -5,9 +5,9 @@ Calculate the total state of the books from bluechips import model from bluechips.model import meta -import sqlalchemy +from bluechips.model.types import Currency -from decimal import Decimal +import sqlalchemy class DirtyBooks(Exception): """ @@ -25,7 +25,7 @@ def debts(): # First, credit everyone for expenditures they've made for user in users: - debts[user] = -sum(map((lambda x: x.amount), user.expenditures)) + debts[user] = Currency(-sum(map((lambda x: x.amount), user.expenditures))) # Next, debit everyone for expenditures that they have an # investment in (i.e. splits) @@ -35,7 +35,7 @@ def debts(): group_by(model.Split.user_id) for split, total_cents in total_splits: - debts[split.user] += (total_cents / 100) + debts[split.user] += total_cents # Finally, move transfers around appropriately # @@ -48,9 +48,9 @@ def debts(): total_credits = transfer_q.group_by(model.Transfer.creditor_id) for transfer, total_amount in total_debits: - debts[transfer.debtor] -= (total_amount / 100) + debts[transfer.debtor] -= total_amount for transfer, total_amount in total_credits: - debts[transfer.creditor] += (total_amount / 100) + debts[transfer.creditor] += total_amount return debts diff --git a/bluechips/model/__init__.py b/bluechips/model/__init__.py index 9261b97..a5ab0a0 100644 --- a/bluechips/model/__init__.py +++ b/bluechips/model/__init__.py @@ -34,7 +34,7 @@ expenditures = sa.Table('expenditures', meta.metadata, sa.Column('id', sa.types.Integer, primary_key=True), sa.Column('spender_id', sa.types.Integer, sa.ForeignKey('users.id'), nullable=False), - sa.Column('amount', types.Currency, nullable=False), + sa.Column('amount', types.DBCurrency, nullable=False), sa.Column('description', sa.types.Text), sa.Column('date', sa.types.Date, default=datetime.now), sa.Column('entered_time', sa.types.DateTime, @@ -47,7 +47,7 @@ splits = sa.Table('splits', meta.metadata, sa.ForeignKey('expenditures.id'), nullable=False), sa.Column('user_id', sa.types.Integer, sa.ForeignKey('users.id'), nullable=False), - sa.Column('share', types.Currency, nullable=False) + sa.Column('share', types.DBCurrency, nullable=False) ) subitems = sa.Table('subitems', meta.metadata, @@ -56,7 +56,7 @@ subitems = sa.Table('subitems', meta.metadata, sa.ForeignKey('expenditures.id'), nullable=False), sa.Column('user_id', sa.types.Integer, sa.ForeignKey('users.id'), nullable=False), - sa.Column('amount', types.Currency, nullable=False) + sa.Column('amount', types.DBCurrency, nullable=False) ) transfers = sa.Table('transfers', meta.metadata, @@ -65,7 +65,7 @@ transfers = sa.Table('transfers', meta.metadata, sa.ForeignKey('users.id'), nullable=False), sa.Column('creditor_id', sa.types.Integer, sa.ForeignKey('users.id'), nullable=False), - sa.Column('amount', types.Currency, nullable=False), + sa.Column('amount', types.DBCurrency, nullable=False), sa.Column('description', sa.Text, default=None), sa.Column('date', sa.types.Date, default=datetime.now), sa.Column('entered_time', sa.types.DateTime, diff --git a/bluechips/model/expenditure.py b/bluechips/model/expenditure.py index 88d65b3..252a93b 100644 --- a/bluechips/model/expenditure.py +++ b/bluechips/model/expenditure.py @@ -1,7 +1,7 @@ from user import User from split import Split from bluechips.model import meta -from bluechips.lib.helpers import round_currency +from bluechips.model.types import Currency from decimal import Decimal import random @@ -47,18 +47,18 @@ class Expenditure(object): amounts_dict = dict() for user, share in split_dict.iteritems(): - amounts_dict[user] = round_currency(split_dict[user] * self.amount) + amounts_dict[user] = Currency(split_dict[user] * self.amount) difference = self.amount - sum(amounts_dict.itervalues()) if difference > 0: for i in xrange(difference * 100): winner = random.choice(amounts_dict.keys()) - amounts_dict[winner] += Decimal('0.01') + amounts_dict[winner] += Currency(1) elif difference < 0: for i in xrange(difference * -100): winner = random.choice(amounts_dict.keys()) - amounts_dict[winner] -= Decimal('0.01') + amounts_dict[winner] -= Currency(1) for user, share in amounts_dict.iteritems(): s = Split() diff --git a/bluechips/model/types.py b/bluechips/model/types.py index a81a8ae..1c54e76 100644 --- a/bluechips/model/types.py +++ b/bluechips/model/types.py @@ -3,10 +3,46 @@ Define special types used in BlueChips """ import sqlalchemy as sa -from bluechips.lib.helpers import round_currency -from decimal import Decimal +from bluechips.lib.subclass import SmartSubclass -class Currency(sa.types.TypeDecorator): +class Currency(object): + __metaclass__ = SmartSubclass(int) + def __init__(self, value): + if isinstance(value, str): + self.value = int(float(value) * 100) + else: + self.value = int(value) + + def __int__(self): + return self.value + def __float__(self): + return float(self.value) + def __long__(self): + return long(self.value) + + def __cmp__(self, other): + try: + return self.value.__cmp__(int(other)) + except: + return self.value.__cmp__(0) + + def __mul__(self, other): + return Currency(self.value * other) + def __rmul__(self, other): + return self.__mul__(other) + + def __str_no_dollar__(self): + return str(self)[1:] + + def __repr__(self): + return '%s("%s")' % (self.__class__.__name__, str(self)) + def __str__(self): + sign = '-' if self.value < 0 else '' + cents = abs(self.value) % 100 + dollars = (abs(self.value) - cents) / 100 + return '$%s%s.%.02d' % (sign, dollars, cents) + +class DBCurrency(sa.types.TypeDecorator): """ A type which represents monetary amounts internally as integers. @@ -16,7 +52,7 @@ class Currency(sa.types.TypeDecorator): impl = sa.types.Integer def process_bind_param(self, value, engine): - return int(value * 100) + return int(value) def convert_result_value(self, value, engine): - return round_currency(Decimal(value) / 100) + return Currency(value) diff --git a/bluechips/templates/base.mako b/bluechips/templates/base.mako index a9865c1..1aa31cb 100644 --- a/bluechips/templates/base.mako +++ b/bluechips/templates/base.mako @@ -49,7 +49,7 @@ ${e.date} ${e.spender.name} ${e.description} - $${h.round_currency(e.amount)} + ${e.amount} % endfor @@ -70,7 +70,7 @@ ${t.debtor.name} ${t.creditor.name} ${t.description} - $${h.round_currency(t.amount)} + ${t.amount} % endfor diff --git a/bluechips/templates/status/index.mako b/bluechips/templates/status/index.mako index 060bd4e..5e9539d 100644 --- a/bluechips/templates/status/index.mako +++ b/bluechips/templates/status/index.mako @@ -17,7 +17,7 @@ ${transfer[0].username} ${transfer[1].username} - $${h.round_currency(transfer[2])} + ${transfer[2]} % endfor @@ -28,23 +28,23 @@ - + - + - + - + - +
Total$${h.round_currency(c.total)}${c.total}
Past year$${h.round_currency(c.year_total)}${c.year_total}
Year to date$${h.round_currency(c.this_year_total)}${c.this_year_total}
Month to date$${h.round_currency(c.this_month_total)}${c.this_month_total}
Last month$${h.round_currency(c.last_month_total)}${c.last_month_total}
diff --git a/bluechips/widgets/__init__.py b/bluechips/widgets/__init__.py index 3509ee2..545b88e 100644 --- a/bluechips/widgets/__init__.py +++ b/bluechips/widgets/__init__.py @@ -5,7 +5,7 @@ from tw.forms import validators from bluechips import model from bluechips.model import meta -from decimal import Decimal +from bluechips.model.types import Currency class UserSelect(forms.SingleSelectField): @staticmethod @@ -28,9 +28,9 @@ class AmountField(forms.TextField): size = 8 validator = validators.All( validators.Wrapper( - to_python=Decimal, - from_python=str), - validators.Regex(r'^[0-9]*(\.[0-9]{2})?$', not_empty=True)) + to_python=(lambda x: Currency(float(x) * 100)), + from_python=Currency.__str_no_dollar__), + validators.Regex(r'^[0-9]*(\.[0-9]{2})?$')) # This is virtually copied from formencode.validator.FieldsMatch, but # I wanted my own version for fields that shouldn't match