Предлагаю попробовать решить 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/
Добавить комментарий