JavaScript · Синтаксис · Проміжний

Регулярні вирази

10 завдань

Пошук за шаблоном за допомогою RegExp. Охоплює test, match, replace та іменовані групи захоплення.

Основи regex у JavaScript

#
Регулярний вираз у JavaScript — це об'єкт, що описує текстовий шаблон. Створюється за допомогою літерала `/pattern/flags` або `new RegExp('pattern', 'flags')`. **Три найпоширеніші методи** ```javascript const text = 'User: alice, age 30'; /\d+/.test(text) // true - чи є патерн десь у рядку? text.match(/\d+/) // ['30'] - перший збіг як масив (або null) text.replace(/\d+/, '??') // 'User: alice, age ??' - замінити перший збіг text.replace(/\d+/g, '??')// 'User: alice, age ??' - замінити ВСІ (прапор g) ``` **Читаємо патерн посимвольно** Візьмемо `/([\w.]+)@([\w.]+\.\w{2,})/` (простий патерн email): ``` ( відкрити групу 1 [\w.]+ один або більше: символи слова або літеральна крапка ) закрити групу 1 @ літеральний "@" ( відкрити групу 2 [\w.]+ один або більше: символи слова або літеральна крапка \. літеральна крапка (екранована - звичайна . означає "будь-який символ") \w{2,} 2 або більше символів слова (домен верхнього рівня) ) закрити групу 2 ``` ```javascript const m = '[email protected]'.match(/([\w.]+)@([\w.]+\.\w{2,})/); m[0] // '[email protected]' - повний збіг m[1] // 'bob' - група 1 m[2] // 'example.com' - група 2 ``` **Літерал regex vs new RegExp** Перевагу надавай літералу `/pattern/` — він компілюється один раз і не потребує екранування рядків. Використовуй `new RegExp(str)` лише коли патерн будується під час виконання: ```javascript // Добре: літерал const valid = /^\d{3}-\d{4}$/; // Коли патерн з змінної: function buildSearch(word) { const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return new RegExp(escaped, 'gi'); } ``` **Пастка `lastIndex` зі збереженням стану** Коли використовуєш прапор `g` або `y` і викликаєш `.test()` або `.exec()` на *тому самому* об'єкті regex кілька разів, рушій запам'ятовує позицію через `regex.lastIndex`. Це призводить до дивного чергування true/false: ```javascript const re = /\d+/g; // зі збереженням стану через 'g' re.test('abc 42 def'); // true (знайшов '42', lastIndex -> 6) re.test('abc 42 def'); // false (продовжує з індексу 6, нічого не знаходить) re.test('abc 42 def'); // true (lastIndex скидається після невдачі) // Безпечний патерн: створюй новий regex щоразу або скидай вручну re.lastIndex = 0; re.test('abc 42 def'); // true (завжди) ``` Найбезпечніше правило: використовуй прапор `g` лише з `str.match()`, `str.matchAll()` або `str.replaceAll()`, а не з повторними викликами `.test()` на тому самому об'єкті regex.

Захоплюючі групи, іменовані групи та matchAll

#
**Захоплюючі групи** дозволяють витягувати частини збігу. Результат `.match()` без прапора `g` містить повний збіг за індексом 0, потім кожну групу. ```javascript const log = '2024-03-15 ERROR disk full'; const m = log.match(/(\d{4}-\d{2}-\d{2}) (\w+) (.+)/); m[1] // '2024-03-15' m[2] // 'ERROR' m[3] // 'disk full' ``` **Іменовані групи** `(?<name>...)` — доступ через `m.groups.name`: ```javascript const m = log.match(/(?<date>\d{4}-\d{2}-\d{2}) (?<level>\w+) (?<msg>.+)/); m.groups.date // '2024-03-15' m.groups.level // 'ERROR' m.groups.msg // 'disk full' ``` Іменовані групи також доступні в `replace` через `$<name>`: ```javascript '2024-03-15'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, '$<d>/$<m>/$<y>') // '15/03/2024' ``` **replace з функцією** Коли заміна є функцією, вона отримує `(fullMatch, ...groups, offset, originalStr)`: ```javascript 'hello world'.replace(/(\w+)/g, (match) => match.toUpperCase()) // 'HELLO WORLD' 'a=3, b=7'.replace(/(\w+)=(\d+)/g, (_, key, val) => `${key}=${+val * 2}`) // 'a=6, b=14' ``` **matchAll — ітерація по всіх збігах з групами** `.match()` з прапором `g` повертає лише повні збіги (групи відкидаються). Використовуй `str.matchAll(regex)`, щоб отримати всі збіги *разом* з їхніми групами: ```javascript const text = 'alice=30, bob=25'; const re = /(\w+)=(\d+)/g; for (const m of text.matchAll(re)) { console.log(m[1], m[2]); // 'alice' '30' потім 'bob' '25' } // або зібрати результати: const pairs = [...text.matchAll(re)].map(m => ({ name: m[1], age: +m[2] })); // [{ name: 'alice', age: 30 }, { name: 'bob', age: 25 }] ``` `matchAll` вимагає прапор `g` і повертає ітератор — розгортай або ітеруй з `for...of`. **Не-захоплюючі групи** `(?:...)` групують без індексу захоплення — зручні для чергування: ```javascript // Знайти 'colour' або 'color' 'colour and color'.match(/colou?r/g) // ['colour', 'color'] // Чергування без захоплюючої групи 'cats and dogs'.match(/(?:cat|dog)s?/g) // ['cats', 'dogs'] ```

Lookahead, lookbehind, жадібні/ліниві та динамічний regex

#
**Lookahead** перевіряє, що йде *після* поточної позиції, не поглинаючи символів. ```javascript // Позитивний lookahead (?=...) - збіг X, лише якщо після нього Y '100px 200em 300px'.match(/\d+(?=px)/g) // ['100', '300'] (числа перед 'px') // Негативний lookahead (?!...) - збіг X, лише якщо після нього НЕ Y '100px 200em'.match(/\d+(?!px)/g) // ['200'] (числа не перед 'px') ``` **Lookbehind** (ES2018) перевіряє, що йде *перед*: ```javascript // Позитивний lookbehind (?<=...) - збіг X, лише якщо перед ним Y '$100 EUR200'.match(/(?<=\$)\d+/g) // ['100'] (числа після '$') // Негативний lookbehind (?<!...) - збіг X, лише якщо перед ним НЕ Y '$100 EUR200'.match(/(?<!\$)\d+/g) // ['200'] (числа не після '$') ``` **Жадібні vs ліниві** Квантифікатори за замовчуванням жадібні. Додай `?`, щоб зробити їх лінивими: ```javascript const html = '<b>bold</b> and <i>italic</i>'; html.match(/<.+>/g) // ['<b>bold</b> and <i>italic</i>'] жадібний html.match(/<.+?>/g) // ['<b>', '</b>', '<i>', '</i>'] лінивий ``` **Екранування введення користувача для динамічного regex** Якщо будуєш regex з рядка, введеного користувачем, спочатку обов'язково екрануй всі спеціальні символи regex — інакше крапка або зірочка зламають патерн: ```javascript function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function highlight(text, query) { const re = new RegExp(escapeRegex(query), 'gi'); return text.replace(re, '<mark>$&</mark>'); } highlight('Ціна: 2.50', '2.50') // 'Ціна: <mark>2.50</mark>' // без екранування '.' збіглася б з будь-яким символом ``` **Типові пастки** - Забути прапор `g`: `'aaa'.replace(/a/, 'b')` дає `'baa'`, а не `'bbb'` - `.` не збігається з новими рядками за замовчуванням — використовуй `[\s\S]` або прапор `s` - `^` і `$` збігаються з межами рядка, а не рядка тексту — якщо не використовується прапор `m` - `.test()` у циклі з тим самим `g` regex викликає зміщення `lastIndex` (див. Блок 1) - Regex не може парсити HTML — використовуй DOM

Кулінарна книга regex-патернів (JavaScript)

#
Готова колекція патернів для поширених задач у JavaScript. **Email (простий)** ```javascript const EMAIL = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g; '[email protected], [email protected]'.match(EMAIL) // ['[email protected]', '[email protected]'] ``` **ISO дата** ```javascript const ISO = /\b(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\b/g; 'Захід 2024-03-15, дедлайн 2024-12-31'.match(ISO) // ['2024-03-15', '2024-12-31'] ``` **URL** ```javascript const URL_RE = /https?:\/\/[^\s<>'"]+/g; 'Дивись https://developer.mozilla.org для деталей'.match(URL_RE) // ['https://developer.mozilla.org'] ``` **Slug (нижній регістр з дефісами)** ```javascript function toSlug(str) { return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); } toSlug('Hello World! 42') // 'hello-world-42' ``` **Camel до snake** ```javascript function camelToSnake(str) { return str.replace(/([A-Z])/g, '_$1').toLowerCase(); } camelToSnake('myVariableName') // 'my_variable_name' ``` **Маска кредитної карти (показати останні 4)** ```javascript '4111 1111 1111 1234'.replace(/\d(?=\d{4})/g, '*') // '**** **** **** 1234' ``` **Довідник прапорів** ``` g global - знайти всі збіги (не лише перший) i без урахування регістру m multiline - ^ та $ збігаються з межами рядків s dotAll - . збігається і з новими рядками u unicode - вмикає \u{...} та класи Unicode d hasIndices - match.indices[n] дає [start, end] для кожної групи ``` **Коли НЕ використовувати regex у JavaScript** - Парсинг HTML - використовуй `document.querySelector`, `DOMParser` або бібліотеку - Парсинг URL - використовуй `new URL(str)` - Парсинг JSON - використовуй `JSON.parse` - Сувора валідація email - regex не може забезпечити RFC 5322; використовуй бібліотеку Regex блискуче підходить для сканування та перетворення звичайного тексту. Для структурованих форматів завжди надавай перевагу спеціалізованому парсеру.
01

#

Напишіть функцію, яка перевіряє, чи є рядок дійсною адресою електронної пошти (містить @, домен і крапку в частині домену).

function isValidEmail(str) {
    // ваш код тут
}

console.log(isValidEmail('[email protected]'));   // true
console.log(isValidEmail('user@example'));        // false
console.log(isValidEmail('notanemail'));          // false
console.log(isValidEmail('[email protected]'));              // true
Рішення
function isValidEmail(str) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}
02

#

Витягніть усі слова з рядка, що починаються з великої літери. Поверніть їх як масив.

function capitalWords(str) {
    // ваш код тут
}

console.log(capitalWords('Hello world, My Name is Alice'));  // ['Hello', 'My', 'Name', 'Alice']
console.log(capitalWords('no capitals here'));               // []
Рішення
function capitalWords(str) {
    return str.match(/\b[A-Z][a-zA-Z]*/g) || [];
}
03

#

Замініть усі послідовності пробільних символів (пробіли, табуляції, переводи рядка) у рядку одним пробілом і обріжте результат.

function normalizeSpaces(str) {
    // ваш код тут
}

console.log(normalizeSpaces('  hello   world  '));         // 'hello world'
console.log(normalizeSpaces('line1\n\nline2\t  line3')); // 'line1 line2 line3'
Рішення
function normalizeSpaces(str) {
    return str.replace(/\s+/g, ' ').trim();
}
04

#

Витягніть усі числа (цілі та десяткові) з рядка і поверніть їх як масив чисел.

function extractNumbers(str) {
    // ваш код тут
}

console.log(extractNumbers('Price: 12.5, qty: 3, discount: 0.15'));  // [12.5, 3, 0.15]
console.log(extractNumbers('no numbers here'));                       // []
Рішення
function extractNumbers(str) {
    const matches = str.match(/\d+\.?\d*/g);
    return matches ? matches.map(Number) : [];
}
05

#

Використайте групу захоплення, щоб витягти ім'я користувача та домен з рядка адреси електронної пошти.

function parseEmail(email) {
    // Повернути { username, domain } або null, якщо недійсна
}

console.log(parseEmail('[email protected]'));   // { username: 'user', domain: 'example.com' }
console.log(parseEmail('notanemail'));          // null
Рішення
function parseEmail(email) {
    const match = email.match(/^([^\s@]+)@([^\s@]+)$/);
    if (!match) return null;
    return { username: match[1], domain: match[2] };
}
06

#

Напишіть функцію, що перетворює рядок camelCase у snake_case (наприклад, 'myVariableName' → 'my_variable_name').

function camelToSnake(str) {
    // ваш код тут
}

console.log(camelToSnake('myVariableName'));    // 'my_variable_name'
console.log(camelToSnake('helloWorld'));        // 'hello_world'
console.log(camelToSnake('alreadylower'));      // 'alreadylower'
Рішення
function camelToSnake(str) {
    return str.replace(/([A-Z])/g, '_$1').toLowerCase();
}
07

#

Підрахуйте, скільки разів слово зустрічається в рядку (без врахування регістру, тільки цілі слова).

function countWord(str, word) {
    // ваш код тут
}

console.log(countWord('The cat sat on the mat. The cat is fat.', 'the'));  // 3
console.log(countWord('cats and cat and cat', 'cat'));                      // 3 (not 'cats')
Рішення
function countWord(str, word) {
    const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const re = new RegExp(`\\b${escaped}\\b`, 'gi');
    return (str.match(re) || []).length;
}
08

#

Розбийте рядок значень, розділених комами/крапками з комою/пробілами, на масив обрізаних непорожніх токенів.

function splitTokens(str) {
    // ваш код тут
}

console.log(splitTokens('a, b;  c , d'));    // ['a', 'b', 'c', 'd']
console.log(splitTokens('one;two,three'));   // ['one', 'two', 'three']
console.log(splitTokens('  , ; '));          // []
Рішення
function splitTokens(str) {
    return str.split(/[,;\s]+/).map(t => t.trim()).filter(Boolean);
}
09

#

Використайте регулярний вираз для маскування всіх цифр, крім останніх чотирьох, в номері кредитної картки (замініть на '*').

function maskCard(number) {
    // ваш код тут
}

console.log(maskCard('1234567890123456'));  // '************3456'
console.log(maskCard('4111 1111 1111 1111')); // '**** **** **** 1111'
Рішення
function maskCard(number) {
    return number.replace(/\d(?=\d{4})/g, '*');
}
10

#

Витягніть усі URL з рядка. URL починається з http:// або https:// і закінчується пробільним символом або кінцем рядка.

function extractUrls(str) {
    // ваш код тут
}

const text = 'Visit https://example.com and http://foo.org/path?q=1 for details.';
console.log(extractUrls(text));
// ['https://example.com', 'http://foo.org/path?q=1']
Рішення
function extractUrls(str) {
    return str.match(/https?:\/\/[^\s]+/g) || [];
}