18 лютого 2023 р.

Липкий прапорець "y", пошук на заданій позиції

Прапорець y дозволяє виконувати пошук на вказаній позиції у вихідному рядку.

Щоб дізнатись як використовувати прапорець y, і краще зрозуміти шляхи використання регулярних виразів, розгляньмо приклад з практики.

Одним із поширених завдань для регулярних виразів є “лексичний аналіз”: для прикладу ми розглядаємо тест написаний певною мовою програмування і хочемо виділити структурні елементи. Наприклад, HTML містить теги та атрибути, код JavaScript – функції, змінні тощо.

Написання лексичних аналізаторів – це особлива сфера, зі своїми інструментами та алгоритмами, тому ми не будемо заглиблюватись в неї, а зосередимось на звичайному завданні: прочитати щось на заданій позиції.

Наприклад, у нас є рядок коду let varName = "значення", і нам потрібно прочитати з нього назву змінної, яка починається з позиції 4.

Ми шукатимемо назву змінної за допомогою регулярного виразу \w+. Насправді імена змінних JavaScript потребують трохи складніших регулярних виразів для точної відповідності, але тут це не важливо.

  • Виклик str.match(/\w+/) знайде лише перше слово в рядку (let). А це не те що нам потрібно.
  • Ми можемо додати прапорець g. Але тоді виклик str.match(/\w+/g) шукатиме всі слова в тексті, тоді як нам потрібно лише одне слово на позиції 4.

Отже, як змусити регулярний вираз шукати саме на заданій позиції?

Спробуймо використати метод regexp.exec(str).

Для regexp без прапорців g і y, цей метод шукає лише перший збіг, він працює, так само як str.match(regexp).

…Але якщо є прапорець g, тоді він виконує пошук у str, починаючи з позиції, збереженої у властивості regexp.lastIndex. І, якщо він знаходить збіг, то змінює regexp.lastIndex на індекс одразу після збігу.

Іншими словами, regexp.lastIndex служить відправною точкою для пошуку, і кожен виклик regexp.exec(str) встановлює нове значення (“після останнього збігу”). Звичайно, якщо є прапорець g.

Отже, послідовні виклики regexp.exec(str) повертають збіги один за одним.

Ось приклад таких викликів:

let str = 'let varName'; // Знайдімо всі слова в цьому рядку
let regexp = /\w+/g;

alert(regexp.lastIndex); // 0 (спочатку lastIndex=0)

let word1 = regexp.exec(str);
alert(word1[0]); // let (перше слово)
alert(regexp.lastIndex); // 3 (позиція після збігу)

let word2 = regexp.exec(str);
alert(word2[0]); // varName (друге слово)
alert(regexp.lastIndex); // 11 (позиція після збігу)

let word3 = regexp.exec(str);
alert(word3); // null (більше немає збігів)
alert(regexp.lastIndex); // 0 (скидається в кінці пошуку)

Ми можемо отримати всі збіги в циклі:

let str = 'let varName';
let regexp = /\w+/g;

let result;

while (result = regexp.exec(str)) {
  alert( `Found ${result[0]} at position ${result.index}` );
  // Знайдено let на позиції 0, після
  // Знайдено varName на позиції 4
}

Таке використання regexp.exec є альтернативою методу str.matchAll, з трохи більшим контролем над процесом.

Повернемося до нашого завдання.

Ми можемо вручну встановити lastIndex на 4, щоб почати пошук із заданої позиції!

Ось так:

let str = 'let varName = "значення"';

let regexp = /\w+/g; // без прапорця "g", властивість lastIndex ігнорується

regexp.lastIndex = 4;

let word = regexp.exec(str);
alert(word); // varName

Ура! Проблема вирішена!

Ми здійснили пошук \w+, починаючи з позиції regexp.lastIndex = 4.

І результат нашого пошуку правильний.

…Але заждіть, не так швидко.

Зауважте: виклик regexp.exec починає пошук із позиції lastIndex, а потім продовжує пошук. Якщо на позиції lastIndex немає слова, але воно знаходиться десь після неї, тоді воно буде знайдено:

let str = 'let varName = "значення"';

let regexp = /\w+/g;

// почати пошук з позиції 3
regexp.lastIndex = 3;

let word = regexp.exec(str);
// знайдено збіг на позиції 4
alert(word[0]); // varName
alert(word.index); // 4

Для деяких завдань, зокрема лексичного аналізу, це неправильно. Нам потрібно знайти збіг в заданій позиції в тексті, а не десь після неї. Це і є головне призначення прапорця y.

Прапорець y змушує regexp.exec шукати саме на позиції lastIndex, а не “починаючи з” неї.

Ось той самий пошук із прапорцем y:

let str = 'let varName = "значення"';

let regexp = /\w+/y;

regexp.lastIndex = 3;
alert( regexp.exec(str) ); // null (на позиції 3 пробіл, а не слово)

regexp.lastIndex = 4;
alert( regexp.exec(str) ); // varName (слово на позиції 4)

Як ми бачимо, регулярний вираз /\w+/y не знаходить збігів на позиції 3 (на відміну від регулярного виразу з прапорцем g), але знаходить збіг на позиції 4.

Але це не всі переваги використання прапорця y, він також збільшує продуктивність пошуку.

Уявіть, у нас довгий текст, а в ньому зовсім немає збігів. Тоді пошук із прапорцем g буде йти до кінця тексту й нічого не знайде, і це займе значно більше часу, ніж пошук із прапорцем y, який перевіряє лише на вказаній позиції.

Лексичний аналіз часто вимагає пошук на конкретній позиції. Використання прапорця y є ключем до правильної реалізації та хорошої продуктивності при виконанні таких завдань.

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