add: проверки на диграммы/триграммы,квадрограммы в английских словах

This commit is contained in:
2025-05-19 20:42:49 +03:00
parent b27c643496
commit 96fa73e43d
4 changed files with 149 additions and 28 deletions

View File

@@ -3,6 +3,10 @@ from etpgrf.config import MODE_UNICODE, MODE_MNEMONIC, MODE_MIXED, SUPPORTED_LAN
from etpgrf.defaults import etpgrf_settings
import os
import regex
import logging
# --- Настройки логирования ---
logger = logging.getLogger(__name__)
def parse_and_validate_mode(
@@ -89,4 +93,48 @@ def parse_and_validate_langs(
if not validated_langs_set:
raise ValueError("etpgrf: не предоставлено ни одного валидного кода языка.")
return frozenset(validated_langs_set)
return frozenset(validated_langs_set)
def is_inside_unbreakable_segment(
word_segment: str,
split_index: int,
unbreakable_set: frozenset[str] | list[str] | set[str],
) -> bool:
"""
Проверяет, находится ли позиция разбиения внутри неразрывного сегмента.
:param word_segment: -- Сегмент слова, в котором мы ищем позицию разбиения.
:param split_index: -- Индекс (позиция внутри сегмента), по которому мы хотим проверить разбиение.
:param unbreakable_set: -- Набор неразрывных сегментов (например: диграфы, триграфы, акронимы...).
:return:
"""
segment_len = len(word_segment)
# Проверяем, что позиция разбиения не выходит за границы сегмента
if not (0 < split_index < segment_len):
return False
# Пер образуем все в верхний регистр, чтобы сравнения строк работали
word_segment_upper = word_segment.upper()
# unbreakable_set_upper = (unit.upper() for unit in unbreakable_set) # <-- С помощью генератора
# Отсортируем unbreakable_set по длине лексем (чем короче, тем больше шансов на "ранний выход")
# и заодно превратим в list
sorted_units = sorted(unbreakable_set, key=len)
# sorted_units = sorted(unbreakable_set_upper, key=len)
for unbreakable in sorted_units:
unit_len = len(unbreakable)
if unit_len < 2:
continue
# Спорно, что преобразование в верхний регистр эффективнее делать тут, но благодаря возможному
# "раннему выходу" это может быть быстрее с помощью генератора (см. выше комментарии)
unbreakable_upper = unbreakable.upper()
for offset in range(1, unit_len):
position_start_in_segment = split_index - offset
position_end_in_segment = position_start_in_segment + unit_len
# Убедимся, что предполагаемое положение 'unit' не выходит за границы word_segment
if position_start_in_segment >= 0 and position_end_in_segment <= segment_len and \
word_segment_upper[position_start_in_segment:position_end_in_segment] == unbreakable_upper:
# Нашли 'unbreakable', и split_index находится внутри него.
return True
return False

View File

@@ -1,8 +1,14 @@
# etpgrf/hyphenation.py
# Представленные здесь алгоритмы реализуют упрощенные правила. Но эти правила лучше, чем их полное отсутствие.
# Тем более что пользователь может отключить переносы из типографа.
# Для русского языка правила реализованы лучше. Для английского дают "разумные" переносы во многих случаях, но из-за
# большого числа беззвучных согласных и их сочетаний, могут давать не совсем корректный результат.
import regex
import logging
from etpgrf.config import LANG_RU, LANG_RU_OLD, LANG_EN, SHY_ENTITIES, MODE_UNICODE
from etpgrf.defaults import etpgrf_settings
from etpgrf.comutil import parse_and_validate_mode, parse_and_validate_langs
from etpgrf.comutil import parse_and_validate_mode, parse_and_validate_langs, is_inside_unbreakable_segment
_RU_VOWELS_UPPER = frozenset(['А', 'О', 'И', 'Е', 'Ё', 'Э', 'Ы', 'У', 'Ю', 'Я'])
_RU_CONSONANTS_UPPER = frozenset(['Б', 'В', 'Г', 'Д', 'Ж', 'З', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х',
@@ -25,6 +31,10 @@ _EN_SUFFIXES_WITHOUT_HYPHENATION_UPPER = frozenset([
"SION", "TION", # decision, action (часто покрываются C-C или V-C-V)
# "ING", "ED", "ER", "EST", "LY" # совсем короткие, но распространенные, не рассматриваем.
])
_EN_UNBREAKABLE_X_GRAPHS_UPPER = frozenset(["SH", "CH", "TH", "PH", "WH", "CK", "NG", "AW", # диграфы с согласными
"TCH", "DGE", "IGH", # триграфы
"EIGH", "OUGH"]) # квадрографы
# --- Настройки логирования ---
logger = logging.getLogger(__name__)
@@ -143,16 +153,18 @@ class Hyphenator:
if i >= start_idx - self.min_chars_per_part and i + self.min_chars_per_part < len(word_segment):
# Проверяем, что после гласной есть минимум символов "хвоста"
ind = i + 1
if (self._is_cons(word_segment[ind]) or self._is_j_sound(word_segment[ind])) and not self._is_vow(word_segment[ind + 1]):
# Й -- полугласная. Перенос после неё только в случае, если дальше идет согласная
# (например, "бой-кий"), но запретить, если идет гласная (например, "ма-йка" не переносится).
ind += 1
# 1. Не отделяем "хвостов" с начала или конца (это некрасиво)
if ind <= self.min_chars_per_part or ind >= len(word_segment) - self.min_chars_per_part:
# Не отделяем 3 символ с начала или конца (это некрасиво)
continue
# 2. Пропускаем мягкий/твердый знак, если перенос начинается или заканчивается
# на них (правило из ГОСТ 7.62-2008)
if self._is_sign(word_segment[ind]) or (ind > 0 and self._is_sign(word_segment[ind-1])):
# Пропускаем мягкий/твердый знак, если перенос начинается или заканчивается на них (ГОСТ 7.62-2008)
continue
# 3. Провека на `Й` (полугласная). Перенос после неё только в случае, если дальше идет
# согласная (например, "бой-кий"), но запретить, если идет гласная (например,
# "ма-йка" не переносится).
if (self._is_cons(word_segment[ind]) or self._is_j_sound(word_segment[ind])) and not self._is_vow(word_segment[ind + 1]):
ind += 1
return ind
return -1 # Не нашли подходящую позицию
@@ -209,13 +221,26 @@ class Hyphenator:
# Проверяем каждый потенциальный индекс переноса по упрощенным правилам
for i in valid_split_indices:
# Упрощенные правила английского переноса (основаны на частых паттернах, не на слогах):
# 1. Перенос между двумя согласными (C-C), например, 'but-ter', 'subjec-tive'
# 1. Запрет переноса между гласными
if self._is_vow(word_segment[i - 1]) and self._is_vow(word_segment[i]):
logger.debug(
f"Skipping V-V split point at index {i} in '{word_segment}' ({word_segment[i - 1]}{word_segment[i]})")
continue # Переходим к следующему кандидату i
# 2. Запрет переноса ВНУТРИ неразрывных диграфов/триграфов и т.д.
if is_inside_unbreakable_segment(word_segment=word_segment,
split_index=i,
unbreakable_set=_EN_UNBREAKABLE_X_GRAPHS_UPPER):
logger.debug(f"Skipping unbreakable segment at index {i} in '{word_segment}'")
continue
# 3. Перенос между двумя согласными (C-C), например, 'but-ter', 'subjec-tive'
# Точка переноса - индекс i. Проверяем символы word[i-1] и word[i].
if self._is_cons(word_segment[i - 1]) and self._is_cons(word_segment[i]):
logger.debug(f"Found C-C split point at index {i} in '{word_segment}'")
return i
# 2. Перенос перед одиночной согласной между двумя гласными (V-C-V), например, 'ho-tel', 'ba-by'
# 4. Перенос перед одиночной согласной между двумя гласными (V-C-V), например, 'ho-tel', 'ba-by'
# Точка переноса - индекс i (перед согласной). Проверяем word[i-1], word[i], word[i+1].
# Требуется как минимум 3 символа для этого паттерна.
if i < word_len - 1 and \
@@ -224,7 +249,7 @@ class Hyphenator:
logger.debug(f"Found V-C-V (split before C) split point at index {i} in '{word_segment}'")
return i
# 3. Перенос после одиночной согласной между двумя гласными (V-C-V), например, 'riv-er', 'fin-ish'
# 5. Перенос после одиночной согласной между двумя гласными (V-C-V), например, 'riv-er', 'fin-ish'
# Точка переноса - индекс i (после согласной). Проверяем word[i-2], word[i-1], word[i].
# Требуется как минимум 3 символа для этого паттерна.
if i < word_len and \
@@ -233,7 +258,7 @@ class Hyphenator:
logger.debug(f"Found V-C-V (split after C) split point at index {i} in '{word_segment}'")
return i
# 4. Правила для распространенных суффиксов (перенос ПЕРЕД суффиксом)
# 6. Правила для распространенных суффиксов (перенос ПЕРЕД суффиксом)
if word_segment[i:].upper() in _EN_SUFFIXES_WITHOUT_HYPHENATION_UPPER:
logger.debug(f"Found suffix '-{word_segment[i:]}' split point at index {i} in '{word_segment}'")
return i