home · contact · privacy
Count alternates as proper (if dependent) Designs, fix Box item counts. master
authorPlom Heller <plom@plomlompom.com>
Tue, 5 May 2026 00:51:36 +0000 (02:51 +0200)
committerPlom Heller <plom@plomlompom.com>
Tue, 5 May 2026 00:51:36 +0000 (02:51 +0200)
bricksplom.py

index 0806ec440bd92da904f7f59a5dd83f8d59ee21e8..22399927e55e8c4c8fdc25bc3b2dbb92edc4dc1c 100755 (executable)
@@ -148,15 +148,32 @@ class Color(Textfiled, Lookupable):
 
 class Design(Textfiled, Lookupable):
     'Shape and texture configurations with descriptions and equalities.'
+    alternate_to: Optional[Self] = None
 
     def __init__(
             self,
             id_: str,
-            description: str
+            description=''
             ) -> None:
         self.id_ = id_
-        self.description = description
-        self.alternates: set[str] = set()
+        self._description = description
+        self.alternate_ids: set[str] = set()
+
+    @property
+    def description(
+            self
+            ) -> str:
+        'Description text wherever found between self and .alternate_to.'
+        return (f'={self.alternate_to.id_}: {self.alternate_to.description}'
+                if self.alternate_to
+                else self._description)
+
+    @property
+    def all_ids(
+            self
+            ) -> tuple[str, ...]:
+        'Own .id_ plus (sorted) .alternate_ids.'
+        return tuple([self.id_] + sorted(list(self.alternate_ids)))
 
     @classmethod
     def from_textfile(
@@ -175,31 +192,21 @@ class Design(Textfiled, Lookupable):
             if char_type == CHAR_DESIGN_ALT:
                 alts[body] = alts.get(body, set())
                 alts[body].add(design_id)
+                collected[design_id] = cls(design_id)
             else:  # == CHAR_DESIGN_DESC
                 collected[design_id] = cls(design_id, body)
-        for id_, alternatives in alts.items():
-            collected[id_].alternates = alternatives
+        for id_, alternate_ids in alts.items():
+            collected[id_].alternate_ids = alternate_ids
+            for alt_id in alternate_ids:
+                collected[alt_id].alternate_to = collected[id_]
         return collected
 
     def raw(
             self
             ) -> str:
-        return '\n'.join([f'{self.id_:>6} _{self.description}']
-                         + [f'{a:>6} ={self.id_}' for a in self.alternates])
-
-    @classmethod
-    def possibly_by_alt(
-            cls,
-            id_queried: str,
-            designs: dict[str, Self]
-            ) -> Optional[Self]:
-        'In designs find one of id_queried, by way of alternates if necessary.'
-        if id_queried in designs:
-            return designs[id_queried]
-        for design in designs.values():
-            if id_queried in design.alternates:
-                return design
-        return None
+        return f'{self.id_:>6} ' + (f'={self.alternate_to.id_}'
+                                    if self.alternate_to
+                                    else f'_{self._description}')
 
 
 class Piece(Textfiled, WithDb, Lookupable):
@@ -246,10 +253,11 @@ class Piece(Textfiled, WithDb, Lookupable):
             self
             ) -> str:
         assert self._db
-        design = Design.possibly_by_alt(self.design_id, self._db.designs)
-        color = self._db.colors[self.color_id]
+        design = self._db.designs[self.design_id]
+        color = str(self._db.colors[self.color_id]).strip()
         comment = f' # {self.comment}' if self.comment else ''
-        return f'{self.id_:>7} {self.design_id:>6} {design.description} ({str(color).strip()}){comment}'
+        return (f'{self.id_:>7} {self.design_id:>6} '
+                f'{design.description} ({color}){comment}')
 
 
 class Collection(Textfiled, WithDb, Lookupable):
@@ -396,43 +404,48 @@ class Box(WithDb, Lookupable):
     def __init__(
             self,
             id_: str,
-            designs: tuple[str, ...],
+            collection: Collection,
             **kwargs
             ) -> None:
-        self.id_ = id_
-        self.designs = designs
         super().__init__(**kwargs)
+        assert self._db
+        self.id_ = id_
+        self._collection = collection
+        designed_listings: list[tuple[Design, list[PieceListing]]] = []
+        for listing in self._collection.piece_listings_flat():
+            design = self._db.designs[self._db.pieces[listing[1]].design_id]
+            design = design.alternate_to or design
+            if not (designed_listings and designed_listings[-1][0] == design):
+                designed_listings += [(design, []), ]
+            designed_listings[-1][1] += [listing]
+        self.designs_to_listings = tuple((d, tuple(ls))
+                                         for d, ls in designed_listings)
 
     def __str__(
             self
             ) -> str:
-        return f'{self.id_:>2} {",".join(self.designs)}'
+        return (f'{self.id_:>2} '
+                + ', '.join('/'.join(design.all_ids)
+                            for design, _ in self.designs_to_listings))
 
     def show(
             self,
             ) -> str:
         assert self._db
         lines = []
-        for design_id in self.designs:
-            design = self._db.designs[design_id]
-            lines += [f'=== {design_id:>6}: {design.description} ===']
-            for piece in [p for p in self._db.pieces.values()
-                          if p.design_id in {design.id_} | design.alternates]:
-                count_pieces = 0
-                color = self._db.colors[piece.color_id]
-                for listings in [c.piece_listings_flat()
-                                 for c in self._db.collections.values()]:
-                    for count in [t[0] for t in listings if t[1] == piece.id_]:
-                        count_pieces += count
-                lines += [f'{count_pieces:>2}× {piece.id_:>7} / {color}']
+        for design, listings in self.designs_to_listings:
+            lines += [
+                f'=== {" / ".join(design.all_ids)}: {design.description} ===']
+            for count, piece_id, comment in listings:
+                color = self._db.colors[self._db.pieces[piece_id].color_id]
+                lines += [f'{count:>2}× {piece_id:>7} / {color} # {comment}']
         return '\n'.join(lines)
 
     def raw(
             self
             ) -> str:
         'Call .raw() of base Collection.'
-        assert self._db
-        return self._db.collections[f'{BOX_PREFIX}{self.id_}'].raw()
+        return self._collection.raw()
 
 
 class BricksDb:
@@ -456,19 +469,10 @@ class BricksDb:
             ) -> dict[str, Box]:
         'Parsed from BOX_PREFIX-prefixed entries in .collections.'
         collected = {}
-        for coll in [c for c in self.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(
-                        self.pieces[piece_id].design_id,
-                        self.designs)
-                assert design
-                if [design.id_] != boxed_designs[-1:]:
-                    boxed_designs += [design.id_]
-            collected[box_id] = Box(box_id, tuple(boxed_designs), db=self)
+        for collection in [c for c in self.collections.values()
+                           if c.id_.startswith(BOX_PREFIX)]:
+            box_id = collection.id_[len(BOX_PREFIX):]
+            collected[box_id] = Box(box_id, collection, db=self)
         return collected
 
     def _check_consistencies_between_tables(
@@ -482,22 +486,15 @@ class BricksDb:
                              if t[1] not in self.pieces]:
                 fails += [f'missing Piece of ID: {piece_id}']
 
-        # check all designs listed in boxes recorded in designs
-        for design_ids in [b.designs for b in self.boxes().values()]:
-            for design_id in [d_id for d_id in design_ids
-                              if d_id not in self.designs]:
-                fails += [f'missing Design of ID: {design_id}']
-
         # check all pieces' designs recorded in designs
         for d_id in [p.design_id for p in self.pieces.values()
-                     if not Design.possibly_by_alt(p.design_id, self.designs)]:
+                     if p.design_id not in self.designs]:
             fails += [f'missing Design of ID: {d_id}']
 
         # check all recorded designs have matching pieces (at least via alts)
-        for design_id, alts in [(k, v.alternates)
-                                for k, v in self.designs.items()]:
-            if not [id_ for id_ in {design_id} | alts
-                    if id_ in [p.design_id for p in self.pieces.values()]]:
+        for design_id in self.designs:
+            if not [p for p in self.pieces.values()
+                    if p.design_id == design_id]:
                 fails += [f'missing Pieces for design of ID: {design_id}']
 
         # check all pieces' colors are recorded
@@ -543,14 +540,28 @@ class BricksDb:
             self,
             piece_id: str,
             ) -> tuple[str, str]:
-        'For Piece of piece_id, print bosition in .boxes() and description.'
-        design = Design.possibly_by_alt(self.pieces[piece_id].design_id,
-                                        self.designs)
-        assert design is not None
-        description = design.description
-        box = [b for b in self.boxes().values() if design.id_ in b.designs][0]
-        idx_in_box = box.designs.index(design.id_) + 1
-        return f'{box.id_:>2}:{idx_in_box:>2}', description
+        'For Piece of piece_id, print position in .boxes() and description.'
+        design = self.designs[self.pieces[piece_id].design_id]
+        owning_box: Optional[Box] = None
+        for box in self.boxes().values():
+            for idx_in_box in [
+                    idx for idx, d_to_ls in enumerate(box.designs_to_listings)
+                    if piece_id in [listing[1] for listing in d_to_ls[1]]]:
+                owning_box = box
+                break
+            if owning_box:
+                break
+        if not owning_box:
+            for box in self.boxes().values():
+                for idx_in_box in [
+                        idx for idx, t in enumerate(box.designs_to_listings)
+                        if design.id_ in t[0].all_ids]:
+                    owning_box = box
+                    break
+                if owning_box:
+                    break
+        assert owning_box
+        return f'{box.id_:>2}:{idx_in_box:>2}', design.description
 
     def print(
             self