16 липня 2023 р.

Набори та діапазони [...]

Декільки символів, або класи символів всередині квадратних дужок […] означають “шукати будь-який символ з-поміж заданих”.

Набори

До прикладу, [eao] означає будь-який з трьох символів: 'a', 'e', or 'o'.

У такий спосіб записується так званий набір. Набір може використовуватись в регулярних виразах разом зі звичайними символами:

// знайти [t або m], а потім "op"
alert( "Mop top".match(/[tm]op/gi) ); // "Mop", "top"

Зверніть увагу, що незважаючи на те, що в наборі вказано два символи, в результаті є співпадіння лише по одному з них.

Тож нижченаведений приклад не знайде співпадінь:

// знайти "V", потім [o чи i], потім "la"
alert( "Voila".match(/V[oi]la/) ); // null, no matches

Шаблон шукає:

  • V,
  • потім одна з літер набору [oi],
  • потім la.

Результатом такого пошуку могли б стати варіанти Vola або Vila.

Діапазони

Квадратні дужки також можуть містити діапазони символів.

До прикладу, [a-z] шукатиме символу в діапазоні від a до z, а [0-5] дорівнює цифрам в діапазоні від 0 до 5.

В нижченаведеному прикладі ми шукатимемо літеру "x" за якою слідують дві цифри, або літери від A до F:

alert( "Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g) ); // xAF

Тут [0-9A-F] має в собі одразу два діапазони: він шукає символ, який є або цифрою від 0 до 9 , або літерою від A до F.

Якби ми захотіли шукати літери не тільки верхнього, а й нижнього регістру, ми могли б додати діапазон a-f: [0-9A-Fa-f]. Або додати флаг i.

Крім того, всередині […] ми можемо використовувати символьні класи.

До прикладу, якщо ми захочемо знайти символ “слова” \w , або дефіс -, набір виглядатиме наступним чином [\w-].

Комбінувати декілька класів теж можливо, наприклад [\s\d] означає “пробіл, або цифра”.

Символьні класи це лише скорочення для деяких наборів символів

До прикладу:

  • \d – це те саме, що й [0-9],
  • \w – це те саме, що й [a-zA-Z0-9_],
  • \s – це те саме, що й [\t\n\v\f\r ], плюс декілька інших рідкісних пробільних символів Unicode.

Приклад: \w в інших мовах світу

Оскільки символьний клас \w це лише скорочений запис [a-zA-Z0-9_], він не зможе знайти китайські ієрогліфи, літери кирилицею тощо.

Існує спосіб написати більш універсальний шаблон, що включатиме в себе буквенні символи будь-якої мови світу. Це легко реалізувати завдяки властивостям Unicode: [\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}].

Давайте розшифруємо цей шаблон. Подібно до \w, ми створюємо свій діапазон, який включає в себе символи з наступними властивостями Unicode:

  • Alphabetic (Alpha) – для літер,
  • Mark (M) – для акцентів,
  • Decimal_Number (Nd) – для цифр,
  • Connector_Punctuation (Pc) – для нижнього підкреслення '_' і тому подібних символів,
  • Join_Control (Join_C) – два спеціальних коди 200c та 200d, які використовуються у лігатурах, зокрема в арабській мові.

Шаблон в дії:

let regexp = /[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/gu;

let str = `Hi 你好 12`;

// знайти всі літери та цифри:
alert( str.match(regexp) ); // H,i,你,好,1,2

Звичайно, ми можемо редагувати вищенаведений шаблон: додавати Unicode властивості, або видаляти їх. Дізнатись більше про Unicode властивості можна за посиланням Юнікод: прапорець "u" та клас \p{...}.

Internet Explorer не підтримує Unicode властивості

Unicode властивості p{…} недоступні у Internet Explorer. Втім, якщо вони нам все ж потрібні, ми можемо скористатись бібліотекою XRegExp.

або просто вказати діапазон потрібних нам символів певною мовою, наприклад [а-я] для літер кирилицею.

Діапазони виключень

Окрім звичайних діапазонів, існують діапазони “виключень” які виглядають наступним чином: [^…].

Вони відрізняються символом каретки ^ на початку і знаходять будь-який символ окрім вказаних в діапазоні.

До прикладу:

  • [^aeyo] – будь-який символ окрім 'a', 'e', 'y' or 'o'.
  • [^0-9] – будь-який символ окрім цифр, так само як і \D.
  • [^\s] – будь-який не пробільний символ \S.

Нижченаведений приклад шукає будь-який символ окрім літер латиницею, цифр та пробільних символів:

alert( "alice15@gmail.com".match(/[^\d\sA-Z]/gi) ); // @ та .

Екранування всередині […]

Зазвичай, коли ми хочемо знайти один зі спецсимволів, нам потрібно екранувати його наступним чином \.. Тобто, якщо нам потрібна зворотня коса риска, ми маємо писати \\, і так далі.

В квадратних дужках ми можемо використовувати велику кількість спецсимволів без екранування:

  • Символ . + ( ) не потребує екранування.
  • Дефіс - не потребує екранування на початку, або в кінці (тобто коли не може означати діапазон).
  • Каретка ^ екранується лише на початку (без екранування означає набір символів-виключень).
  • Закриваюча квадратна дужка ] завжди потребує екранування (у випадках, коли нам потрібно знайти цей символ).

Інакше кажучи, всі спецсимволи можна використовувати без екранування тоді, коли вони не мають додаткового значення в квадратних дужках.

Крапка . всередині квадратних дужок означає просто крапку. Шаблон [.,] шукатиме на один з двох символів, або крапку, або кому.

В нижченаведеному прикладі регулярний вираз [-().^+] шукає один з вказаних символів -().^+:

// Не потрібно екранувати
let regexp = /[-().^+]/g;

alert( "1 + 2 - 3".match(regexp) ); // Знаходить +, -

…Але якщо ви вирішите все ж таки екранувати їх, “про всяк випадок”, гірше від того не буде:

// Всі символи екрановані
let regexp = /[\-\(\)\.\^\+]/g;

alert( "1 + 2 - 3".match(regexp) ); // так само находить: +, -

Діапазони і прапорець “u”

Якщо в діапазоні є сурогатні пари, для коректної роботи регулярного виразу, прапорець u є обов’язковим.

До прикладу, давайте знайдемо [𝒳𝒴] у рядку 𝒳:

alert( '𝒳'.match(/[𝒳𝒴]/) ); // виводить дивний символ, схожий на [?]
// (пошук було виконано некореткно, повернуто тільки половину символу)

Результат є некоректним, оскільки типово регулярні вирази “не знають” про сурогатні пари.

Регулярний вираз сприймає [𝒳𝒴] – не як два, а як чотири символи:

  1. ліва половина 𝒳 (1),
  2. права половина 𝒳 (2),
  3. ліва половина 𝒴 (3),
  4. права половина 𝒴 (4).

Якщо вивести їх кодове значення ми побачимо наступне:

for(let i=0; i<'𝒳𝒴'.length; i++) {
  alert('𝒳𝒴'.charCodeAt(i)); // 55349, 56499, 55349, 56500
};

Отже, вищенаведений приклад знайшов і вивів ліву половину 𝒳.

Якщо ми додамо прапорець u, то поведінка буде коректною:

alert( '𝒳'.match(/[𝒳𝒴]/u) ); // 𝒳

Подібна ситуація складається коли ми шукаємо в діапазоні, наприклад [𝒳-𝒴].

Якщо ми забудемо додати прапорець u, то отримаємо помилку:

'𝒳'.match(/[𝒳-𝒴]/); // Error: Invalid regular expression

Причина в тому, що без прапорцю u сурогатні пари сприймаються як два окремих символи, тобто [𝒳-𝒴] обробляються як [<55349><56499>-<55349><56500>] (кожна сурогатна пара замінюється на набір кодів). Таким чином, ми бачимо, що діапазон 56499-55349 є некоректним: його початковий номер 56499 більший за останній 55349. Це і є причиною помилки.

З прапорцем u шаблон працює коректно:

// шукає символи від 𝒳 до 𝒵
alert( '𝒴'.match(/[𝒳-𝒵]/u) ); // 𝒴

Завдання

У нас є регулярний вираз /Java[^script]/.

Чи знайде він співпадіння у рядку Java? А у рядку JavaScript?

Відповідь: ні, так.

  • В скрипті Java немає співпадінь, оскільки [^script] означає “пошук будь-яких символів окрім заданих”. Тож регулярний вираз шукає "Java" за яким слідує один такий символ, але це кінець рядку і далі немає символів.

    alert( "Java".match(/Java[^script]/) ); // null
  • Так, оскільки частина патерну [^script] співпадає з символом "S". Його немає в переліку script. Бо регулярний вираз розрізняє регістри букв (не вказаний прапорець i), то для нього "S"та "s" це різні символи.

    alert( "JavaScript".match(/Java[^script]/) ); // "JavaS"

Час можна записати у форматі години:хвилини або години-хвилини. У будь-якому разі потрібні дві цифри для позначення годин і хвилин: 09:00 або 21-30.

Напишіть регулярний вираз для пошуку часового формату:

let regexp = /your regexp/g;
alert( "Сніданок о 09:00. Вечеря о 21-30".match(regexp) ); // 09:00, 21-30

P.S. В цій задачі ми завжди маємо коректний час, не потрібно перевіряти неіснуючі комбінації цифр, як-то “45:67”. Пізніше ми роглянемо і такі випадки.

Відповідь: \d\d[-:]\d\d.

let regexp = /\d\d[-:]\d\d/g;
alert( "Сніданок о 09:00. Вечеря о 21-30".match(regexp) ); // 09:00, 21-30

Зверніть увагу, що риска '-' має спеціальне значення у квадратних дужках але тільки між іншими символами, не на початку чи в кінці виразу, тож немає необхідності її екранувати.

Навчальна карта