mod: изменен алгоритм переноса в русских словах (императивнный на декларативный) с весами и приоритетами

This commit is contained in:
2025-07-24 21:02:40 +03:00
parent 086adc1f7b
commit d716d394bb
2 changed files with 80 additions and 73 deletions

View File

@@ -143,60 +143,67 @@ class Hyphenator:
# Поиск допустимой позиции для переноса около заданного индекса
def find_hyphen_point_ru(word_segment: str, start_idx: int) -> int:
vow_indices = [i for i, char_w in enumerate(word_segment) if self._is_vow(char_w)]
# Если в слове нет гласных, то перенос невозможен
if not vow_indices:
word_len = len(word_segment)
min_part = self.min_chars_per_part
# --- Вложенная функция для оценки качества точки переноса ---
def get_split_score(i: int) -> int:
"""
Вычисляет "оценку" для точки переноса `i`. Чем выше оценка, тем качественнее перенос.
-1 означает, что перенос в этой точке запрещен.
"""
# --- Сначала идут ЗАПРЕТЫ (жесткие "нельзя") ---
# Если правило нарушено, сразу дисквалифицируем точку.
if self._is_sign(word_segment[i]) or self._is_j_sound(word_segment[i]):
return -1 # ЗАПРЕТ 1: Новая строка не может начинаться с Ь, Ъ или Й.
if self._is_j_sound(word_segment[i - 1]) and self._is_vow(word_segment[i]):
return -1 # ЗАПРЕТ 2: Нельзя отрывать Й от следующей за ней гласной.
# --- Теперь идут РАЗРЕШЕНИЯ с разными приоритетами ---
# РАЗРЕШЕНИЕ 1: Перенос между сдвоенными согласными.
if self._is_cons(word_segment[i - 1]) and word_segment[i - 1] == word_segment[i]:
return 10
# РАЗРЕШЕНИЕ 2: Перенос после "слога" с Ь/Ъ, если дальше идет СОГЛАСНАЯ.
# Пример: "строитель-ство", но НЕ "компь-ютер".
# По-хорошему нужно проверять, что перед Ь/Ъ нет йотированной гласной
if self._is_sign(word_segment[i - 1]) and self._is_cons(word_segment[i]):
return 9
# РАЗРЕШЕНИЕ 3: Перенос после "слога" если предыдущий Й (очень качественный перенос).
if self._is_j_sound(word_segment[i - 1]):
return 7
# РАЗРЕШЕНИЕ 4: Перенос между тремя согласными (C-CС), чуть лучше, чем после гласной.
if self._is_cons(word_segment[i]) and self._is_cons(word_segment[i-1]) and self._is_cons(word_segment[i+1]):
return 6
# # РАЗРЕШЕНИЕ 5 (?): Перенос между согласной и согласной (C-C).
# if self._is_cons(word_segment[i - 1]) and self._is_cons(word_segment[i]):
# return 5
# РАЗРЕШЕНИЕ 6 (Основное правило): Перенос после гласной.
if self._is_vow(word_segment[i - 1]):
return 5
# Если ни одно правило не подошло, точка не подходит для переноса.
return 0
# 1. Собираем всех кандидатов и их оценки
candidates = []
possible_indices = range(min_part, word_len - min_part + 1)
for i in possible_indices:
score = get_split_score(i)
if score > 0:
# Добавляем только подходящих кандидатов
distance_from_center = abs(i - start_idx)
candidates.append({'score': score, 'distance': distance_from_center, 'index': i})
# 2. Если подходящих кандидатов нет, сдаемся
if not candidates:
return -1
word_segment_len = len(word_segment)
# Ищем ближайшую гласную до или после start_idx
for i in vow_indices:
if i >= start_idx - self.min_chars_per_part and i + self.min_chars_per_part < word_segment_len:
# Проверяем, что после гласной есть минимум символов "хвоста"
ind = i + 1
# 1. Не отделяем "хвостов" с начала или конца (это некрасиво)
if ind <= self.min_chars_per_part or ind >= word_segment_len - self.min_chars_per_part:
continue
# 2. Сдвигаем перенос за мягкий/твердый знак, если он сразу за согласной (ГОСТ 7.62-2008)
if self._is_sign(word_segment[ind]):
# 2.1 Текущая буква мягкий/твердый знак. Ставим перенос за ней (индекс ind+1).
return ind + 1
if (self._is_cons(word_segment[ind]) and
i+1 < word_segment_len and self._is_sign(word_segment[ind+1])):
# 2.2 Текущая буква согласная, а следующая мягкий/твердый знак. Ставим перенос за ней
return ind+2
# 3. Проверка на `Й` (полугласная). Не бывает слов, когда сразу не ней идет гласная,
# или перед ней идет согласная. Сдвигает перенос за полугласную букву если она идет после
# гласной.
if self._is_j_sound(word_segment[ind+1]):
# 3.1 Текущая буква `й`. Ставим за ней перенос (индекс ind+1).
return ind+1
if (self._is_vow(word_segment[ind]) and
i+1 < word_segment_len and self._is_j_sound(word_segment[ind+1])):
# 3.2 Текущая буква гласная, а следующая `й`. Ставим перенос за `й` (индекс ind+2).
# Ставим перенос за `й` (индекс ind+2).
return ind+2
# 4. Проверка на сдвоенная-согласная (C-C).
if (self._is_cons(word_segment[ind]) and
i+1 < word_segment_len and word_segment[ind] == word_segment[ind+1]):
print("сдвоенная согласная")
# 4.1 Текущая буква согласная и следующая така же (сдвоенная согласная). Ставим перенос
# за ней (индекс ind+1).
return ind + 1
if (self._is_cons(word_segment[ind]) and
i+1 < word_segment_len and self._is_cons(word_segment[ind+1])):
# 4.2 НЕ ОБЯЗАТЕЛЬНОЕ ПРАВИЛО: Текущая буква согласная, а следующая тоже согласная.
# Ставим перенос за ней (индекс ind+1).
return ind+1
# 5. Проверка на гласная-гласная (V-V).
if (self._is_vow(word_segment[ind]) and
i+1 < word_segment_len and self._is_vow(word_segment[ind+1])):
# 5.1 Текущая буква гласная, а следующая гласная. Перенос не делаем. Возможно,
# надо дальше искать до ближайшей согласной, но это усложнит алгоритм.
continue
# 6. TODO (опционально): Проверка на суффикс и приставку (не разбивать). Нужен словарь.
# 7. TODO (опционально): Проверка на короткий корень (не разбивать). Нужен очень большой словарь.
return ind
return -1 # Не нашли подходящую позицию
# 3. Сортируем кандидатов: сначала по убыванию ОЦЕНКИ, потом по возрастанию УДАЛЕННОСТИ от центра.
# Это гарантирует, что перенос "н-н" (score=10) будет выбран раньше, чем "е-н" (score=5),
# даже если "е-н" чуть ближе к центру.
best_candidate = sorted(candidates, key=lambda c: (-c['score'], c['distance']))[0]
return best_candidate['index'] # Не нашли подходящую позицию
# Рекурсивное деление слова
def split_word_ru(word_to_split: str) -> str: