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

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое