home · contact · privacy
For auto-balancing per comment instruction allow (even) distribution among multiple... master
authorChristian Heller <c.heller@plomlompom.de>
Fri, 13 Feb 2026 04:16:02 +0000 (05:16 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Fri, 13 Feb 2026 04:16:02 +0000 (05:16 +0100)
src/ledgplom/ledger.py
src/templates/edit_structured.js
src/tests/full.edit_structured.0
src/tests/full.edit_structured.3
src/tests/full.edit_structured.4

index 6609d9614700a2b2be8457ed47caee4e3cb9bc5c..610b7970049730cbb988a0e3322f7718fe805f56 100644 (file)
@@ -12,7 +12,7 @@ _INDENT_CHARS = {SPACE, '\t'}
 _SEP_ACCNAME_STEPS = ':'
 SEP_COMMENTS = ';'
 _PREFIX_INSTRUCTION = '#'
 _SEP_ACCNAME_STEPS = ':'
 SEP_COMMENTS = ';'
 _PREFIX_INSTRUCTION = '#'
-_ACC_MOD_INSTRUCTORS = {'description', 'balancer'}
+_ACC_MOD_INSTRUCTORS = {'description', 'balancers'}
 
 
 class _Wealth():
 
 
 class _Wealth():
@@ -66,7 +66,7 @@ class _Wealth():
 class _Account:
     'Combine name, position in tree of owner, and wealth of self + children.'
     description = ''
 class _Account:
     'Combine name, position in tree of owner, and wealth of self + children.'
     description = ''
-    balancer = ''
+    _balancers: tuple[str, ...] = tuple()
 
     def __init__(self, parent: Optional[Self], basename: str) -> None:
         self._wealth_diffs: dict[int, _Wealth] = {}
 
     def __init__(self, parent: Optional[Self], basename: str) -> None:
         self._wealth_diffs: dict[int, _Wealth] = {}
@@ -117,6 +117,15 @@ class _Account:
                              + step_name)
             yield rebuilt_path, step_name
 
                              + step_name)
             yield rebuilt_path, step_name
 
+    @property
+    def balancers(self) -> tuple[str, ...]:
+        "Accounts among which this account's amount be balanced out."
+        return self._balancers
+
+    @balancers.setter
+    def balancers(self, balancers_str: str) -> None:
+        self._balancers = tuple(balancers_str.split())
+
 
 class _DatLine:
     'Line of .dat file parsed into comments and machine-readable data.'
 
 class _DatLine:
     'Line of .dat file parsed into comments and machine-readable data.'
@@ -737,7 +746,7 @@ class Ledger:
         accounts = self._calc_accounts(id_)
         if lines:
             return {'raw_gap_lines': [dl.raw for dl in block.gap_lines],
         accounts = self._calc_accounts(id_)
         if lines:
             return {'raw_gap_lines': [dl.raw for dl in block.gap_lines],
-                    'account_balancers': {k: v.balancer
+                    'account_balancers': {k: v.balancers
                                           for k, v in accounts.items()},
                     'booking_lines': ([line.as_dict
                                        for line in block.booking.booking_lines]
                                           for k, v in accounts.items()},
                     'booking_lines': ([line.as_dict
                                        for line in block.booking.booking_lines]
index 94459d2b717f281ec3a134a0f63e61fc94869086..2c524d2737d3f3e468c25debc48392686421c487 100644 (file)
@@ -21,7 +21,7 @@ eslint
 ],
 "max-lines": [
     "error",
 ],
 "max-lines": [
     "error",
-    {"max": 394, "skipBlankLines": true, "skipComments": true}
+    {"max": 427, "skipBlankLines": true, "skipComments": true}
 ],
 "max-lines-per-function": [
     "error",
 ],
 "max-lines-per-function": [
     "error",
@@ -57,6 +57,7 @@ import {
 
 const
     BALANCERS_DEFAULT = {"": "?"},
 
 const
     BALANCERS_DEFAULT = {"": "?"},
+    FACTOR_CENTS = 100,
     IDX_LAST = -1,
     IDX_PAST_INTRO_LINE = 1,
     IDX_START = 0,
     IDX_LAST = -1,
     IDX_PAST_INTRO_LINE = 1,
     IDX_START = 0,
@@ -72,6 +73,7 @@ const
     LEN_INDENT_INPUT_DEFAULT = 2,
     LEN_INDENT_INPUT_MINIMUM = 1,
     LEN_INTRO_LINE = 4,
     LEN_INDENT_INPUT_DEFAULT = 2,
     LEN_INDENT_INPUT_MINIMUM = 1,
     LEN_INTRO_LINE = 4,
+    LEN_INVERSION_DECIMAL = 2,
     LEN_LEN_INDENT = 1,
     LEN_LINE_STEP = 1,
     LEN_STEP_INPUT_LEN_INDENT = 1,
     LEN_LEN_INDENT = 1,
     LEN_LINE_STEP = 1,
     LEN_STEP_INPUT_LEN_INDENT = 1,
@@ -105,26 +107,47 @@ const taintAndUpdateForm = () => {
 };
 
 const balance = (bookingLine) => {
 };
 
 const balance = (bookingLine) => {
-    let invertedAmount = TOK_NONE;
-    if (bookingLine.amount !== TOK_NONE) {
-        invertedAmount = `-${bookingLine.amount}`;
-        const doubleMinus = "--";
-        if (invertedAmount.startsWith(doubleMinus)) {
-            invertedAmount = invertedAmount.slice(doubleMinus.length);
-        }
+    const
+        nameSteps = bookingLine.account.split(SEP_ACCNAME_STEPS),
+        toReturn = [];
+    let
+        balancers = [],
+        leftToInvert = null,
+        partInverted = TOK_NONE;
+    while (balancers.length === LEN_EMPTY) {
+        balancers = accountBalancers[nameSteps.join(SEP_ACCNAME_STEPS)] || [];
+        nameSteps.pop();
     }
     }
-    let balancer = "";
-    const accNameSteps = bookingLine.account.split(SEP_ACCNAME_STEPS);
-    while (balancer === "") {
-        balancer = accountBalancers[accNameSteps.join(SEP_ACCNAME_STEPS)];
-        accNameSteps.pop();
+    if (bookingLine.amount && bookingLine.amount !== TOK_NONE) {
+        leftToInvert = Math.abs(bookingLine.amount);
+        partInverted = leftToInvert / balancers.length;
     }
     }
-    return newBookingLine(
-        balancer,
-        invertedAmount,
-        bookingLine.currency,
-        LEN_INDENT_INPUT_BALANCED
+    const centsRound = (amt) => Math.round(FACTOR_CENTS * amt) / FACTOR_CENTS;
+    const signFixed = (amt
+    ) => (-Math.sign(bookingLine.amount) * amt).toFixed(LEN_INVERSION_DECIMAL);
+    Array.from(balancers).forEach(
+        (balancer, idx) => {
+            let amountInput = TOK_NONE;
+            if (partInverted !== TOK_NONE) {
+                if (idx === balancers.length + IDX_LAST) {
+                    amountInput = signFixed(centsRound(leftToInvert));
+                } else {
+                    const rounded = centsRound(partInverted);
+                    leftToInvert -= rounded;
+                    amountInput = signFixed(rounded);
+                }
+            }
+            toReturn.push(
+                newBookingLine(
+                    balancer,
+                    amountInput,
+                    bookingLine.currency,
+                    LEN_INDENT_INPUT_BALANCED
+                )
+            );
+        }
     );
     );
+    return toReturn;
 };
 
 const updateForm = () => {
 };
 
 const updateForm = () => {
@@ -354,7 +377,7 @@ const updateForm = () => {
                 () => bookingLines.splice(
                     idx + LEN_LINE_STEP,
                     LEN_EMPTY,
                 () => bookingLines.splice(
                     idx + LEN_LINE_STEP,
                     LEN_EMPTY,
-                    balance(bookingLine)
+                    ...balance(bookingLine)
                 )
             );
             addButton(
                 )
             );
             addButton(
@@ -410,7 +433,7 @@ const mirror = () => {
         bookingLines.splice(
             idx,
             LEN_EMPTY,
         bookingLines.splice(
             idx,
             LEN_EMPTY,
-            balance(bookingLines[idx - LEN_LINE_STEP])
+            ...balance(bookingLines[idx - LEN_LINE_STEP])
         );
     }
     taintAndUpdateForm();
         );
     }
     taintAndUpdateForm();
index 4f6ae3a4bb8aeecca1abba08ccf2e8c3988398a9..b812ba15e7ed3bbdf138847a875ec971bd43a50d 100644 (file)
@@ -91,8 +91,7 @@ input.amount {
 </span>
 <hr>
 
 </span>
 <hr>
 
-<span class="disable_on_taint">
-<input id="btn_mirror" type="button" value="mirror">
+<input id="btn_mirror" type="button" value="balance all">
 <input id="btn_sink" type="button" value="fill sink">
 |
 <input id="btn_replace" type="button" value="replace string">
 <input id="btn_sink" type="button" value="fill sink">
 |
 <input id="btn_replace" type="button" value="replace string">
@@ -100,7 +99,6 @@ from
 <input id="replace_from">
 to
 <input id="replace_to">
 <input id="replace_from">
 to
 <input id="replace_to">
-</span>
 <hr>
 <div>Gap:</div>
 <textarea id="gap_lines" name="raw_lines" cols="100" rows="3" class="tainter">;#description bar:x bla bla bla
 <hr>
 <div>Gap:</div>
 <textarea id="gap_lines" name="raw_lines" cols="100" rows="3" class="tainter">;#description bar:x bla bla bla
index f897a4d10e67bf83a07758eb8ee2154c8e00c881..9bd836e3320c6e057ba2e7e107375eca32f8472c 100644 (file)
@@ -91,8 +91,7 @@ input.amount {
 </span>
 <hr>
 
 </span>
 <hr>
 
-<span class="disable_on_taint">
-<input id="btn_mirror" type="button" value="mirror">
+<input id="btn_mirror" type="button" value="balance all">
 <input id="btn_sink" type="button" value="fill sink">
 |
 <input id="btn_replace" type="button" value="replace string">
 <input id="btn_sink" type="button" value="fill sink">
 |
 <input id="btn_replace" type="button" value="replace string">
@@ -100,7 +99,6 @@ from
 <input id="replace_from">
 to
 <input id="replace_to">
 <input id="replace_from">
 to
 <input id="replace_to">
-</span>
 <hr>
 <div>Gap:</div>
 <textarea id="gap_lines" name="raw_lines" cols="100" rows="2" class="tainter">
 <hr>
 <div>Gap:</div>
 <textarea id="gap_lines" name="raw_lines" cols="100" rows="2" class="tainter">
index 2316dac9eb6079be586b45c1a133f883b6c3ab3a..abc7b3d4974d9f0fc285d19f7a524b593fa3aaf8 100644 (file)
@@ -97,8 +97,7 @@ input.amount {
 </div>
 <hr>
 
 </div>
 <hr>
 
-<span class="disable_on_taint">
-<input id="btn_mirror" type="button" value="mirror">
+<input id="btn_mirror" type="button" value="balance all">
 <input id="btn_sink" type="button" value="fill sink">
 |
 <input id="btn_replace" type="button" value="replace string">
 <input id="btn_sink" type="button" value="fill sink">
 |
 <input id="btn_replace" type="button" value="replace string">
@@ -106,7 +105,6 @@ from
 <input id="replace_from">
 to
 <input id="replace_to">
 <input id="replace_from">
 to
 <input id="replace_to">
-</span>
 <hr>
 <div>Gap:</div>
 <textarea id="gap_lines" name="raw_lines" cols="100" rows="4" class="tainter">
 <hr>
 <div>Gap:</div>
 <textarea id="gap_lines" name="raw_lines" cols="100" rows="4" class="tainter">