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