commit cb2707e8ac8c4777381b30e852abf63b35f88f29
parent 88df6833c0e674227227ac5fa92104eb5a992f15
Author: lash <dev@holbrook.no>
Date: Tue, 24 Mar 2026 19:29:19 -0600
Move rates calculations to unit
Diffstat:
5 files changed, 193 insertions(+), 115 deletions(-)
diff --git a/dummy/tests/balance.py b/dummy/tests/balance.py
@@ -4,7 +4,7 @@ import datetime
import os
from usawa import EntryPart, Entry, UnitIndex
-from usawa.balance import Balancer
+from usawa.balance import Translator
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
@@ -22,14 +22,30 @@ class TestBalancer(unittest.TestCase):
self.dtreg = datetime.datetime.now()
- def test_balancer_basic(self):
- dst = EntryPart('FOO', 'asset', 'foo', 1337)
- src = EntryPart('FOO', 'income', 'baz', 1337, debit=True)
- entry = Entry(42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg)
- entry.add_part(src)
- entry.add_part(dst)
- o = Balancer(self.uidx, entry, base='FOO')
- self.assertTrue(o.balanced())
+ def test_translate_ex(self):
+ o = Translator(self.uidx, 'FOO')
+ o.set_ex('BAR', 230000)
+ r = o.val('BAR', 42333)
+ print(r)
+
+# def test_balancer_process(self):
+# dst = EntryPart('FOO', 'asset', 'foo', 1337)
+# src = EntryPart('FOO', 'income', 'baz', 1337, debit=True)
+# entry = Entry(42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg)
+# entry.add_part(src)
+# entry.add_part(dst)
+# o = Balancer(self.uidx, entry=entry, base='FOO')
+# self.assertTrue(o.balanced())
+#
+#
+# def test_balancer_parts_simple(self):
+# o = Balancer(self.uidx, base='FOO')
+# src = EntryPart('FOO', 'income', 'baz', 1337, debit=True)
+# o.apply_part(src)
+# dst = EntryPart('FOO', 'asset', 'foo', 1337)
+# o.apply_part(dst)
+# self.assertTrue(o.balanced())
+
if __name__ == '__main__':
diff --git a/dummy/tests/unit.py b/dummy/tests/unit.py
@@ -1,7 +1,10 @@
import unittest
+import logging
from usawa import UnitIndex
+logging.basicConfig(level=logging.DEBUG)
+
class TestUnit(unittest.TestCase):
@@ -9,7 +12,7 @@ class TestUnit(unittest.TestCase):
self.uidx_default = UnitIndex('FOO')
self.uidx_three = UnitIndex('FOO', precision=3)
self.uidx_none = UnitIndex('FOO', precision=0)
- self.uidx_default.add('BAR', precision=4, ex=0.2)
+ self.uidx_default.add('BAR', precision=4, rate=0.23)
def test_tostring(self):
@@ -46,5 +49,21 @@ class TestUnit(unittest.TestCase):
self.assertEqual(v, 12345)
+ def test_unit_rates(self):
+ #self.uidx_default.add('BAR', precision=3)
+ #r = self.uidx_default.val('BAR', 42333)
+ with self.assertRaises(ValueError):
+ self.uidx_default.set_rate('FOO', 230000)
+ #self.uidx_default.set_rate('BAR', 230000)
+ r = self.uidx_default.val('BAR', 4233300)
+ self.assertEqual(r[0], 9736)
+ self.assertEqual(r[1], 590000)
+
+ r = self.uidx_default.set_rate('BAR', 1000000)
+ r = self.uidx_default.val('BAR', 4233300)
+ self.assertEqual(r[0], 42333)
+ self.assertEqual(r[1], 0)
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/dummy/usawa/balance.py b/dummy/usawa/balance.py
@@ -5,78 +5,110 @@ logg = logging.getLogger('usawa.balancer')
class Balancer:
- def __init__(self, uidx, entry, base=None):
+ def __init__(self, translator):
+ self.tr = translator
self.r = 0
- self.uidx = uidx
- self.max_precision = 0
- self.ex = {}
- self.base = base
- self.scan(entry)
- self.process(entry)
- def scan(self, entry):
- base = None
- for part in entry:
- precision = self.uidx.get(part.unit)
- if precision > self.max_precision:
- self.max_precision = precision
- self.ex[part.unit] = 1000000000
- if base == None:
- base = part.unit
- continue
- if base != part.unit:
- if self.base == None:
- logg.warning('base is not set with entry with different units')
- logg.debug('have max precision {}'.format(self.max_precision))
- self.base = base
-
-
- def set_rate(self, unit, rate):
- if unit == self.base:
- raise ValueError('base rate against itself')
- self.ex[unit] = rate
-
-
- def process(self, entry):
- for part in entry:
- precision = self.uidx.get(part.unit)
- mod = 10 ** (self.max_precision - precision)
- amount = part.amount * mod
- ex = self.ex[part.unit] / 1000000000
- amount *= ex
- fn = getattr(self, 'handle_' + part.typ)
- fn(amount, part.isdebit)
- logg.debug('after {} {} (ex {}) = {}'.format(part.unit, amount, ex, self.r))
-
-
- def balanced(self):
- return self.r == 0
-
-
- def handle_income(self, amount, issrc=False):
- if issrc:
- self.r += amount
- else:
- self.r -= amount
-
-
- def handle_expense(self, amount, issrc=False):
- if issrc:
- self.r -= amount
- else:
- self.r += amount
-
-
- def handle_asset(self, amount, issrc=False):
- if issrc:
- self.r += amount
- else:
- self.r -= amount
-
-
- def handle_liability(self, amount, issrc=False):
- if issrc:
- self.r -= amount
- else:
- self.r += amount
+ def apply_part(self, part):
+ precision = self.uidx.get(part.unit)
+ mod = 10 ** (self.max_precision - precision)
+ amount = part.amount * mod
+ ex = self.ex[part.unit] / 1000000000
+ amount *= ex
+ fn = getattr(self, 'handle_' + part.typ)
+ fn(amount, part.isdebit)
+ logg.debug('after {} {} (ex {}) = {}'.format(part.unit, amount, ex, self.r))
+
+
+#class Balancer:
+#
+# def __init__(self, uidx, entry=None, base=None):
+# self.r = 0
+# self.uidx = uidx
+# self.max_precision = 0
+# self.ex = {}
+# self.running = {}
+# self.base = base
+# if entry != None:
+# self.scan(entry)
+# self.process_entry(entry)
+#
+#
+# def scan_units(self, uidx):
+#
+#
+# def scan(self, entry):
+# base = None
+# for part in entry:
+# precision = self.uidx.get(part.unit)
+# if precision > self.max_precision:
+# self.max_precision = precision
+# self.ex[part.unit] = 1000000000
+# if base == None:
+# base = part.unit
+# continue
+# if base != part.unit:
+# if self.base == None:
+# logg.warning('base is not set with entry with different units')
+# logg.debug('have max precision {}'.format(self.max_precision))
+# self.base = base
+#
+#
+# def init_unit(self, unit):
+# self.ex[unit] = 100000000
+# self.
+#
+#
+# def set_rate(self, unit, rate):
+# if unit == self.base:
+# raise ValueError('base rate against itself')
+# self.ex[unit] = rate
+#
+#
+# def process_entry(self, entry):
+# for part in entry:
+# self.apply_part(part)
+#
+#
+# def apply_part(self, part):
+# precision = self.uidx.get(part.unit)
+# mod = 10 ** (self.max_precision - precision)
+# amount = part.amount * mod
+# ex = self.ex[part.unit] / 1000000000
+# amount *= ex
+# fn = getattr(self, 'handle_' + part.typ)
+# fn(amount, part.isdebit)
+# logg.debug('after {} {} (ex {}) = {}'.format(part.unit, amount, ex, self.r))
+#
+#
+# def balanced(self):
+# return self.r == 0
+#
+#
+# def handle_income(self, amount, issrc=False):
+# if issrc:
+# self.r += amount
+# else:
+# self.r -= amount
+#
+#
+# def handle_expense(self, amount, issrc=False):
+# if issrc:
+# self.r -= amount
+# else:
+# self.r += amount
+#
+#
+# def handle_asset(self, amount, issrc=False):
+# if issrc:
+# self.r += amount
+# else:
+# self.r -= amount
+#
+#
+# def handle_liability(self, amount, issrc=False):
+# if issrc:
+# self.r -= amount
+# else:
+# self.r += amount
diff --git a/dummy/usawa/data/schema.xsd b/dummy/usawa/data/schema.xsd
@@ -79,12 +79,6 @@
<xs:complexType name="Unit">
<xs:sequence>
<xs:element name="precision" type="xs:positiveInteger" maxOccurs="1" minOccurs="1" />
- <xs:choice>
- <xs:element name="exchange" type="xs:positiveInteger" maxOccurs="1" minOccurs="1" />
- <xs:sequence>
- <xs:element name="rate" type="Rate" minOccurs="1" maxOccurs="unbounded" />
- </xs:sequence>
- </xs:choice>
</xs:sequence>
<xs:attribute name="sym" type="xs:string" />
</xs:complexType>
diff --git a/dummy/usawa/unit.py b/dummy/usawa/unit.py
@@ -1,4 +1,5 @@
import logging
+import copy
import rencode
import lxml.etree
@@ -27,12 +28,48 @@ class UnitIndex:
:param precision: The decimal precision of the base unit. Default is 2.
:type precision: int
"""
- def __init__(self, base, precision=None):
+ def __init__(self, base=None, precision=None):
+ self.detail = {}
+ self.rate = {}
self.base = base
- if precision == None:
- precision = UnitIndex.default_precision
- self.detail = {base: precision}
- self.exchange = {base: UnitIndex.default_exchange}
+ if base != None:
+ if precision == None:
+ precision = UnitIndex.default_precision
+ self.detail = {base: precision}
+ self.exchange = {base: UnitIndex.default_exchange}
+
+
+ def clone(self):
+ o = UnitIndex()
+ o.detail = self.detail
+ o.base = self.base
+ o.exchange = copy.copy(self.exchange)
+ return o
+
+
+ def set_rate(self, unit, rate):
+ if unit == self.base:
+ raise ValueError('cannot adjust rate for base')
+ if not isinstance(rate, float):
+ rate /= 1000000
+ self.rate[unit] = rate
+
+
+ def val(self, unit, amount):
+ r = self.rate[unit] * amount
+ # TODO: embed in rate
+ base_precision = self.detail[self.base]
+ adj = base_precision - self.detail[unit]
+ if adj < 0:
+ logg.debug('r {}'.format(r))
+ r /= (10 ** abs(adj))
+ else:
+ r *= (10 ** adj)
+
+ v = int(r)
+ m = int((r - v) * 1000000)
+ logg.debug('val {} -> {},{} adj {}'.format(r, v, m, adj))
+ return (v, m,)
"""Add a unit to the index.
@@ -46,11 +83,9 @@ class UnitIndex:
:param ex: The exchange rate of the unit, relative to the base unit. Default is 1000000000 (1.0).
:type ex: int or float
"""
- def add(self, sym, precision=2, ex=1000000000):
+ def add(self, sym, precision=2, rate=1000000):
self.detail[sym] = precision
- if isinstance(ex, float):
- ex = int(ex*1000000000) # nano resolution
- self.exchange[sym] = ex
+ self.set_rate(sym, rate)
"""Create a unit index object from XML.
@@ -72,7 +107,6 @@ class UnitIndex:
for o in tree.iter(NSPREFIX + 'unit'):
logg.debug('add unit ' + o.get('sym'))
r.detail[o.get('sym')] = int(o.find('precision', namespaces=nsmap()).text)
- r.exchange[o.get('sym')] = int(o.find('exchange', namespaces=nsmap()).text)
r.check()
return r
@@ -113,20 +147,6 @@ class UnitIndex:
return k
- """Retrieve the exchange rate for the unit.
-
- The value represents a decimal number with nano precision. For example, a value of 4200000000 corresponds to a float value of 4.2.
-
- :param k: Unit symbol.
- :type k: str
- :raises: KeyError if symbol not found.
- :returns: Rate
- :rtype: int
- """
- def ex(self, k):
- return self.exchange[k]
-
-
"""Retrieve a list of all the units in the index.
:returns: The list of symbols.
@@ -261,9 +281,6 @@ class UnitIndex:
o = lxml.etree.SubElement(unit, 'precision')
o.text = str(self.detail[k])
unit.append(o)
- o = lxml.etree.SubElement(unit, 'exchange')
- o.text = str(self.exchange[k])
- unit.append(o)
tree.append(unit)
return tree