From: Christian Heller Date: Sat, 7 Feb 2026 02:06:54 +0000 (+0100) Subject: Overhaul calculation/positioning of blocks within ledger. X-Git-Url: https://plomlompom.com/repos/%7B%7B%20web_path%20%7D%7D/static/%7B%7Bprefix%7D%7D/test.html?a=commitdiff_plain;ds=inline;p=ledgplom Overhaul calculation/positioning of blocks within ledger. --- diff --git a/src/ledgplom/http.py b/src/ledgplom/http.py index 6eaf6c9..eae6f22 100644 --- a/src/ledgplom/http.py +++ b/src/ledgplom/http.py @@ -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.' diff --git a/src/ledgplom/ledger.py b/src/ledgplom/ledger.py index e5ed20f..e84fabc 100644 --- a/src/ledgplom/ledger.py +++ b/src/ledgplom/ledger.py @@ -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.'