add: Препозиционные сокращения ('и.о.', 'т.о.', 'т.к.', 'т.е.' и прочее-прочее)

This commit is contained in:
2025-10-03 01:00:03 +03:00
parent ccab350cb3
commit 79cc4e03cf
4 changed files with 83 additions and 29 deletions

View File

@@ -240,6 +240,23 @@ typo = etpgrf.Typographer(process_units=False)
result = typo.process("100 км/ч") # Останется без изменений
```
#### Сокращения
Типограф также обрабатывает распространённые русскоязычные сокращения, чтобы они корректно отображались и не разрывались
при переносе строк. Правила делятся на два типа:
* Финальные сокращения. Сокращения, которые обычно стоят в конце фразы (например, и т. д., и т. п.),
обрабатываются особым образом: их части «склеиваются» тонкой шпацией, а перед всей конструкцией ставится неразрывный
пробел, чтобы она не «повисла» на новой строке. `...и так далее, и т. д.``...и так далее, и т. д.`
Это правило работает независимо от того, как сокращение было написано в исходном тексте (т.д. или т. д.).
* Препозиционные сокращения. Сокращения, которые стоят перед другим словом (например, и. о. директора, т. е. сказать),
также «склеиваются» внутри, но неразрывный пробел ставится после них, чтобы привязать их к последующему слову.
`Назначить и. о. директора``Назначить и. о. директора`
Библиотека знает небольшой набор самых распространённых сокращений. Но не все, а некоторые принципиально невозможны
к обработке. Например, сокращение `пр.` может оказаться как финальным (в значении «и так далее»), так и препозиционным
(в значении «профессор» или «проспект»). Так же типограф не обрабатывает сокращения, связанные с адресами (ул., д.,
кв., пл., наб. ...) так как они могут быть как финальными, так и препозиционными.
## P.S.

View File

@@ -659,9 +659,12 @@ UNIT_MATH_OPERATORS = ['/', '*', '×', CHAR_MIDDOT, '÷']
# Эти сокращения (обычно в конце фразы) будут "склеены" тонкой шпацией, а перед ними будет поставлен неразрывный пробел.
# Важно, чтобы многосложные сокращения (типа "и т. д.") были в списке с разделителем пробелом (иначе мы не сможем их найти).
ABBR_COMMON_FINAL = [
'т. д.', 'т. п.', 'др.', 'пр.',
# 'т. д.', 'т. п.', 'др.', 'пр.',
# УБРАНЫ из-за неоднозначности: др. -- "другой", "доктор", "драм" / пр. -- "прочие", "профессор", "проект", "проезд" ...
'т. д.', 'т. п.',
]
ABBR_COMMON_PREPOSITION = [
'т.е.', 'т.к.', 'т.о.', 'и.о.', 'ио', 'вр.и.о.', 'врио'
'т. е.', 'т. к.', 'т. о.', 'и. о.', 'ио', 'вр. и. о.', 'врио', 'тов.', 'г-н.', 'г-жа.', 'им.',
'д. о. с.', 'д. о. н.', 'д. м. н.', 'к. т. д.', 'к. т. п.',
]

View File

@@ -5,7 +5,7 @@ import regex
import logging
from etpgrf.config import (LANG_RU, LANG_EN, CHAR_NBSP, CHAR_THIN_SP, CHAR_NDASH, CHAR_MDASH, CHAR_HELLIP,
CHAR_UNIT_SEPARATOR, DEFAULT_POST_UNITS, DEFAULT_PRE_UNITS, UNIT_MATH_OPERATORS,
ABBR_COMMON_FINAL)
ABBR_COMMON_FINAL, ABBR_COMMON_PREPOSITION)
from etpgrf.comutil import parse_and_validate_langs
@@ -124,6 +124,39 @@ class LayoutProcessor:
# По умолчанию (и для русского) — отбивка пробелами.
return f'{CHAR_NBSP}{dash} '
def _process_abbreviations(self, text: str, abbreviations: list[str], mode: str) -> str:
"""
Универсальный обработчик для разных типов сокращений.
:param text: Входной текст.
:param abbreviations: Список сокращений для обработки.
:param mode: 'final' (NBSP ставится перед) или 'prepositional' (NBSP ставится после).
:return: Обработанный текст.
"""
processed_text = text
# Шаг 1: "Склеиваем" многосоставные сокращения временным разделителем CHAR_UNIT_SEPARATOR
for abbr in sorted(abbreviations, key=len, reverse=True):
if ' ' in abbr:
pattern = regex.escape(abbr).replace(r'\ ', r'\s*')
replacement = abbr.replace(' ', CHAR_UNIT_SEPARATOR)
processed_text = regex.sub(pattern, replacement, processed_text, flags=regex.IGNORECASE)
# Шаг 2: Ставим неразрывный пробел.
glued_abbrs = [a.replace(' ', CHAR_UNIT_SEPARATOR) for a in abbreviations]
all_abbrs_pattern = '|'.join(map(regex.escape, sorted(glued_abbrs, key=len, reverse=True)))
if mode == 'final':
# Ставим nbsp перед сокращением, если перед ним есть пробел
nbsp_pattern = regex.compile(r'(\s)(' + all_abbrs_pattern + r')(?=[.,!?]|\s|$)', flags=regex.IGNORECASE)
processed_text = nbsp_pattern.sub(fr'{CHAR_NBSP}\2', processed_text)
elif mode == 'prepositional':
# Ставим nbsp после сокращения, если после него есть пробел
nbsp_pattern = regex.compile(r'(' + all_abbrs_pattern + r')(\s)', flags=regex.IGNORECASE)
processed_text = nbsp_pattern.sub(fr'\1{CHAR_NBSP}', processed_text)
# Шаг 3: Заменяем временный разделитель на правильную тонкую шпацию
return processed_text.replace(CHAR_UNIT_SEPARATOR, CHAR_THIN_SP)
def process(self, text: str) -> str:
"""Применяет правила компоновки к тексту."""
@@ -139,24 +172,9 @@ class LayoutProcessor:
# 3. Обработка пробела перед отрицательными числами/минусом.
processed_text = self._negative_number_pattern.sub(f'{CHAR_NBSP}-\\1', processed_text)
# 4. Обработка финальных сокращений (т.д., т.п. и т.д.)
# Шаг 1: "Склеиваем" многосоставные сокращения временным разделителем.
temp_processed_text = processed_text
for abbr in ABBR_COMMON_FINAL:
if ' ' in abbr: # Обрабатываем только многосоставные
pattern = regex.escape(abbr).replace(r'\ ', r'\s*')
replacement = abbr.replace(' ', CHAR_UNIT_SEPARATOR)
temp_processed_text = regex.sub(pattern, replacement, temp_processed_text, flags=regex.IGNORECASE)
# Шаг 2: Ставим неразрывный пробел перед всеми финальными сокращениями (уже "склеенными").
# Создаем паттерн из всех вариантов - и простых, и "склеенных".
glued_abbrs = [a.replace(' ', CHAR_UNIT_SEPARATOR) for a in ABBR_COMMON_FINAL]
all_final_abbrs_pattern = '|'.join(map(regex.escape, sorted(glued_abbrs, key=len, reverse=True)))
nbsp_pattern = regex.compile(r'(\s)(' + all_final_abbrs_pattern + r')(?=[.,!?]|\s|$)', flags=regex.IGNORECASE)
processed_text = nbsp_pattern.sub(fr'{CHAR_NBSP}\2', temp_processed_text)
# Шаг 3: Заменяем временный разделитель на правильную тонкую шпацию.
processed_text = processed_text.replace(CHAR_UNIT_SEPARATOR, CHAR_THIN_SP)
# 4. Обработка сокращений.
processed_text = self._process_abbreviations(processed_text, ABBR_COMMON_FINAL, 'final')
processed_text = self._process_abbreviations(processed_text, ABBR_COMMON_PREPOSITION, 'prepositional')
# 5. Обработка инициалов и акронимов (если включено).
if self.process_initials_and_acronyms:

View File

@@ -127,20 +127,36 @@ LAYOUT_TEST_CASES = [
('ru', "За окном 15 °C", f"За окном 15{CHAR_NBSP}°C"),
('ru', "HiFi 20 Гц - 20 кГц", f"HiFi 20{CHAR_NBSP}Гц - 20{CHAR_NBSP}кГц"),
# Финальные сокращения
('ru', "1 и т.д.", f"1 и{CHAR_NBSP}т.{CHAR_THIN_SP}д."),
('ru', "2 и т. д.", f"2 и{CHAR_NBSP}т.{CHAR_THIN_SP}д."),
('ru', "3 и т.д., и др.", f"3 и{CHAR_NBSP}т.{CHAR_THIN_SP}д., и{CHAR_NBSP}др."), # Слитное написание
('ru', "4 и т.п., и пр.", f"4 и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и{CHAR_NBSP}пр."), # Слитное написание
('ru', "5 и т. п., и т.п., и пр.", f"5 и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и{CHAR_NBSP}пр."), # Слитное и раздельное написание
# Сложные единицы (склеиваются тонкой шпацией, привязываются к числу неразрывным пробелом)
('ru', "Дом 120 кв.м. / Участок 6 сот.", f"Дом 120{CHAR_NBSP}кв.{CHAR_THIN_SP}м. / Участок 6{CHAR_NBSP}сот."),
# ('ru', "Гробик кладут в ямку 2 кв. м.", f"Гробик кладут в ямку 2 кв. м."),
('ru', "500 до н. э.", f"500 до н.{CHAR_THIN_SP}э."),
('ru+en', "Хаммурапи (1792 - 1750 до н. э.)", f"Хаммурапи (1792 - 1750 до н.{CHAR_THIN_SP}э.)"),
# Финальные сокращения
('ru', "1 и т.д.", f"1 и{CHAR_NBSP}т.{CHAR_THIN_SP}д."),
('ru', "2 и т. \n д.", f"2 и{CHAR_NBSP}т.{CHAR_THIN_SP}д."),
('ru', "3 и т.д., и др.", f"3 и{CHAR_NBSP}т.{CHAR_THIN_SP}д., и др."), # Слитное написание
('ru', "4 и т.п., и пр.", f"4 и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и пр."), # Слитное написание
('ru', "5 и т. п., и т.п., и пр.", f"5 и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и пр."), # Слитное и раздельное написание
# Препозиционные сокращения
('ru', "Назначить и.о. директора", f"Назначить и.{CHAR_THIN_SP}о.{CHAR_NBSP}директора"),
('ru', "Назначить ио директора", f"Назначить ио{CHAR_NBSP}директора"), # без точек
('ru', "замечаний не было, т. е. не было и ошибок", f"замечаний не было, т.{CHAR_THIN_SP}е.{CHAR_NBSP}не было и ошибок"),
('ru', "Назначить и. о. директора", f"Назначить и.{CHAR_THIN_SP}о.{CHAR_NBSP}директора"), # с пробелом
('ru', "Назначить и.о. директора", f"Назначить и.{CHAR_THIN_SP}о.{CHAR_NBSP}директора"), # без пробела
('ru', "Назначить ио директора", f"Назначить ио{CHAR_NBSP}директора"), # без точек
('ru', "то есть т. е. сказать", f"то есть т.{CHAR_THIN_SP}е.{CHAR_NBSP}сказать"),
('ru', "таким образом, т. о. мы видим", f"таким образом, т.{CHAR_THIN_SP}о.{CHAR_NBSP}мы видим"),
('ru', "потому что т.к. это важно", f"потому что т.{CHAR_THIN_SP}к.{CHAR_NBSP}это важно"),
('ru', "Назначить вр. и. о. начальника", f"Назначить вр.{CHAR_THIN_SP}и.{CHAR_THIN_SP}о.{CHAR_NBSP}начальника"),
('ru', "Назначить врио начальника", f"Назначить врио{CHAR_NBSP}начальника"),
('ru', "Выступает тов. Сухов", f"Выступает тов.{CHAR_NBSP}Сухов"),
('ru', "Приехал г-н. Петров", f"Приехал г-н.{CHAR_NBSP}Петров"),
('ru', "Институт им. Курчатова", f"Институт им.{CHAR_NBSP}Курчатова"),
('ru', "собаку оперировал д. м. н. профессор Преображенский", f"собаку оперировал д.{CHAR_THIN_SP}м.{CHAR_THIN_SP}н.{CHAR_NBSP}профессор Преображенский"),
# --- Комбинированные случаи ---
('ru', f"Да — это так{CHAR_HELLIP} а может и нет. Счёт -10.",
f"Да{CHAR_NBSP}— это так{CHAR_HELLIP}{CHAR_NBSP}а может и нет. Счёт{CHAR_NBSP}-10."),