home · contact · privacy
Overhaul calculation/positioning of blocks within ledger. master
authorChristian Heller <c.heller@plomlompom.de>
Sat, 7 Feb 2026 02:06:54 +0000 (03:06 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Sat, 7 Feb 2026 02:06:54 +0000 (03:06 +0100)
src/ledgplom/http.py
src/ledgplom/ledger.py

index 6eaf6c988c7794bdbc452e2978c01ff026e50236..eae6f22d79e1bf401184835b40c5a38ce4ec6817 100644 (file)
@@ -124,12 +124,12 @@ class _Handler(PlomHttpHandler):
             return Path('/', _PAGENAME_EDIT_STRUCTURED, f'{id_}')
         keys_prefixed = self.postvars.keys_prefixed(_PREFIX_LEDGER)
         action, id_str = keys_prefixed[0].split('_', maxsplit=2)[1:]
-        id_ = int(id_str)
+        block = self.server.ledger.blocks[int(id_str)]
         if action.startswith('move'):
-            id_ = self.server.ledger.move_block(id_, action == 'moveup')
-            return Path(self.path, f'#block_{id_}')
-        id_ = self.server.ledger.copy_block(id_)
-        return Path(self.path, f'#block_{id_}')
+            block.move(action == 'moveup')
+        else:
+            block = block.copy_to_current_date()
+        return Path(self.path, f'#block_{block.id_}')
 
     def do_GET(self) -> None:  # pylint: disable=invalid-name
         'Route GET requests to respective handlers.'
index e5ed20fade0dd4f9658095002faf07d8b726b592..e84fabc1a7c8cf6b1e08cba24ad9605637f982a6 100644 (file)
@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
 from datetime import date as dt_date
 from decimal import Decimal, InvalidOperation as DecimalInvalidOperation
 from pathlib import Path
-from typing import Any, Callable, Iterator, Optional, Self
+from typing import Any, Callable, Iterator, Optional, Self, Union
 
 
 SPACE = ' '
@@ -402,23 +402,55 @@ class _Booking(_LinesBlock):
         return self.intro_line.target
 
 
-class _DatBlock(_LinesBlock):
+class _LedgerNode:
+    _next: Optional['_DatBlock'] = None
+
+    def _set_neighbor(
+            self,
+            new_this: Optional[Union['_DatBlock', '_StartNode']],
+            this: str,
+            that: str,
+            ) -> None:
+        if (old_this := getattr(self, f'_{this}')):
+            setattr(old_this, f'_{that}', None)
+        if new_this:
+            if (new_this_that := getattr(new_this, f'_{that}')):
+                setattr(new_this_that, f'_{this}', None)
+            setattr(new_this, f'_{that}', self)
+        setattr(self, f'_{this}', new_this)
+
+    @property
+    def next(self) -> Optional['_DatBlock']:
+        'Successor in chain.'
+        return self._next
+
+    @next.setter
+    def next(self, new_next: Optional['_DatBlock']) -> None:
+        self._set_neighbor(new_next, 'next', 'prev')
+
+
+class _StartNode(_LedgerNode):
+    pass
+
+
+class _DatBlock(_LinesBlock, _LedgerNode):
     'Unit of lines with optional .booking, and (possibly zero) .gap_lines.'
-    _prev: Optional[Self] = None
-    _next: Optional[Self] = None
+    _prev: Optional[Self | _StartNode] = None
 
     @classmethod
     def from_lines(cls, lines: tuple[str, ...]) -> tuple['_DatBlock', ...]:
         'Sequence of DatBlocks parsed from lines.'
         i_block: _DatBlock = cls()
-        blocks = [i_block]
-        for dat_line in (_DatLine(line) for line in lines):
+        for dat_line in (_DatLine(line.rstrip()) for line in lines):
             if (not dat_line.len_indent) and i_block.indented:
-                blocks += [i_block]
                 i_block.next = _DatBlock()
                 i_block = i_block.next
             i_block.add((dat_line, ))
-        return tuple(blocks)
+        blocks = [i_block]
+        while i_block.prev:
+            i_block = i_block.prev
+            blocks += [i_block]
+        return tuple(reversed(blocks))
 
     @property
     def indented(self) -> bool:
@@ -467,9 +499,9 @@ class _DatBlock(_LinesBlock):
     def id_(self) -> int:
         'Return index in chain.'
         count = -1
-        block_iterated: Optional[_DatBlock] = self
-        while block_iterated:
-            block_iterated = block_iterated.prev
+        i_block: Optional[_DatBlock] = self
+        while isinstance(i_block, _DatBlock):
+            i_block = i_block.prev
             count += 1
         return count
 
@@ -480,36 +512,19 @@ class _DatBlock(_LinesBlock):
             return 'date < previous date'
         return ''
 
-    def _set_neighbor(
-            self,
-            new_this: Optional[Self],
-            this: str,
-            that: str,
-            ) -> None:
-        if (old_this := getattr(self, f'_{this}')):
-            setattr(old_this, f'_{that}', None)
-        if new_this:
-            if (new_this_that := getattr(new_this, f'_{that}')):
-                setattr(new_this_that, f'_{this}', None)
-            setattr(new_this, f'_{that}', self)
-        setattr(self, f'_{this}', new_this)
-
     @property
-    def next(self) -> Optional[Self]:
-        'Successor in chain.'
-        return self._next
-
-    @next.setter
-    def next(self, new_next: Optional[Self]) -> None:
-        self._set_neighbor(new_next, 'next', 'prev')
+    def prev_any(self) -> Self | _StartNode:
+        'Predecessor in chain – including _StartNode.'
+        assert self._prev is not None
+        return self._prev
 
     @property
     def prev(self) -> Optional[Self]:
-        'Predecessor in chain.'
-        return self._prev
+        'Predecessor in chain – unless _StartNode, then None.'
+        return self._prev if isinstance(self._prev, _DatBlock) else None
 
     @prev.setter
-    def prev(self, new_prev: Optional[Self]):
+    def prev(self, new_prev: Optional[Self | _StartNode]):
         self._set_neighbor(new_prev, 'prev', 'next')
 
     @property
@@ -527,12 +542,12 @@ class _DatBlock(_LinesBlock):
 
     def move(self, up: bool) -> None:
         'Move up/down in chain.'
-        old_prev = self.prev
+        old_prev = self.prev_any
         old_next = self.next
         if up:
-            assert old_prev is not None
-            if old_prev.prev:
-                old_prev.prev.next = self
+            assert isinstance(old_prev, _DatBlock)
+            if old_prev.prev_any:
+                old_prev.prev_any.next = self
             self.next = old_prev
             old_prev.next = old_next
         else:
@@ -542,14 +557,26 @@ class _DatBlock(_LinesBlock):
             self.prev = old_next
             old_next.prev = old_prev
 
-    def fix_position(self):
-        'Move around in chain until properly positioned by .date.'
-        while self.prev and self.prev.date > self.date:
-            self.move(up=True)
-        while self.next and self.next.date < self.date:
-            self.move(up=False)
-
-    def copy_to_current_date(self) -> Self:
+    def fix_position(self) -> '_DatBlock':
+        'Move around in chain until properly positioned by .date, maybe fuse.'
+        if self.date:
+            while self.prev and self.prev.date and self.prev.date > self.date:
+                self.move(up=True)
+            while self.next and self.next.date and self.next.date <= self.date:
+                self.move(up=False)
+        if self.next and not self.booking:
+            old_next = self.next
+            old_next.add(self.lines, at_end=False)
+            old_next.prev = self.prev
+            return old_next
+        if self.prev and not self.prev.booking:
+            old_prev = self.prev
+            old_prev.add(self.lines)
+            old_prev.next = self.next
+            return old_prev
+        return self
+
+    def copy_to_current_date(self) -> '_DatBlock':
         'Make copy of same lines but date of now, position accordingly.'
         copy = self.__class__()
         copy.add(tuple(_DatLine(line.raw) for line in self.gap_lines))
@@ -561,16 +588,18 @@ class _DatBlock(_LinesBlock):
                      at_end=False)
             copy.add(tuple(_DatLine(line.raw)
                            for line in self.booking.transfer_lines))
-        if self.next:
-            self.next.prev = copy
-        self.next = copy
-        copy.fix_position()
-        return copy
+        copy.insert_self_after(self)
+        return copy.fix_position()
+
+    def insert_self_after(self, other: Union[_StartNode, Self]) -> None:
+        'Insert self between other and other.next.'
+        self.next = other.next
+        other.next = self
 
 
 class Ledger:
     'Collection of _DatBlocks, _Bookings and _Accounts derived from them.'
-    _blocks_start: Optional[_DatBlock]
+    _start_node: _StartNode
 
     def __init__(self, path_dat: Path) -> None:
         self._path_dat = path_dat
@@ -578,16 +607,18 @@ class Ledger:
 
     def load(self) -> None:
         'Re-)read ledger from file at ._path_dat.'
-        blocks = _DatBlock.from_lines(
-                tuple(self._path_dat.read_text(encoding='utf8').splitlines()))
-        self._blocks_start = blocks[0] if blocks else _DatBlock()
+        self._start_node = _StartNode()
+        if (blocks := _DatBlock.from_lines(
+                            tuple(self._path_dat.read_text(encoding='utf8'
+                                                           ).splitlines()))):
+            self._start_node.next = blocks[0]
         self.last_save_hash = self._hash_dat_lines()
 
     @property
     def blocks(self) -> list[_DatBlock]:
         'Return blocks chain as list.'
         blocks = []
-        block = self._blocks_start
+        block = self._start_node.next
         while block:
             blocks += [block]
             block = block.next
@@ -661,44 +692,25 @@ class Ledger:
         'If ._dat_lines different to those of last .load().'
         return self._hash_dat_lines() != self.last_save_hash
 
-    def move_block(self, idx_from: int, up: bool) -> int:
-        'Move _DatBlock of idx_from step up or downwards.'
-        block = self.blocks[idx_from]
-        block.move(up)
-        return block.id_
-
     def rewrite_block(self, old_id: int, new_lines: list[str]) -> int:
         'Rewrite with new_lines, move if changed date.'
         old_block = self.blocks[old_id]
-        old_prev, old_next = old_block.prev, old_block.next
-        new_blocks = _DatBlock.from_lines(tuple(new_lines))
+        old_prev, old_next = old_block.prev_any, old_block.next
+        new_blocks = list(_DatBlock.from_lines(tuple(new_lines)))
         if old_next:
-            if not new_blocks[0].booking:
-                assert len(new_blocks) == 1
-                old_next.add(new_blocks[0].lines)
-                old_next.prev = old_prev
-                return old_next.id_
             old_next.prev = new_blocks[-1]
-        if old_prev:
-            old_prev.next = new_blocks[0]
-        else:
-            self._blocks_start = new_blocks[0]
-        for block in new_blocks:
-            block.fix_position()
+        old_prev.next = new_blocks[0]
+        for idx, block in enumerate(new_blocks):
+            new_blocks[idx] = block.fix_position()
         return new_blocks[0].id_
 
     def add_empty_block(self) -> int:
-        'Add new _DatBlock of empty _Booking to end of ledger.'
+        'Add new _DatBlock of empty _Booking of today to ledger.'
         new_block = _DatBlock()
-        new_block.add((_DatLine(f'{dt_date.today().isoformat()} ?'), ))
-        self.blocks[-1].next = new_block
-        new_block.fix_position()
-        return new_block.id_
-
-    def copy_block(self, id_: int) -> int:
-        'Add copy _DatBlock of id_ but with current date.'
-        copy = self.blocks[id_].copy_to_current_date()
-        return copy.id_
+        date_today = dt_date.today().isoformat()
+        new_block.add((tuple(_DatLine(s) for s in ('', f'{date_today} ?'))))
+        new_block.insert_self_after(self._start_node)
+        return new_block.fix_position().id_
 
     def view_ctx_balance(self, id_: int = -1) -> dict[str, Any]:
         'All context data relevant for rendering a balance view.'