Скрытое в таблице не парсим
Правило: если строка или столбец в таблице скрыт, его данные не попадают в расписание — раньше скрытые столбцы пропускались только при поиске групп. - xlsx: у листа появился набор скрытых строк (Worksheet.hidden_rows); - расписание, недели: скрытые строки пропускаются; - аудитории и справочник ФИО: скрытые столбцы не читаются вовсе (убрана двухпроходная логика «сначала видимые, потом скрытые»); - тесты: билдер умеет скрытые строки, добавлены проверки «скрытое не парсится». Проверено на таблице: группы и всё расписание совпали байт в байт (скрытых строк нет), из аудиторий ушли только те, что лежат в скрытых столбцах (402, 405, 415), справочник ФИО стал короче на 2 скрытые записи, у 223 осталось описание из видимого столбца (20 мест, интерактивная панель). Тесты: 171.
This commit is contained in:
@@ -83,7 +83,9 @@ def extract_week_ranges(sheets: dict[str, Worksheet]) -> list[dict]:
|
||||
return []
|
||||
ranges: list[dict] = []
|
||||
seen: set[tuple[str, date]] = set()
|
||||
for row in sheet.rows[1:]:
|
||||
for row_idx, row in enumerate(sheet.rows[1:], start=1):
|
||||
if row_idx in sheet.hidden_rows:
|
||||
continue # скрытые строки не парсим
|
||||
if not any(row[i] for i in range(4)):
|
||||
continue
|
||||
for parity, start_col, end_col in (("ч", 0, 1), ("з", 2, 3)):
|
||||
@@ -148,7 +150,9 @@ def parse_schedule(sheets: dict[str, Worksheet]) -> dict:
|
||||
parity: str | None = None
|
||||
rows_in_day = 0
|
||||
|
||||
for row in rows[1:]:
|
||||
for row_idx, row in enumerate(rows[1:], start=1):
|
||||
if row_idx in sheet.hidden_rows:
|
||||
continue # скрытые строки не парсим
|
||||
if not any(row[i] for i in range(4)):
|
||||
continue
|
||||
day = _normalize_header(row[_COL_DAY]).lower()
|
||||
@@ -485,7 +489,7 @@ def parse_rooms_info(sheets: dict[str, Worksheet]) -> dict:
|
||||
return
|
||||
label = f"ауд. {m.group(1)}"
|
||||
if label in info:
|
||||
return # видимый столбец важнее скрытого дубля с тем же номером
|
||||
return # дубль с тем же номером: первый столбец важнее
|
||||
text = _strip_marks(
|
||||
" ".join(ln.strip() for ln in str(desc[col]).split("\n") if ln.strip())
|
||||
)
|
||||
@@ -500,12 +504,8 @@ def parse_rooms_info(sheets: dict[str, Worksheet]) -> dict:
|
||||
"available_days": room_weekdays(text),
|
||||
}
|
||||
|
||||
columns = range(_FIRST_GROUP_COL, max(len(header), len(desc)))
|
||||
for col in columns: # сначала видимые
|
||||
if col not in sheet.hidden_cols:
|
||||
add(col)
|
||||
for col in columns: # потом скрытые: аудитория без видимого столбца тоже нужна
|
||||
if col in sheet.hidden_cols:
|
||||
for col in range(_FIRST_GROUP_COL, max(len(header), len(desc))):
|
||||
if col not in sheet.hidden_cols: # скрытое не парсим
|
||||
add(col)
|
||||
return info
|
||||
|
||||
@@ -541,6 +541,8 @@ def parse_teachers_full(sheets: dict[str, Worksheet]) -> dict:
|
||||
header = sheet.rows[1]
|
||||
result: dict = {}
|
||||
for col in range(_FIRST_GROUP_COL, len(header)):
|
||||
if col in sheet.hidden_cols: # скрытое не парсим
|
||||
continue
|
||||
value = _strip_marks(str(header[col]))
|
||||
parts = [p.strip() for p in re.split(r"[\s\n]+", value) if p.strip()]
|
||||
if not parts:
|
||||
|
||||
@@ -14,7 +14,7 @@ import io
|
||||
import re
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from .errors import ScheduleError
|
||||
@@ -32,6 +32,7 @@ _REF_RE = re.compile(r"^([A-Z]+)(\d+)$")
|
||||
class Worksheet:
|
||||
rows: list[list[str]]
|
||||
hidden_cols: set[int]
|
||||
hidden_rows: set[int] = field(default_factory=set)
|
||||
|
||||
def cell(self, row_idx: int, col_idx: int) -> str:
|
||||
"""Значение ячейки, "" вне границ матрицы."""
|
||||
@@ -135,11 +136,14 @@ def _parse_sheet(xml_bytes: bytes, shared: list[str]) -> Worksheet:
|
||||
if col.get("hidden") == "1":
|
||||
hidden_cols.update(range(c_min - 1, c_max))
|
||||
|
||||
hidden_rows: set[int] = set()
|
||||
for row in root.iter(f"{{{XLSX_NS}}}row"):
|
||||
row_num = row.get("r")
|
||||
if not row_num:
|
||||
continue
|
||||
r = int(row_num) - 1
|
||||
if row.get("hidden") == "1":
|
||||
hidden_rows.add(r)
|
||||
for cell in row.iter(f"{{{XLSX_NS}}}c"):
|
||||
ref = cell.get("r")
|
||||
if not ref:
|
||||
@@ -194,7 +198,7 @@ def _parse_sheet(xml_bytes: bytes, shared: list[str]) -> Worksheet:
|
||||
[cells.get((r, c), "") for c in range(max_col + 1)]
|
||||
for r in range(max_row + 1)
|
||||
]
|
||||
return Worksheet(rows=rows, hidden_cols=hidden_cols)
|
||||
return Worksheet(rows=rows, hidden_cols=hidden_cols, hidden_rows=hidden_rows)
|
||||
|
||||
|
||||
def _iter_text(node: ET.Element):
|
||||
|
||||
@@ -302,7 +302,7 @@ def test_lowercase_except_abbreviations():
|
||||
|
||||
|
||||
def test_room_visible_column_wins_over_hidden_duplicate():
|
||||
"""У 223 в таблице два столбца: видимый (с панелью) и скрытый (без). Берём видимый."""
|
||||
"""Скрытый столбец-дубль (у 223) не парсится вовсе — берём видимый."""
|
||||
from xlsx_builder import SheetSpec
|
||||
|
||||
spec = SheetSpec(
|
||||
@@ -322,8 +322,8 @@ def test_room_visible_column_wins_over_hidden_duplicate():
|
||||
assert room["type"] == "комп.класс linux/windows"
|
||||
|
||||
|
||||
def test_room_only_in_hidden_column_still_parsed():
|
||||
"""Аудитория, у которой есть только скрытый столбец, не пропадает."""
|
||||
def test_room_only_in_hidden_column_not_parsed():
|
||||
"""Скрытое не парсим: аудитория со скрытым столбцом описания не получает."""
|
||||
from xlsx_builder import SheetSpec
|
||||
|
||||
spec = SheetSpec(
|
||||
@@ -334,5 +334,24 @@ def test_room_only_in_hidden_column_still_parsed():
|
||||
hidden_cols=[(6, 7)],
|
||||
)
|
||||
info = parsing.parse_rooms_info(make_sheets({parsing.ROOMS_SHEET: spec}))
|
||||
assert info["ауд. 402"]["available_days"] == ["четверг", "пятница"]
|
||||
assert info["ауд. 410"]["capacity"] == 30
|
||||
assert info == {}
|
||||
|
||||
|
||||
def test_hidden_rows_not_parsed(monkeypatch):
|
||||
"""Скрытые строки листа в расписание не попадают."""
|
||||
from fixtures import fixture_sheets
|
||||
from xlsx_builder import SheetSpec, build_workbook
|
||||
|
||||
sheets = fixture_sheets()
|
||||
main = sheets[parsing.MAIN_SHEET]
|
||||
# строки: 0 заголовок, 1 с/по, 2..5 понедельник, 6..7 вторник — скроем строку с первым вторником
|
||||
spec = SheetSpec(
|
||||
rows=main.rows,
|
||||
merges=main.merges,
|
||||
hidden_cols=main.hidden_cols,
|
||||
hidden_rows=[6],
|
||||
)
|
||||
data = parsing.parse_schedule(make_sheets({parsing.MAIN_SHEET: spec}))
|
||||
tuesday = data["ИВТ-11БО"]["days"].get("вторник", {})
|
||||
assert not tuesday, "строка вторника скрыта — занятий быть не должно"
|
||||
assert data["ИВТ-11БО"]["days"]["понедельник"][1]["ч"] # понедельник на месте
|
||||
|
||||
@@ -28,6 +28,7 @@ class SheetSpec:
|
||||
rows: list[list]
|
||||
merges: list[str] = field(default_factory=list)
|
||||
hidden_cols: list[tuple[int, int]] = field(default_factory=list) # 0-based, инклязивно
|
||||
hidden_rows: list[int] = field(default_factory=list) # 0-based
|
||||
|
||||
|
||||
def build_workbook(sheets: dict[str, SheetSpec]) -> bytes:
|
||||
@@ -83,7 +84,8 @@ def build_workbook(sheets: dict[str, SheetSpec]) -> bytes:
|
||||
t = f' t="{t_attr}"' if t_attr else ""
|
||||
cells.append(f'<c r="{col_letter(c)}{r}"{t}><v>{escape(str(v))}</v></c>')
|
||||
if cells:
|
||||
row_xml.append(f'<row r="{r}">{"".join(cells)}</row>')
|
||||
hidden = ' hidden="1"' if (r - 1) in spec.hidden_rows else ""
|
||||
row_xml.append(f'<row r="{r}"{hidden}>{"".join(cells)}</row>')
|
||||
|
||||
merges_xml = ""
|
||||
if spec.merges:
|
||||
|
||||
Reference in New Issue
Block a user