Skip to content

Latest commit

 

History

History
1005 lines (669 loc) · 80.2 KB

09_regexp.md

File metadata and controls

1005 lines (669 loc) · 80.2 KB

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

{{quote {author: «Jamie Zawinski», chapter: true}}

Деякі люди, коли стикаються з проблемою, думають: «Я знаю, я використаю регулярні вирази». Тепер вони мають дві проблеми.

quote}}

{{index «Zawinski, Jamie»}}

{{if інтерактивний

{{quote {автор: «Master Yuan-Ma», title: «Книга про програмування», chapter: true}} {{quote {автор: “Майстер Юань-Ма”, title: “Книга про програмування”, chapter: true}}

Коли ви ріжете проти волокон дерева, потрібно багато сили. Коли ви програмуєте проти суті проблеми, потрібно багато коду.

quote}}

if}}

{{figure {url: «img/chapter_picture_9.jpg», alt: «Ілюстрація залізничної системи, що представляє синтаксичну структуру регулярних виразів», chapter: «square-framed"}}}

{{Еволюція, прийняття, інтеграція індексів}}

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

{{index «domain-specific language»}}

У цій главі я розповім про один з таких інструментів, ((регулярні вирази))s. Регулярні вирази - це спосіб опису ((шаблону))s у рядкових даних. Вони утворюють невелику окрему мову, яка є частиною JavaScript та багатьох інших мов і систем.

{{index [інтерфейс, дизайн]}}

Регулярні вирази є одночасно жахливо незручними і надзвичайно корисними. Їх синтаксис загадковий, а програмний інтерфейс, який надає JavaScript для них, незграбний. Але вони є потужним ((інструментом)) для перевірки та обробки рядків. Правильне розуміння регулярних виразів зробить вас більш ефективним програмістом.

Створення регулярного виразу

{{index [«регулярний вираз», створення], «клас RegExp», «буквальний вираз», «коса риска»}}

Регулярний вираз - це тип об'єкта. Його можна створити за допомогою конструктора RegExp або записати як буквальне значення, уклавши шаблон у символи прямої похилої риски (/).

let re1 = new RegExp(«abc»);
let re2 = /abc/;

Обидва ці об'єкти регулярних виразів представляють один і той самий ((шаблон)): символ a, за яким слідує символ b, за яким слідує символ c.

{{index [«символ зворотного слешу», «у регулярних виразах»], «клас RegExp»}}

При використанні конструктора RegExp шаблон записується як звичайний рядок, тому для зворотної косої риски застосовуються звичайні правила.

{{index [«регулярний вираз», екранування], [екранування, «у regexps»], «символ слеша»}}

У другому записі, де шаблон відображається між символами похилої риски, зворотні косі риски розглядаються дещо по-іншому. По-перше, оскільки пряма коса риска завершує шаблон, то перед будь-якою прямою косою рискою, яку ми хочемо зробити частиною шаблону, слід ставити зворотну косу риску. Крім того, зворотні слеші, які не є частиною спеціальних кодів символів (наприклад, \n), буде збережено, а не проігноровано, як у рядках, і вони змінять значення шаблону. Деякі символи, такі як знаки питання і знаки плюс, мають спеціальне значення у регулярних виразах, і перед ними слід ставити зворотну косу похилу риску, якщо вони призначені для позначення самого символу.

нехай aPlus = /A\+/;

Перевірка на збіг

{{index matching, «test method», [«регулярний вираз», methods]}}

Об'єкти регулярних виразів мають ряд методів. Найпростіший з них - test. Якщо ви передасте йому рядок, він поверне значення ((Boolean)), яке покаже вам, чи містить рядок збіг з шаблоном у виразі.

console.log(/abc/.test(«abcde»));
// → true
console.log(/abc/.test(«abxde»));
// → false

{{індексний шаблон}}

((регулярний вираз)), що складається лише з неспеціальних символів, просто представляє цю послідовність символів. Якщо abc зустрічається в будь-якому місці рядка, який ми перевіряємо (не тільки на початку), test поверне true.

Набори символів

{{index «регулярний вираз», «indexOf методу»}}

З'ясувати, чи містить рядок abc, можна також за допомогою виклику indexOf. Регулярні вирази корисні тим, що дозволяють описувати більш складні ((шаблони)).

Скажімо, ми хочемо знайти будь-яке ((число)). У регулярному виразі, якщо помістити ((набір)) символів між квадратними дужками, ця частина виразу буде відповідати будь-якому з символів у дужках.

Обидва наступні вирази відповідають усім рядкам, які містять ((цифру)):

console.log(/[0123456789]/.test(«in 1992»));
// → true
console.log(/[0-9]/.test(«in 1992»));
// → true

{{index «символ дефісу»}}

У квадратних дужках дефіс (-) між двома символами можна використовувати для позначення ((діапазону)) символів, де порядок визначається номером символу ((Unicode)). Символи від 0 до 9 знаходяться поруч один з одним у цьому порядку (коди від 48 до 57), тому [0-9] охоплює їх усі і відповідає будь-якій ((цифрі)).

{{індекс [пробіл, відповідність], «алфавітно-цифровий символ», «символ крапки»}}

Для деяких поширених груп символів існують власні вбудовані комбінації клавіш. Однією з них є цифри: \d означає те саме, що й [0-9].

{{index «символ нового рядка», [пробіл, збіг]}}

{{table {cols: [1, 5]}}}

| \d | Будь-який ((цифра)) символ | \w | Буквено-цифровий символ («((символ слова)») | \s | Будь-який пробіл (пробіл, табуляція, новий рядок тощо) | Символ, який не є цифрою | Не буквено-цифровий символ | не пробіл, не символ пробілу | Будь-який символ, окрім нового рядка

Ви можете зіставити формат ((дата)) і ((час)), наприклад, 01-30-2003 15:20, з наступним виразом:

нехай dateTime = /\d\d-\d\d-\d\d\d\d\d \d\d:\d\d/;
console.log(dateTime.test(«01-30-2003 15:20»));
// → true
console.log(dateTime.test(«30-jan-2003 15:20»));
// → false

{{index [«символ зворотного слешу», «у регулярних виразах»]}}

Цей регулярний вираз виглядає просто жахливо, чи не так? Половину його складають символи зворотної косої риски, що створюють фоновий шум, який заважає розпізнати власне ((шаблон)) вираз. Ми побачимо дещо покращену версію цього виразу пізніше.

{{index [екранування, «у regexps»], «регулярний вираз», set}}

Ці коди зворотної косої риски також можна використовувати всередині ((квадратних дужок)). Наприклад, [\d.] означає будь-яку цифру або символ крапки. Сама крапка між квадратними дужками втрачає своє особливе значення. Те саме стосується інших спеціальних символів, таких як знак плюс (+).

{{індекс «квадратні дужки», інверсія, «символ каретки»}}

Щоб інвертувати набір символів, тобто вказати, що ви хочете знайти будь-який символ , окрім тих, що є у наборі, ви можете написати символ лапок (^) після відкриваючої дужки.

нехай nonBinary = /[^01]/;
console.log(nonBinary.test(«1100100010100110»));
// → false
console.log(nonBinary.test(«0111010112101001»));
// → true

Міжнародні символи

{{індекс інтернаціоналізації, Unicode, [«регулярний вираз», інтернаціоналізація]}}

Через початкову спрощену реалізацію JavaScript і той факт, що цей спрощений підхід пізніше був закріплений як ((стандартна)) поведінка, регулярні вирази JavaScript досить тупо реагують на символи, які не зустрічаються в англійській мові. Наприклад, з точки зору регулярних виразів JavaScript, «((символ слова))» - це лише один з 26 символів латинського алфавіту (великі або малі літери), десяткові цифри і, чомусь, символ підкреслення. Такі символи, як é або β, які, безумовно, є символами слів, не відповідатимуть \ww відповідатиме великій букві \W, категорії не-слів).

{{індекс [пробіли, відповідність]}}

За дивним історичним збігом обставин, \s (пробіл) не має цієї проблеми і відповідає усім символам, які стандарт Юнікоду вважає пробілами, включаючи такі, як ((нерозривний пробіл)) та ((монгольський роздільник голосних)).

{{index «категорія символів», [Unicode, властивість]}}

Можна використовувати \p у регулярному виразі для зіставлення усіх символів, яким стандарт Юнікоду присвоює певну властивість. Це дозволяє нам зіставляти такі речі, як літери, у більш космополітичний спосіб. Однак, знову ж таки через сумісність зі стандартами оригінальної мови, вони розпізнаються лише тоді, коли ви додаєте символ u (для ((Unicode))) після регулярного виразу.

{{table {cols: [1, 5]}}}

| \p{L} | Будь-яка літера | \p{N} | Будь-який цифровий символ | Будь-який знак пунктуації | \P{L} | Будь-яка нелітера (інвертує велику літеру P) | \p{Script=Hangul} | Будь-який символ із заданого сценарію (див. Глава ?)

Використання \w для обробки неанглійського тексту (або навіть англійського тексту із запозиченими словами, як-от «кліше») є ризикованим, оскільки такі символи, як «é», не вважатимуться літерами. Хоча групи властивостей \p, як правило, є дещо багатослівнішими, вони є більш надійними.

console.log(/\p{L}/u.test(«α»));
// → true
console.log(/\p{L}/u.test(«!»));
// → false
console.log(/\p{Script=Greek}/u.test(«α»));
// → true
console.log(/\p{Script=Arabic}/u.test(«α»));
// → false

{{index «Числова функція»}}

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

Повторення частин шаблону

{{index [«регулярний вираз», повторення]}}

Тепер ми знаємо, як знайти одну цифру. А що, якщо ми хочемо знайти ціле число - ((послідовність)) з однієї або декількох ((цифр))?

{{індекс «знак плюс», повторення, «+ оператор»}}

Коли ви ставите знак плюс (+) після чогось у регулярному виразі, це означає, що елемент може повторюватися більше одного разу. Таким чином, /\d+/ відповідає одному або декільком цифровим символам.

console.log(/'\d+'/.test(«“123”»));
// → true
console.log(/'\d+'/.test(«“”»));
// → false
console.log(/'\d*'/.test(«“123”»));
// → true
console.log(/'\d*'/.test(«“”»));
// → true

{{index «* operator», asterisk}}

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

{{індекс «британська англійська», «американська англійська», «знак питання»}}

Знак питання (?) входить до складу шаблону ((необов'язково)), тобто він може зустрічатися нуль разів або один раз. У наведеному нижче прикладі дозволено використовувати символ u, але шаблон збігається і за його відсутності:

нехай neighbor = /neighbou?r/;
console.log(neighbor.test(«neighbour»));
// → true
console.log(neighbor.test(«neighbor»));
// → true

{{повторення індексу, [дужки, «у регулярному виразі»]}}

Щоб вказати, що шаблон повинен зустрічатися точну кількість разів, використовуйте фігурні дужки. Наприклад, додавання {4} після елемента означає, що він зустрінеться рівно чотири рази. Також можна вказати ((діапазон)) таким чином: {2,4} означає, що елемент має зустрітися щонайменше двічі і щонайбільше чотири рази.

{{id date_regexp_counted}}

Ось ще одна версія шаблону ((дата)) і ((час)), яка дозволяє використовувати як одинарні, так і подвійні ((цифри)) дні, місяці та години. Його також трохи легше розшифрувати.

нехай dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(dateTime.test(«1-30-2003 8:45»));
// → true

Ви також можете вказати відкриті ((діапазон))и, використовуючи фігурні дужки, опускаючи число після коми. Наприклад, {5,} означає п'ять або більше разів.

Групування підвиразів

{{індекс [«регулярний вираз», групування], групування, [дужки, «у регулярних виразах»]}}

Щоб використати оператор типу * або + для більш ніж одного елемента одночасно, ви повинні використовувати круглі дужки. Частина регулярного виразу, взята в круглі дужки, вважається одним елементом для операторів, що йдуть після неї.

нехай cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test(«Boohoooohoohooo»));
// → true

{{index crying}}

Перший і другий символи + застосовуються лише до другого o у словах boo і hoo відповідно. Третій + застосовується до всієї групи (hoo+), що відповідає одній або декільком таким послідовностям.

{{індекс «чутливість до регістру», великі літери, [«регулярний вираз», прапори]}}

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

Співпадіння та групування

{{index [«регулярний вираз», групування], «метод виконання», [array, «RegExp match»]}}

Метод test - це найпростіший спосіб перевірки регулярного виразу. Він повідомляє вам лише про те, чи співпав вираз, і нічого більше. Регулярні вирази також мають метод exec (виконати), який поверне null, якщо співпадіння не знайдено, і поверне об'єкт з інформацією про співпадіння у протилежному випадку.

let match = /\d+/.exec(«one two 100»);
console.log(match);
// → [«100»]
console.log(match.index);
// → 8

{{index «властивість індексу», [рядок, індексація]}}

Об'єкт, що повертається з exec, має властивість index, яка вказує нам де у рядку починається успішний збіг. В іншому об'єкт виглядає як (і фактично є) масивом рядків, першим елементом якого є рядок, який було знайдено. У попередньому прикладі це послідовність ((цифра)), яку ми шукали.

{{index [string, methods], «match method»}}

Рядкові значення мають метод match, який поводиться аналогічно.

console.log(«one two 100».match(/\d+/));
// → [«100»]

{{групування індексів, «група захоплення», «метод виконання»}}

Якщо регулярний вираз містить під-вирази, згруповані за допомогою круглих дужок, текст, який відповідає цим групам, також буде показано у масиві. Першим елементом завжди буде весь вираз. Наступним елементом буде частина, що відповідає першій групі (та, чия відкриваюча дужка стоїть першою у виразі), потім другій групі, і так далі.

нехай quotedText = /'([^']*)'/;
console.log(quotedText.exec(«she said “hello”»));
// → [«“hello”», «hello»]

{{index «capture group»}}

Якщо група не буде знайдена взагалі (наприклад, якщо після неї стоїть знак питання), її позиція у вихідному масиві буде невизначеною. Якщо групу буде знайдено декілька разів (наприклад, після +), до масиву буде занесено лише останню знайдену групу.

console.log(/bad(ly)?/.exec(«bad»));
// → [«bad», undefined].
console.log(/(\d)+/.exec(«123»));
// → [«123», «3»]

Якщо ви хочете використовувати дужки суто для групування, не показуючи їх у масиві збігів, ви можете поставити ?: після відкриваючої дужки.

console.log(/(?:na)+/.exec(«banana»));
// → [«nana»]

{{index «exec method», [«регулярний вираз», methods], extraction}}

Групи можуть бути корисними для вилучення частин рядка. Якщо ми хочемо не лише перевірити, чи містить рядок ((дата)), але й витягти його та створити об'єкт, який його представляє, ми можемо взяти дужки навколо шаблонів цифр і безпосередньо витягти дату з результату виконання exec.

Але спочатку ми зробимо невеликий обхідний маневр, щоб обговорити вбудований спосіб представлення значень дати і ((time)) в JavaScript.

Клас Date

{{index-конструктор, «Клас Date»}}

У JavaScript є стандартний клас Date для представлення ((date)), а точніше, точок у ((time)). Якщо ви просто створите об'єкт дати за допомогою new, ви отримаєте поточну дату і час.

console.log(new Date());
// → Fri Feb 02 2024 18:03:06 GMT+0100 (CET)

{{index «Date class»}}

Ви також можете створити об'єкт для конкретного часу.

console.log(new Date(2009, 11, 9));
// → Wed Dec 09 2009 00:00:00 GMT+0100 (CET)
console.log(new Date(2009, 11, 9, 12, 59, 59, 999));
// → Wed Dec 09 2009 12:59:59 GMT+0100 (CET)

{{index «zero-based counting», [interface, design]}}

JavaScript використовує угоду, згідно з якою номери місяців починаються з нуля (тому грудень - це 11), а номери днів починаються з одиниці. Це заплутано і безглуздо. Будьте уважні.

Останні чотири аргументи (години, хвилини, секунди і мілісекунди) є необов'язковими і приймаються за нуль, якщо їх не вказано.

{{index «getTime method», timestamp}}

Мітки часу зберігаються як кількість мілісекунд з початку 1970 року у UTC (часовому поясі). Це відповідає домовленості, встановленій «((Unix time))», яку було винайдено приблизно у той самий час. Ви можете використовувати від'ємні числа для часу до 1970 року. Метод getTime на об'єкті дати повертає це число. Як ви можете собі уявити, воно велике.

console.log(new Date(2013, 11, 19).getTime());
// → 1387407600000
console.log(new Date(1387407600000));
// → Thu Dec 19 2013 00:00:00 GMT+0100 (CET)

{{index «Date.now function», «Date class»}}

Якщо ви передаєте конструктору Date єдиний аргумент, цей аргумент розглядається як відлік у мілісекундах. Ви можете отримати поточний відлік мілісекунд, створивши новий об'єкт Date і викликавши для нього getTime або викликавши функцію Date.now.

{{index «метод getFullYear», «метод getMonth», «метод getDate», «метод getHours», «метод getMinutes», «метод getSeconds», «метод getYear»}}

Об'єкти типу Date надають такі методи, як getFullYear, getMonth, getDate, getHours, getMinutes та getSeconds для вилучення їхніх компонентів. Окрім getFullYear, існує також getYear, який повертає рік мінус 1900 (наприклад, 98 або 125) і здебільшого є марним.

{{індекс «група захоплення», «метод getDate», [дужки, «у регулярних виразах»]}}

Поставивши дужки навколо частин виразу, які нас цікавлять, ми можемо створити об'єкт дати з рядка.

function getDate(string) {
  let [_, місяць, день, рік] =
    /(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string);
  return new Date(рік, місяць - 1, день);
}
console.log(getDate(«1-30-2003»));
// → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)

{{деструктуризація індексу, «символ підкреслення»}}

Прив'язка підкреслення (_) ігнорується і використовується лише для того, щоб пропустити елемент повного збігу у масиві, повернутому exec.

Межі та забігання наперед

{{індекс збігу, [«регулярний вираз», межа]}}

На жаль, getDate також успішно витягне дату з рядка «100-1-30000». Збіг може статися будь-де у рядку, тому у цьому випадку він просто почнеться з другого символу і закінчиться передостаннім символом.

{{межа індексу, «символ каретки», «знак долара»}}

Якщо ми хочемо, щоб збіг охоплював увесь рядок, ми можемо додати маркери ^ і $. Символ каретки відповідає початку вхідного рядка, а знак долара - його кінцю. Таким чином, /^\d+$/ відповідає рядку, що складається з однієї або більше цифр, /^!/ відповідає будь-якому рядку, що починається зі знаку оклику, а /x^/ не відповідає жодному рядку (перед початком рядка не може стояти x).

{{індекс «межа слова», «символ слова»}}

Існує також маркер \b, який відповідає межам слів, позиціям, які з одного боку містять символ слова, а з іншого - несловесний символ. На жаль, ці маркери використовують таке саме спрощене поняття символів слова, як і \w, і тому не є дуже надійними.

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

{{index «look-ahead»}}

Тести_Look-ahead_ роблять щось подібне. Вони надають шаблон і спричиняють невдачу, якщо вхідні дані не відповідають цьому шаблону, але насправді не переміщують позицію збігу вперед. Вони пишуться між (?= і ).

console.log(/a(?=e)/.exec(«braeburn»));
// → [«a»]
console.log(/a(?! )/.exec(«a b»));
// → null

Символ e у першому прикладі необхідний для співставлення, але не є частиною рядка, що співставляється. Запис (?! ) виражає негативний перехід на крок вперед. Він спрацьовує, тільки якщо шаблон у дужках не спрацьовує, тому у другому прикладі спрацьовують лише символи a, які не містять пробілу після себе.

Шаблони вибору

{{розгалуження індексу, [«регулярний вираз», альтернативи], «приклад ферми»}}

Скажімо, ми хочемо дізнатися, чи містить фрагмент тексту не лише число, але й число, за яким слідує одне зі слів pig, cow, або chicken, або будь-яка з їхніх форм множини.

Ми могли б написати три регулярні вирази і перевірити їх по черзі, але є кращий спосіб. Символ ((труба)) (|) позначає ((вибір)) між шаблоном ліворуч і шаблоном праворуч. Ми можемо використовувати його у виразах на зразок цього:

нехай animalCount = /\d+ (свиня|корова|курка)s?/;
console.log(animalCount.test(«15 свиней»));
// → true
console.log(animalCount.test(«15 мопсів»));
// → false

{{index [дужки, «у регулярних виразах»]}}

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

Механіка зіставлення

{{index [«регулярний вираз», співставлення], [співставлення, алгоритм], «задача пошуку»}}

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

{{index [«регулярний вираз», співставлення], [співставлення, алгоритм]}}

Щоб виконати власне співставлення, рушій обробляє регулярний вираз чимось на кшталт ((блок-схема)). Це діаграма для виразу livestock у попередньому прикладі:

{{figure {url: «img/re_pigchickens.svg», alt: «Схема залізниці, яка спочатку проходить через клітинку з позначкою «цифра», яка має цикл, що повертається від неї до попередньої, а потім через клітинку для пробілу. Після цього залізниця розділяється на три частини, проходячи через клітинки для «свині», «корови» та «курки». Після цього вона знову з'єднується і проходить через клітинку з літерою «s», яка, будучи необов'язковою, також має залізницю, що проходить повз неї. Нарешті, лінія досягає прийнятного стану."}}}.

{{обхід індексів}}

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

{{id відкочування}}

Відкат назад

{{індекс [«регулярний вираз», відстеження], «двійкове число», «десяткове число», «шістнадцяткове число», «блок-схема», [відповідність, алгоритм], відстеження}}

Регулярний вираз /^([01]+b|[\da-f]+h|\d+)$/ відповідає або двійковому числу, до якого додається b, або шістнадцятковому числу (тобто основа 16, де літери a - f позначають цифри від 10 до 15), до якого додається h, або звичайному десятковому числу без суфікса. Ось відповідна діаграма:

{{figure {url: «img/re_number.svg», alt: «Залізнична діаграма для регулярного виразу '^([01]+b|\d+|[\da-f]+h)$'"}}}}

{{розгалуження індексів}}

При збігу цього виразу часто буде введено верхнє (двійкове) розгалуження, навіть якщо вхідні дані не містять двійкового числа. Наприклад, при перевірці рядка «103» стає зрозуміло лише на 3, що ми знаходимося у неправильній гілці. Рядок співпадає з виразом, але не з тією гілкою, у якій ми зараз перебуваємо.

{{index backtracking, «проблема пошуку»}}

Отже, відповідник відстежує. При вході у гілку він запам'ятовує свою поточну позицію (у цьому випадку на початку рядка, одразу за першою рамкою на діаграмі), щоб можна було повернутися назад і спробувати іншу гілку, якщо поточна не спрацює. Для рядка «103», зустрівши символ 3, пошуковик починає пробувати гілку для шістнадцяткових чисел, яка знову закінчується невдачею, оскільки після числа немає символу h. Після цього він пробує гілку десяткових чисел. Ця гілка підходить, і зрештою повідомляється про збіг.

{{індекс [співпадіння, алгоритм]}}

Якщо знайдено повний збіг, пошук зупиняється. Це означає, що якщо рядку потенційно може відповідати декілька гілок, буде використано лише першу з них (упорядковану за місцем появи гілок у регулярному виразі).

Відстеження також відбувається для операторів ((повторення)), таких як + і *. Якщо ви зіставите /^.*x/ з «abcxe», частина .* спочатку спробує використати весь рядок. Потім рушій зрозуміє, що йому потрібен x, щоб відповідати шаблону. Оскільки після кінця рядка немає символу x, оператор зірочки намагається знайти на один символ менше. Але після abcx він також не знаходить символу x, тому повертається назад, зіставляючи оператор зірочки лише з abc. Тепер він знаходить x там, де потрібно, і повідомляє про успішний збіг з позицій від 0 до 4.

{Продуктивність індексів, складність}}

Можна написати регулярні вирази, які будуть робити багато відкотів назад. Ця проблема виникає, коли шаблон може відповідати фрагменту вхідних даних багатьма різними способами. Наприклад, якщо ми заплутаємося під час написання двійково-числового регулярного виразу, ми можемо випадково написати щось на кшталт /([01]+)+b/.

{{figure {url: «img/re_slow.svg», alt: «Схема залізниці для регулярного виразу '([01]+)+b'», width: “6cm”}}}}

{{index «internal loop», [nesting, «in regexps»]}}

Якщо програма намагається зіставити довгу серію нулів та одиниць без кінцевого символу b, вона спочатку проходить внутрішній цикл, доки не вичерпає всі цифри. Потім він помічає, що там немає символу b, тому повертається на одну позицію назад, проходить один раз по зовнішньому циклу і знову зупиняється, намагаючись знову вийти з внутрішнього циклу. Він буде продовжувати пробувати всі можливі маршрути через ці дві петлі. Це означає, що обсяг роботи подвоюється з кожним додатковим символом. Навіть для декількох десятків символів пошук буде тривати практично вічно.

Метод заміни

{{index «метод заміни», «регулярний вираз»}}

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

console.log(«papa».replace(«p», «m»));
// → mapa

{{index [«регулярний вираз», flags], [«регулярний вираз», global]}}

Перший аргумент також може бути регулярним виразом, у цьому випадку буде замінено перший збіг регулярного виразу. Коли після регулярного виразу додається опція g (для global), будуть замінені всі збіги у рядку, а не лише перший.

console.log(«Borobudur».replace(/[ou]/, «a»));
// → Barobudur
console.log(«Borobudur».replace(/[ou]/g, «a»));
// → Barabadar

{{групування індексів, «група захоплення», «знак долара», «метод заміни», [«регулярний вираз», групування]}}

Справжня сила використання регулярних виразів з замінити полягає у тому, що ми можемо посилатися на відповідні групи у рядку заміни. Наприклад, скажімо, у нас є великий рядок, що містить імена людей, по одному імені у рядку, у форматі Прізвище, Ім'я. Якщо ми хочемо поміняти місцями ці імена і видалити кому, щоб отримати формат Прізвище, Ім'я, ми можемо використати наступний код:

console.log(
  «Лісков, Барбара\nМаккарті, Джон\nМілнер, Робін»
    .replace(/(\p{L}+), (\p{L}+)/gu, «$2 $1»));
// → Барбара Лісков
// Джон Маккарті
// Робін Мілнер

Символи $1 і $2 у рядку заміни посилаються на групи у дужках у шаблоні. $1 замінюється текстом, який збігається з першою групою, $2 - з другою, і так далі, аж до $9. На весь збіг можна посилатися за допомогою $&.

{{index [функція, «вищого порядку»], групування, «група захоплення»}}

Ви можете передати функцію, а не рядок, як другий аргумент до replace. Для кожної заміни буде викликано функцію з відповідними групами (а також цілим збігом) як аргументами, а її значення, що повертається, буде вставлено у новий рядок.

Ось приклад:

нехай stock = «1 лимон, 2 капустини і 101 яйце»;
function minusOne(match, amount, unit) {
  amount = Number(amount) - 1;
  if (amount == 1) { // залишилось тільки одне, видаляємо 's'
    unit = unit.slice(0, unit.length - 1);
  } else if (amount == 0) { // тільки один, видаляємо 's'.
    amount = «no»;
  }
  return amount + « » + unit;
}
console.log(stock.replace(/(\d+) (\p{L}+)/gu, minusOne));
// → немає лимона, 1 капустини та 100 яєць

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

Група (\d+) стає аргументом функції amount, а група (\p{L}+) прив'язується до unit. Функція перетворює amount на число - що завжди працює, оскільки раніше вона відповідала \d+ - і робить деякі корективи, якщо залишається лише одиниця або нуль.

Жадібність

{{index greed, «регулярний вираз»}}

Ми можемо використовувати replace для написання функції, яка видаляє всі ((коментар))и з фрагмента JavaScript ((код)). Ось перша спроба:

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*\*\//g, «»);
}
console.log(stripComments(«1 + /* 2 */3»));
// → 1 + 3
console.log(stripComments(«x = 10;// десять!»));
// → x = 10;
console.log(stripComments(«1 /* a */+/* b */ 1»));
// → 1 1

{{index «символ крапки», «символ косої риски», «символ нового рядка», «порожній набір», «коментар блоку», «коментар рядка»}}

Частина перед оператором | відповідає двом символам косої риски, за якими слідує довільна кількість символів, що не є символами нового рядка. Частина для багаторядкових коментарів є більш складною. Ми використовуємо [^] (будь-який символ, який не входить до порожньої множини символів) як спосіб зіставлення з будь-яким символом. Ми не можемо просто використати крапку, оскільки блокові коментарі можуть продовжуватися з нового рядка, а символ крапки не співпадає з символами нового рядка.

Але виведення останнього рядка, схоже, пішло не так. Чому?

{{індексний відкат, жадібність, «регулярний вираз»}}

Частина виразу [^]*, як я описав у розділі про відкочування, спочатку збігатиметься настільки, наскільки це можливо. Якщо це призводить до невдачі у наступній частині шаблону, зрівнювач повертається на один символ назад і повторює спробу з цього місця. У наведеному прикладі зіставлювач спочатку спробує збігтися з усією рештою рядка, а потім повернеться на один символ назад. Після повернення на чотири символи назад він знайде входження */ і порівняє його. Це не те, чого ми хотіли - ми хотіли зіставити один коментар, а не пройти весь шлях до кінця коду і знайти кінець останнього блочного коментаря.

Через таку поведінку ми говоримо, що оператори повторення (+, *, ? і {}) є ((жадібними))y, тобто вони збігаються настільки, наскільки це можливо, і відступають назад звідти. Якщо після них поставити ((знак питання)) (+?, *?, ??, {}?), то вони стануть нежадібними і почнуть підбирати якомога менше, підбираючи більше лише тоді, коли решта шаблонів не підходить до меншого шаблону.

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

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*?\*\//g, «»);
}
console.log(stripComments(«1 /* a */+/* b */ 1»));
// → 1 + 1

Багато ((помилок)) у ((регулярний вираз)) програмах можна простежити через ненавмисне використання жадібного оператора там, де краще працював би нежадібний. При використанні оператора ((повторення)) віддавайте перевагу нежадібному варіанту.

Динамічне створення об'єктів RegExp

{{index [«регулярний вираз», створення], «символ підкреслення», «клас RegExp»}}

У деяких випадках під час написання коду ви можете не знати точного ((шаблону)), з яким вам потрібно зіставити результат. Скажімо, ви хочете перевірити наявність імені користувача у фрагменті тексту. Ви можете створити рядок і використати для цього RegExp ((конструктор)).

let name = «harry»;
let regexp = new RegExp("(^|\\s)» + name + «($|\\s)», «gi»);
console.log(regexp.test(«Harry is a dodgy character.»));
// → true

{{index [«регулярний вираз», flags], [«символ зворотного слешу», «у регулярних виразах»]}}

При створенні частини рядка \s ми повинні використовувати дві зворотні косі риски, оскільки ми записуємо їх у звичайному рядку, а не у регулярному виразі, укладеному у косу риску. Другий аргумент конструктора RegExp містить опції регулярного виразу - у цьому випадку «gi»` для глобального та нечутливого до регістру.

Але що, якщо ім'я буде «dea+hl[]rd», тому що наш користувач - ((nerd))y підліток? Це призведе до безглуздого регулярного виразу, який не відповідатиме імені користувача.

{{index [«символ зворотного слешу», «у регулярних виразах»], [екранування, «у regexps»], [«регулярний вираз», екранування]}}

Щоб обійти цю проблему, ми можемо додати зворотну косу риску перед будь-яким символом, що має особливе значення.

let name = «dea+hl[]rd»;
let escaped = name.replace(/[\\[.+*?(){|^$]/g, «\\$&»);
let regexp = new RegExp("(^|\\s)» + escaped + «($|\\s)»,
                        «gi");
let text = «Цей dea+hl[]rd guy is super annoying.»;
console.log(regexp.test(text));
// → true

Метод пошуку

{{index [«регулярний вираз», methods], «indexOf методу», «метод пошуку»}}

Хоча метод indexOf для рядків не можна викликати з регулярним виразом, існує інший метод, earch, який очікує регулярний вираз. Як і indexOf, він повертає перший індекс, за яким вираз було знайдено, або -1, якщо його не було знайдено.

console.log(«word».search(/\S/));
// → 2
console.log(» ».search(/\S/));
// → -1

На жаль, немає можливості вказати, що пошук має починатися з заданого зсуву (як це можна зробити з другим аргументом indexOf), а це часто буває корисно.

Властивість lastIndex

{{index «метод виконання», «регулярний вираз»}}

Метод exec також не надає зручного способу розпочати пошук із заданої позиції у рядку. Але він надає зручний спосіб всередині.

{{index [«регулярний вираз», відповідність], відповідність, «властивість source», «властивість lastIndex»}}

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

{{index [інтерфейс, дизайн], «метод виконання», [«регулярний вираз», глобальний]}}

Ці обставини полягають у тому, що регулярний вираз повинен мати опцію global (g) або sticky (y), а збіг повинен відбуватися за допомогою методу exec. Знову ж таки, менш заплутаним рішенням було б просто дозволити передачу додаткового аргументу до exec, але заплутаність є невід'ємною рисою інтерфейсу регулярних виразів JavaScript.

нехай pattern = /y/g;
pattern.lastIndex = 3;
let match = pattern.exec(«xyzzy»);
console.log(match.index);
// → 4
console.log(pattern.lastIndex);
// → 5

{{index «побічний ефект», «властивість lastIndex»}}

Якщо збіг знайдено успішно, виклик exec автоматично оновлює властивість lastIndex, щоб вона вказувала на точку після збігу. Якщо збігу не знайдено, lastIndex повертається до 0, що також є значенням, яке він має у новоствореному об'єкті регулярного виразу.

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

нехай global = /abc/g;
console.log(global.exec(«xyz abc»));
// → [«abc»].
let sticky = /abc/y;
console.log(sticky.exec(«xyz abc»));
// → null

{{Індексна помилка}}

При використанні спільного значення регулярного виразу для декількох викликів exec ці автоматичні оновлення властивості lastIndex можуть спричинити проблеми. Ваш регулярний вираз може випадково починатися з індексу, що залишився від попереднього виклику.

нехай digit = /\d/g;
console.log(digit.exec(«ось воно: 1»));
// → [«1»]
console.log(digit.exec(«а тепер: 1»));
// → null

{{index [«регулярний вираз», global], «метод порівняння»}}

Ще одним цікавим ефектом опції global є те, що вона змінює спосіб роботи методу match на рядках. При виклику з глобальним виразом, замість того, щоб повертати масив, подібний до того, що повертає exec, match знайде всі збіги з шаблоном у рядку і поверне масив, що містить знайдені рядки.

console.log(«Banana».match(/an/g));
// → [«an», «an»]

Тому будьте обережні з глобальними регулярними виразами. Випадки, коли вони необхідні - це виклики replace і місця, де ви хочете явно використовувати lastIndex - це, як правило, ситуації, де ви хочете їх використовувати.

{{index «lastIndex property», «exec method», loop}}

Поширеною задачею є пошук усіх збігів регулярного виразу у рядку. Це можна зробити за допомогою методу matchAll.

нехай input = «Рядок, що містить 3 числа... 42 і 88.»;
let matches = input.matchAll(/\d+/g);
for (let match of matches) {
  console.log(«Знайдено», match[0], «at», match.index);
}
// → Знайдено 3 при 14
// Знайдено 42 з 33
// Знайдено 88 при 40

{{index [«регулярний вираз», global]}}

Цей метод повертає масив масивів збігів. Регулярний вираз, переданий matchAll повинен мати g увімкненим.

{{id ini}}

Розбір INI-файлу

{{index comment, «file format», «enemies example», «INI file»}}

На завершення розділу ми розглянемо задачу, яка вимагає використання ((регулярних виразів))s. Уявіть, що ми пишемо програму для автоматичного збору інформації про наших ворогів з ((Інтернету)). (Тут ми не будемо писати саму програму, лише ту частину, яка читає файл ((конфігурації)). Вибачте). Конфігураційний файл має такий вигляд:

searchengine=https://duckduckgo.com/?q=$1
spitefulness=9.7

; перед коментарями ставиться крапка з комою...
; кожен розділ стосується окремого ворога
[larry]
повне ім'я=Ларрі Доу
тип=дитячий хуліган
сайт=http://www.geocities.com/CapeCanaveral/11451

[davaeorn]
повне ім'я=Давеорн
type=злий чарівник
outputdir=/home/marijn/enemies/davaeorn

{{індексна граматика}}

Точні правила для цього формату - широко використовуваного формату файлів, який зазвичай називають INI-файлом - такі:

  • Порожні рядки та рядки, що починаються з крапки з комою, ігноруються.

  • Рядки, обгорнуті символами [ і ], починають новий ((розділ)).

  • Рядки, що містять алфавітно-цифровий ідентифікатор, за яким слідує символ =, додають параметр до поточної секції.

  • Все інше є невірним.

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

{{індекс «повернення каретки», «переведення рядка», «символ нового рядка»}}

Оскільки формат має оброблятися ((рядок)) рядок за рядком, розбиття файлу на окремі рядки є гарним початком. Ми розглядали метод split у Розділ ?. Однак деякі операційні системи використовують для розділення рядків не просто символ нового рядка, а символ повернення каретки, за яким слідує новий рядок («\r\n»). Враховуючи, що метод plit також допускає регулярний вираз як аргумент, ми можемо використовувати регулярний вираз типу /\r?\n/ для розділення у спосіб, який дозволяє використовувати як «\n», так і «\r\n» між рядками.

function parseINI(string) {
  // Почати з об'єкту для зберігання полів верхнього рівня
  let result = {};
  let section = result;
  for (let line of string.split(/\r?\n/)) {
    let match;
    if (match = line.match(/^(\w+)=(.*)$/)) {
      section[match[1]] = match[2];
    } else if (match = line.match(/^\[(.*)\]$/)) {
      section = result[match[1]] = {};
    } else if (!/^\s*(;|$)/.test(line)) {
      throw new Error(«Рядок “» + line + «” є недійсним.»);
    }
  };
  повернути результат;
}

console.log(parseINI(`
ім'я=Vasilis
[адреса]
city=Tessaloniki`));
// → {name: «Vasilis», address: {city: «Салоніки"}}

{{index «parseINI function», parsing}}

Код переглядає рядки файлу і створює об'єкт. Властивості у верхній частині зберігаються безпосередньо у цьому об'єкті, тоді як властивості, знайдені у секціях, зберігаються в окремому об'єкті секції. Прив'язка секція вказує на об'єкт для поточної секції.

Існує два типи значущих рядків - заголовки секцій або рядки властивостей. Коли рядок є звичайною властивістю, він зберігається у поточній секції. Якщо це заголовок секції, створюється новий об'єкт секції, і секція встановлюється для вказівки на нього.

{{індекс «символ кегля», «знак долара», межа}}

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

{{індекс «ключове слово if», присвоєння, [«= оператор», «як вираз»]}}

Шаблон if (match = string.match(...)) використовує той факт, що значення виразу ((присвоєння)) (=) є присвоєним значенням. Часто ви не впевнені, що ваш виклик match буде успішним, тому ви можете отримати доступ до результуючого об'єкту тільки всередині інструкції if, яка перевіряє це. Щоб не розривати приємний ланцюжок форм else if, ми присвоюємо результат співпадіння прив'язці і одразу використовуємо це присвоєння як тест для оператора if.

{{index [дужки, «у регулярних виразах»]}}

Якщо рядок не є заголовком розділу або властивістю, функція перевіряє, чи є він коментарем або порожнім рядком, використовуючи вираз /^\s*(;|$)/ для пошуку рядків, які містять лише пробіли або пробіли з крапкою з комою (решта рядка стає коментарем). Якщо рядок не відповідає жодній з очікуваних форм, функція згенерує виключення.

Одиниці коду та символи

Ще одна помилка проектування, яка була стандартизована у регулярних виразах JavaScript, полягає в тому, що за замовчуванням оператори типу . або ? працюють з одиницями коду (як описано в Глава ?), а не з дійсними символами. Це означає, що символи, які складаються з двох кодових одиниць, поводяться дивно.

console.log(/🍎{3}/.test(«🍎🍎🍎»));
// → false
console.log(/<.>/.test(«<🌹>»));
// → false
console.log(/<.>/u.test(«<🌹>»));
// → true

Проблема в тому, що 🍎 у першому рядку сприймається як дві кодові одиниці, а {3} застосовується лише до другої одиниці. Аналогічно, крапка відповідає одній кодовій одиниці, а не двом, з яких складається троянда ((емодзі)).

Ви повинні додати опцію u (Юнікод) до вашого регулярного виразу, щоб він правильно обробляв такі символи.

console.log(/🍎{3}/u.test(«🍎🍎🍎»));
// → true

{{id summary_regexp}}

Підсумок

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

{{table {cols: [1, 5]}}}

| /abc/ | Послідовність символів | /[abc]/ | Будь-який символ з набору символів | /[^abc]/ | Будь-який символ не з набору символів | /[0-9]/ | Будь-який символ з діапазону символів | /x+/ | Одне або більше входжень шаблону x | Одне або більше входжень, не жадібне | Нуль або більше входжень | Нуль або одне входження | Від двох до чотирьох випадків. | Група | a|b|c/| Будь-який з декількох шаблонів | Будь-який цифровий символ |/\w/| Буквено-цифровий символ («символ слова») |/\s/| Будь-який пробіл |/./| Будь-який символ, окрім нових рядків |/\p{L}/u| Будь-який символ літери | Початок введення |/$/| Кінець введення |/(?=a)/` | Попередній тест

Регулярний вираз має метод test для перевірки відповідності заданого рядка. Він також має метод exec, який, якщо збіг знайдено, повертає масив, що містить усі знайдені групи. Такий масив має властивість index, яка вказує на те, де почався збіг.

Рядки мають метод match для зіставлення їх з регулярним виразом і метод search для пошуку, який повертає лише початкову позицію збігу. Їхній метод replace може замінити збіги шаблону на рядок або функцію.

Регулярні вирази можуть мати опції, які записуються після закриваючої косої риски. Опція i робить вираз нечутливим до регістру символів. Опція g робить вираз global, що, серед іншого, призводить до того, що метод replace замінює всі екземпляри, а не тільки перший. Опція y робить вираз липким, що означає, що він не буде шукати наперед і пропускатиме частину рядка під час пошуку збігу. Опція u вмикає режим Unicode, який дозволяє використовувати синтаксис \p і виправляє ряд проблем, пов'язаних з обробкою символів, які займають дві кодові одиниці.

Регулярні вирази - це гострий ((інструмент)) з незручною ручкою. Вони значно спрощують деякі завдання, але можуть швидко стати некерованими, коли застосовуються до складних проблем. Частиною вміння користуватися регулярними виразами є протистояння бажанню впихнути в них те, що вони не можуть виразити в чистому вигляді.

Вправи

{Налагодження індексів, виправлення помилок

Майже неминуче, що під час роботи над цими вправами ви заплутаєтесь і розчаруєтесь через незрозумілу ((поведінку)) деякого регулярного виразу. Іноді корисно ввести вираз у онлайн-інструмент на кшталт debuggex.com, щоб побачити, чи відповідає його візуалізація тому, що ви задумали, і ((поекспериментувати)) з тим, як він реагує на різні вхідні рядки.

Regexp golf

{{index «size of program», «code golf», «regexp golf (exercise)»}}

Кодовий гольф_ - це термін, який використовується для позначення гри, що полягає у спробі виразити певну програму якомога меншою кількістю символів. Аналогічно, regexp-гольф - це практика написання якомога меншого регулярного виразу, який відповідає заданому шаблону і лише цьому шаблону.

{{межа індексу, відповідність}}

Для кожного з наступних елементів запишіть ((регулярний вираз)), щоб перевірити, чи зустрічається заданий шаблон у рядку. Регулярний вираз повинен знаходити тільки ті рядки, які містять шаблон. Якщо ваш вираз працює, подивіться, чи можна зробити його ще меншим.

  1. car та cat
  2. pop та prop
  3. тхір_, пором_ та феррарі
  4. Будь-яке слово, що закінчується на ious.
  5. Пробіл, за яким слідує крапка, кома, двокрапка або крапка з комою
  6. Слово довше шести літер
  7. Слово без літери e (або E)

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

{{якщо інтерактивно

// Заповніть регулярні вирази

verify(/.../,
       [«моя машина», «погані коти»],
       [«кемпер», «високе мистецтво»]);

verify(/.../,
       [«поп-культура», «божевільний реквізит»],
       [«plop», «prrrrop»]);

verify(/.../,
       [«тхір», «пором», «феррарі»],
       [«ferrum», «transfer A»]);

verify(/.../,
       [«як смачно», «простора кімната»],
       [«руйнівний», «свідомість»]);

verify(/.../,
       [«погана пунктуація»],
       [«уникнути крапки»]);

verify(/.../,
       [«Siebentausenddreihundertzweiundzwanzig»],
       [«ні», «три маленьких слова»]);

verify(/.../,
       [«червоний качконіс», «гніздо, що хитається»],
       [«земляна грядка», «bedrøvet abe», «BEET»]);


function verify(regexp, yes, no) {
  // Ігнорувати незавершені вправи
  if (regexp.source == «...») return;
  for (let str of yes) if (!regexp.test(str)) { // Ігнорувати незавершені вправи
    console.log(`Failure to match '${str}'`);
  }
  for (let str of no) if (regexp.test(str)) {
    console.log(`Неочікуваний збіг для '${str}'`);
  }
}

if}}

Стиль цитування

{{індекс «стиль цитування (вправа)», «одинарні лапки», «подвійні лапки»}}

Уявіть, що ви написали оповідання і використовували одинарні ((лапки)) для позначення фрагментів діалогів. Тепер ви хочете замінити всі лапки у діалогах на подвійні лапки, зберігши при цьому одинарні лапки, що використовуються у репліках на кшталт aren't.

{{index «replace method»}}

Придумайте шаблон, який розрізняє ці два типи використання лапок, і створіть виклик методу replace, який виконує відповідну заміну.

{{if інтерактивний

let text = «»Я кухар«, - сказав він, - “це моя робота”.»;
// Змініть цей виклик.
console.log(text.replace(/A/g, «B»));
// → «I'm the cook,» he said, «it's my job.»

if}}

{{hint

{{index «стиль цитування (вправа)», boundary}}

Найочевиднішим рішенням є заміна лише лапок на нелітерні символи принаймні з одного боку - щось на кшталт /\P{L}'|'\P{L}/u. Але ви також повинні взяти до уваги початок і кінець рядка.

{{групування індексів, «метод заміни», [дужки, «у регулярних виразах»]}}

Крім того, ви повинні переконатися, що заміна також включає символи, які було знайдено за шаблоном \P{L}, щоб вони не були відкинуті. Це можна зробити, взявши їх у круглі дужки і включивши їхні групи у рядок заміни ($1, $2). Групи, які не співпадають, буде замінено нічим.

підказка}}

Знову числа

{{знак індексу, «дробове число», [синтаксис, число], мінус, «знак плюс», експонента, «наукові позначення», «знак крапки»}}

Напишіть вираз, який відповідає лише формату JavaScript ((число))s. Він повинен підтримувати необов'язковий знак мінус або плюс перед числом, десяткову крапку та запис експоненти - 5e-3 або 1E10 - знову ж таки з необов'язковим знаком перед експонентою. Також зверніть увагу, що не обов'язково, щоб перед або після крапки були цифри, але число не може бути лише крапкою. Тобто, .5 і 5. є допустимими JavaScript-числами, але одна крапка - ні.

{{якщо інтерактивний

// Заповніть цей регулярний вираз.
let number = /^...$/;

// Тести:
for (let str of [«1», «-1», «+15», «1.55», «.5», «5.»,
                 «1.3e2», “1E-4”, “1e+12”]) {
  if (!number.test(str)) {
    console.log(`Failed to match '${str}'`);
  }
}
for (let str of [«1a», «+-1», «1.2.3», «1+1», «1e4.5,
                 «.5.», “1f5”, “.”]) {
  if (number.test(str)) {
    console.log(`Неправильно прийнято '${str}'`);
  }
}

if}}

{{hint

{{index [«регулярний вираз», екранування], [«символ зворотної косої риски», «у регулярних виразах»]}}

По-перше, не забувайте про зворотну косу риску перед крапкою.

Відповідність необов'язкового ((знак)) перед ((число)), а також перед ((показник)) можна зробити за допомогою [+\-]? або (\+|-|) (плюс, мінус або нічого).

{{index «pipe character»}}

Більш складною частиною вправи є проблема зіставлення «5.» і «.5» без зіставлення «.». Для цього гарним рішенням буде використання оператора | для розділення двох випадків - або одна чи більше цифр, за бажанням, з крапкою та нулем чи більше цифр або з крапкою та однією чи більше цифрами.

{{вираз індексу, «чутливість до регістру», [«регулярний вираз», прапори]}}

Нарешті, щоб зробити регістр e нечутливим, додайте до регулярного виразу опцію i або використовуйте [eE].

підказка}}