Перейти к содержанию

Строки и Unicode

Поле цели ограничивалось свойством length, а название курса могло содержать любые текстовые символы. Для обычных букв это выглядит понятным подсчётом, но emoji и комбинируемые знаки показывают другое поведение. Разберём три уровня строки и сделаем сокращение названия, которое не разрывает воспринимаемый человеком символ.

Используем независимый снимок advanced/lesson-30. Его опыт не создаёт новый поиск и не заменяет курс поиска по сайту. Мы рассматриваем отображение, сравнение нормализованных строк и границы длины. Программа не запускалась; ожидаемые количества выведены из структуры конкретного примера.

Кодовые единицы

Строки JavaScript хранят последовательности кодовых единиц UTF-16. Свойство length считает именно их. Некоторые символы представлены парой единиц, поэтому одна видимая фигура может занимать больше одного места. Это не ошибка самого значения и не особенность шрифта.

const icon = '👩‍💻';
console.log(icon.length);
console.log([...icon].length);

Ожидаются пять и три. Составная иконка содержит кодовую точку женщины, соединитель и кодовую точку ноутбука. Две фигуры используют по две кодовые единицы, соединитель — одну. Распаковка строки через итерацию получает кодовые точки, а не те же единицы, поэтому второй результат отличается.

Однако три кодовые точки всё ещё отображаются как одна составная иконка. Значит, замена length на длину распакованного массива решает только часть задачи. Если интерфейс обещает число воспринимаемых символов, нужно определить более подходящую единицу — графему.

Не исправляйте проблему ручным удалением всех «необычных» символов. Названия и цели могут содержать знаки разных языков и корректные составные буквы. Ограничение приложения должно быть связано с его обязанностью, например допустимым объёмом текста или длиной видимой подписи, а не с предположением об одном алфавите.

Сегменты отображения

Intl.Segmenter позволяет делить строку по графемам. Для нашего опыта используем гранулярность grapheme:

const segmenter = new Intl.Segmenter('ru', { granularity: 'grapheme' });
console.log([...segmenter.segment(icon)].length);

Ожидается единица. Возвращаемые записи содержат поле segment и сведения о позиции. Мы считаем сегменты, не предполагая фиксированное число кодовых точек внутри каждого. Механизм и поддержка описаны в MDN о Segmenter.

Для старой среды без такого API безопасный запасной вариант отображения может сохранить полное название. Обрезание по кодовым единицам с обещанием правильных графем было бы неверным. Сначала определите поддерживаемые браузеры или стратегию совместимости, затем применяйте новую обязанность.

Графемы также не являются количеством байтов файла. Если ограничение API измеряется байтами UTF-8, потребуется другой расчёт. Одно слово «длина» скрывает разные договоры. В форме 25 техническое ограничение связано с кодовыми единицами, а в этом отображении — с сегментами человека.

Канонически похожие строки

Буква может иметь составное и готовое представление. Рассмотрим русскую краткую «й»:

const composed = 'й';
const decomposed = 'и\u0306';
console.log(composed === decomposed);
console.log(composed.normalize('NFC') === decomposed.normalize('NFC'));

Первое сравнение ожидаемо ложно, второе истинно. Видимый текст похож, но последовательности кодовых значений различаются. Нормализация NFC приводит канонически эквивалентные формы к согласованному представлению. Она не превращает любые похожие буквы разных алфавитов в одну идентичность.

Правила форм нормализации приведены в MDN о normalize. Для машинного id курса у нас по-прежнему используется узкий латинский договор. Нормализация названия не заменяет стабильный идентификатор и не связывает две карточки автоматически.

Не применяйте агрессивную совместимую нормализацию ко всему отображаемому тексту без причины. Некоторые преобразования могут менять различия, которые автор хотел сохранить. Для сравнения и для хранения оригинального названия могут существовать разные представления; договор должен назвать каждое из них.

Сокращение по графемам

Полная функция снимка строит список сегментов, оставляет заданное количество и добавляет многоточие:

export function shortenTitle(title, limit) {
  if (!Number.isInteger(limit) || limit < 1) {
    throw new TypeError('Неверный предел');
  }
  const pieces = [...segmenter.segment(title)].map(item => item.segment);
  return pieces.length <= limit
    ? title : pieces.slice(0, limit).join('') + '…';
}
console.log(shortenTitle('👩‍💻 Веб-разработка', 5));

Ожидаемая сокращённая подпись начинается с цельной иконки, пробела и первых трёх букв слова, затем многоточия. Она не содержит половины суррогатной пары и не разрывает составную иконку на отдельные части. Исходная строка сохраняется и может использоваться для полного названия.

Предел здесь относится к сохранённым сегментам до многоточия. Если продукт требует общий предел вместе с многоточием, правило нужно изменить явно. Подобные мелкие детали важно определять до проверки: две функции могут обе выглядеть аккуратно, но соблюдать разные ожидания дизайна.

Такое сокращение не знает ширину шрифта и контейнера. Пять графем разной формы занимают разное место на экране. Для чисто визуального ограничения часто удобнее CSS-представление полного текста. Наша функция подходит, когда приложение действительно создаёт короткую текстовую подпись по заданному количеству сегментов.

Следствия для интерфейса

Полное доступное название не должно исчезать лишь ради компактного вида. Если ссылка получает только сокращённую строку, читатель может потерять смысл выбора. Решение зависит от компонента: сохранить полный текст в другом месте, использовать визуальное ограничение или показать подробности. Не заменяйте эту задачу случайным атрибутом без проверки поведения.

Для ручного опыта сравните обычные русские буквы, составную «й», нашу иконку и строку с несколькими разными emoji. Предскажите отдельно кодовые единицы, кодовые точки и графемы. Не называйте один из результатов просто «правильной длиной»: правильность зависит от выбранного договора.

Что именно нужно сохранять целиком

Сокращённая подпись является производным значением. Она не должна заменить исходное название в courses или стать ключом индекса. Два разных длинных названия могут иметь одинаковое начало и одинаковое многоточие. Их видимое сходство не означает совпадение курсов; для этого продолжает использоваться постоянный id.

У составной иконки важно сохранять последовательность, но число графем тоже не равно ширине. Одна emoji может занимать больше места, чем несколько узких букв. Поэтому функция с пределом пяти сегментов выполняет текстовое правило, а CSS с ограничением ширины — правило размещения. Эти варианты нельзя сравнивать по одному ожидаемому количеству пикселей без измерения выбранного шрифта и контейнера.

Нормализация NFC полезна для канонически эквивалентных форм, однако не исправляет орфографию и не объединяет похожие символы всех языков. Латинская буква и похожая кириллическая остаются различными значениями. Если интерфейс проверяет идентификатор, он применяет узкий машинный договор. Если показывает название, сохраняет корректный исходный текст. Если сравнивает канонические формы, создаёт отдельное представление для этой конкретной операции.

Обратите внимание и на место обрезания. Если сначала взять slice по кодовым единицам, а затем сегментировать остаток, повреждение уже могло произойти. Границы нужно определить на полной строке до сокращения. Точно так же нормализация после сравнения не может изменить ранее принятое решение: выбранное представление должно использоваться именно в операции, для которой оно предназначено.

Для ручной сверки включите составную букву в начало короткой подписи, затем добавьте нашу иконку и обычный пробел. Ожидается сохранение целых сегментов и неизменный оригинал. Посмотрите отдельно массив кодовых точек и записи Segmenter: они описывают разные разбиения одной строки. Такой опыт полезнее предположения, что все emoji занимают одинаковое число единиц и потому достаточно одного постоянного поправочного коэффициента.

Теперь строки имеют ясно названные представления и операции. Следующий урок вернётся к структурам данных: постоянный идентификатор позволит получать карточку через Map, а Set хранить уникальные выборы независимо от названий и их текстового оформления.

Оглавление курса