From 2cbb19312e88b44a45b65c8463edf7686634057b Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Sat, 10 Oct 2026 13:26:15 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D1=83=D0=B4=D0=B8=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=B8:=20=D0=BE=D0=B1=D0=BE=D0=B7=D0=BD=D0=B0=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D0=BD=D0=B8=D0=B6=D0=BD?= =?UTF-8?q?=D0=B5=D0=BC=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B5?= =?UTF-8?q?,=20=D0=B0=D0=B1=D0=B1=D1=80=D0=B5=D0=B2=D0=B8=D0=B0=D1=82?= =?UTF-8?q?=D1=83=D1=80=D1=8B=20=E2=80=94=20=D0=B7=D0=B0=D0=B3=D0=BB=D0=B0?= =?UTF-8?q?=D0=B2=D0=BD=D1=8B=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Тип, оснащение и пометки приводятся к одному виду: слова пишутся со строчной, а аббревиатуры («ПК», «МО», «СТ») остаются полностью заглавными; в подписях фильтров то же самое. В карточках теперь «комп.класс · 12 мест · ПК, маркер», «МО и СТ; без проектора». Тесты: 162. --- src/backend/schedule/parsing.py | 16 ++++++++++++++-- src/frontend/static/app.js | 9 ++++++++- tests/test_parsing.py | 22 +++++++++++++++------- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/backend/schedule/parsing.py b/src/backend/schedule/parsing.py index 1198344..f9dd508 100644 --- a/src/backend/schedule/parsing.py +++ b/src/backend/schedule/parsing.py @@ -384,6 +384,18 @@ _ROOM_PART_SPLIT = re.compile(r"[;,]") _NEGATION_RE = re.compile(r"\bбез\b\s*(.*)", re.I) +_WORD_RE = re.compile(r"[A-Za-zА-ЯЁа-яё]+") + + +def lower_except_abbr(text: str) -> str: + """Слова в нижний регистр; аббревиатуры («ПК», «МО», «СТ») остаёмся заглавными.""" + def repl(match: re.Match) -> str: + word = match.group(0) + return word if len(word) >= 2 and word.isupper() else word.lower() + + return _WORD_RE.sub(repl, text) + + def room_capacity(text: str) -> int | None: """Вместимость: «N мест», «N раб.» (рабочие места в комп.классе).""" m = re.search(r"(\d+)\s*мест", text, re.I) @@ -400,7 +412,7 @@ def room_type(text: str) -> str: for pattern in _ROOM_TYPE_PATTERNS: m = pattern.search(text) if m: - return re.sub(r"\s{2,}", " ", m.group(0).strip(" .,")) + return lower_except_abbr(re.sub(r"\s{2,}", " ", m.group(0).strip(" .,"))) return "" @@ -440,7 +452,7 @@ def room_equipment(text: str) -> list[str]: if pattern.search(m.group(1)): negated.add(tag) return [ - tag + lower_except_abbr(tag) for tag, pattern in _ROOM_EQUIPMENT if tag not in negated and pattern.search(text) ] diff --git a/src/frontend/static/app.js b/src/frontend/static/app.js index c15622d..d5d1348 100644 --- a/src/frontend/static/app.js +++ b/src/frontend/static/app.js @@ -436,6 +436,13 @@ function saveRoomsFilter() { localStorage.setItem(ROOMS_FILTER_KEY, JSON.stringify(roomsState.filter)); } +/** Слова в нижний регистр, аббревиатуры («ПК», «МО») остаются заглавными. */ +function lcWords(text) { + return String(text || "").replace(/[A-Za-zА-ЯЁа-яё]+/g, (word) => + word.length >= 2 && word === word.toUpperCase() ? word : word.toLowerCase(), + ); +} + /** «Четверг» -> «четвергам» (для фразы «по четвергам и пятницам»). */ function weekdayPlural(day) { if (day.endsWith("ье")) return day.slice(0, -2) + "ьям"; // воскресенье @@ -492,7 +499,7 @@ function roomInfoText(room) { if (tokens.every(isKnown)) return ""; const rest = [...tokens]; while (rest.length && isKnown(rest[0])) rest.shift(); // убираем ведущие известные слова - return rest.join(" "); + return lcWords(rest.join(" ")); }) .filter(Boolean) .join("; "); diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 597ea16..cae60ae 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -184,7 +184,7 @@ def test_rooms_info(sheets): assert info["ауд. 301"]["info"] == "интерактивная панель, 24 места" assert info["ауд. 301"]["capacity"] == 24 - assert info["ауд. 301"]["equipment"] == ["Интерактивная панель"] + assert info["ауд. 301"]["equipment"] == ["интерактивная панель"] assert info["ауд. 220"]["info"] == "20 мест" assert info["ауд. 220"]["type"] == "" @@ -238,17 +238,17 @@ def test_room_attributes_from_descriptions(): assert info["ауд. 201"]["type"] == "комп.класс" assert info["ауд. 201"]["capacity"] == 12 # «без проектора» — проектора нет, хотя слово в тексте есть - assert info["ауд. 201"]["equipment"] == ["ПК", "Маркер"] + assert info["ауд. 201"]["equipment"] == ["ПК", "маркер"] assert info["ауд. 201"]["available_days"] is None assert info["ауд. 203"]["type"] == "аудитория для практических" - assert info["ауд. 203"]["equipment"] == ["Меловая доска"] + assert info["ауд. 203"]["equipment"] == ["меловая доска"] assert info["ауд. 203"]["capacity"] == 44 - assert info["ауд. 210"]["equipment"] == ["ПК", "Проектор", "Маркер"] + assert info["ауд. 210"]["equipment"] == ["ПК", "проектор", "маркер"] assert info["ауд. 305"]["type"] == "кабинет английского" - assert info["ауд. 305"]["equipment"] == ["Телевизор", "Маркер"] + assert info["ауд. 305"]["equipment"] == ["телевизор", "маркер"] assert info["ауд. 402"]["available_days"] == ["четверг", "пятница"] assert info["ауд. 402"]["capacity"] is None @@ -265,7 +265,7 @@ def test_room_payload_shape(): "type": "лекционная", "type_group": "лекционная", "capacity": 96, - "equipment": ["Проектор"], + "equipment": ["проектор"], "available_days": None, }) assert payload == { @@ -274,7 +274,7 @@ def test_room_payload_shape(): "type": "лекционная", "type_group": "лекционная", "capacity": 96, - "equipment": ["Проектор"], + "equipment": ["проектор"], "available_days": None, } @@ -291,3 +291,11 @@ def test_room_type_group_merges_similar(): assert parsing.room_type_group("лекционно-практическая") == "лекционно-практическая" assert parsing.room_type_group("аудитория для практических") == "аудитория для практических" assert parsing.room_type_group("") == "" + + +def test_lowercase_except_abbreviations(): + """Обозначения — в нижнем регистре, аббревиатуры остаются заглавными.""" + assert parsing.room_type("комп.класс Linux; 10 мест") == "комп.класс linux" + assert parsing.room_type("Кабинет Английского") == "кабинет английского" + assert parsing.room_equipment("ПК, Проектор, интернет МО") == ["ПК", "проектор"] + assert parsing.lower_except_abbr("МО и СТ; без проектора") == "МО и СТ; без проектора"