May 16th, 2021

JavaScript: интернационализация, сортировка слов с Е и Ё

Прочел подраздел 14.6 «Intl: интернационализация в JavaScript» четырнадцатого раздела «Разное» первой части «Язык программирования JavaScript» учебника по JavaScript.

Стоит отметить, что в англоязычной версии этого учебника этого подраздела почему-то нет.

Под термином «интернационализация» в языках программирования, в том числе и в JavaScript, подразумевается настройка представления дат, временных отрезков, чисел и валют для разных стран, а также порядок сравнения букв в алфавитах разных стран, сортировки слов в разных языках общения. В языке программирования JavaScript для этих целей существует специальный объект Intl. Этот объект содержит функции-конструкторы Intl.Collator (для зависимого от языка общения сравнения букв и строк, сортировки), Intl.DateTimeFormat (для зависимого от языка общения представления даты и времени), Intl.NumberFormat (для зависимого от языка общения представления чисел и валют).

В этом посте из всего перечисленного выше богатства возможностей я немного затрону только особенности работы объектов, полученных с помощью функции-конструктора Intl.Collator, а, конкретнее, некоторые особенности сортировки строк на русском языке.

В подразделе 5.3 «Строки» учебника сказано, что внутренний формат (кодировка) для строк в языке программирования JavaScript — всегда UTF-16. Это один из способов кодирования (форм представления) символов Юникода. Лично я, когда мне нужно посмотреть таблицу символов Юникода онлайн, пользуюсь вот этим сайтом (очень удобный, я о нем уже неоднократно писал ранее):
https://unicode-table.com/ru/

Быстрый переход к той части таблицы, где находится русский алфавит:
https://unicode-table.com/ru/#cyrillic
https://unicode-table.com/ru/blocks/cyrillic/

У Юникода в части русского алфавита (одного из алфавитов, созданных на основе кириллицы) есть общеизвестные особенности и проблемы.

В принципе, в таблице Юникода кириллические буквы расположены в том порядке, в каком они расположены в русском алфавите. Это удобно для программирования сортировки слов на русском языке, потому что, как известно, при сортировке слова сравниваются побуквенно, а при сравнении букв сравниваются коды букв (числа) из таблицы Юникода. Но есть, как уже говорилось выше, особенности и проблемы (исключения).

Главная особенность, которую нужно знать: прописные (большие) буквы сгруппированы отдельно, а строчные (маленькие) буквы — тоже отдельно. При этом группа прописных букв идет в таблице Юникода раньше (их коды меньше), чем группа строчных букв. Эти группы примыкают друг к другу.

То есть буквы расположены не вот так (как обычно в русском алфавите):
АаБбВбГгДдЕеЁёЖжЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭэЮюЯя

А вот так:
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя

В результате получается, что при сравнении букв оказывается верным неравенство "а" > "Я", что противоречит алфавитному порядку, по которому должно быть верным противоположное неравенство "а" < "Я", ведь и прописная буква «Я», и строчная буква «я» в русском алфавите находятся в самом конце, а потому должны иметь самые большие коды и быть больше любых других букв русского алфавита.

Я пометил красным буквы «Ё» и «ё» в ряду букв выше неспроста. В Юникоде они вынесены из вышеуказанного порядка букв. Русские буквы в таблице Юникода, на самом деле, расположены так:
Ё...АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя...ё
Многоточием я обозначил тот факт, что между буквой «Ё» и началом группы прописных русских букв расположено 14 других символов. А между концом группы строчных русских букв и буквой «ё» расположен один другой символ.

Эти два исключения тоже затрудняют программирование сравнения и сортировки русских букв и слов.

* * *

Итак, посмотрим, как происходит сортировка русских слов по умолчанию, то есть при этом учитывается только вышеизложенное расположение русских букв в таблице Юникода:
let arr = ["абажур", "Яма", "яма", "Абажур"];

arr.sort();   // сортировка по умолчанию

alert( arr ); // Абажур,Яма,абажур,яма
Тут у всех слов первые буквы разные, а потому сортировка идет именно по кодам этих первых букв, в соответствии со следующим неравенством: "А" < "Я" < "а" < "я". Если бы первые буквы у сравниваемых слов были бы одинаковыми, сравнение перешло бы ко второй букве и так далее.

Теперь добавим в тестовый массив русских слов слова, начинающиеся на буквы «е», «ё», «Е» и «Ё»:
let arr = ["абажур", "Яма", "яма", "Абажур", "еда", "ёж", "Езда", "Ёлка"];

arr.sort();   // сортировка по умолчанию

alert( arr ); // Ёлка,Абажур,Езда,Яма,абажур,еда,яма,ёж
Всё отсортировалось в соответствии с расположением первых букв этих слов в таблице Юникода. С одной стороны, всё логично, а с другой стороны — это не то, что нам обычно требуется, потому что слова отсортированы не в соответствии с порядком букв в русском алфавите.

* * *

Попробуем воспользоваться для сортировки русских слов объектом, полученным от функции-конструктора Intl.Collator:
let arr = ["абажур", "Яма", "яма", "Абажур", "еда", "ёж", "Езда", "Ёлка"];

let collator = new Intl.Collator("ru");
arr.sort( collator.compare );

alert( arr ); // абажур,Абажур,еда,ёж,Езда,Ёлка,яма,Яма
Так гораздо лучше.

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

Например, для пары слов абажур,Абажур или для пары слов яма,Яма сортировку с помощью функции (метода) collator.compare можно настроить так, что в каждой паре слово, начинающееся с прописной буквы, встанет первым:
let arr = ["абажур", "Яма", "яма", "Абажур", "еда", "ёж", "Езда", "Ёлка"];

let collator = new Intl.Collator("ru", { caseFirst: "upper" });
arr.sort( collator.compare );

alert( arr ); // Абажур,абажур,еда,ёж,Езда,Ёлка,Яма,яма

Значит ли такая настройка, что прописные буквы буквы стали «весить» при сортировке больше строчных? Нет, всё сложнее. Если бы прописные буквы стали «весить» больше строчных, то четверка слов еда,ёж,Езда,Ёлка должна была бы после сортировки выглядеть как Езда,Ёлка,еда,ёж, а этого не произошло. Я не смог настроить функцию-конструктор Intl.Collator так, чтобы после сортировки получить Езда,Ёлка,еда,ёж в вышеприведенном коде.

Как тогда в данном случае работает сортировка русских слов?

Я представляю себе это как процесс из двух шагов:

1) На первом этапе регистр буквы не имеет значения, то есть, к примеру, буквы «А» и «а» считаются одной и той же буквой. Единственное уточнение: четыре буквы «Ё», «ё, «Е» и «е» на этом этапе тоже считаются одной и той же буквой.

(Правила русской орфографии и пунктуации действуют с 1956 года. Но в 2006 году их довольно сильно подредактировали. В статье википедии про букву «Ё» можно найти выдержки о правилах применения этой буквы из обеих этих редакций правил. В редакции правил от 2006 года сказано, что «В словарях слова с буквой ё размещаются в общем алфавите с буквой е, напр.: еле, елейный, ёлка, еловый, елозить, ёлочка, ёлочный, ель; веселеть, веселить(ся), весёлость, весёлый, веселье.». Отсюда и сделан вывод, что буквы «Ё» и «Е» при сортировке следует считать одной и той же буквой. Как я понимаю, это реализовано и в поведении объекта, получаемого от функции-конструктора Intl.Collator.)

2) Если на первом этапе сортировки некоторые слова определены как равные (содержат одинаковые буквы, имеют одинаковую длину), то анализируются дополнительные признаки (если они указаны программистом), в том числе регистр букв (caseFirst: "upper" из примера выше). В случае одинаковых слов, но в которых на одной и той же позиции есть буквы «е» и «ё», первым должно быть слово с буквой «е» (например, еж,ёж или Еж,Ёж).

Таким образом, на первом этапе в четверке слов еда,ёж,Езда,Ёлка сначала сравниваются первые буквы слов. Эти буквы, на основании вышеизложенного, считаются одной и той же буквой. Поэтому переходим к сравнению вторых букв указанных слов. Здесь оказывается верным неравенство "д" < "ж" < "з" < "л", поэтому слова оставляются в указанном порядке, сортировки не требуется, они уже и так отсортированы. Второй этап процесса сортировки не происходит, потому что среди этих четырех слов нет одинаковых (часть из них различается по длине, а совпадающие по длине имеют в своем составе разные буквы).

* * *

В принципе, никто не мешает вместо collator.compare написать свою функцию, реализовать там любое желаемое поведение и передать ее функции arr.sort в качестве параметра. Но, как отметили в комментариях к обсуждаемому подразделу учебника, такое решение, скорее всего, будет работать медленнее встроенного. Впрочем, наверное, это важно лишь для достаточно больших проектов.

Учебник по JavaScript, ч.1: Разное

Прочел четырнадцатый раздел «Разное» первой части («Язык программирования JavaScript») учебника по JavaScript.

https://learn.javascript.ru

Часть 1. Язык программирования JavaScript (в т.ч. 93 подраздела)

Разделы:

14. Разное (6 подразделов)

14.1 Proxy и Reflect
14.2 Eval: выполнение строки кода
14.3 Каррирование
14.4 Побитовые операторы
14.5 BigInt
14.6 Intl: интернационализация в JavaScript

По подразделам 14.1 и 14.6 у меня есть отдельные посты:
1. JavaScript: объекты класса Proxy, решение задачи Observable
2. JavaScript: интернационализация, сортировка слов с Е и Ё

В подразделе 14.2 рассказано про встроенную функцию eval, позволяющую выполнять любой код, переданный этой функции в виде строки в первый параметр. Отмечено, что сегодня считается, что в использовании этой функции нет необходимости, в большинстве случаев ей есть лучшие альтернативы (по этому поводу можно почитать подраздел 6.7 «Синтаксис "new Function"» и раздел 13 «Модули» учебника). Более того, ее использование многие не одобряют, есть даже такая поговорка на английском: «eval is evil», что по-русски означает «eval — это зло».

В подразделе 14.3 рассмотрена техника для работы с функциями, которая называется «каррирование». Про это понятие в википедии есть отдельная статья:

https://ru.wikipedia.org/wiki/Каррирование

Мне эта статья не понравилась, так как там каррирование рассмотрено в основном с математической точки зрения и лично я ничего там не понял. Англоязычная статья написана гораздо понятнее и вообще она больше:

https://en.wikipedia.org/wiki/Currying

В самом слове «каррирование» не стоит искать смысла (а я это обычно делаю, потому что так легче разбираться в сложных предметах), потому что оно произошло от фамилии американского математика и логика Хаскелла Карри (в честь этого ученого еще назван язык программирования Haskell).

Так что же такое «каррирование»? Это процесс, трансформация. В результате каррирования (трансформации) из одной функции получают другую функцию, которая делает то же самое, но которую становится возможным применять в более удобном виде (ниже приведен пример для функции с тремя параметрами, но исходная функция может быть с любым числом параметров, большим одного; суть в том, что функция с несколькими параметрами трансформируется в функцию с одним параметром):
func(a, b, c) --> каррирование --> func(a)(b)(c)

Вид func(a)(b)(c) удобен не всегда, а только в некоторых случаях. Что это за случаи? Например, в нашей программе есть функция func(a, b, c) и она применяется очень часто. При этом, скажем, из десяти случаев применения этой функции в девяти случаях первый параметр a имеет одно и то же значение. При такой ситуации было бы удобно иметь функцию func_a, у которой только два параметра — b и c, а значение a задано в самой функции локально, раз уж оно остается неизменным. Тогда в одном случае из десяти применяем функцию func(a, b, c), а в девяти случаях из десяти применяем функцию func_a(b, c). Это может сделать код более читабельным и понятным.

Еще пример. Та же ситуация, только теперь в одном случае из десяти удобно применить функцию func(a, b, c), в четырех из десяти — функцию func_a(b, c), а в пяти из десяти — функцию func_ab(c) (то есть в этом случае параметры a и b остаются неизменными, а меняется только третий параметр c). Что тут делать программисту? Можно написать три функции func(a, b, c), func_a(b, c) и func_ab(c), но они будут делать одно и то же, а один из общеизвестных принципов хорошего программирования — в программе не должно быть дублирующего кода!

Один из способов решить эту проблему — каррирование. Мы пишем только функцию func(a, b, c), а затем каррируем ее. Теперь можно делать следующее (это псевдокод):
function func(a, b, c) {         // пишем функцию
    // ... тело функции ...
}

func = каррирующаяФункция(func); // каррируем ее

let func_a = func(a);            // получаем функцию func_a

let func_ab = func_a(b);         // получаем функцию func_ab

// Далее можно применять в нужных случаях нужный вариант:
func(a, b, c);
func_a(b, c);
func_ab(c);
И вуаля! У нас появились три нужные функции и только одно тело функции! Каррирующую функцию можно написать самому, в подразделе 14.3 рассказано, как. Либо можно взять готовую из какой-нибудь библиотеки (в подразделе 14.3 в пример приводится каррирующая функция _.curry из библиотеки Lodash).

В программировании есть еще термин «частичное применение функции», родственный «каррированию»:
https://ru.wikipedia.org/wiki/Частичное_применение
https://en.wikipedia.org/wiki/Partial_application

В подразделе 14.4 рассказано про побитовые операторы, которые интерпретируют операнды как последовательность из 32 битов (нулей и единиц). Мне понравилось объяснение формата 32-битного целого со знаком. Еще мне понравилось, что рассказано про самые частые варианты применения побитовых операторов в программировании на языке JavaScript (в качестве маски, для округления чисел, для проверки на -1, для шифрования, для умножения и деления на степени двойки).

В подразделе 14.5 рассказано про один из базовых типов в языке JavaScript — BigInt. Это относительно новая возможность в языке. Ранее про этот тип данных уже рассказывалось в подразделе 2.5 «Типы данных» учебника.

Стоит отметить, что в англоязычной версии учебника отсутствуют подразделы 14.4 «Побитовые операторы» и 14.6 «Intl: интернационализация в JavaScript». Зато там есть раздел 14.4 «Reference Type», которого нет в русскоязычной версии учебника.

Но не надо думать, что читатель русскоязычной версии что-то пропустит, потому что в русскоязычной версии содержание англоязычного подраздела 14.4 «Reference Type» дословно (на русском языке) вставлено в подраздел 4.4 «Методы объекта, "this"»:

https://learn.javascript.ru/object-methods#vnutrennyaya-realizatsiya-ssylochnyy-tip

О чем там речь? Там рассказано об ошибках, возникающих при потере контекста «this» (об этом также можно почитать и в подразделе 6.10 «Привязка контекста к функции»), почему эти ошибки происходят. Рассказано про внутренний тип для языка JavaScript — «ссылочный тип» (по-английски «Reference Type»), этот тип программист не может использовать, он предназначен для внутреннего использования движком языка.