Вступление
В IT-мире очень много сущностей, которые называется словом класс. Практически во всех языках программирования можно встретить слово класс, имеющих порой разные смыслы. О каком классе символа идёт речь в этой публикации?
Здесь мы будем говорить классе символа, который является частью производства Атом — частью синтаксиса шаблона регулярного выражения.
Поиск проблемы. В чём суть?
Посмотрите на строку:
"НужноNeedкупитьtoпродуктыbuyвinмагазинеthe store"
Это две слепленные строки из разных символов двух языков:
- На русском языке — «Нужно купить продукты в магазине«
- На английском языке — «Need to buy groceries in the store«
Задача — Отобрать слова на русском языке в виде строки.
Решение при помощи класса символа:
"НужноNeedкупитьtoпродуктыbuyвinмагазинеthe store".replace(/[^А-яЁё]+/g," ") "Нужно купить продукты в магазине "
Такое короткое регулярное выражение и такой впечатляющий результат!
Что произошло? Квадратными скобками [ и ] внутри регулярного выражения мы обозначили класс символа. Знак ^ в начале класса символа говорит об отрицании всего что справа от него до закрывающей квадратной скобки. А что у нас справа от ^? Справа мы перечисляем те символы, которые хотим отыскать внутри строки. Мы записали диапазон русских букв А-я, а также русские буквы Ё и ё. То есть мы хотим отыскать все НЕРУССКИЕ символы в строке и заменить их на один пробел.
Сами квадратные скобки говорят нам только об одном символе сопоставления. Но нам нужно использовать квантификатор +, чтобы искать самую длинную последовательность из наших классов символа. Квантификатор + позволяет нам находить символы с последовательностью от 1 до бесконечности раз.
Вот в этом и заключается магия использования класса символа!
Теория. Стандарт ECMAScript. Примеры.
На 2021 год в JavaScript существует 6 производств атома Atom, который является частью синтаксиса шаблона регулярного выражения.
Atom [UnicodeMode, N] ::
.
\ AtomEscape [?UnicodeMode, ?N]
CharacterClass [?UnicodeMode]
( GroupSpecifier [?UnicodeMode] Disjunction [?UnicodeMode, ?N] )
( ? : Disjunction [?UnicodeMode, ?N] )
Скриншот из стандарта:
В этой публикации нас интересует четвёртое производство — CharacterClass [?UnicodeMode]
Его дословно можно перевести как Класс Символа. Именно одного символа. Это важно!
Производство CharacterClass
В свою очередь производство CharacterClass состоит из двух производств:
CharacterClass [UnicodeMode] ::
[ [lookahead ≠ ^] ClassRanges [?UnicodeMode] ]
[ ^ ClassRanges [?UnicodeMode] ]
Скриншот из стандарта:
Внутри квадратных скобок — внутри каждого класса символа, нам нужно указывать те символы, которые должны подпадать под сопоставление одного символа. Они указываются последовательно без пробелов, без запятых и тому подобного. То есть то что будет указано в квадратных скобках, то и будет сопоставляться по одному символу.
Оба производства оформляются квадратными скобками. Но есть один нюанс. Второе производство класса символа (CharacterClass) содержит символ ^, который обозначает отрицание. Предлагаю сразу рассмотреть два примера:
// ПРИМЕР № 1 /[аб]/.exec("спорт") null
Мы получили null из метода exec(), потому что в строке «спорт» не встречается символ, который может быть равен «а» или «б«. Результат выполнения регулярного выражения не нашёл сопоставления внутри строки.
// ПРИМЕР № 2 /[^аб]/.exec("спорт")
На этот раз нам вернулся массив с результатом под нулевым индексом — буква «с«. Это первое сопоставление в строке слева-направо. Мы нашли то место в строке, где НЕ встречается символ, который может быть равен «а» или «б«.
В этих двух примерах мы использовали НЕ ПУСТОЙ диапазон класса из ClassRanges.
Рассмотрим ещё два примера, в которых будем использовать класс символа, с символами из строки:
// ПРИМЕР № 3 [..."спорт".matchAll(/[пт]/g)] // Результат работы итератора регулярного выражения - массив из массивов [ ["п", index: 1, input: "спорт", groups: undefined], ["т", index: 4, input: "спорт", groups: undefined] ]
В этом случае мы получили массив, который состоит из двух массивов. Это результат работы объекта итератора регулярного выражения, извлечённого в массив при помощи оператора spread … . В получении итератора нам помог метод matchAll()
.
Глобальное сопоставление нашего класса символа по всей строке нашло два соответствия. Сначала мы нашли «п» под первым индексом, а потом мы нашли «т» под четвёртым индексом. То есть в каждом случае мы получали какой-то ОДИН символ на выбор, который присутствовал в классе.
// ПРИМЕР № 4 [..."спорт".matchAll(/[^пт]/g)] // Результат работы итератора регулярного выражения - массив из массивов [ ["с", index: 0, input: "спорт", groups: undefined], ["о", index: 2, input: "спорт", groups: undefined], ["р", index: 3, input: "спорт", groups: undefined] ]
Сопоставление нашло 3 случая, когда в строке «спорт» встречается ОДИН символ, который не равен «п» или «т«.
Производство ClassRanges
ClassRanges [UnicodeMode] ::
[empty]
NonemptyClassRanges [?UnicodeMode]
Из производства диапазона класса видно, что он может быть всего в двух вариантах: ПУСТОЙ и НЕПУСТОЙ.
ПУСТОЙ диапазон класса
Диапазон класса (ClassRanges) присутствует в двух производствах класса символа (CharacterClass), поэтому мы можем сформировать два примера реализации (два выражения)
// Пример № 5 - Производство CharacterClass БЕЗ отрицания [..."дом".matchAll(/[]/g)] []
В этом примере у нас регулярное выражение пытается сопоставить строку с ПУСТЫМ диапазоном класса для одного символа. Наборы символов не заданы, а значит и сопоставление производится по «НИЧЕМУ» то есть null.
«НИЧЕГО» — это не символ, а значит его точно нет в строке. В результате мы получим пустой массив.
Метод exec()
более наглядный для этого результата:
/[]/.exec("дом") null
В примере № 6 мы используем отрицание. То есть НЕ «НИЧЕГО» — то есть «ВСЁ».
//Пример № 6 - Производство CharacterClass С отрицанием [..."дом".matchAll(/[^]/g)] [ ["д", index: 0, input: "дом", groups: undefined], ["о", index: 1, input: "дом", groups: undefined], ["м", index: 2, input: "дом", groups: undefined] ]
Под «ВСЁ» у нас попадает любой возможный символ. В результате мы видим массив, в котором перечислены все буквы из строки «дом»
НЕПУСТОЙ диапазон класса
В отличии от ПУСТОГО диапазона, НЕПУСТОЙ диапазон класса более насыщенный на производства. В стандарте ECMAScript их три.
Производство NonemptyClassRanges
NonemptyClassRanges [UnicodeMode] ::
ClassAtom [?UnicodeMode]
ClassAtom [?UnicodeMode] NonemptyClassRangesNoDash [?UnicodeMode]
ClassAtom [?UnicodeMode] — ClassAtom [?UnicodeMode] ClassRanges [?UnicodeMode]
Скриншот из стандарта:
Производство ClassAtom
ClassAtom [UnicodeMode] ::
—
ClassAtomNoDash [?UnicodeMode]
Скриншот из стандарта:
Непустой диапазон класса может содержать внутри себя управляющий символ дефиса «—«, который является первым производством ClassAtom. Как это может выглядеть на практике?
Случай только с дефисом
[..."окно".matchAll(/[-]/g)] [] [..."окно".matchAll(/[^-]/g)] (4) [Array(1), Array(1), Array(1), Array(1)]
В таком виде это ничем не отличается от ПУСТОГО диапазона класса.
Случай с дефисом и одним символом
Попробуем добавить какой-нибудь символ слева от дефиса:
[..."окно".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]
Случай с SourceCharacter — это любой символ Unicode, но не один из \ или ] или —. Мы рассмотрели это в самом начале публикации.
Альтернативный пример к публикации
Представьте, что у нас есть массив из строк:
["белый", "красный", "угрюмый", "выйди"]
Предположим у нас есть задача: отобрать слова, в которых есть последовательность символов «ый«. Это одно условие можно без проблем поместить в метод filter()
.
["белый", "красный", "угрюмый", "выйди"].filter(i=>/ый/g.exec(i)) ["белый", "красный", "угрюмый", "выйди"]
Так как во всех строках из массива встречается сочетания символов «ый«, то мы обратно получим такой же массив.
Если мы захотим отобрать слова в которых есть последовательности символов «ный» и «мый«, то мы должны будем передать в метод filter()
два условия.
["белый", "красный", "угрюмый", "выйди"].filter(i=>/ный|мый/g.exec(i)) ["белый", "красный", "угрюмый", "выйди"]
Но ситуация немного меняется, если все четыре слова будут находиться в одной строке «белый красный угрюмый выйди». Это уже не массив. Нам нужно в шаблоне регулярного выражения как-то отразить эти условия. Как это сделать?
Первый способ — это создание альтернатив для дизъюнкций. Но так придётся перечислять варианты, где меняется только одна буква. Выражение будет длинным.
/ный|мый/
Второй способ — это использование Класса Символа. Позволяет существенно сократить длину записи регулярного выражения и сделать её более читаемой и структурированной.
/[нм]ый/
Информационные ссылки
Производство Atom — https://tc39.es/ecma262/#prod-Atom