commit 88df6833c0e674227227ac5fa92104eb5a992f15
parent 816414ef26259d09c04254235bb6199c6ecdac36
Author: lash <dev@holbrook.no>
Date: Mon, 23 Mar 2026 21:12:49 -0600
Start balancer logic
Diffstat:
5 files changed, 147 insertions(+), 0 deletions(-)
diff --git a/dummy/tests/balance.py b/dummy/tests/balance.py
@@ -0,0 +1,36 @@
+import logging
+import unittest
+import datetime
+import os
+
+from usawa import EntryPart, Entry, UnitIndex
+from usawa.balance import Balancer
+
+logging.basicConfig(level=logging.DEBUG)
+logg = logging.getLogger()
+
+testdir = os.path.realpath(os.path.dirname(__file__))
+
+class TestBalancer(unittest.TestCase):
+
+ def setUp(self):
+ self.parent = bytes.fromhex('0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6')
+ self.ref = '1bda7dfa-b8fd-400d-8b42-1d2861ad7f70'
+ self.uidx = UnitIndex('FOO')
+ self.uidx.add('BAR', 3)
+ self.description = "foo bar baz"
+ 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())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/dummy/tests/entry.py b/dummy/tests/entry.py
@@ -126,5 +126,18 @@ class TestEntry(unittest.TestCase):
entry = Entry.from_tree(tree, self.uidx)
+ def test_entry_mux(self):
+ dst_one = EntryPart('FOO', 'asset', 'foo', 1337)
+ dst_two = EntryPart('FOO', 'asset', 'bar', 42)
+ src = EntryPart('FOO', 'income', 'baz', 1337+42, debit=True)
+ o = Entry(42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg)
+ o.add_part(src, debit=True)
+ o.add_part(dst_one)
+ o.add_part(dst_two)
+ wallet = DemoWallet()
+ data = o.wrap(wallet=wallet)
+ print(list(o))
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/dummy/usawa/balance.py b/dummy/usawa/balance.py
@@ -0,0 +1,82 @@
+import logging
+
+logg = logging.getLogger('usawa.balancer')
+
+
+class Balancer:
+
+ def __init__(self, uidx, entry, base=None):
+ 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
diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py
@@ -204,6 +204,7 @@ class Entry(UsawaElement):
self.credit = []
self.lookup = None
self.lookup_algo = None
+ self.parts = []
@staticmethod
@@ -658,5 +659,16 @@ class Entry(UsawaElement):
return b.encode('utf-8')
+ def __iter__(self):
+ self.parts = self.debit + self.credit
+ return self
+
+
+ def __next__(self):
+ if len(self.parts) == 0:
+ raise StopIteration()
+ return self.parts.pop(0)
+
+
def __str__(self):
return 'entry serial {} date {} parent {}'.format(self.serial, self.dt, self.parent.hex())
diff --git a/dummy/usawa/error.py b/dummy/usawa/error.py
@@ -16,3 +16,7 @@ class SocketError(Exception):
class AccountError(Exception):
pass
+
+
+class BalanceError(Exception):
+ pass