File size: 6,511 Bytes
f32c034 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | # TabuLM β tabular data serializer
# Converts CSV / dict-of-records tables into a flat token string
# plus a parallel word_metadata list for row/column tracking.
import csv
import re
from enum import IntEnum
from typing import List, Tuple
# ββ Cell type taxonomy ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class CellType(IntEnum):
PAD = 0 # padding / special tokens with no cell identity
HEADER = 1 # column header row
NUMERIC = 2 # quantity, count, percentage, measurement
TEXT = 3 # free-form text longer than a label
CATEGORICAL = 4 # short label / enum value
DATE = 5 # year or full date
NUM_CELL_TYPES = 6
# ββ Regex heuristics ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_NUMERIC_RE = re.compile(
r'^[\d,.\s]+(%|Frw|RWF|km|kg|ha|m|mΒ²|L|MW|USD|acres|ha)?$',
re.IGNORECASE,
)
_DATE_RE = re.compile(
r'^(1[89]\d{2}|2[012]\d{2})(-\d{2}(-\d{2})?)?$|^\d{1,2}/\d{1,2}/\d{2,4}$'
)
_NULL_RE = re.compile(r'^[-β]$|^n/?a$|^null$|^none$|^$', re.IGNORECASE)
# ββ Core data structures ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TableCell:
"""One cell in a table, with its grid coordinates and semantic type."""
__slots__ = ('content', 'row_id', 'col_id', 'cell_type')
def __init__(self, content: str, row_id: int, col_id: int, cell_type: CellType):
self.content = content.strip()
self.row_id = row_id # 1-based; 1 = header row
self.col_id = col_id # 1-based
self.cell_type = cell_type
def __repr__(self):
return (f'TableCell(r={self.row_id}, c={self.col_id}, '
f'type={self.cell_type.name}, "{self.content[:20]}")')
# ββ Cell type detection βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def detect_cell_type(value: str, is_header: bool = False) -> CellType:
if is_header:
return CellType.HEADER
v = value.strip()
if _NULL_RE.match(v):
return CellType.TEXT
if _DATE_RE.match(v):
return CellType.DATE
if _NUMERIC_RE.match(v):
return CellType.NUMERIC
if len(v) <= 40 and '\n' not in v and ' ' not in v:
return CellType.CATEGORICAL
return CellType.TEXT
# ββ Table loading βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def serialize_csv(filepath: str,
max_rows: int = 64,
max_cols: int = 24) -> List[TableCell]:
"""Read a CSV file and return an ordered list of TableCell objects."""
cells: List[TableCell] = []
try:
with open(filepath, newline='', encoding='utf-8-sig') as f:
rows = [r for r in csv.reader(f) if any(c.strip() for c in r)]
except Exception:
return cells
if not rows:
return cells
header = rows[0][:max_cols]
for col_id, h in enumerate(header, start=1):
cells.append(TableCell(
h or f'col_{col_id}', row_id=1, col_id=col_id,
cell_type=CellType.HEADER,
))
for row_offset, row in enumerate(rows[1: max_rows + 1], start=2):
for col_id, val in enumerate(row[:max_cols], start=1):
cells.append(TableCell(
val, row_id=row_offset, col_id=col_id,
cell_type=detect_cell_type(val),
))
return cells
def serialize_records(records: List[dict],
max_rows: int = 64,
max_cols: int = 24) -> List[TableCell]:
"""Convert a list of dicts (e.g. from pandas .to_dict('records')) to cells."""
if not records:
return []
cells: List[TableCell] = []
keys = list(records[0].keys())[:max_cols]
for col_id, k in enumerate(keys, start=1):
cells.append(TableCell(
str(k), row_id=1, col_id=col_id,
cell_type=CellType.HEADER,
))
for row_offset, rec in enumerate(records[:max_rows], start=2):
for col_id, k in enumerate(keys, start=1):
val = str(rec.get(k, ''))
cells.append(TableCell(
val, row_id=row_offset, col_id=col_id,
cell_type=detect_cell_type(val),
))
return cells
# ββ Serialization βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# WordMeta: (row_id, col_id, cell_type_int)
WordMeta = Tuple[int, int, int]
def table_cells_to_text(
cells: List[TableCell],
) -> Tuple[str, List[WordMeta]]:
"""
Flatten table cells into a single space-separated string with structure
tokens, plus a parallel per-token metadata list.
Special tokens emitted:
[TAB] β start of header row
[ROW] β start of any data row
[CEL] β start of each individual cell
Returns:
text β string ready for morpho_stub.parse_text_stub()
word_meta β list of (row_id, col_id, cell_type) with one entry per
space-separated token in `text` (including special tokens,
which get row_id=0, col_id=0, cell_type=PAD)
"""
parts: List[str] = []
word_meta: List[WordMeta] = []
PAD = (0, 0, int(CellType.PAD))
cur_row = -1
for cell in cells:
if cell.row_id != cur_row:
sep = '[TAB]' if cell.row_id == 1 else '[ROW]'
parts.append(sep)
word_meta.append(PAD)
cur_row = cell.row_id
parts.append('[CEL]')
word_meta.append((cell.row_id, cell.col_id, int(cell.cell_type)))
content_words = cell.content.split() if cell.content else ['[EMPTY]']
if not content_words:
content_words = ['[EMPTY]']
for w in content_words:
parts.append(w)
word_meta.append((cell.row_id, cell.col_id, int(cell.cell_type)))
return ' '.join(parts), word_meta
|