JavaScript | Класс символа в регулярных выражениях

JavaScript | Класс символа в регулярных выражениях

Вступление

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

Здесь мы будем говорить классе символа, который является частью производства Атом — частью синтаксиса шаблона регулярного выражения.

 

Поиск проблемы. В чём суть?

Посмотрите на строку:

"НужноNeedкупитьtoпродуктыbuyвinмагазинеthe store"

Это две слепленные строки из разных символов двух языков:

  • На русском языке — «Нужно купить продукты в магазине«
  • На английском языке — «Need to buy groceries in the store«

Задача — Отобрать слова на русском языке в виде строки.

Решение при помощи класса символа:

"НужноNeedкупитьtoпродуктыbuyвinмагазинеthe store".replace(/[^А-яЁё]+/g," ")
"Нужно купить продукты в магазине "

Такое короткое регулярное выражение и такой впечатляющий результат!

Получили строку из русских символов при помощи класса символа и квантификатора - JavaScript
Получили строку из русских символов при помощи класса символа и квантификатора — JavaScript

Что произошло? Квадратными скобками [ и ] внутри регулярного выражения мы обозначили класс символа. Знак ^ в начале класса символа говорит об отрицании всего что справа от него до закрывающей квадратной скобки. А что у нас справа от ^? Справа мы перечисляем те символы, которые хотим отыскать внутри строки. Мы записали диапазон русских букв А-я, а также русские буквы Ё и ё. То есть мы хотим отыскать все НЕРУССКИЕ символы в строке и заменить их на один пробел.

Сами квадратные скобки говорят нам только об одном символе сопоставления. Но нам нужно использовать квантификатор +, чтобы искать самую длинную последовательность из наших классов символа. Квантификатор + позволяет нам находить символы с последовательностью от 1 до бесконечности раз.

Вот в этом и заключается магия использования класса символа!

 

Теория. Стандарт ECMAScript. Примеры.

На 2021 год в JavaScript существует 6 производств атома Atom, который является частью синтаксиса шаблона регулярного выражения.

Atom [UnicodeMode, N] ::

PatternCharacter

.

\ AtomEscape [?UnicodeMode, ?N]

CharacterClass [?UnicodeMode]

( GroupSpecifier [?UnicodeMode] Disjunction [?UnicodeMode, ?N] )

( ? : Disjunction [?UnicodeMode, ?N] )

Скриншот из стандарта:

Производства Atom - ECMAScript 2021
Производства Atom — ECMAScript 2021

 

В этой публикации нас интересует четвёртое производство — CharacterClass [?UnicodeMode]

Его дословно можно перевести как Класс Символа. Именно одного символа. Это важно!

 

Производство CharacterClass

В свою очередь производство CharacterClass состоит из двух производств:

CharacterClass [UnicodeMode] ::

[ [lookahead ≠ ^] ClassRanges [?UnicodeMode] ]

[ ^ ClassRanges [?UnicodeMode] ]

Скриншот из стандарта:

Производства CharacterClass - ECMAScript 2021
Производства CharacterClass — ECMAScript 2021

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

Оба производства оформляются квадратными скобками. Но есть один нюанс. Второе производство класса символа (CharacterClass) содержит символ ^, который обозначает отрицание. Предлагаю сразу рассмотреть два примера:

// ПРИМЕР № 1

/[аб]/.exec("спорт")

null

Мы получили null из метода exec(), потому что в строке «спорт» не встречается символ, который может быть равен «а» или «б«. Результат выполнения регулярного выражения не нашёл сопоставления внутри строки.

Получили null из метода exec
Получили null из метода exec
// ПРИМЕР № 2

/[^аб]/.exec("спорт")

На этот раз нам вернулся массив с результатом под нулевым индексом — буква «с«. Это первое сопоставление в строке слева-направо. Мы нашли то место в строке, где НЕ встречается символ, который может быть равен «а» или «б«.

Класс символа сопоставление в начале строки
Класс символа сопоставление в начале строки

В этих двух примерах мы использовали НЕ ПУСТОЙ диапазон класса из ClassRanges.

Рассмотрим ещё два примера, в которых будем использовать класс символа, с символами из строки:

// ПРИМЕР № 3

[..."спорт".matchAll(/[пт]/g)]

// Результат работы итератора регулярного выражения - массив из массивов

[
   ["п"index1input"спорт"groupsundefined],
   ["т"index4input"спорт"groupsundefined]
]

В этом случае мы получили массив, который состоит из двух массивов. Это результат работы объекта итератора регулярного выражения, извлечённого в массив при помощи оператора spread . В получении итератора нам помог метод matchAll().

matchAll и класс символа для пт
matchAll и класс символа для пт

Глобальное сопоставление нашего класса символа по всей строке нашло два соответствия. Сначала мы нашли «п» под первым индексом, а потом мы нашли «т» под четвёртым индексом. То есть в каждом случае мы получали какой-то ОДИН символ на выбор, который присутствовал в классе.

// ПРИМЕР № 4

[..."спорт".matchAll(/[^пт]/g)]

// Результат работы итератора регулярного выражения - массив из массивов

[
   ["с"index0input"спорт"groupsundefined],
   ["о"index2input"спорт"groupsundefined],
   ["р"index3input"спорт"groupsundefined]
]

Сопоставление нашло 3 случая, когда в строке «спорт» встречается ОДИН символ, который не равен «п» или «т«.

matchAll и класс символа для НЕ пт
matchAll и класс символа для НЕ пт

 

 

Производство ClassRanges

ClassRanges [UnicodeMode] ::

[empty]

NonemptyClassRanges [?UnicodeMode]

Из производства диапазона класса видно, что он может быть всего в двух вариантах: ПУСТОЙ и НЕПУСТОЙ.

ПУСТОЙ диапазон класса

Диапазон класса (ClassRanges) присутствует в двух производствах класса символа (CharacterClass), поэтому мы можем сформировать два примера реализации (два выражения)

// Пример № 5 - Производство CharacterClass БЕЗ отрицания
[..."дом".matchAll(/[]/g)]
[]

В этом примере у нас регулярное выражение пытается сопоставить строку с ПУСТЫМ диапазоном класса для одного символа. Наборы символов не заданы, а значит и сопоставление производится по «НИЧЕМУ» то есть null.

Метод matchAll по пустому диапазону класса символа без отрицания вернул пустой массив - JavaScript
Метод matchAll по пустому диапазону класса символа без отрицания вернул пустой массив — JavaScript

«НИЧЕГО» — это не символ, а значит его точно нет в строке. В результате мы получим пустой массив.

Метод exec() более наглядный для этого результата:

/[]/.exec("дом")
null
Метод exec по пустому диапазону класса символа без отрицания вернул null - JavaScript
Метод exec по пустому диапазону класса символа без отрицания вернул null — JavaScript

 

В примере № 6 мы используем отрицание. То есть НЕ «НИЧЕГО» — то есть «ВСЁ».

//Пример № 6 - Производство CharacterClass С отрицанием
[..."дом".matchAll(/[^]/g)]
[
   ["д"index0input"дом"groupsundefined],
   ["о"index1input"дом"groupsundefined],
   ["м"index2input"дом"groupsundefined]
]

Под «ВСЁ» у нас попадает любой возможный символ. В результате мы видим массив, в котором перечислены все буквы из строки «дом»

НЕПУСТОЙ диапазон класса

В отличии от ПУСТОГО диапазона, НЕПУСТОЙ диапазон класса более насыщенный на производства. В стандарте ECMAScript их три.

 

Производство NonemptyClassRanges

NonemptyClassRanges [UnicodeMode] ::

ClassAtom [?UnicodeMode]

ClassAtom [?UnicodeMode] NonemptyClassRangesNoDash [?UnicodeMode]

ClassAtom [?UnicodeMode]ClassAtom [?UnicodeMode] ClassRanges [?UnicodeMode]

Скриншот из стандарта:

Производства NonemptyClassRanges - ECMAScript 2021
Производства NonemptyClassRanges — ECMAScript 2021

 

Производство ClassAtom

ClassAtom [UnicodeMode] ::

ClassAtomNoDash [?UnicodeMode]

Скриншот из стандарта:

Производства ClassAtom - ECMAScript 2021
Производства ClassAtom — ECMAScript 2021

Непустой диапазон класса может содержать внутри себя управляющий символ дефиса ««, который является первым производством ClassAtom. Как это может выглядеть на практике?

 

Случай только с дефисом

[..."окно".matchAll(/[-]/g)]
[]

[..."окно".matchAll(/[^-]/g)]
(4) [Array(1), Array(1), Array(1), Array(1)]
ClassAtom дефис только из одного дефиса внутри класса символа
ClassAtom дефис только из одного дефиса внутри класса символа

В таком виде это ничем не отличается от ПУСТОГО диапазона класса.

 

Случай с дефисом и одним символом

Попробуем добавить какой-нибудь символ слева от дефиса:

[..."окно".matchAll(/[к-]/g)]
Буква слева от дефиса в классе символа
Буква слева от дефиса в классе символа

Попробуем добавить какой-нибудь символ справа от дефиса:

[..."окно".matchAll(/[-к]/g)]
Буква справа от дефиса в классе символа
Буква справа от дефиса в классе символа

Оба варианта ничем не отличаются от обычной дизъюнкции. Дефис в этом случае не отрабатывает никак.

 

Случай с дефисом, одним символом и отрицанием

[..."окно".matchAll(/[^к-]/g)]
Отрицание, буква слева и дефис в классе символа
Отрицание, буква слева и дефис в классе символа
[..."окно".matchAll(/[^-к]/g)]
Отрицание, буква справа и дефис в классе символа
Отрицание, буква справа и дефис в классе символа

Оба варианта ничем не отличаются от обычной дизъюнкции.

 

 

Случай с дефисом посередине между двух символов

Это интересный вариант оформления Класса Символа т.к. он позволяет нам устанавливать наборы для Класса Символа без прямого перечисления каждого символа. Мы просто указываем начальный символ по таблице Unicode, а затем через дефис указываем конечный символ по таблице Unicode. Класс Символа понимает о какой таблице символьных кодов идёт речь и находит их последовательности нужные для набора в символы. Визуально запись сокращается:

[..."абвгдеёжзик".matchAll(/[в-е]/g)]
Дефис между двух букв в классе символа без отрицания
Дефис между двух букв в классе символа без отрицания
[..."абвгдеёжзик".matchAll(/[^в-е]/g)]
Дефис между двух букв в классе символа с отрицанием
Дефис между двух букв в классе символа с отрицанием

Но есть нюансы. Например, если мы говорим о буквах русского алфавита, то в подобное производство не захватит все буквы. Чтобы это понять, посмотрите на таблицы символьных кодов для кириллических символов Русского алфавита.

Код Символ Значение
0401 Ё CYRILLIC CAPITAL LETTER IO

Заглавная буква Русского алфавита — Кириллические расширения — Cyrillic extensions

 

Код Символ Значение
0410 А CYRILLIC CAPITAL LETTER A
0411 Б CYRILLIC CAPITAL LETTER BE
0412 В CYRILLIC CAPITAL LETTER VE
0413 Г CYRILLIC CAPITAL LETTER GHE
0414 Д CYRILLIC CAPITAL LETTER DE
0415 Е CYRILLIC CAPITAL LETTER IE
0416 Ж CYRILLIC CAPITAL LETTER ZHE
0417 З CYRILLIC CAPITAL LETTER ZE
0418 И CYRILLIC CAPITAL LETTER I
0419 Й CYRILLIC CAPITAL LETTER SHORT I
041A К CYRILLIC CAPITAL LETTER KA
041B Л CYRILLIC CAPITAL LETTER EL
041C М CYRILLIC CAPITAL LETTER EM
041D Н CYRILLIC CAPITAL LETTER EN
041E О CYRILLIC CAPITAL LETTER O
041F П CYRILLIC CAPITAL LETTER PE
0420 Р CYRILLIC CAPITAL LETTER ER
0421 С CYRILLIC CAPITAL LETTER ES
0422 Т CYRILLIC CAPITAL LETTER TE
0423 У CYRILLIC CAPITAL LETTER U
0424 Ф CYRILLIC CAPITAL LETTER EF
0425 Х CYRILLIC CAPITAL LETTER HA
0426 Ц CYRILLIC CAPITAL LETTER TSE
0427 Ч CYRILLIC CAPITAL LETTER CHE
0428 Ш CYRILLIC CAPITAL LETTER SHA
0429 Щ CYRILLIC CAPITAL LETTER SHCHA
042A Ъ CYRILLIC CAPITAL LETTER HARD SIGN
042B Ы CYRILLIC CAPITAL LETTER YERU
042C Ь CYRILLIC CAPITAL LETTER SOFT SIGN
042D Э CYRILLIC CAPITAL LETTER E
042E Ю CYRILLIC CAPITAL LETTER YU
042F Я CYRILLIC CAPITAL LETTER YA

Заглавные буквы — Базовый русский алфавит Unicode — Basic Russian alphabet Unicode

 

Код Символ Значение
0430 а CYRILLIC SMALL LETTER A
0431 б CYRILLIC SMALL LETTER BE
0432 в CYRILLIC SMALL LETTER VE
0433 г CYRILLIC SMALL LETTER GHE
0434 д CYRILLIC SMALL LETTER DE
0435 е CYRILLIC SMALL LETTER IE
0436 ж CYRILLIC SMALL LETTER ZHE
0437 з CYRILLIC SMALL LETTER ZE
0438 и CYRILLIC SMALL LETTER I
0439 й CYRILLIC SMALL LETTER SHORT I
043A к CYRILLIC SMALL LETTER KA
043B л CYRILLIC SMALL LETTER EL
043C м CYRILLIC SMALL LETTER EM
043D н CYRILLIC SMALL LETTER EN
043E о CYRILLIC SMALL LETTER O
043F п CYRILLIC SMALL LETTER PE
0440 р CYRILLIC SMALL LETTER ER
0441 с CYRILLIC SMALL LETTER ES
0442 т CYRILLIC SMALL LETTER TE
0443 у CYRILLIC SMALL LETTER U
0444 ф CYRILLIC SMALL LETTER EF
0445 х CYRILLIC SMALL LETTER HA
0446 ц CYRILLIC SMALL LETTER TSE
0447 ч CYRILLIC SMALL LETTER CHE
0448 ш CYRILLIC SMALL LETTER SHA
0449 щ CYRILLIC SMALL LETTER SHCHA
044A ъ CYRILLIC SMALL LETTER HARD SIGN
044B ы CYRILLIC SMALL LETTER YERU
044C ь CYRILLIC SMALL LETTER SOFT SIGN
044D э CYRILLIC SMALL LETTER E
044E ю CYRILLIC SMALL LETTER YU
044F я CYRILLIC SMALL LETTER YA

Строчные буквы — Базовый русский алфавит Unicode — Basic Russian alphabet Unicode

 

Код Символ Значение
0451 ё CYRILLIC SMALL LETTER IO

Строчная буква Русского алфавита — Кириллические расширения — Cyrillic extensions

 

Заглавная буква «Ё» находится до основной последовательности русских букв в стандарте Unicode.

Строчная буква «ё» находится после основной последовательности русских букв в стандарте Unicode.

То есть все буквы русского алфавита не идут последовательно в стандарте Unicode.

 

Случай с дефисом и другим классом символа — Не работает в JavaScript

Это редкий пример. В основном им пользуются в именованных классах символа. Но для понимания мы рассмотрим простой пример:

[…»абвгдеёжзиклмнопрст».matchAll(/[г-к-[ёж]]/g)]

 

 

Производство ClassAtomNoDash

ClassAtomNoDash [UnicodeMode] ::

SourceCharacter но не один из \ или ] или

\ ClassEscape [?UnicodeMode]

Производство ClassAtomNoDash - ECMAScript 2021
Производство ClassAtomNoDash — ECMAScript 2021

Случай с SourceCharacter — это любой символ Unicode, но не один из \ или ] или . Мы рассмотрели это в самом начале публикации.

 

Альтернативный пример к публикации

Представьте, что у нас есть массив из строк:

["белый", "красный", "угрюмый", "выйди"]

Предположим у нас есть задача: отобрать слова, в которых есть последовательность символов «ый«. Это одно условие можно без проблем поместить в метод filter().

["белый", "красный", "угрюмый", "выйди"].filter(i=>/ый/g.exec(i))
["белый", "красный", "угрюмый", "выйди"]

Так как во всех строках из массива встречается сочетания символов «ый«, то мы обратно получим такой же массив.

Отобрали все слова с ый - JavaScript
Отобрали все слова с ый — JavaScript

Если мы захотим отобрать слова в которых есть последовательности символов «ный» и «мый«, то мы должны будем передать в метод filter() два условия.

["белый", "красный", "угрюмый", "выйди"].filter(i=>/ный|мый/g.exec(i))
["белый", "красный", "угрюмый", "выйди"]

Но ситуация немного меняется, если все четыре слова будут находиться в одной строке «белый красный угрюмый выйди». Это уже не массив. Нам нужно в шаблоне регулярного выражения как-то отразить эти условия. Как это сделать?

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

/ный|мый/

Второй способ — это использование Класса Символа. Позволяет существенно сократить длину записи регулярного выражения и сделать её более читаемой и структурированной.

/[нм]ый/
А что если мы захотим отыскать в строке места, где гласные буквы соседствуют с «р», например. Перечислять все условия в виде одного выражения было бы некрасиво.

 

Информационные ссылки

Производство Atom — https://tc39.es/ecma262/#prod-Atom