Аудитории: обозначения в нижнем регистре, аббревиатуры — заглавными

Тип, оснащение и пометки приводятся к одному виду: слова пишутся со строчной,
а аббревиатуры («ПК», «МО», «СТ») остаются полностью заглавными; в подписях
фильтров то же самое. В карточках теперь «комп.класс · 12 мест · ПК, маркер»,
«МО и СТ; без проектора». Тесты: 162.
This commit is contained in:
2026-10-10 13:26:15 +03:00
parent ee5faeff77
commit 2cbb19312e
3 changed files with 37 additions and 10 deletions

View File

@@ -384,6 +384,18 @@ _ROOM_PART_SPLIT = re.compile(r"[;,]")
_NEGATION_RE = re.compile(r"\bбез\b\s*(.*)", re.I) _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: def room_capacity(text: str) -> int | None:
"""Вместимость: «N мест», «N раб.» (рабочие места в комп.классе).""" """Вместимость: «N мест», «N раб.» (рабочие места в комп.классе)."""
m = re.search(r"(\d+)\s*мест", text, re.I) 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: for pattern in _ROOM_TYPE_PATTERNS:
m = pattern.search(text) m = pattern.search(text)
if m: 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 "" return ""
@@ -440,7 +452,7 @@ def room_equipment(text: str) -> list[str]:
if pattern.search(m.group(1)): if pattern.search(m.group(1)):
negated.add(tag) negated.add(tag)
return [ return [
tag lower_except_abbr(tag)
for tag, pattern in _ROOM_EQUIPMENT for tag, pattern in _ROOM_EQUIPMENT
if tag not in negated and pattern.search(text) if tag not in negated and pattern.search(text)
] ]

View File

@@ -436,6 +436,13 @@ function saveRoomsFilter() {
localStorage.setItem(ROOMS_FILTER_KEY, JSON.stringify(roomsState.filter)); 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) { function weekdayPlural(day) {
if (day.endsWith("ье")) return day.slice(0, -2) + "ьям"; // воскресенье if (day.endsWith("ье")) return day.slice(0, -2) + "ьям"; // воскресенье
@@ -492,7 +499,7 @@ function roomInfoText(room) {
if (tokens.every(isKnown)) return ""; if (tokens.every(isKnown)) return "";
const rest = [...tokens]; const rest = [...tokens];
while (rest.length && isKnown(rest[0])) rest.shift(); // убираем ведущие известные слова while (rest.length && isKnown(rest[0])) rest.shift(); // убираем ведущие известные слова
return rest.join(" "); return lcWords(rest.join(" "));
}) })
.filter(Boolean) .filter(Boolean)
.join("; "); .join("; ");

View File

@@ -184,7 +184,7 @@ def test_rooms_info(sheets):
assert info["ауд. 301"]["info"] == "интерактивная панель, 24 места" assert info["ауд. 301"]["info"] == "интерактивная панель, 24 места"
assert info["ауд. 301"]["capacity"] == 24 assert info["ауд. 301"]["capacity"] == 24
assert info["ауд. 301"]["equipment"] == ["Интерактивная панель"] assert info["ауд. 301"]["equipment"] == ["интерактивная панель"]
assert info["ауд. 220"]["info"] == "20 мест" assert info["ауд. 220"]["info"] == "20 мест"
assert info["ауд. 220"]["type"] == "" assert info["ауд. 220"]["type"] == ""
@@ -238,17 +238,17 @@ def test_room_attributes_from_descriptions():
assert info["ауд. 201"]["type"] == "комп.класс" assert info["ауд. 201"]["type"] == "комп.класс"
assert info["ауд. 201"]["capacity"] == 12 assert info["ауд. 201"]["capacity"] == 12
# «без проектора» — проектора нет, хотя слово в тексте есть # «без проектора» — проектора нет, хотя слово в тексте есть
assert info["ауд. 201"]["equipment"] == ["ПК", "Маркер"] assert info["ауд. 201"]["equipment"] == ["ПК", "маркер"]
assert info["ауд. 201"]["available_days"] is None assert info["ауд. 201"]["available_days"] is None
assert info["ауд. 203"]["type"] == "аудитория для практических" assert info["ауд. 203"]["type"] == "аудитория для практических"
assert info["ауд. 203"]["equipment"] == ["Меловая доска"] assert info["ауд. 203"]["equipment"] == ["меловая доска"]
assert info["ауд. 203"]["capacity"] == 44 assert info["ауд. 203"]["capacity"] == 44
assert info["ауд. 210"]["equipment"] == ["ПК", "Проектор", "Маркер"] assert info["ауд. 210"]["equipment"] == ["ПК", "проектор", "маркер"]
assert info["ауд. 305"]["type"] == "кабинет английского" assert info["ауд. 305"]["type"] == "кабинет английского"
assert info["ауд. 305"]["equipment"] == ["Телевизор", "Маркер"] assert info["ауд. 305"]["equipment"] == ["телевизор", "маркер"]
assert info["ауд. 402"]["available_days"] == ["четверг", "пятница"] assert info["ауд. 402"]["available_days"] == ["четверг", "пятница"]
assert info["ауд. 402"]["capacity"] is None assert info["ауд. 402"]["capacity"] is None
@@ -265,7 +265,7 @@ def test_room_payload_shape():
"type": "лекционная", "type": "лекционная",
"type_group": "лекционная", "type_group": "лекционная",
"capacity": 96, "capacity": 96,
"equipment": ["Проектор"], "equipment": ["проектор"],
"available_days": None, "available_days": None,
}) })
assert payload == { assert payload == {
@@ -274,7 +274,7 @@ def test_room_payload_shape():
"type": "лекционная", "type": "лекционная",
"type_group": "лекционная", "type_group": "лекционная",
"capacity": 96, "capacity": 96,
"equipment": ["Проектор"], "equipment": ["проектор"],
"available_days": None, "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("аудитория для практических") == "аудитория для практических" 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("МО и СТ; без проектора") == "МО и СТ; без проектора"