Проходим челлендж от Callum Macrae на 100%

от автора

Предлагаю попробовать решить 10 regex тестов от Callum Macrae. В отличии от моего предыдущего разбора челленджа, здесь нет откровенно простых и даже средних задач. Как говорится — только regex, только хардкор.

Так как челлендж довольно сложный, не обязательно следовать всем правилам как я, любое прохождение теста на 100% — означает что вы супер-профессионал. Welcome!

 
Да, знаю, этот челлендж уже был выложен однажды. Но автор поста не представил работающих решений, а в комментариях люди не смогли решить дальше 4 задачи, а чаще даже не понимали смысл задачи и что от них хотят.

Поэтому выкладываю ещё раз, с подробным переводом, объяснением и всеми полагающимися плюшками.

Задача 1 — выделить повторяющиеся слова

http://callumacrae.github.io/regex-tuesday/challenge1.html

Имеется набор предложений, в этом предложении могут быть повторяющиеся слова. Необходимо выделить повторяющиеся слова.

Пример:

This is is a test

В данном случае два раза повторяется слово «is», выделяем его жирным шрифтом:

This is <strong>is</strong> a test

Подсказка

Нужно найти повторы слов, а слова разделяются пробелом, следовательно — пробельный символ обязателен. В регулярных выражениях, найти повторы технически можно только через обратную ссылку.

Решение

Выражение:

/\b([\w']+)\s(\1)\b/gi

Замена:

$1 <strong>$2</strong>

Разбор решения

  • «\b» — начинаться должно с границы слова
  • «([\w’]+)» — любое количество букв, цифр и апостроф (так же можно решить через любой кроме пробела) и обязательно захватываем в группу, т.к. далее нужно найти повторения этой группы.
  • «\s(\1)» — т. к. мы знаем что повторение идёт после пробела, то ставим пробел «\s» и далее пишем что после обязательно должно идти повторение ранее захваченной первой группы «(\1)».
  • «\b» — повторение должно заканчиваться границей слова, иначе мы рискуем захватить только часть слова.

Задача 2 — оттенки серого

http://callumacrae.github.io/regex-tuesday/challenge2.html

Имеются коды цветов в разных форматах, задача найти все оттенки серого.

Примеры верных кодов:

#eEe #6F6F6F rgb(2.5, 2.5,2.5) hsl(0, 10%, 100%)

Примеры не верных кодов:

#eEf #11111e rgb(1.5%, 1.5%, 1.6%) hsl(20, 20%, 20%)

Разъяснения по кодам

Самый главный вопрос в этой задаче — что считается серым цветом?

Согласно Википедии серый цвет это:

Множество всех цветов, получаемых путём совмещения трёх основных цветов цветовой модели RGB — красного, зелёного и синего в равных концентрациях.

Коды начинающиеся с # — это формат hex, он бывает двух видов. Сокращенный, три символа (#rgb) и полный, шесть символов $rrggbb. Где r, g, b — это три основных цвета.
Коды rgb(r, g, b) — это ровно тоже самое, только записываются они цифрами от 0 до 255.
С форматом hsl немного сложнее, цифры здесь означают — тон, насыщенность и светлоту. Что бы понять при каких условиях получаются три основных цвета в равных пропорциях можно например поиграться с этим визуальным редактором.

Подсказка

Для сокращенного hex правильное вхождение будет повторение всех трех символов, например #aaa. Для полного hex — повторение двух символов, например #efefef. Для циферного rgb — повторение цифр, например rgb(2, 2, 2). С пониманием формата hsl немного сложнее, но всё равно зная описанное выше можно понять что тут серым цветом считается цвет при которых тон равен 0 или насыщенность равна 0 или 100.

Соответственно, как и в предыдущей задаче, нужно использовать обратную ссылку. Итоговое регулярное выражение получится большое (это нормально), поскольку нужно учесть много разных вариантов, в том числе написанных не правильно.

Решение

/^(?:#(\w)\1\1|#(\w{2})\2\2|rgb\(((?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])%?(?:\.\d+)?),[ ]?[0]*\3,[ ]?[0]*\3\)|rgba\(([\d.]+%?),[0 ]*\4,[0 ]*\4,[^)]+\)|hsla?\([\d.]+,[ ]*(0%[^)]+|[\d.]+%,[ ]*(0|100)%[^\)]*)\))$/i

Разбор решения

Для каждого цвета пишется отдельная ругулярка, разберем их отдельно:

#(\w)\1\1

  • «(\w)» берем в группу один одиночный символ.
  • «\1\1» — и указываем что он должен повториться 2 раза.

Для двух символов тоже самое — повторяться не буду.

rgb\(((?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])%?(?:\.\d+)?),[ ]?[0]*\3,[ ]?[0]*\3\)

Я бы хотел написать «rgba?», но возможен кейс когда в rgb() указан четвертый параметр, поэтому rgb и rgba нужно описывать отдельно:

  • «\d{1,2}|1\d{2}|2[0-4]\d|25[0-5]» — диапазон от 0 до 255. Подробно разбирать не буду, можете глянуть 5 задачу здесь.
  • «(?:.\d+)?» — не обязательная группа которой не присваивается номер. Возможны точка и число после точки (это для не целых чисел).
  • «,[ ]?[0]*\3» — обязательная запятая, далее 0 или 1 пробел, 0 или много нулей, после чего значение захваченной ранее группы должно повториться.

В rgba() — тоже самое, но обязателен 4 параметр.

hsla?\([\d.]+,[ ]*(0%[^)]+|[\d.]+%,[ ]*(0|100)%[^\)]*)\)

Тут, по-хорошему, так же нужно разделить hsl и hsla, но в тест-кейчах такого кейса нет, поэтому немного схитрим написав «hsla?».

  • «[\d.]+,[ ]*» — сначала идёт обязательная цифра «[\d.]+» (в т.ч. не целая цифра) с обязательной запятой и не обязательным пробелом «[ ]*».
  • «(0%[^)]+|[\d.]+%,[ ]*(0|100)%[^)]*» — а дальше возможны два варианта: 1) где сначала идёт 0% и потом любой символ кроме символа закрытия скобки [^)]+; 2) идёт любое число с обязательным знаком процента и запятой «[\d.]+%,» и далее либо 0%, либо 100% «(0|100)%».

Задача 3 — найти даты

http://callumacrae.github.io/regex-tuesday/challenge3.html

Имеется список дат, из этих дат найти даты с 1000 по 2012 год включительно написанные в формате YYYY/MM/DD HH:MM(:SS). Где каждая буква — это обязательная цифра, а в скобках — не обязательное условие.

Пример

2001/09/30 23:59:11

Подсказка

«[0-9]» — это не диапазон чисел, это выражение которое означает то что допустим одиночный символ от 0-9. В регулярных выражениях нет диапазона для больших чисел, но из таких маленьких кусочков можно составить регулярное выражение покрывающее нужный диапазон. Пример: «1[0-9]» — диапазон от 10 до 19.

Решение

/^(1[\d]{3}|200\d|201[0-2])\/(0[1-9]|1[0-2])\/(0[1-9]|1[0-9]|2[0-9]|3[0-2])\s(0[0-9]|1[0-9]|2[0-3]):([0-5][\d])(:([0-5][\d]))?$/

Разбор решения

  • Допустимый год «(1[\d]{3}|200\d|201[0-2])», где по порядку от 1000 до 1999, от 2000 до 2009, от 2010 до 2012.
  • Месяц «(0[1-9]|1[0-2])». От 01 до 09 и от 10 до 12.
  • День «(0[1-9]|1[0-9]|2[0-9]|3[0-2])». От 01 до 09 и от 10 до 19, от 20 до 29 и от 30 до 32.
  • Час «(0[0-9]|1[0-9]|2[0-3])». От 00 до 09 и от 10 до 19, от 20 до 23.
  • Минута «([0-5][\d])». От 00 до 59.
  • (:([0-5][\d]))? — не обязательные секунды, от 00 до 59.

Задача 4 — выделение курсивом

http://callumacrae.github.io/regex-tuesday/challenge4.html

Имеется текст с MarkDown разметкой (прямо как на Хабре). Необходимо написать регулярное выражение которое будет заменять слова между звездочками на тег <em>.

Пример

*This text is italic.* -> <em>This text is italic.</em>

Подсказка

Нужно найти звездочку перед и после которой не идёт другая звездочка. Есть с заглядыванием только вперед и с заглядыванием вперед и назад (самое простое, но не кроссбраузерное.)

Решение

Выражение:

/(^|[^*])\*([^*].*?[^*]|[^*])\*((?!\*)|$)/g

Замена:

$1<em>$2</em>

Разбор решения

  • «(^|[^*])» — начнём либо с начала строки, либо с любого символа кроме звездочки. Группа нужна что захватить этот символ и поставить его перед тегом <em>.
  • ((?!*)|$) — закончим либо концом строки, либо любым символом кроме звездочки, поскольку тут заглядывание — пробел не захватывается.
  • «([^*].*?[^*]|[^*])» — в середине у нас «[^*].*?[^*]» любой текст который не должен начинаться и заканчиваться на звездочку и выражение или «|[^*]» просто что бы учесть одиночный символ внутри тега (для прохождения теста не обязательно).

Задача 5 — формат чисел

http://callumacrae.github.io/regex-tuesday/challenge5.html

Из списка чисел выбрать только числа с правильным форматом. Общепринято записывать числа с права на лево с разбивкой на группы по три цифры в каждой.

Примеры правильно записанных цифр:

1,024 8,205,500.4672 10.444444444444 30 000,7302

Подсказка

Важно учесть что цифры записываются именно справа на лево, а не наоборот. Это значит что начинаться число может с 1-3 цифр, а далее может быть только по три цифры в группе. В не целой части может быть сколько угодно чисел (или не быть вовсе). Учесть что разделителем групп может быть запятая или пробел, а разделителем целой и не целой части — запятая или точка.

Решение

Выражение:

/^\d{1,3}([ ,]\d{3})*([.,]\d+)?$/

Разбор решения

  • «^\d{1,3}» — в начале от 1 до 3 цифр.
  • «([ ,]\d{3})*» — далее разделитель и группа из 3 чисел, звездочка указывает что наш формат может встречаться 0 или много раз.
  • «([.,]\d+)?$» — в конце группа с разделителем и числом, знак вопроса — квантификатор который говорит что наличие не целой части — не обязательное условие.

Задача 6 — ip-адреса

http://callumacrae.github.io/regex-tuesday/challenge6.html

Из списка ip-адресов в самых разных форматах, найти валидные ip-адреса. Пожалуй, самая муторная задачи из всех. Не сколько супер-сложная, сколько именно муторная.

Примеры валидных записей ip-адресов и пояснение:

  • 192.0.2.235 — десятичный с точками.
  • 0300.0000.0002.0353 — восьмеричный с точками.
  • 0xC0.0x00.0x02.0xEB — шестнадцатеричный с точками.
  • 0xC00002EC — шестнадцетиричный.
  • 287454020 — десятичный.
  • 030000001353 — восьмеричный.

Мешать разные форматы — это плохо. Особенно цифры. Ситуация ещё усложняется тем что форматы ip-адресов с точками могут быть смешаны, например — 0xFF.255.0377.0×12. Лично моё мнение что это бэд-практикс, но тем не менее по тесту такие варианты возможны и поэтому это нужно учитывать.

Подсказка

  • 192.0.2.235 — десятичный с точками. Общепринятая запись, может быть выражена от 1 до 3 цифр между точками (значения от 0 до 255).
  • 0300.0000.0002.0353 — восьмеричный с точками. 4 цифры между точками со значениями от 0 до 8.
  • 0xC0.0x00.0x02.0xEB — шестнадцатеричный с точками. Четыре символа между точками. Ведущий «0x», далее два символа (значениями цифры или от «a» до «f»).
  • 0xC00002EC — шестнадцетиричный. Ведущий «0x», далее 8 символов (значениями цифры или от «a» до «f»).
  • 287454020 — десятичный. Любые цифры в диапазоне от 0 до 4294967295.
  • 030000001353 — восьмеричный. Ведущий 0. Цифры от 0 до 7. Диапазон от 0 до 077777777777.

Регулярное выражение будет большое.

Решение

/^((((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])|(0x[\da-f]{2})|([0-7]{4}))\.){3}(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])|(0x[\da-f]{2})|([0-7]{4})))|(0x[\da-f]{8})|(0([0-7]{1,11}))|(2874540[2-8][0-9]|28745409[0-9]|287454[1-9][0-9]{2}|28745[5-9][0-9]{3}|2874[6-9][0-9]{4}|287[5-9][0-9]{5}|28[89][0-9]{6}|29[0-9]{7}|[3-9][0-9]{8}|[1-3][0-9]{9}|4[01][0-9]{8}|42[0-8][0-9]{7}|429[0-3][0-9]{6}|4294[0-8][0-9]{5}|42949[0-5][0-9]{4}|429496[0-6][0-9]{3}|4294967[01][0-9]{2}|42949672[0-8][0-9]|429496729[0-5]))$/i

Разбор решения

Для ip-адресов с точками возможно смешивание, поэтому пишем варианты через «|» по такому шаблону: ((десятичный|шестнадцатеричный|восьмеричный).){3}(десятичный|шестнадцатеричный|восьмеричный).

  • «(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])» — для десятичной с точкой записи.
  • «(0x[\da-f]{2})» — для шестнадцетиричной с точкой записи.
  • «([0-7]{4})» — для восьмеричной с точкой записи.

И остальные форматы записи:

  • «(0x[\da-f]{8})» — для шестндацетиричной записи.
  • «(2874540[2-8][0-9]|28745409[0-9]|287454[1-9][0-9]{2}|28745[5-9][0-9]{3}|2874[6-9][0-9]{4}|287[5-9][0-9]{5}|28[89][0-9]{6}|29[0-9]{7}|[3-9][0-9]{8}|[1-3][0-9]{9}|4[01][0-9]{8}|42[0-8][0-9]{7}|429[0-3][0-9]{6}|4294[0-8][0-9]{5}|42949[0-5][0-9]{4}|429496[0-6][0-9]{3}|4294967[01][0-9]{2}|42949672[0-8][0-9]|429496729[0-5])» — для десятичной записи. И тут, признаться, для более короткого выражения я схитрил включив в диапазон только входящие в тест десятичные ip-адреса. По хорошему, тут нужно учитывать любые цифры от 0 и до 4294967295. Писать это вручную — дело не благодарное, поэтому пользуемся.
  • (0([0-7]{1,11})) — для восьмеричной записи.

Задача 7 — url-адреса

http://callumacrae.github.io/regex-tuesday/challenge7.html

Из списка url-адресов, найти валидные.

Примеры валидных адресов:

http://a.b https://example.com/ http://test.this-test.com/ http://1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa

Подсказка

Адрес должен обязательно начинаться на http:// или https://, а заканчиваться слэшем, буквой (если домен) или числом если ip-адрес. На каждом домене может быть поддомен. По стандарту длинна каждого домена не может превышать 63 символа при общей длине в 255 символов. А вложенность домена в поддомен ограничена 127 доменами. К сожалению движок JavaScript Regex в полной мере не даст включить эти ограничения, но написать выражение которое примерно будет соответствовать правилам и проходить тест можно. Зачёркнуто то что можно обойти регулируя другие параметры.

Решение

/^https?:\/\/(((\b[a-z\d-]{1,63}\b)\.){1,40}(\b[a-z\d-]{1,63}\b))\/?$/i

Разбор решения

  • «^https?:\/\/» — http:// или https://

Разберем отдельно ((\b[a-z\d-]{1,63}\b).){1,40}

  • «\b» в конце и начале домена что бы убедится что домен не начинается и не заканчивается ничем не допустимым.
  • «[a-z\d-]{1,63}» — внутри доменного имени допустимы буквы, цифры и дефис внутри
  • «{1,63}» — всё это не больше 63 символов.
  • «((доменное-имя).){1,40}» — хотелось бы поставить тут 127, но в регулярных выражениях квантификатор {,} означает интервал повторения. В случае применения []{} — это и есть количество символов, но в случае без [] — это именно количество повторений шаблона (доменное-имя).). Поэтому ограничиваем повторение 40 что бы не превысить общее ограничение длинны, которое мы тоже по этой причине жёстко задать не можем.

Задача 8 — повторяющиеся элементы

http://callumacrae.github.io/regex-tuesday/challenge8.html

Задача во многом похожа на 1 задачу, но здесь требуется найти и выделить двумя звездочками повторяющиеся элементы списка MarkDown.

Такой список:

* Repeated list item * Repeated list item

Должен быть преобразован в такой:

* Repeated list item * **Repeated list item**

Подсказка

Используем обратную ссылку, символ перевода строки, ключи global, multi-line и insensitive.

Решение

Выражение:

/^(\*\s+([^\n]+)\n\*\s+)(\2)$/gmi

Замена:

$1**$3**

Задача 9 — MarkDown ссылки

http://callumacrae.github.io/regex-tuesday/challenge9.html

Заменить валидные MarkDown ссылки на html ссылки.

Пример преобразования:

[Another](http://example.com/) -> <a href="http://example.com/">Another</a>

Подсказка

Можно сделать вообще без заглядываний или с только заглядыванием вперед. Вместо заглядывания назад — замена.

Решение

Выражение:

/(^|\s+)\[([^\]\[]+)\]\s*\((https?:\/\/\b[a-z\d-]+\b(\.[a-z-]+)*\.\w+\/*)\)(?=$|\s+)/i

Замена:

$1<a href="$3">$2</a>

Разбор решения

  • «(^|\s+)» — перед MarkDown ссылкой допустимы либо начало строки, либо пробел. Берем это в группу, что бы подставить захваченный пробел в замене $1.
  • «[([^][]+)]\s*» — в заголовке допустимые любые символы кроме квадратных ковычек.
  • «(https?:\/\/\b[a-z\d-]+\b(.[a-z-]+).\w+\/)» — проверяем что бы url адрес, был валидным.
  • «(?=$|\s+)» — в конце либо пробел, либо конец строки.

Задача 10 — ключевые слова

http://callumacrae.github.io/regex-tuesday/challenge10.html

Самая хардкорная задача из всех задач. С помощью регулярного выражения с заменой превратить имеющийся текст в ключевые слова через запятую.

Правила:

  • Слова в ковычках — это одно ключевое слово.
  • Имена написанные через дефис — это одно ключевое слово.
  • Слово может содержать апостроф.
  • Символы (; — ‘ «) должны быть убраны.

Пример, вот это:

don’t tell Suzie Smith-Hopper that I broke Daniel’s toy horse

 
Должно быть преобразовано в это:

don’t,tell,Suzie,Smith-Hopper,that,I,broke,Daniel’s,toy,horse

 
На первый взгляд звучит не сложно, но это не так. Дело в том что подобная задача не решается и не приводится в окончательный вид только одним регулярным выражением с заменой. Но тест-кейсы составлены таким образом что бы всё таки сделать решение задачи возможным.

Подсказка

Нужно определиться где ставить запятую, что подставлять рядом с этой запятой и с какой стороны. В задаче есть допущение — первое слово в каждом тест-кейсе не требует никаких изменений. Это означает что ставить запятую нужно слева от заменяемого слова, кроме первого слова.

Решение

Выражение:

/\s(['"])([^'"]+)\1|(;? |['"]? | ['"]|-{2,})(\w+)/g

Замена:

,$2$4

Разбор решения

Поскольку с местом где ставить запятую уже определились, то определимся на что мы будем заменять эту запятую, а что удалять.

  • «\s([‘»])([^’»]+)» — заменяем шаблон {пробел»слова через пробел в кавычках»} на {, слова через пробел}. «\s» тут не просто так, а для того что бы исключить ложные вхождения с неправильно расставленными кавычками.
  • «(;? |[‘»]? | [‘»]|-{2,})(\w+)» — далее остались одиночные слова перед которыми стоят символы которые нужно удалить, а перед этими словами поставить запятую.


ссылка на оригинал статьи https://habr.com/ru/post/474184/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *