home · contact · privacy
Generate Box from Collections, count Collections as additive/subtractive/neither.
authorPlom Heller <plom@plomlompom.com>
Wed, 29 Apr 2026 22:03:13 +0000 (00:03 +0200)
committerPlom Heller <plom@plomlompom.com>
Wed, 29 Apr 2026 22:03:13 +0000 (00:03 +0200)
bricksplom.py

index fc1c2c737d0e7e0a8113e6688882ea655ed1a316..7018671a7c67b725dda99af6e45dd17d43feba5e 100755 (executable)
@@ -4,7 +4,6 @@ from abc import ABC, abstractmethod
 from pathlib import Path
 from typing import Callable, Optional, Self
 
-PATH_BOXES = 'boxes.txt'
 PATH_COLLECTIONS = 'collections.txt'
 PATH_COLORS = 'colors.txt'
 PATH_DESIGNS = 'designs.txt'
@@ -17,10 +16,16 @@ CHAR_MINUS = '-'
 CHAR_COMMA = ','
 CHAR_EQ = '='
 CHAR_UNDER = '_'
-CHAR_COMMENT = '#'
-CHAR_SEPARATOR_COLUMN = '-'
+CHAR_HASH = '#'
+CHAR_IN = '+'
+CHAR_OUT = '-'
+CHAR_INACTIVE = CHAR_HASH
+CHAR_COMMENT = CHAR_HASH
+CHAR_SEPARATOR_COLUMN = CHAR_MINUS
 CHAR_SEPARATOR_PAGE = CHAR_EQ
 
+BOX_PREFIX = 'box:'
+
 PieceListing = tuple[int, str, str]
 PageColumn = tuple[PieceListing, ...]
 Page = tuple[PageColumn, ...]
@@ -192,10 +197,12 @@ class Collection(Textfiled):
     def __init__(
             self,
             id_: str,
+            is_in: Optional[bool],
             description: str,
             piece_listings: tuple[Page, ...]
             ) -> None:
         self.id_ = id_
+        self.is_in = is_in
         self.description = description
         self.piece_listings = piece_listings
 
@@ -204,13 +211,19 @@ class Collection(Textfiled):
             cls,
             path: str
             ) -> dict[str, Self]:
-        collected: dict[str, tuple[str, list[list[list[PieceListing]]]]] = {}
+        collected: dict[str, tuple[Optional[bool],
+                                   str,
+                                   list[list[list[PieceListing]]]]]
+        collected = {}
         i_listings: list[list[list[PieceListing]]] = [[[]]]
         for line in cls.lines_of(path):
             if not line.startswith(CHAR_SPACE):
-                id_, description = cls.tokify(line, 2)
+                id_, is_in_str, description = cls.tokify(line, 3)
+                assert is_in_str in {CHAR_IN, CHAR_OUT, CHAR_INACTIVE}
+                is_in = (None if is_in_str == CHAR_INACTIVE
+                         else is_in_str == CHAR_IN)
                 i_listings = [[[]]]
-                collected[id_] = description, i_listings
+                collected[id_] = is_in, description, i_listings
             elif line[1:2] == CHAR_SEPARATOR_COLUMN:
                 i_listings[-1] += [[]]
             elif line[1:2] == CHAR_SEPARATOR_PAGE:
@@ -223,15 +236,18 @@ class Collection(Textfiled):
                 assert len(id_) > 0
                 i_listings[-1][-1] += [(int(count), id_, comment)]
         return {
-            k: cls(k, v[0], tuple(tuple(tuple(column) for column in page)
-                                  for page in v[1]))
+            k: cls(k, v[0], v[1], tuple(tuple(tuple(column) for column in page)
+                                        for page in v[2]))
             for k, v in collected.items()}
 
     def __str__(
             self
             ) -> str:
-        return f'\n{self.id_} {self.description}\n' + self._format_paginated(
-                lambda count, p_id, comment: f' {count:2} {p_id:>7} {comment}')
+        is_in_str = (CHAR_INACTIVE if self.is_in is None
+                     else (CHAR_IN if self.is_in else CHAR_OUT))
+        return (f'\n{self.id_} {is_in_str} {self.description}\n'
+                + self._format_paginated(lambda count, p_id, comment:
+                                         f' {count:2} {p_id:>7} {comment}'))
 
     def _format_paginated(
             self,
@@ -303,7 +319,7 @@ class Collection(Textfiled):
             print(line)
 
 
-class Box(Textfiled):
+class Box:
     'Order of designs.'
 
     def __init__(
@@ -315,13 +331,27 @@ class Box(Textfiled):
         self.designs = designs
 
     @classmethod
-    def from_textfile(
+    def from_collections(
             cls,
-            path: str
+            collections: dict[str, 'Collection'],
+            pieces: dict[str, 'Piece'],
+            designs: dict[str, 'Design']
             ) -> dict[str, Self]:
-        return {id_: cls(id_, tuple(order.split(CHAR_COMMA)))
-                for id_, order in [cls.tokify(line, 2)
-                                   for line in cls.lines_of(path)]}
+        'Parse from "box:"-prefixed entries in collections.'
+        collected = {}
+        for coll in [c for c in collections.values()
+                     if c.id_.startswith(BOX_PREFIX)]:
+            box_id = coll.id_[len(BOX_PREFIX):]
+            assert box_id
+            boxed_designs: list[str] = []
+            for piece_id in [t[1] for t in coll.piece_listings_flat()]:
+                design = Design.possibly_by_alt(pieces[piece_id].design_id,
+                                                designs)
+                assert design
+                if [design.id_] != boxed_designs[-1:]:
+                    boxed_designs += [design.id_]
+            collected[box_id] = cls(box_id, tuple(boxed_designs))
+        return collected
 
     def __str__(
             self
@@ -333,9 +363,10 @@ class Box(Textfiled):
             pieces: dict[str, 'Piece'],
             designs: dict[str, 'Design'],
             colors: dict[str, 'Color'],
-            collections: dict[str, 'Collections']
+            collections: dict[str, 'Collection']
             ) -> None:
         'Print explanatory listings of pieces expected by .designs.'
+        print(f'box:{self.id_} - storage box')
         for design_id in self.designs:
             design = designs[design_id]
             print(f'=== {design_id:>6}: {design.description} ===')
@@ -394,6 +425,17 @@ def check_consistencies_between_tables(
     for color_id in [v.color_id for v in pieces.values()]:
         assert color_id in colors, color_id
 
+    # check collections in-out directions even out
+    counts: dict[str, int] = {}
+    for coll in collections.values():
+        if coll.is_in is None:
+            continue
+        direction = 1 if coll.is_in else (-1)
+        for count, piece_id, _ in coll.piece_listings_flat():
+            counts[piece_id] = counts.get(piece_id, 0) + (direction * count)
+    for piece_id, count in counts.items():
+        assert count == 0, (piece_id, count)
+
 
 def main(
         ) -> None:
@@ -401,7 +443,7 @@ def main(
     pieces = Piece.from_textfile(PATH_PIECES)
     collections = Collection.from_textfile(PATH_COLLECTIONS)
     designs = Design.from_textfile(PATH_DESIGNS)
-    boxes = Box.from_textfile(PATH_BOXES)
+    boxes = Box.from_collections(collections, pieces, designs)
     check_consistencies_between_tables(colors, pieces, collections, designs,
                                        boxes)
     for title, items in (('COLORS', colors),