Как стать автором
Обновить

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

Время на прочтение10 мин
Количество просмотров1.3K

Предлагаю попробовать решить 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.0x12. Лично моё мнение что это бэд-практикс, но тем не менее по тесту такие варианты возможны и поэтому это нужно учитывать.


Подсказка
  • 192.0.2.235 — десятичный с точками. Общепринятая запись, может быть выражена от 1 до 3 цифр между точками (значения от 0 до 255).
  • 0300.0000.0002.0353 — восьмеричный с точками. 4 цифры между точками со значениями от 0 до 7.
  • 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+)" — далее остались одиночные слова перед которыми стоят символы которые нужно удалить, а перед этими словами поставить запятую.
Теги:
Хабы:
+5
Комментарии0

Публикации

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн