Хороший код читается легко, как проза. Многие книги учат нас тому, как важно делить код на небольшие, повторно используемые, легко потребляемые блоки.

Но почему-то, в случае с регэкспами у программистов как будто появляется слепое пятно на чувстве стиля. Вот такая регулярка – совершенно обычное дело:

/^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.]((19|20)\d\d)$/

Долго вглядываясь в нее, мы, наверное, поймем рано или поздно, что она содержит четыре логических блока - три чиселка, по-разному ограниченных, и некоторый разделитель [- /.]

Не лучше ли ее записать вот так?

"/^" + month + delimiter + day + delimiter + year + "$/"

Матерь божья, да это же дата! Сколько наносекунд вам потребовалось для того, чтобы это понять?

Почему же за однострочник вроде тех, что пишет легендарный Stefan Pochmann c leetcode, тебе сразу оторвут руки, а на художества с регулярками смотрят сквозь пальцы? Мне не слишком понятно почему.

// отвратительный однострочник Стефана Почманна
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
    return root1&&root2 ? new TreeNode(root1->val+root2->val,mergeTrees(root1->left,root2->left),mergeTrees(root1->right,root2->right)):root1?root1:root2;
}

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

Итак: не склеивайте регулярки в одну большую регулярку, как будто вы – первокурсник, понтующийся своим однострочником. Регулярки – это код должно быть легко читать, отлаживать и модифицировать. Чтобы он стал таковым, надо большое и непонятное содержимое разбить на группы маленького и понятного. В педагогике это называется chucking, у нас – decomposition.

Действовать будем исходя из следующих положений:

  • В большинстве языков регулярное выражение может создаваться на основе строки.

  • В большинстве сложных регулярок можно выделить составные части.

  • Строки можно конкатенировать.

Подход нехитрый: выделяем смысловые кусочки в переменные и функции, потом все склеиваем. Для примера возьмем регулярочку из другой хабростатьи:

// 1. Строка начинается только с заглавной латинской буквы или цифры
// 2. За ним может быть разрешенный спецсимвол или единичный пробел
// 3. Нельзя использовать кириллицу и другие спецсимволы
const someEngPattern = /^[A-Z0-9]+([a-zA-Z0-9\\!\\#\\%]|\\s(?!\\s))*$/;

Это не очень хороший код, на его чтение у меня ушло много времени.

Кроме того, этот код предваряется длинным комментарием, описывающий, что происходит в регулярке. Каким бы ни был болтуном и сектантом автор книги Clean Code Дядюшка Боб (Robert Martin), с его мнением о комментариях в коде я согласен. Комментарии врут. Если они не врут прямо сейчас, то они будут врать в будущем, когда кто-то внесет изменения в код и забудет обновить комментарий. Альтернатива комментариям - это промежуточные переменные и функции с говорящими именами.

Я буду декомпозировать нашу регулярочку "снаружи вовнутрь", шаг за шагом, а потом посмотрим, что получилось. Примеры будут на JS/TS, но для других языков все будет так же.

Шаг 1: начало и конец ввода

Например, давайте сразу избавимся от пары /^ и $/

function wholeInput(regex) {
  return  "/^" + regex + "$/";
}
const someEngPattern = wholeInput("[A-Z0-9]+([a-zA-Z0-9!#%]|\\s(?!\\s))*")

Шаг 2: выделим крупные логические блоки

Два блока, которые вижу я - это первая группа буковок (начинается с большой буквы или с цифры) и все остальное. По лингвистической традиции обзовем их префиксом и суффиксом:

const prefix = "[A-Z0-9]+"
const suffix = "([a-zA-Z0-9\\!\\#\\%]|\\s(?!\\s))*"
const someEngPattern=wholeInput(prefix + suffix)

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

Шаг 3. опять выделяем логические блоки

Простенькая функция для скобочек и звездочки (а скобочки обозначают группу, то есть группа может повторяться от нуля до бесконечности раз):

function group(regex) {
  return "(" + regex + ")";
}
const suffix = group("[a-zA-Z0-9\\!\\#\\%]|\\s(?!\\s)") + "*"

Шаг 4. Суффикс и предел декомпозиции

В суффиксе в конце у нас есть что-то хитрое с пробелами:

const onlyOneWhiteSpace="\\s(?!\\s)";

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

Шаг 5. Экранирование

В кусочке [a-zA-Z0-9\\!\\#\\%] префиксе у нас налицо куча экранированных симовлов. У меня от этих бесконечных палок рябит в глазах, поэтому сделаю-ка я функцию escape:

function escape(rawChar) {
  return "\\" + rawChar;
}

Для куска a-zA-Z0-9 можно придумать имя:

const letterOrNumber = "a-zA-Z0-9";

Шаг 6. Переменные или функции для управляющих конструкций

Одна из сложностей в регекспах - это понять, где у нас управляющие конструкции, а где символы, которые мы match'им. Создадим функцию charClass, которая будет обрамлять свое содержимое квадратными скобочками.

function charClass(regex) {
   return "[" + regex + "]";
}

А еще я создал переменную or – специально чтобы никто не подумал, что это просто match символа вертикальной палки.

const or = "|"
const suffixLetter = charClass(letterOrNumber+specialChar) + or + onlyOneWhiteSpace;

Результат декомпозиции

Было: большая регулярка

// 1. Строка начинается только с заглавной латинской буквы или цифры
// 2. За ним может быть разрешенный спецсимвол или единичный пробел
// 3. Нельзя использовать кириллицу и другие спецсимволы
const someEngPattern = /^[A-Z0-9]+([a-zA-Z0-9\\!\\#\\%]|\\s(?!\\s))*$/;

Стало: куча функций и переменных, скомбинированных друг с другом


function escape(rawChar) {
   return "\\" + rawChar;
}
function charClass(regex) {
   return "[" + regex + "]";
}
function wholeInput(regex) {
   return  "/^" + regex + "$/";
}
function group(regex) {
   return "(" + regex + ")";
}
const prefix = "[A-Z0-9]+";
const letterOrNumber = "a-zA-Z0-9";
const specialChar = escape("!") + escape("#") + escape("%");
const onlyOneWhiteSpace="\\s(?!\\s)";
const or = "|"
const suffixLetter = charClass(letterOrNumber+specialChar) + or + onlyOneWhiteSpace;
const suffix = group(suffixLetter) + "*";
const someEngPattern=wholeInput(prefix + suffix)

Возможно, еще стоило бы переименовать переменные префикс и суффикс во что-то удобоваримое, но уж очень громоздкими получаются имена переменных: whitespaceSeparatedLetterNumericalWithSpecChars, брр.

Тут налицо ограничения нашего декомпозиционного подхода. В отличие от семантически ясных day, month и year из предыдущего примера, крупным и сложным сущностям без ясной семантики сложновато подобрать звучные и короткие названия. Как следствие, их природу приходится порой скрывать за безликими лингвистическими жаргонизмами вроде prefix и suffix.

Анализ

Стоит ли овчинка выделки? Этот код я писал дольше, чем записал бы гига-регэксп выше, зато:

  • его легче читать;

  • для его понимания почти не надо лезть в справочник;

  • его куски можно повторно использовать. Функции group, escape и wholeInput понадобятся и потом;

  • его куски можно напрямую отлаживать;

  • про производительность - не смешите меня, все заинлайнится как миленькое даже в хилом V8, не говоря о дюжем gcc;

  • если ты - гигант и умеешь читать гига-регэкспы разом, то ты просто можешь добавить console.log(someEngPattern).

Принципы и лучшие практики

Степень декомпозиции зависит от вас и вашей команды, а остановиться можно в любой момент. Код, который у меня получился, может быть в два раза короче, например вот таким:

function wholeInput(regex) {
   return  "/^" + regex + "$/";
}
function zeroOrMore(regex) {
   return "(" + regex + ")*";
}
const or = "|"
const onlyOneWhiteSpace="\\s(?!\\s)";
const suffix = zeroOrMore ("[a-zA-Z0-9\\!\\#\\%]" + or + onlyOneWhiteSpace)
const someEngPattern = wholeInput( "[A-Z0-9]+" + suffix)

Правда ж он менее мерзкий?

Я сейчас собираю принципы для работы с регулярками, вот кое-что:

  • выносим отдельно надо то, от чего рябит в глазах. Кучи скобочек, обилие слэшей, все, в чем можно запутаться;

  • выносим то, что представляет собой понятный логический блок. Примеры выше - день, месяц, год, не более одного пробела;

  • выносим то, для чего требуется редко используемый синтаксис. a-zA-Z знают многие, а вот в \\s(?!\\s) сразу и не въедешь;

  • выносим управляющие символы, которые легко перепутать с искомыми символами;

  • группируем так, чтобы было понятно, к чему относится тот или иной управляющий символ;

  • если позволяет язык и есть потребность - используем multiline-строки и режим игнорирования whitespace'ов, тогда можно форматировать их с отступами, прям как в нормальном коде, гляньте на пример, предоставленный @shoorick. Для наших примеров использовать обратнокавычечные строки из JS смысла не было, да и переменные внутри них выглядят довольно неуклюже.

Другие подходы

За 15 лет в индустрии я лишь один раз видел, чтобы разработчики декомпозировали свои регулярки. Остальные хреначат в одну строчку и полагаются на авось.

Ну конечно, это плохой способ подводить статистику, и на просторах интернета я нашел несколько других интересных направлений:

  • Библиотека mol-regex – это когда подход, взятый в нашей статье, доведен до логического завершения. Если у вас действительно много регулярок и api библиотеки вам по душе – надо брать! Коллега @ninjin описывает его в своей статье:

// /4(?:\d){12,}?(?:(?:\d){3,}?){0,1}/gsu
const VISA = from([
    '4',
    repeat( decimal_only, 12 ),
    [ repeat( decimal_only, 3 ) ],
])
const tester = VerEx()
    .startOfLine()
    .then('http')
    .maybe('s')
    .then('://')
    .maybe('www.')
    .anythingBut(' ')
    .endOfLine();
  • Библиотека SuperExpressive - еще один билдер регулярок, который вспомнил @FanatPHP. Обратите внимание на функцию end() и табуляцию:

const SuperExpressive = require('super-expressive');

const myRegex = SuperExpressive()
  .startOfInput
  .optional.string('0x')
  .capture
    .exactly(4).anyOf
      .range('A', 'F')
      .range('a', 'f')
      .range('0', '9')
    .end()
  .end()
  .endOfInput
  .toRegex();

// Produces the following regular expression:
/^(?:0x)?([A-Fa-f0-9]{4})$/
(?<duplicateWord>\w+)\s\k<duplicateWord>\W(?<nextWord>\w+)

Наконец для сложных штук, типа написания своих раскрашивателей кода или анализаторов DSL, можно уже и грамматиками воспользоваться, а они нам парсеров нагенерируют как делают в PEG.js. Объемная статья про парсинг (в js).

Буду рад, если кто-нибудь принесет примеров из респектабельных open-source проектов, и мы вместе вместе покумекаем над принципами и границами применимости.

Не забывайте о гибридном подходе

И еще один принцип, о котором часто забывают: каждому инструменту – свое применение. Молотки, гвозди, ну вы поняли.

Даже если вы декомпозируете пример с датой из начала статьи, конструкция (0[1-9]|[12][0-9]|3[01]) - это плохой и невкусно пахнущий код.

Почему? Да потому, что он использует текстовые методы для анализа чиселки. Выковыряйте чиселку года вульгарным \d{1,4} , приведите в тип числа и верифицируйте уже численными методами:

function isValidYear(year: number): boolean {
   if (isNaN(year)) {
      return false;
   }
   return year > 0 && year < 3000; // ну уж тысячу лет мой код точно проживет
}

Другой понятный случай применения гибридного подхода: это когда часть выражения проще/читабельнее верифицировать через старые добрые циклы и строковые операции, чем через регэкспы.

Спасибо хабраюзерам @DirectoriX и @0x131315 за то, что они начали отличную ветку о гибридном подходе в соседнем посте.

Комментарии (44)


  1. ionicman
    17.10.2022 09:58
    +12

    Знаете, все, конечно, ИМХО. Но я занимаюсь регулярками практически с самого их появления, и то, что Вы описали в статье — скорее вред, чем польза.

    Обьясню почему — вы привнесли в регулярки еще один язык высокого уровня, который во-первых замедляет их выполнение, во-вторых лишает кроссмплатформенности и быстроты проверки. Да, я понимаю, что можно в конце концов вытащить итоговое выражение и оно будет работать везде — но его еще собрать надо.

    Кроме того, большинство программистов привыкли именно к такому виду, который Вы показали в начале статьи + есть огромное количество инструментов для визуализации и работы с регулярками.

    Если хочется более-менее порядка и стройности в данных выражениях, то для этого есть флаг PCRE-EXTENDED и после этого у вас появляется возможность использовать пробелы, табуляцию и перевод строки для разделения кусков регулярного выражения, возможность писать комментарии, а если совместить это с именованием групп — то вообще все будет с читаемостью отлично. И при этом никаких лишних сущностей не требуется.

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


    1. ganqqwerty Автор
      17.10.2022 10:07

      Насчет того, что привыкли – а насколько это ценно? В плюсах раньше триграфами писали - а глядишь, переучились. Можно простую метрику ввести. Берем сто человек, даем им почитать длинную регулярку и декомпозированную. Засекаем время, которое им потребуется для того, чтобы решить типичные задачи программиста: понять/рассказать, что в коде происходит, устранить ошибку, переместить код куда-то, модифицировать поведение и прочее. У меня определенный оптимизм есть насчет результатов.


      1. ionicman
        17.10.2022 10:10
        +2

        То, к чему привыкли — очень ценно. Привычки, знаете-ли тяжело менять — что в интерфейсах, что в программировании. И если и менять — то должен быть очень веский повод для этого.

        В приведенном вами варианте регулярка становится размазанной по коду, начинает зависеть от еще одного языка иперестает быть воспринимаемой как регулярка и читаемости прибавляется только на последнем шаге при сборке — оно того ИМХО не стоит никаким образом, тем более — не стоит того чтобы менять устоявшиеся практики, ибо минусов куда больше, чем плюсов.


        1. aelaa
          17.10.2022 10:17

          В современном мире новых программистов приходит больше чем есть привыкнувших. А порог входа таки высокий.


          1. ionicman
            17.10.2022 10:22
            +4

            В современном мире программисты стараются из кубиков складывать, а в нутро не лезть) Тем более — новые программисты. Есть такие вещи, которые не в тренде — регулярки именно оттуда.

            На одной из конференций в 2021 делали опрос про регулярки — примерно 60% их знало, около 35% могли писать что-то, и только около 8% понимали как оно работает. А теперь внезапно возраст — первая группа 18-45, вторая — 20-45, третья 29-45. Так что увы и ах.

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

            А вот попытки перетащить то, что надо просто понимать в то, в чем ты уже понимаешь — это как раз черта новых программистов — не в обиду вам будет сказано. Это как вечные попытки натянуть классическое ООП на JS с прототипами, хотя последнее при понимании отнюдь не хуже. Ну и из-за количества этих программистов в конце-концов в JS таки втянули это дело, до регулярок пока не добрались т-т-т )


            1. ganqqwerty Автор
              17.10.2022 10:36

              Думаю, что знало там тоже на уровне: знало, что звездочка - это повтор.


            1. aelaa
              17.10.2022 10:39
              +1

              Программисты уже 70 лет строят высокоуровневые языки поверх низкоуровневых. Олдфагов с хекскодами я не видел уже лет 20 (с ассемблером не сильно меньше), но ворчание их хорошо помню. Но все равно люди будут это делать, потому что удобно всем.


              1. ionicman
                17.10.2022 10:43
                +2

                Если высокоуровневый язык дает хорошие преимущества — почему бы и нет? Олдфаги здесь не причем, это простая рациональность.

                А вот если преимущество сводится лишь к тому «хочу, чтобы было так, как я знаю сейчас и как мне удобно, а как там было до меня — все равно» — это, ИМХО, тупик.


    1. SiteCenter
      17.10.2022 10:42
      +1

      есть огромное количество инструментов для визуализации и работы с регулярками

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


    1. domix32
      17.10.2022 11:12
      +2

      кроссмплатформенности 

      извините, не удержался

      замедляет их выполнение

      А вот это сильно не правда. В компилируемых языках можно такие конструкции сделать статически вычисляемыми и оно ничем не будет отличаться от того же месива, что и сплошной строке. В каком-нибудь JS почти наверняка в какой-то момент сработает оптимизация hotpath, однако я слабо представляю ситуацию, когда вы с каждым циклом пересобираете регулярку заново. То есть единоразовый вызов сборки регулярки не сказать чтобы сильно замедлит ваш код. Если конечно вы не запускаете эти регулярки на чем-то особенном.


      1. ionicman
        17.10.2022 11:14
        +1

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

        Грубо — все это очень сильно зависит от среды и языка — фишка в том, что вполне можно обойтись без этого и вообще не напрягаться по этому поводу.


        1. domix32
          17.10.2022 11:40

          В статье примеры на JS, но суть статьи про декомпозицию регулярок, а не про JS.


          1. ionicman
            17.10.2022 12:08
            +2

            Это было про оптимизацию, которую Вы затронули.

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

            Декомпозиция регулярок возможна как стандартными, встроенными методами (о которых я писал в первом комментарии), так и спец. инструментарием, а не дополнительными сущностями на абсолютно другом языке.


            1. domix32
              17.10.2022 12:27

              Тогда можно сразу к старой доброй фразе вернуться - если у вас есть проблема и вы решаете её регулярками - у вас две проблемы.

              Кстати, про кроссплатформенность пошутил, но забыл спросить, а какие собственно с этим могут быть проблемы? perl/js/c++/вставить свой вариант вроде везде относительно единообразно работают.


              1. ionicman
                17.10.2022 12:33
                +2

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

                С кроссплатформенностью тоже есть нюансы — как и любой инструмент — регулярки развивались, мало того, что там существуют как минимум два стандарта, так еще и они внутри могут отличаться — например не поддерживать именованные группы — о чем тут уже было. Не поддерживать просмотр вперед/назад (или даже отмену или включение жадности). Есть очень интересная и крутая фича современных регулярок — рекурсивные регулярки (правда она еще сложнее для понимания, чем обычные варианты) — вот она поддерживается далеко не всеми. Короче, нюансов тоже предостаточно.


                1. domix32
                  17.10.2022 12:45

                  А, ну то есть кроссдвижковость, а не кроссплатформенность.


                  1. ionicman
                    17.10.2022 12:48

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

                    Но вообще, если брать PCRE — то таки да, с небольшими допущениями он практически везде работает одинаково.


  1. steelratty
    17.10.2022 10:00
    +3

    Выражу другое мнение. Размазанная регулярка это как собираемый из кусков sql запрос. Кажется, лучше даже разделять пробелами и комменты сверху над блоками

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


  1. ShashkovS
    17.10.2022 10:05
    +11

    function wholeInput(regex) {
       return  "/^" + regex + "$/";
    }
    function zeroOrMore(regex) {
       return "(" + regex + ")*";
    }
    const or = "|"
    const onlyOneWhiteSpace="\\s(?!\\s)";
    const suffix = zeroOrMore ("[a-zA-Z0-9\\!\\#\\%]" + or + onlyOneWhiteSpace)
    const someEngPattern = wholeInput( "[A-Z0-9]+" + suffix)


    Я вот не могу согласиться с тем, что этот код менее мерзкий.
    Я знаю синтаксис регулярок, мне не нужно объяснять, что (regex)* — это сколько угодно повторов, я это сразу вижу. Если уж что-то и нужно объяснять, то мотивацию того, почему или зачем вы делаете именно так.
    Зато регулярку текстом я могу скопировать и вставить в условный regex101, чтобы её потестировать (или поправить). А такой код мне нужно частично исполнить, чтобы получить регулярку, которую можно тестировать. И после того, как я её потестирую, мне придётся исправления назад мучительно накатывать в код (с возможными ошибками в процессе).

    Вот пример того, как длинная и сложная регулярка оформена в модуле fractions.py:
    _RATIONAL_FORMAT = re.compile(r"""
        \A\s*                                 # optional whitespace at the start,
        (?P<sign>[-+]?)                       # an optional sign, then
        (?=\d|\.\d)                           # lookahead for digit or .digit
        (?P<num>\d*|\d+(_\d+)*)               # numerator (possibly empty)
        (?:                                   # followed by
           (?:\s*/\s*(?P<denom>\d+(_\d+)*))?  # an optional denominator
        |                                     # or
           (?:\.(?P<decimal>d*|\d+(_\d+)*))?  # an optional fractional part
           (?:E(?P<exp>[-+]?\d+(_\d+)*))?     # and optional exponent
        )
        \s*\Z                                 # and optional whitespace to finish
    """, re.VERBOSE | re.IGNORECASE)


    Эту регулярку можно как есть вставить в regex101 (нужно не забывать ставить флаг extended).
    Вот так это выглядит:
    Там, если нужно, можно отладить все аспекты работы регулярки (группы и т.п.), а потом как есть скопировать назад в код.


    1. ganqqwerty Автор
      17.10.2022 10:12

      Собрать конкретно эту регулярку часто можно даже на этапе препроцессинга, не исключено, что даже плагин к vscode есть, который ее подсветит: все куски-то статичные. Ну а если не подсветит, всегда можно console.log впихнуть и посмотреть.


      1. ShashkovS
        17.10.2022 11:39
        +1

        Ну, это такое. Если склейка регулярки из именованных кусков — это норм, я таки и вправду могу сунуть в console.log, то функции типа wholeInput — это провал.
        Такие функции первые претендуют на то, чтобы выехать в какой-то общий для всех модуль.
        И вот после этого, чтобы собрать регулярку в консоли, мне нужно искать по проекту, где живут эти функции, копировать их отдельно в консольку, после этого уже собирать регулярку.
        Кроме того, хоть слова wholeInput довольно понятные, но регулярки очень формальный язык, в которых каждый символ может иметь большое значение. Если я такое увижу в коде, то мне придётся полезть посмотреть, что именно имел в виду автор. А то мало ли, может он исходил из того, что у регулярки ещё какие-то флаги не выставлены?
        Если я сделаю так:

        const reg = new RegExp(wholeInput('\d+'), 'gm'); // wholeInput же умная, наверное?
        // [...'123\nasdf\n53'.matchAll(reg)] — это два match'а
        

        то могу получить не тот результат, который ожидаю.


  1. ubx7b8
    17.10.2022 10:37
    +9

    А если воспользоваться встроенными возможностями, чтобы сразу увидеть, какая группа для чего и заодно получить их по имени?

    /^(?<day>0[1-9]|1[012])[- /.](?<month>0[1-9]|[12][0-9]|3[01])[- /.](?<year>(19|20)\d\d)$/


    1. ganqqwerty Автор
      17.10.2022 10:46

      Вот это, кстати, очень правильно! Именованные группы много где поддерживаются?


      1. ionicman
        17.10.2022 10:52
        +1

        В большинстве языков. Если про JS — то с ECMAScript 2018.


      1. ganqqwerty Автор
        17.10.2022 10:54
        +1

        Вот тут и здесь про них и их поддержку. Думаю, есть смысл добавить абзац про них в основную статью.


  1. Juribiyan
    17.10.2022 10:57
    +1

    Какой кошмар. Любят же некоторые на пустом месте всё усложнить и понавязывать другим правила.
    А чтобы быть конструктивным, предложу альтернативное решение проблемы запутанных регэкспов в коде. Некое расширение для IDE, превращающее регэкспы в интерактивные объекты, по нажатию на которые открывается окошко с парсером и редактором (подобных инструментов создано достаточно). Почти уверен, что в каком-нибудь VS Code это точно уже реализовано.


  1. datacompboy
    17.10.2022 11:15
    +2

    Зачастую regexp это write-only code. Его не надо читать, в него надо верить.

    Если есть недоверие -- переписать. Благо, однострочник.

    Да, я знаю про https://emailregex.com/ вариации. Это использование неподходящего инструмента для неподходящей цели. Но ведь можно же!


    1. ganqqwerty Автор
      17.10.2022 11:45
      +1

      Это уж совсем капитуляция перед задачей. По-моему, не должно быть write-only кода, ни в тестах, ни в регулярках, ни в css или xpath-селекторах, ни даже в bash и perl-скриптах, если их читают и запускают несколько раз несколько людей.


      1. datacompboy
        17.10.2022 11:57
        +4

        Это избегание постановки ненужных задач. Задача вообще решить бизнес-проблему.

        Регулярка это хороший способ быстро (по времени разработчика) решить часть из них.

        Обертка если и нужна, то нужна не над регуляркой, а над доменной областью. Тут как с ORM -- мы упрощаем доступ к стандартно-структурированным данным, получая одновременно гомогенность и гарантии минимального качества. Когда ORM начинают использовать чтобы конструировать произвольной сложности SQL запросы вместо использования SQL запросов -- это серьёзная протечка абстракции, а не "мы сделали SQL читаемым".

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


      1. OldNileCrocodile
        19.10.2022 10:46

        А клиенту и не надо знать код приложения. Написание регулярок - задача программиста. А разбираться, как она работает будут тестировщики. Если там написана фигня- претензии к программисту. Я понимаю, что есть уж совсем маленькие компании, где нет отдельного штата тестировщиков, и отдел разработки занимается несколькими задачами одновременно. Тут программист может подложить свинью коллегам.


  1. FanatPHP
    17.10.2022 11:17

    Бывает нечто, о чем говорят: "смотри, вот это новое"; но это было уже в веках, бывших прежде нас. Эккл. 1:10

    Сказать по правде, уже известное решение, с его действительно читабельным


    const myRegex = SuperExpressive()
      .startOfInput
      .optional.string('0x')
      .capture
        .exactly(4).anyOf
          .range('A', 'F')
          .range('a', 'f')
          .range('0', '9')
        .end()
      .end()
      .endOfInput
      .toRegex();
    

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


    1. ganqqwerty Автор
      17.10.2022 11:26

      Неплохое, сейчас добавим! Тут, наверное, та же ситуация, что и с другими библиотеками – не хочется тащить ее в код, если в проекте регулярок меньше пары десятков, но вполне можно – если их много, а сменяющие друг друга программисты уже стонут.


      1. ionicman
        17.10.2022 11:30
        +1

        а сменяющие друг друга программисты уже стонут
        А привнесение еще одного синтаксиса еще одного не стандартного фреймворка вместо стандарта, стоны, конечно-же, прекратит )


  1. musk
    17.10.2022 11:35
    +3

    Даже если вы декомпозируете пример с датой из начала статьи, конструкция (0[1-9]|[12][0-9]|3[01]) - это плохой и невкусно пахнущий код. Выковыряйте чиселку года вульгарным \d{1,4}

    Это как раз-таки вполне здравый подход в данном случае в рамках применения регулярок, а ваши дополнительные проверки в коде как раз и есть плохой и невкусно пахнущий код.

    Регулярки на то и регулярки, чтобы ими оставаться. А вы превратили относительно просто читаемое выражение в вырвиглазный псевдоскрипт, который малопереносим, да и сложен в проверке в каком-нибудь regex101. Вы с тем же успехом поучите математиков формулы писать. Вполне достаточно давать понятные имена самое переменной, использовать именованные группы или разбивку по строкам.


    1. ganqqwerty Автор
      17.10.2022 11:40

      Мне не понятна ваша логика. Моя логика такая: проверка того, что выковырянное значение года больше нуля и меньше 3000 - это численная проверка. Натягивать на нее текстовый метод, пользуясь тем, что наш способ записи чисел таков – это применять инструмент не по назначению.

      Вот если бы надо было проверить, что 988 год будет записан как 0988 или что 8 марта записывается не как 8.3, а как 08.03 - тогда да, это явно текстуальная вещь, регэкспы очень в тему.


      1. datacompboy
        17.10.2022 11:47
        +4

        это не численная проверка а минимизация срабатываний.


        1. ganqqwerty Автор
          17.10.2022 11:51

          Ммм, о чем речь? Не слишком понял.


          1. datacompboy
            17.10.2022 12:00
            +1

            в тексте можно встретить много цифр самых разнообразных. задание 0[1-9]|[12][0-9]3[0-1] лучше чем \d\d просто потому, что минимизирует выкусывания не-дат. да, это не гарантия, это просто микрооптимизация, чтобы не кусать лишнего.


      1. musk
        17.10.2022 14:25
        +1

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


  1. nivorbud
    17.10.2022 11:39

    Опять эти войны остроконечников с тупоконечниками... Ни к чему это. Оба подхода имеют право на жизнь. Да, приведенные в статье примеры (статичных по сути) регулярок нет смысла декомпозировать в переменные/функции. Но в некоторых сложных случаях без такой декомпозиции не обойтись, особенно когда некоторые данные для регулярок подтягиваются извне и/или когда построение регулярки зависит от разных условий, т.е. когда регулярку надо формировать динамически. Пример: yargy-парсер, основанный на правилах.


    1. ganqqwerty Автор
      17.10.2022 11:49
      -1

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


      1. ValeriyFilatov
        17.10.2022 18:09
        +2

        Если рассматривать регулярку как "функцию", коей она и является под капотом, то вполне нормально, что на осмысление может потребоваться более "0.5 секунд"


  1. ReinRaus
    17.10.2022 17:16
    +1

    Сугубо личное мнение (обрабатываю много текста регулярными выражениями и вообще их фанат):
    Декомпозиция описанная в статье излишня. Она порождает нагромождение высокоуровневого кода над регулярным выражением.
    В большинстве случаев даже для сложных регулярных выражений достаточно использовать именованные группы и форматирование отступами для того, чтобы минимизировать проблемы восприятия регулярных выражений.
    Есть исключение: иногда нужно переиспользовать отдельные части регулярных выражений и иметь возможность менять шаблон в одной точке кода. Тогда лучше выделить часть шаблона в отдельную переменную и после этого включать эту часть в другое регулярное выражение через replace, чтобы не смешивать высокоуровневый синтаксис с регулярным выражением:

    userID = "user\d+";
    regex = "^USERID$";
    regex = regex.replace( "USERID", userID );
    


  1. mentin
    17.10.2022 19:33
    +1

    Для питона есть похожая библиотека

    https://github.com/manoss96/pregex

    Узнал о ней из вот этой статьи, хорошее описание как она улучшает читабельность

    https://towardsdatascience.com/pregex-write-human-readable-regular-expressions-in-python-9c87d1b1335