modm_data.html.table

  1# Copyright 2022, Niklas Hauser
  2# SPDX-License-Identifier: MPL-2.0
  3
  4import re
  5from functools import cached_property
  6from collections import defaultdict
  7from .text import replace as html_replace, ReDict, Text
  8
  9
 10class Cell(Text):
 11    def __init__(self, x: int, y: int, xs: int=1, ys: int=1, head: bool=False):
 12        super().__init__()
 13        self._span = (xs, ys)
 14        self._pos = [(x, y)]
 15        self._head = head
 16
 17    @property
 18    def span(self) -> tuple[int,int]:
 19        return self._span
 20
 21    @property
 22    def positions(self) -> list[tuple[int,int]]:
 23        return self._pos
 24
 25    @property
 26    def x(self) -> int:
 27        return self._pos[0][0]
 28
 29    @property
 30    def y(self) -> int:
 31        return self._pos[0][1]
 32
 33    @property
 34    def xspan(self) -> int:
 35        return self._span[0]
 36
 37    @property
 38    def yspan(self) -> int:
 39        return self._span[1]
 40
 41    def __hash__(self) -> int:
 42        return hash(str(self._pos))
 43
 44    def __repr__(self) -> str:
 45        return f"Cell({','.join(map(str, self._pos))})"
 46
 47
 48class Domains:
 49    def __init__(self, table, domains, columns, pattern=None):
 50        self._table = table
 51        self._domains = domains
 52        self._columns = columns
 53        self._pattern = pattern
 54
 55    def domains_y(self, pattern=None, **subs) -> list[str]:
 56        domains = sorted(self._table._domains_y(self._columns, **subs).keys())
 57        if pattern is not None:
 58            domains = [d for d in domains if re.search(pattern, d, re.IGNORECASE)]
 59        return domains
 60
 61    def cells(self, pattern_x: str, pattern_y: str = None, **subs) -> list[Cell]:
 62        domains_y = self.domains_y(pattern_y, **subs)
 63        domains_x = self._table.domains_x(pattern_x, **subs)
 64        cells = defaultdict(lambda: defaultdict(set))
 65        for dom_y in domains_y:
 66            for dom_x in domains_x:
 67                for x in self._table._domains_x(**subs)[dom_x]:
 68                    for y in self._table._domains_y(self._columns, **subs)[dom_y]:
 69                        # print(x, y, dom_x, dom_y)
 70                        cells[dom_y][dom_x].add(self._table.cell(x, y))
 71
 72        return ReDict({k:ReDict({vk:list(vv) for vk,vv in v.items()}) for k,v in cells.items()})
 73
 74    def __repr__(self) -> str:
 75        return f"Domains({self.domains_x()}, {self.domains_y()})"
 76
 77
 78class Table:
 79    def __init__(self, heading=None):
 80        self._heading = heading or Heading("")
 81        self._cells = []
 82        self._size = (0,0)
 83        self._grid = None
 84        self._hrows = 0
 85        self._caption = Text("")
 86
 87    def __repr__(self) -> str:
 88        return f"Table({self.columns}×{self.rows})"
 89
 90    def heading(self, **filters):
 91        return self._heading.text(**filters)
 92
 93    def caption(self, **filters):
 94        return self._caption.text(**filters)
 95
 96    def _domains_x(self, **subs) -> dict[str, list[int]]:
 97        domains = defaultdict(list)
 98        for x in range(self.columns):
 99            cell = None
100            domain = []
101            for y in range(self._hrows):
102                if (ncell := self.cell(x, y)) != cell:
103                    cell = ncell
104                    domain.append(cell)
105            domain = ":".join(cell.text(**subs).replace(":", "") for cell in domain)
106            domains[domain].append(x)
107        return dict(domains)
108
109    def _domains_y(self, columns: list[int], **subs) -> dict[str, list[int]]:
110        domains = defaultdict(list)
111        for y in range(self._hrows, self.rows):
112            cell = None
113            cells = []
114            for x in columns:
115                if (ncell := self.cell(x, y)) != cell:
116                    cell = ncell
117                    cells.append(cell)
118            if cells:
119                domain = ":".join(cell.text(**subs).replace(":", "") for cell in cells)
120                domains[domain].append(y)
121        return dict(domains)
122
123    def domains_x(self, pattern=None, **subs) -> list[str]:
124        domains = sorted(self._domains_x(**subs).keys())
125        if pattern is not None:
126            domains = [d for d in domains if re.search(pattern, d, re.IGNORECASE)]
127        return domains
128
129    def domains(self, pattern: str, **subs) -> Domains:
130        domains = []
131        columns = []
132        for domain, cols in self._domains_x(**subs).items():
133            if re.search(pattern, domain, re.IGNORECASE):
134                domains.append(domain)
135                columns.extend(cols)
136        return Domains(self, domains, columns, pattern)
137
138    def cell_rows(self, pattern: str = None, **subs) -> dict[str, list[Cell]]:
139        columns = []
140        for domain, cols in self._domains_x(**subs).items():
141            if pattern is None or re.search(pattern, domain, re.IGNORECASE):
142                columns.append((domain, cols))
143        for y in range(self._hrows, self.rows):
144            values = defaultdict(list)
145            for domain, cols in columns:
146                values[domain].extend(self.cell(c, y) for c in cols)
147            yield ReDict(values)
148
149    def cell(self, x: int, y: int) -> Cell:
150        assert x < self.columns
151        assert y < self.rows
152        return self._grid[y][x]
153
154    @property
155    def columns(self) -> int:
156        return self._size[0]
157
158    @property
159    def rows(self) -> int:
160        return self._size[1]
161
162    @property
163    def size(self) -> tuple[int,int]:
164        return self._size
165
166    def cell(self, x, y) -> Cell:
167        return self._grid[y][x]
168
169    def _normalize(self):
170        xsize = sum(c._span[0] for c in self._cells if c._pos[0][1] == 0)
171        ysize = max(c._pos[0][1] + c._span[1] for c in self._cells)
172        self._size = (xsize, ysize)
173        self._grid = [[None for _ in range(xsize)] for _ in range(ysize)]
174
175        xpos, ypos = 0, 0
176        for cell in self._cells:
177            # print(cell._pos, cell._span, cell._data)
178            ypos = cell._pos[0][1]
179            if cell._head:
180                self._hrows = ypos + 1
181            cell._pos = []
182            # Previous cells with multiple rows may already have taken our current xpos
183            # We must find the next cell that's still empty
184            xpos = next((x for x, c in enumerate(self._grid[ypos]) if c is None), xpos)
185            for y in range(ypos, min(ypos + cell._span[1], ysize)):
186                for x in range(xpos, min(xpos + cell._span[0], xsize)):
187                    self._grid[y][x] = cell
188                    cell._pos.append((x, y))
189            xpos += cell._span[0]
190
191    def render(self):
192        for y in range(self.rows):
193            for x in range(self.columns):
194                print(self.cell(x, y).text()[:15] if self.cell(x, y) is not None else "None", end="\t")
195            print()
class Cell(modm_data.html.text.Text):
11class Cell(Text):
12    def __init__(self, x: int, y: int, xs: int=1, ys: int=1, head: bool=False):
13        super().__init__()
14        self._span = (xs, ys)
15        self._pos = [(x, y)]
16        self._head = head
17
18    @property
19    def span(self) -> tuple[int,int]:
20        return self._span
21
22    @property
23    def positions(self) -> list[tuple[int,int]]:
24        return self._pos
25
26    @property
27    def x(self) -> int:
28        return self._pos[0][0]
29
30    @property
31    def y(self) -> int:
32        return self._pos[0][1]
33
34    @property
35    def xspan(self) -> int:
36        return self._span[0]
37
38    @property
39    def yspan(self) -> int:
40        return self._span[1]
41
42    def __hash__(self) -> int:
43        return hash(str(self._pos))
44
45    def __repr__(self) -> str:
46        return f"Cell({','.join(map(str, self._pos))})"
Cell(x: int, y: int, xs: int = 1, ys: int = 1, head: bool = False)
12    def __init__(self, x: int, y: int, xs: int=1, ys: int=1, head: bool=False):
13        super().__init__()
14        self._span = (xs, ys)
15        self._pos = [(x, y)]
16        self._head = head
span: tuple[int, int]
18    @property
19    def span(self) -> tuple[int,int]:
20        return self._span
positions: list[tuple[int, int]]
22    @property
23    def positions(self) -> list[tuple[int,int]]:
24        return self._pos
x: int
26    @property
27    def x(self) -> int:
28        return self._pos[0][0]
y: int
30    @property
31    def y(self) -> int:
32        return self._pos[0][1]
xspan: int
34    @property
35    def xspan(self) -> int:
36        return self._span[0]
yspan: int
38    @property
39    def yspan(self) -> int:
40        return self._span[1]
Inherited Members
modm_data.html.text.Text
html
text
class Domains:
49class Domains:
50    def __init__(self, table, domains, columns, pattern=None):
51        self._table = table
52        self._domains = domains
53        self._columns = columns
54        self._pattern = pattern
55
56    def domains_y(self, pattern=None, **subs) -> list[str]:
57        domains = sorted(self._table._domains_y(self._columns, **subs).keys())
58        if pattern is not None:
59            domains = [d for d in domains if re.search(pattern, d, re.IGNORECASE)]
60        return domains
61
62    def cells(self, pattern_x: str, pattern_y: str = None, **subs) -> list[Cell]:
63        domains_y = self.domains_y(pattern_y, **subs)
64        domains_x = self._table.domains_x(pattern_x, **subs)
65        cells = defaultdict(lambda: defaultdict(set))
66        for dom_y in domains_y:
67            for dom_x in domains_x:
68                for x in self._table._domains_x(**subs)[dom_x]:
69                    for y in self._table._domains_y(self._columns, **subs)[dom_y]:
70                        # print(x, y, dom_x, dom_y)
71                        cells[dom_y][dom_x].add(self._table.cell(x, y))
72
73        return ReDict({k:ReDict({vk:list(vv) for vk,vv in v.items()}) for k,v in cells.items()})
74
75    def __repr__(self) -> str:
76        return f"Domains({self.domains_x()}, {self.domains_y()})"
Domains(table, domains, columns, pattern=None)
50    def __init__(self, table, domains, columns, pattern=None):
51        self._table = table
52        self._domains = domains
53        self._columns = columns
54        self._pattern = pattern
def domains_y(self, pattern=None, **subs) -> list[str]:
56    def domains_y(self, pattern=None, **subs) -> list[str]:
57        domains = sorted(self._table._domains_y(self._columns, **subs).keys())
58        if pattern is not None:
59            domains = [d for d in domains if re.search(pattern, d, re.IGNORECASE)]
60        return domains
def cells( self, pattern_x: str, pattern_y: str = None, **subs) -> list[Cell]:
62    def cells(self, pattern_x: str, pattern_y: str = None, **subs) -> list[Cell]:
63        domains_y = self.domains_y(pattern_y, **subs)
64        domains_x = self._table.domains_x(pattern_x, **subs)
65        cells = defaultdict(lambda: defaultdict(set))
66        for dom_y in domains_y:
67            for dom_x in domains_x:
68                for x in self._table._domains_x(**subs)[dom_x]:
69                    for y in self._table._domains_y(self._columns, **subs)[dom_y]:
70                        # print(x, y, dom_x, dom_y)
71                        cells[dom_y][dom_x].add(self._table.cell(x, y))
72
73        return ReDict({k:ReDict({vk:list(vv) for vk,vv in v.items()}) for k,v in cells.items()})
class Table:
 79class Table:
 80    def __init__(self, heading=None):
 81        self._heading = heading or Heading("")
 82        self._cells = []
 83        self._size = (0,0)
 84        self._grid = None
 85        self._hrows = 0
 86        self._caption = Text("")
 87
 88    def __repr__(self) -> str:
 89        return f"Table({self.columns}×{self.rows})"
 90
 91    def heading(self, **filters):
 92        return self._heading.text(**filters)
 93
 94    def caption(self, **filters):
 95        return self._caption.text(**filters)
 96
 97    def _domains_x(self, **subs) -> dict[str, list[int]]:
 98        domains = defaultdict(list)
 99        for x in range(self.columns):
100            cell = None
101            domain = []
102            for y in range(self._hrows):
103                if (ncell := self.cell(x, y)) != cell:
104                    cell = ncell
105                    domain.append(cell)
106            domain = ":".join(cell.text(**subs).replace(":", "") for cell in domain)
107            domains[domain].append(x)
108        return dict(domains)
109
110    def _domains_y(self, columns: list[int], **subs) -> dict[str, list[int]]:
111        domains = defaultdict(list)
112        for y in range(self._hrows, self.rows):
113            cell = None
114            cells = []
115            for x in columns:
116                if (ncell := self.cell(x, y)) != cell:
117                    cell = ncell
118                    cells.append(cell)
119            if cells:
120                domain = ":".join(cell.text(**subs).replace(":", "") for cell in cells)
121                domains[domain].append(y)
122        return dict(domains)
123
124    def domains_x(self, pattern=None, **subs) -> list[str]:
125        domains = sorted(self._domains_x(**subs).keys())
126        if pattern is not None:
127            domains = [d for d in domains if re.search(pattern, d, re.IGNORECASE)]
128        return domains
129
130    def domains(self, pattern: str, **subs) -> Domains:
131        domains = []
132        columns = []
133        for domain, cols in self._domains_x(**subs).items():
134            if re.search(pattern, domain, re.IGNORECASE):
135                domains.append(domain)
136                columns.extend(cols)
137        return Domains(self, domains, columns, pattern)
138
139    def cell_rows(self, pattern: str = None, **subs) -> dict[str, list[Cell]]:
140        columns = []
141        for domain, cols in self._domains_x(**subs).items():
142            if pattern is None or re.search(pattern, domain, re.IGNORECASE):
143                columns.append((domain, cols))
144        for y in range(self._hrows, self.rows):
145            values = defaultdict(list)
146            for domain, cols in columns:
147                values[domain].extend(self.cell(c, y) for c in cols)
148            yield ReDict(values)
149
150    def cell(self, x: int, y: int) -> Cell:
151        assert x < self.columns
152        assert y < self.rows
153        return self._grid[y][x]
154
155    @property
156    def columns(self) -> int:
157        return self._size[0]
158
159    @property
160    def rows(self) -> int:
161        return self._size[1]
162
163    @property
164    def size(self) -> tuple[int,int]:
165        return self._size
166
167    def cell(self, x, y) -> Cell:
168        return self._grid[y][x]
169
170    def _normalize(self):
171        xsize = sum(c._span[0] for c in self._cells if c._pos[0][1] == 0)
172        ysize = max(c._pos[0][1] + c._span[1] for c in self._cells)
173        self._size = (xsize, ysize)
174        self._grid = [[None for _ in range(xsize)] for _ in range(ysize)]
175
176        xpos, ypos = 0, 0
177        for cell in self._cells:
178            # print(cell._pos, cell._span, cell._data)
179            ypos = cell._pos[0][1]
180            if cell._head:
181                self._hrows = ypos + 1
182            cell._pos = []
183            # Previous cells with multiple rows may already have taken our current xpos
184            # We must find the next cell that's still empty
185            xpos = next((x for x, c in enumerate(self._grid[ypos]) if c is None), xpos)
186            for y in range(ypos, min(ypos + cell._span[1], ysize)):
187                for x in range(xpos, min(xpos + cell._span[0], xsize)):
188                    self._grid[y][x] = cell
189                    cell._pos.append((x, y))
190            xpos += cell._span[0]
191
192    def render(self):
193        for y in range(self.rows):
194            for x in range(self.columns):
195                print(self.cell(x, y).text()[:15] if self.cell(x, y) is not None else "None", end="\t")
196            print()
Table(heading=None)
80    def __init__(self, heading=None):
81        self._heading = heading or Heading("")
82        self._cells = []
83        self._size = (0,0)
84        self._grid = None
85        self._hrows = 0
86        self._caption = Text("")
def heading(self, **filters):
91    def heading(self, **filters):
92        return self._heading.text(**filters)
def caption(self, **filters):
94    def caption(self, **filters):
95        return self._caption.text(**filters)
def domains_x(self, pattern=None, **subs) -> list[str]:
124    def domains_x(self, pattern=None, **subs) -> list[str]:
125        domains = sorted(self._domains_x(**subs).keys())
126        if pattern is not None:
127            domains = [d for d in domains if re.search(pattern, d, re.IGNORECASE)]
128        return domains
def domains(self, pattern: str, **subs) -> Domains:
130    def domains(self, pattern: str, **subs) -> Domains:
131        domains = []
132        columns = []
133        for domain, cols in self._domains_x(**subs).items():
134            if re.search(pattern, domain, re.IGNORECASE):
135                domains.append(domain)
136                columns.extend(cols)
137        return Domains(self, domains, columns, pattern)
def cell_rows( self, pattern: str = None, **subs) -> dict[str, list[Cell]]:
139    def cell_rows(self, pattern: str = None, **subs) -> dict[str, list[Cell]]:
140        columns = []
141        for domain, cols in self._domains_x(**subs).items():
142            if pattern is None or re.search(pattern, domain, re.IGNORECASE):
143                columns.append((domain, cols))
144        for y in range(self._hrows, self.rows):
145            values = defaultdict(list)
146            for domain, cols in columns:
147                values[domain].extend(self.cell(c, y) for c in cols)
148            yield ReDict(values)
def cell(self, x, y) -> Cell:
167    def cell(self, x, y) -> Cell:
168        return self._grid[y][x]
columns: int
155    @property
156    def columns(self) -> int:
157        return self._size[0]
rows: int
159    @property
160    def rows(self) -> int:
161        return self._size[1]
size: tuple[int, int]
163    @property
164    def size(self) -> tuple[int,int]:
165        return self._size
def render(self):
192    def render(self):
193        for y in range(self.rows):
194            for x in range(self.columns):
195                print(self.cell(x, y).text()[:15] if self.cell(x, y) is not None else "None", end="\t")
196            print()