From d716d394bbd3c5245c47da44b381addd642cd3e8 Mon Sep 17 00:00:00 2001 From: erjemin Date: Thu, 24 Jul 2025 21:02:40 +0300 Subject: [PATCH] =?UTF-8?q?mod:=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B0=D0=BB=D0=B3=D0=BE=D1=80=D0=B8=D1=82=D0=BC=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=D0=B0=20=D0=B2=20?= =?UTF-8?q?=D1=80=D1=83=D1=81=D1=81=D0=BA=D0=B8=D1=85=20=D1=81=D0=BB=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=85=20(=D0=B8=D0=BC=D0=BF=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D0=B2=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=B5=D0=BA=D0=BB=D0=B0=D1=80=D0=B0=D1=82=D0=B8=D0=B2?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9)=20=D1=81=20=D0=B2=D0=B5=D1=81=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D0=BF=D1=80=D0=B8=D0=BE=D1=80=D0=B8=D1=82?= =?UTF-8?q?=D0=B5=D1=82=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etpgrf/hyphenation.py | 113 ++++++++++++++++++++------------------ tests/test_hyphenation.py | 40 +++++++------- 2 files changed, 80 insertions(+), 73 deletions(-) diff --git a/etpgrf/hyphenation.py b/etpgrf/hyphenation.py index 4dab4b7..b75b218 100755 --- a/etpgrf/hyphenation.py +++ b/etpgrf/hyphenation.py @@ -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: diff --git a/tests/test_hyphenation.py b/tests/test_hyphenation.py index d3e8fc5..2c25d17 100644 --- a/tests/test_hyphenation.py +++ b/tests/test_hyphenation.py @@ -7,28 +7,28 @@ from etpgrf import Hyphenator # Используем \u00AD - это Unicode-представление мягкого переноса (­) RUSSIAN_HYPHENATION_CASES = [ ("дом", "дом"), # Сочень короткое (короче max_unhyphenated_len) не должно меняться - ("проверка", "проверка"), # Короткое слово не должно меняться - ("тестирование", "тести\u00ADрование"), - ("благотворительностью", "благотво\u00ADритель\u00ADностью"), # Слово с переносом на мягкий знак - ("гиперподъездной", "гипер\u00ADподъ\u00ADездной"), # Слово с переносом на твердый знак + ("проверка", "про\u00ADверка"), + ("тестирование", "тести\u00ADрова\u00ADние"), + ("благотворительностью", "бла\u00ADготво\u00ADритель\u00ADностью"), # Слово с переносом на мягкий знак ("фотоаппаратура", "фотоап\u00ADпара\u00ADтура"), # проверка слова со сдвоенной согласной - ("программирование", "програм\u00ADмиро\u00ADвание"), # слова со сдвоенной согласной - ("сверхзвуковой", "сверхзву\u00ADковой"), + ("программирование", "про\u00ADграм\u00ADмиро\u00ADвание"), # слова со сдвоенной согласной + ("сверхзвуковой", "сверх\u00ADзву\u00ADковой"), ("автомобиль", "авто\u00ADмобиль"), - ("интернационализация", "интерна\u00ADциона\u00ADлизация"), - ("суперкомпьютер", "супер\u00ADком\u00ADпьютер"), - ("электронный", "электрон\u00ADный"), - ("информационный", "информа\u00ADционный"), - ("автоматизация", "авто\u00ADмати\u00ADзация"), - ("многоклеточный", "многок\u00ADлеточ\u00ADный"), - ("многофункциональный", "многофун\u00ADкцио\u00ADнальный"), - ("непрерывность", "непре\u00ADрывность"), - ("сверхпроводимость", "сверхпро\u00ADводи\u00ADмость"), - ("многообразие", "много\u00ADобразие"), - ("противоречивость", "противо\u00ADречи\u00ADвость"), - ("сверхчувствительный", "сверхчув\u00ADстви\u00ADтельный"), # Будет неправильный перенос, (словарь "корней") - ("непревзойденный", "непрев\u00ADзойден\u00ADный"), # Будет неправильный перенос - ("многослойный", "многос\u00ADлойный"), # Будет неправильный перенос, + ("интернационализация", "инте\u00ADрнаци\u00ADонали\u00ADзация"), + ("электронный", "элек\u00ADтрон\u00ADный"), + ("информационный", "инфо\u00ADрма\u00ADцион\u00ADный"), + ("автоматизация", "автома\u00ADтиза\u00ADция"), + ("многоклеточный", "мно\u00ADгокле\u00ADточный"), + ("многофункциональный", "мно\u00ADгофун\u00ADкцио\u00ADналь\u00ADный"), + ("непрерывность", "непре\u00ADрывно\u00ADсть"), + ("сверхпроводимость", "сверх\u00ADпрово\u00ADдимо\u00ADсть"), + ("многообразие", "мно\u00ADгоо\u00ADбра\u00ADзие"), + ("противоречивость", "про\u00ADтиво\u00ADречи\u00ADвость"), + ("непревзойденный", "непре\u00ADвзой\u00ADден\u00ADный"), + ("многослойный", "мно\u00ADгослой\u00ADный"), + ("суперкомпьютер", "супе\u00ADрко\u00ADмпью\u00ADтер"), # Неправильный перенос (нужен словарь "приставок/корней/суффиксов") + ("сверхчувствительный", "свер\u00ADхчув\u00ADстви\u00ADтель\u00ADный"), # Неправильный перенос + ("гиперподъездной", "гипе\u00ADрпо\u00ADдъез\u00ADдной"), # Неправильный перенос ]