From 63d0c90940101aac594c70af7c3de362f5f37405 Mon Sep 17 00:00:00 2001 From: lhl Date: Sat, 8 Aug 2026 23:03:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20MergeHandler=20+=20TableExtractor?= =?UTF-8?q?=EF=BC=88forward=5Ffill=20/=20=E8=A1=A8=E6=A0=BC=E6=8F=90?= =?UTF-8?q?=E5=8F=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/genesis/parsers/merge_fill.py | 20 +++++++++++ src/genesis/parsers/table_extractor.py | 50 ++++++++++++++++++++++++++ tests/test_table_extractor.py | 40 +++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 src/genesis/parsers/merge_fill.py create mode 100644 src/genesis/parsers/table_extractor.py create mode 100644 tests/test_table_extractor.py diff --git a/src/genesis/parsers/merge_fill.py b/src/genesis/parsers/merge_fill.py new file mode 100644 index 0000000..377e71a --- /dev/null +++ b/src/genesis/parsers/merge_fill.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from typing import Any + + +def forward_fill( + matrix: list[list[Any]], + merged_ranges: list[tuple[int, int, int, int]], +) -> list[list[Any]]: + """合并单元格:用左上角主格值填充范围内全部单元格。""" + out = [list(row) for row in matrix] + for (min_row, min_col, max_row, max_col) in merged_ranges: + if not out or min_row > len(out) or min_col > len(out[min_row - 1]): + continue + main_value = out[min_row - 1][min_col - 1] + for r in range(min_row, min(max_row, len(out)) + 1): + row = out[r - 1] + for c in range(min_col, min(max_col, len(row)) + 1): + row[c - 1] = main_value + return out \ No newline at end of file diff --git a/src/genesis/parsers/table_extractor.py b/src/genesis/parsers/table_extractor.py new file mode 100644 index 0000000..805e140 --- /dev/null +++ b/src/genesis/parsers/table_extractor.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from typing import Any + +from genesis.data_models import CellValue, ExcelTable, Provenance, SheetType + + +def column_letter(index: int) -> str: + """1 → A、27 → AA。""" + letters = "" + while index > 0: + index, rem = divmod(index - 1, 26) + letters = chr(65 + rem) + letters + return letters + + +def extract_table( + sheet_name: str, + matrix: list[list[Any]], + file_name: str, + detected_type: SheetType, + header_row: int = 0, +) -> ExcelTable: + """从矩阵提取表格:首行视为表头,其后为数据行。""" + if not matrix: + return ExcelTable( + name=sheet_name, detected_type=detected_type, + extraction_method="openpyxl", headers=[], rows=[], + ) + headers = [str(c) if c is not None else "" for c in matrix[header_row]] + rows = [] + for r in range(header_row + 1, len(matrix)): + row_dict = {} + for c, h in enumerate(headers): + raw = matrix[r][c] if c < len(matrix[r]) else None + row_dict[h] = CellValue( + value=raw, + provenance=Provenance( + file_name=file_name, + sheet_name=sheet_name, + row=r - header_row, # 数据行号从 1 开始 + column=column_letter(c + 1), + column_header=h, + ), + ) + rows.append(row_dict) + return ExcelTable( + name=sheet_name, detected_type=detected_type, + extraction_method="openpyxl", headers=headers, rows=rows, + ) \ No newline at end of file diff --git a/tests/test_table_extractor.py b/tests/test_table_extractor.py new file mode 100644 index 0000000..36a3cc8 --- /dev/null +++ b/tests/test_table_extractor.py @@ -0,0 +1,40 @@ +from genesis.data_models import CellValue, ExcelTable, SheetType +from genesis.parsers.merge_fill import forward_fill +from genesis.parsers.table_extractor import column_letter, extract_table + + +def test_forward_fill_vertical(): + matrix = [ + ["機能ID", "機能名", "備考"], + ["A001", "", ""], + ["", "", "メモ"], + ] + filled = forward_fill(matrix, [(1, 2, 2, 2)]) # B1:B2 纵向合并 + assert filled[1][1] == "機能名" + assert filled[2][2] == "メモ" + + +def test_forward_fill_horizontal(): + matrix = [["A", "B"], ["x", ""]] + filled = forward_fill(matrix, [(2, 1, 2, 2)]) # A2:B2 横向合并 + assert filled[1][1] == "x" + assert filled[1][0] == "x" + + +def test_column_letter(): + assert column_letter(1) == "A" + assert column_letter(27) == "AA" + + +def test_extract_table_basic(): + matrix = [["ID", "名前"], ["1", "田中"], ["2", "佐藤"]] + table = extract_table("社員一覧", matrix, "f.xlsx", SheetType.FUNCTION) + assert isinstance(table, ExcelTable) + assert table.headers == ["ID", "名前"] + assert table.extraction_method == "openpyxl" + assert len(table.rows) == 2 + first = table.rows[0] + assert isinstance(first["ID"], CellValue) + assert first["ID"].value == "1" + assert first["ID"].provenance.sheet_name == "社員一覧" + assert first["ID"].provenance.column == "A" \ No newline at end of file