From 2578fdca75530d0b11349df08fd98d0d6f05f484 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= <oddstr13@openshell.no>
Date: Tue, 2 Nov 2021 23:43:16 +0100
Subject: [PATCH] Fancy text tables

---
 .gitignore         |   1 +
 draw_memory_map.py |  65 ++++++++++++++++++++++++++
 table_drawing.py   | 113 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 179 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 draw_memory_map.py
 create mode 100644 table_drawing.py

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f0b7d96
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.py[ocd]
diff --git a/draw_memory_map.py b/draw_memory_map.py
new file mode 100644
index 0000000..5139033
--- /dev/null
+++ b/draw_memory_map.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+from typing import Iterable
+
+from table_drawing import table
+
+
+def memory_table(data: Iterable[int], start: int = 0, wordsize: int = 2):
+    data_iter = iter(data)
+    data_rows = []
+    position = start
+    try:
+        while True:
+            try:
+                row_id = position
+                row = []
+                for _ in range(16):
+                    try:
+                        cell = []
+                        for _ in range(wordsize):
+                            d = data_iter.__next__() & 0xFF
+                            cell.append(d)
+                            position += 1
+                    finally:
+                        if cell:
+                            row.append(cell)
+            finally:
+                if row:
+                    row_hex = [" ".join([f"{n:02X}" for n in cell]) for cell in row]
+                    row_ascii = [
+                        " ".join(
+                            [
+                                " " + chr(n) if chr(n).isprintable() else "  "
+                                for n in cell
+                            ]
+                        )
+                        for cell in row
+                    ]
+                    row_head = f"{row_id:04X}"[:3] + "·"
+                    data_rows.append([row_head] + row_hex)
+                    if " ".join(row_ascii).strip():
+                        data_rows.append([""] + row_ascii)
+    except StopIteration:
+        pass
+
+    headers = [""] + [
+        ("" if wordsize % 2 else " ") + "···{:01X}".format(x) for x in range(16)
+    ]
+    return table(
+        data_rows,
+        headers=headers,
+        justify="^",
+        header_interval=8 * 2,
+    )
+
+
+data = list(range(256))
+
+
+print(memory_table(data, wordsize=1))
+
+#
+# rows = [[f"{x:03X}·"] for x in range(32)]
+
+
+#
diff --git a/table_drawing.py b/table_drawing.py
new file mode 100644
index 0000000..c44f7f5
--- /dev/null
+++ b/table_drawing.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+from typing import Collection, Optional, Union
+
+C_UL = "┌"
+C_UR = "┐"
+C_LL = "└"
+C_LR = "┘"
+
+C_V = "│"
+C_VL = "├"
+C_VR = "┤"
+
+C_H = "─"
+C_HD = "┬"
+C_HU = "┴"
+C_X = "┼"
+
+
+def row(
+    fields: Collection[str],
+    sep: str = C_V,
+    start: str = C_V,
+    end: str = C_V,
+) -> str:
+    res = []
+
+    for i, field in enumerate(fields):
+        if not i:
+            res.append(start)
+        else:
+            res.append(sep)
+
+        res.append(field)
+
+    res.append(end)
+
+    return "".join(res)
+
+
+def table(
+    data: Collection[Collection[str]],
+    headers: Optional[Collection] = None,
+    header_interval: Optional[int] = None,
+    justify: Optional[Union[str, Collection[str]]] = None,
+):
+    res = []
+
+    if not data:
+        return ""
+
+    columns = max([len(x) for x in data])
+    if headers:
+        columns = max(columns, len(headers))
+
+    if isinstance(justify, str):
+        justify = [justify] * columns
+    elif not justify:
+        justify = []
+
+    justify = list(justify) + ["^"] * (columns - len(justify))
+
+    colwidths = [0] * columns
+
+    if headers:
+        for i, col in enumerate(headers):
+            colwidths[i] = max(colwidths[i], len(col))
+
+    for data_row in data:
+        for i, col in enumerate(data_row):
+            colwidths[i] = max(colwidths[i], len(col))
+
+    def row_iterable():
+        if headers and not header_interval:
+            yield headers
+
+        for i, data_row in enumerate(data):
+            if header_interval and i % header_interval == 0:
+                yield headers
+            yield data_row
+
+    for i, data_row in enumerate(row_iterable()):
+        if not i:
+            res.append(
+                row([C_H * colw for colw in colwidths], start=C_UL, sep=C_HD, end=C_UR)
+            )
+        else:
+            res.append(
+                row([C_H * colw for colw in colwidths], start=C_VL, sep=C_X, end=C_VR)
+            )
+        padding = [""] * (columns - len(data_row))
+        padded_row = [
+            f"{cell:{justify[j]}{colwidths[j]}s}"
+            for j, cell in enumerate(list(data_row) + padding)
+        ]
+        res.append(row(padded_row))
+
+    res.append(row([C_H * colw for colw in colwidths], start=C_LL, sep=C_HU, end=C_LR))
+
+    return "\n".join(res)
+
+
+if __name__ == "__main__":
+    headers = [""] + ["···{:01X}".format(x) for x in range(16)]
+    data = [[f"{x:03X}·"] for x in range(32)]
+
+    print(
+        table(
+            data,
+            headers=headers,
+            justify=">",
+            header_interval=8,
+        )
+    )