Сопоставьте правильное добавление двух двоичных чисел с регулярным выражением PCRE

Возможно ли совпадение с добавлением в форме (?<a>[01]+)\s*\+\s*(?<b>[01]+)\s*=\s*(?<c>[01]+), где a + b == c (как в двоичном добавлении) должно быть выполнено?

Они должны соответствовать:

0 + 0 = 0
0 + 1 = 1
1 + 10 = 11
10 + 111 = 1001
001 + 010 = 0011
1111 + 1 = 10000
1111 + 10 = 10010

Они не должны совпадать:

0 + 0 = 10
1 + 1 = 0
111 + 1 = 000
1 + 111 = 000
1010 + 110 = 1000
110 + 1010 = 1000

Ответ 1

TL; DR: Да, это действительно возможно (используйте флаги Jx):

(?(DEFINE)
(?<add> \s*\+\s* )
(?<eq> \s*=\s* )
(?<carry> (?(cl)(?(cr)|\d0)|(?(cr)\d0|(*F))) )
(?<digitadd> (?(?= (?(?= (?(l1)(?(r1)|(*F))|(?(r1)(*F))) )(?&carry)|(?!(?&carry))) )1|0) )
(?<recursedigit>
  (?&add) 0*+ (?:\d*(?:0|1(?<r1>)))? (?(ro)|(?=(?<cr>1)?))\k<r> (?&eq) \d*(?&digitadd)\k<f>\b
| (?=\d* (?&add) 0*+ (?:\k<r>(?<ro>)|\d*(?<r>\d\k<r>)) (?&eq) \d*(?<f>\d\k<f>)\b) \d(?&recursedigit)
)
(?<checkdigit> (?:0|1(?<l1>)) (?=(?<cl>1)?) (?<r>) (?<f>) (?&recursedigit) )
(?<carryoverflow>
  (?<x>\d+) 0 (?<y> \k<r> (?&eq) 0*\k<x>1 | 1(?&y)0 )
| (?<z> 1\k<r> (?&eq) 0*10 | 1(?&z)0 )
)
(?<recurseoverflow>
  (?&add) 0*+ (?(rlast) \k<r> (?&eq) 0*+(?(ro)(?:1(?=0))?|1)\k<f>\b
                | (?:(?<remaining>\d+)(?=0\d* (?&eq) \d*(?=1)\k<f>\b)\k<r> (?&eq) (*PRUNE) 0*\k<remaining>\k<f>\b
                   | (?&carryoverflow)\k<f>\b))
| (?=\d* (?&add) 0*+ (?:\k<r>(?<ro>)|(?=(?:\d\k<r>(?&eq)(?<rlast>))?)\d*(?<r>\d\k<r>)) (?&eq) \d*(?<f>\d\k<f>)\b)
  \d(?&recurseoverflow)
)
(?<s>
  (| 0*? (?<arg>[01]+) (?&add) 0+ | 0+ (?&add) 0*? (?<arg>[01]+)) (?&eq) (*PRUNE) 0* \k<arg>
| 0*+
  (?=(?<iteratedigits> (?=(?&checkdigit))\d (?:\b|(?&iteratedigits)) ))
  (?=[01]+ (?&add) [01]+ (?&eq) [01]+ \b)
  (?<r>) (?<f>) (?&recurseoverflow)
)
)
\b(?&s)\b

Live demo: https://regex101.com/r/yD1kL7/26

[ Обновление: из-за ошибки в PCRE код работает только для всех случаев с помощью PCRE JIT активный; благодаря Qwerp-Derp для замечать; без JIT, например. 100 + 101 = 1001 не соответствует.]

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

Подсказка. Для более легкого запоминания и последующего объяснения позвольте мне объяснить имена одно- или двухзначных групп групп захвата (все, кроме первых двух, являются флагами [см. ниже]):

r => right; it contains the part of the right operand right to a given offset
f => final; it contains the part of the final operand (the result) right to a given offset
cl => carry left; the preceding digit of the left operand was 1
cr => carry right; the preceding digit of the right operand was 1
l1 => left (is) 1; the current digit of the left operand is 1
r1 => right (is) 1; the current digit of the right operand is 1
ro => right overflow; the right operand is shorter than the current offset
rlast => right last; the right operand is at most as long as the current offset

Для более читаемых + и = с возможными начальными и конечными пробелами существуют две группы захвата (?<add> \s*\+\s*) и (?<eq> \s*=\s*).

Мы выполняем дополнение. Поскольку это регулярное выражение, нам нужно проверить каждую цифру сразу. Итак, посмотрите на математику за этим:

Проверка добавления одной цифры

current digit = left operand digit + right operand digit + carry of last addition

Как мы узнаем перенос?

Мы можем просто посмотреть на последнюю цифру:

carry =    left operand digit == 1 && right operand digit == 1
        || (left operand digit == 1 || right operand digit == 1) && result digit == 0

Эта логика предоставляется группой захвата carry, определяемой следующим образом:

(?<carry> (?(cl)(?(cr)|\d0)|(?(cr)\d0|(*F))) )

с cl будет ли последняя левая цифра операнда равна 1 или нет, а cr будет ли последняя цифра операнда равна 1 или нет; \d0 - проверить, была ли последняя цифра в результате 0.

Примечание: (?(cl) ... | ...) является условной конструкцией, проверяющей, определена ли группа захвата или нет. Из-за того, что группы захвата привязаны к каждому уровню рекурсии, это легко использовать в качестве механизма для установки логического флага (может быть установлено с помощью (?<cl>) в любом месте), что впоследствии может быть обусловлено условным действием.

Тогда фактическое добавление является простым:

is_one = ((left operand digit == 1) ^ (right operand digit == 1)) ^ carry

выраженный группой захвата digitadd (используя a ^ b == (a && !b) || (!a && b), используя l1, является ли цифра левого операнда равным 1 и r1 эквивалентно для правой цифры:

(?<digitadd> (?(?= (?(?= (?(l1)(?(r1)|(*F))|(?(r1)(*F))) )(?&carry)|(?!(?&carry))) )1|0) )

Проверка добавления при заданном смещении

Теперь мы можем проверить, заданы ли определенные cr, cl, l1 и r1 правильные ли цифры в результате, просто вызывая (?&digitadd) при этом смещении.

... при этом смещении. Это следующая задача, нашедшая указанное смещение.

Основная проблема заключается в том, что три строки с известным разделителем между ними, как найти правильный смещение справа.

Например 1***+****0***=****1*** (разделители + и = здесь, а * обозначает любую произвольную цифру).

Или даже более фундаментально: 1***+****0***=1.

Это можно сопоставить с:

(?<fundamentalrecursedigit>
  \+ \d*(?:1(?<r1>)|0)\k<r> = (?(r1) (?(l1) 0 | 1) | (?(l1) 1 | 0) ) \b
| (?=\d* + \d*(?<r>\d\k<r>) =) \d (?&fundamentalrecursedigit)
)
(?<fundamentalcheckdigit>
  # Note: (?<r>) is necessary to initialize the capturing group to an empty string
  (?:1(?<l1>)|0) (?<r>) (?&fundamentalrecursedigit)
)

(Большое спасибо здесь nhahdth за его решение по этой проблеме - здесь немного изменилось пример)

Сначала мы сохраняем информацию о значении в текущем смещении ((?:1(?<l1>)|0)) - установите флаг (т.е. группу захвата, которую можно проверить с помощью (?(flag) ... | ...)) l1, когда текущая цифра равна 1.

Затем мы строим строку справа от найденного смещения правого операнда рекурсивно с помощью (?=\d* + \d*(?<r>\d\k<r>) =) \d (?&fundamentalrecursedigit), который продвигается на одну цифру (слева) на каждом уровне рекурсии и добавляет еще одну цифру в правую часть из правого операнда: (?<r> \d \k<r>) переопределяет группу захвата r и добавляет еще одну цифру к уже существующему захвату (с ссылкой \k<r>).

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

Теперь, в конце рекурсии (т.е. когда достигается разделитель +), мы можем перейти прямо к правильному смещению через \d*(?:1(?<r1>)|0)\k<r>, так как искомая цифра теперь будет точно равна значению до того, что захват группа r.

Теперь также условно установлен флаг r1, мы можем идти до конца, проверяя результат на корректность с помощью простых условных выражений: (?(r1) (?(l1) 0 | 1) | (?(l1) 1 | 0).

Учитывая это, тривиально распространять это на 1***+****0***=****1***:

(?<fundamentalrecursedigit>
  \+ \d*(?:1(?<r1>)|0)\k<r> = \d*(?(r1) (?(l1) 0 | 1) | (?(l1) 1 | 0) )\k<f> \b
| (?=\d* + \d*(?<r>\d\k<r>) = \d*(?<f>\d\k<f>)\b) \d (?&fundamentalrecursedigit)
)
(?<fundamentalcheckdigit>
  (?:1(?<l1>)|0) (?<r>) (?<f>) (?&fundamentalrecursedigit)
)

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

Пусть добавлена ​​поддержка переноса, которая на самом деле просто устанавливает флаги cr и cl, будет ли следующая цифра равна 1 через (?=(?<cr/cl>1)?) после текущих цифр левого и правого операндов:

(?<carryrecursedigit>
  \+ \d* (?:1(?<r1>)|0) (?=(?<cr>1)?) \k<r> = \d* (?&digitadd) \k<f> \b
| (?=\d* + \d*(?<r>\d\k<r>) = \d*(?<f>\d\k<f>)\b) \d (?&carryrecursedigit)
)
(?<carrycheckdigit>
  (?:1(?<l1>)|0) (?=(?<cl>1)?) (?<r>) (?<f>) (?&carryrecursedigit)
)

Проверка входов равной длины

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

\b
(?=(?<iteratedigits> (?=(?&carrycheckdigit)) \d (?:\b|(?&iteratedigits)) ))
[01]+ (?&add) [01]+ (?&eq) [01]+
\b

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

Но, очевидно, мы еще не закончили. Как насчет:

  • левый операнд длиннее правого?
  • правый операнд длиннее левого?
  • левый операнд будет таким же длинным или длинным, чем правый операнд, и результат, имеющий перенос на наиболее значащую цифру (т.е. нужен ведущий 1)?

Обработать левый операнд длиннее правого

Это довольно тривиально, просто перестань пытаться добавлять цифры в группу захвата r, когда мы ее полностью захватили, установите флаг (здесь: ro), чтобы он не считал его пригодным для ношения и сделал разрядный r необязательный (по (?:\d* (?:1(?<r1>)|0))?):

(?<recursedigit>
  \+ (?:\d* (?:1(?<r1>)|0))? (?(ro)|(?=(?<cr>1)?)) \k<r> = \d* (?&digitadd) \k<f> \b
| (?=\d* + (?:\k<r>(?<ro>)|\d*(?<r>\d\k<r>)) = \d*(?<f>\d\k<f>)\b) \d (?&recursedigit)
)
(?<checkdigit>
  (?:1(?<l1>)|0) (?=(?<cl>1)?) (?<r>) (?<f>) (?&recursedigit)
)

Теперь он обрабатывает правый операнд, как если бы он был нулевым; r1 и cr теперь просто никогда не будут установлены после этой точки. Это все, что нам нужно.

Здесь может быть легко запутаться, почему мы можем сразу установить флаг ro, превысив правильную длину оператора и сразу же проигнорировать перенос; причина в том, что checkdigit уже потребляет первую цифру в текущей позиции, таким образом, мы на самом деле уже более чем на одну цифру за конец правого операнда.

Правильный операнд длиннее левого

Теперь это немного сложнее. Мы не можем втиснуть его в recursedigit, так как он будет повторяться так же часто, как цифры в левом операнде. Таким образом, для этого нам нужно отдельное соответствие.

В настоящее время рассмотрено несколько случаев:

  • Существует перенос с добавлением самой значащей цифры левого операнда
  • Нет переноса.

В первом случае мы хотим сопоставить 10 + 110 = 1000, 11 + 101 = 1000; для последнего случая мы хотим сопоставить 1 + 10 = 11 или 1 + 1000 = 1001.

Чтобы упростить нашу задачу, на данный момент мы будем игнорировать ведущие нули. Затем мы знаем, что самая значащая цифра равна 1. Теперь нет переноски и только если:

  • цифра при текущем смещении (т.е. смещение самой значащей цифры левого операнда) в правом операнде равна 0
  • и не было переноса из предыдущего смещения, то есть цифра в текущем в результате равна 1.

Это означает следующее утверждение:

\d+(?=0)\k<r> (?&eq) \d*(?=1)\k<f>\b

В этом случае мы можем записать первый \d+ с (?<remaining>\d+) и потребовать, чтобы он находился перед \k<f> (часть справа от текущего смещения результата):

(?<remaining>\d+)\k<r> (?&eq) \k<remaining>\k<f>\b

В противном случае, если есть перенос, нам нужно увеличить левую часть правого операнда:

(?<carryoverflow>
  (?<x>\d+) 0 (?<y> \k<r> (?&eq) \k<x>1 | 1(?&y)0 )
| (?<z> 1\k<r> (?&eq) 10 | 1(?&z)0 )
)

и используйте его как:

(?&carryoverflow)\k<f>\b

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

Или выразить это менее словесно (с тем, что n является произвольным, чтобы оно соответствовало):

  (?<x>\d+) 0 1^n \k<r> (?&eq) \k<x> 1 0^n \k<f>\b
| 1^n \k<r> (?&eq) 1 0^n \k<f>\b

Итак, применим нашу обычную методику для определения частей справа от операндов:

(?<recurselongleft>
  (?&add) (?:(?<remaining>\d+)(?=(?=0)\k<r> (?&eq) \d*(?=1)\k<f>\b)\k<r> (?&eq) (*PRUNE) \k<remaining>\k<f>\b
            | (?&carryoverflow)\k<f>\b)
| (?=\d* (?&add) \d*(?<r>\d\k<r>) (?&eq) \d*(?<f>\d\k<f>)\b) \d(?&recurselongleft)
)

Обратите внимание, что я добавил (*PRUNE) после (?&eq) в первую ветвь, чтобы избежать возврата к второй ветки с помощью carryoverflow, если не будет переноса и результат не будет соответствовать.

Примечание. Мы не проводим никаких проверок с частью \k<f> здесь, это управляется группой захвата carrycheckdigit сверху.

Случай ведущего 1

Мы не хотим, чтобы 1 + 1 = 0 соответствовал. Что бы это ни было, если мы пойдем только на checkdigit. Таким образом, существуют разные обстоятельства, когда это первое 1 необходимо (если еще не охвачено предыдущим случаем, когда правый операнд длиннее):

  • Оба операнда (без начальных нулей) имеют одинаковую длину (т.е. они оба имеют 1 в своей самой значащей цифре, которая вместе с ним оставляет перенос)
  • Левый операнд длиннее и есть перенос на самой значащей цифре, или обе строки так же длинны.

Первый случай обрабатывает входы, такие как 10 + 10 = 100, второй случай обрабатывает 110 + 10 = 1000, а также 1101 + 11 = 10100, а последний тривиально 111 + 10 = 1001.

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

(?<recurseequallength>
  (?&add) \k<r> (?&eq) (?(ro)|1)\k<f>\b
| (?=\d* (?&add) (?:\k<r>(?<ro>) | \d*(?<r>\d\k<r>)) (?&eq) \d*(?<f>\d\k<f>)\b) \d(?&recurseequallength)
)

Второй случай означает, что нам просто нужно проверить существование переноса в случае ro (т.е. правый операнд короче). Как правило, перенос может быть проверен (так как самая значащая цифра всегда 1, а цифра правильных операндов неявно 0) с тривиальным (?:1(?=0))?\k<f>\b - если была перенесена цифра при текущем смещении в результате будет 0.

Это легко возможно, так как, в конце концов, все остальные цифры до текущего смещения будут проверены ранее checkdigit. Поэтому мы могли бы просто проверить перенос локально здесь.

Таким образом, мы можем добавить это в первую ветвь чередования recurseequallength:

(?<recurseoverflow>
  (?&add) (?(rlast) \k<r> (?&eq) (?(ro)(?:1(?=0))?|1)\k<f>\b
                | (?:(?<remaining>\d+)(?=0\d* (?&eq) \d*(?=1)\k<f>\b)\k<r> (?&eq) (*PRUNE) \k<remaining>\k<f>\b
                   | (?&carryoverflow)\k<f>\b))
| (?=\d* (?&add) (?:\k<r>(?<ro>)|(?=(?:\d\k<r>(?&eq)(?<rlast>))?)\d*(?<r>\d\k<r>)) (?&eq) \d*(?<f>\d\k<f>)\b)
  \d(?&recurseoverflow)
)

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

\b
(?=(?<iteratedigits> (?=(?&checkdigit))\d (?:\b|(?&iteratedigits)) ))
(?=[01]+ (?&add) [01]+ (?&eq) [01]+ \b)
(?<r>) (?<f>) (?&recurseoverflow)
\b

Как насчет нулей?

0 + x = x и x + 0 = x все еще не обработаны и не будут выполнены.

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

(0*? (?<arg>[01]+) (?&add) 0+ | 0+ (?&add) 0*? (?<arg>[01]+)) (?&eq) 0* \k<arg>

Примечание. При использовании в чередовании с основной ветвью нам нужно поместить (*PRUNE) после (?&eq), чтобы избежать перехода в эту основную ветвь, когда любой операнд равен нулю, а не соответствует.

Теперь мы также всегда предполагали, что во входных данных для упрощения наших выражений не было начальных нулей. Если вы посмотрите на исходное регулярное выражение, вы найдете много вхождений 0* и 0*+ (обладатель, чтобы избежать возврата в него и... неожиданных вещей), чтобы пропустить ведущие нули, поскольку мы предполагаем в некоторых местах, что самая левая цифра - 1.

Заключение

И что это. Мы достигли соответствия только правильных дополнений двоичных чисел.

Небольшое примечание относительно относительно нового флага J: он позволяет переопределять группы захвата. Это на первом месте важно для инициализации групп захвата пустым значением. Кроме того, он упрощает некоторые условные выражения (например, addone), поскольку нам нужно только проверить одно значение вместо двух. Сравните (?(a) ... | ...) и (?(?=(?(a)|(?(b)|(*F)))) ... | ...). Кроме того, невозможно упорядочить многократно определенные группы захвата произвольно внутри конструкции (?(DEFINE) ...).

Заключительное примечание: двоичное добавление не является типом 3-го типа Хомского (т.е. регулярным). Это специфический ответ PCRE с использованием специфических функций PCRE. [Другие варианты регулярных выражений, такие как .NET, могут также решить эту проблему, но не все могут.]

Если есть какие-либо вопросы, оставьте комментарий, я попытаюсь разъяснить это в этом ответе.