使用 PCRE 正则表达式匹配两个二进制数的正确加法

Posted

技术标签:

【中文标题】使用 PCRE 正则表达式匹配两个二进制数的正确加法【英文标题】:Match correct addition of two binary numbers with PCRE regex 【发布时间】:2017-01-25 23:23:25 【问题描述】:

是否可以匹配(?<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

【问题讨论】:

这应该属于 CodeGolf.StacExchange @ŁukaszNiemier 哈哈! “在不将数字转换为整数的情况下验证二进制数的加法”。但老实说,我的回答不是那么好,主要是大...... 相关元讨论:Permission to start a series of advanced regex articles。顺便说一句,好问题。感谢您抽出宝贵时间发布。 @ŁukaszNiemier 很快就会因为离题或不清楚而关闭。 @sln 实际的后续是做乘法,但我目前在如何处理多位进位方面失败了... // 不管怎样,你对二进制结构的确切含义是什么?例子? 【参考方案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

现场演示:https://regex101.com/r/yD1kL7/26

[更新:由于bug in PCRE,该代码仅适用于PCRE JIT激活的所有情况;感谢Qwerp-Derp 为noticing;没有 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

对于更易读的+=(可能带有前导和尾随空格),有两个捕获组(?&lt;add&gt; \s*\+\s*)(?&lt;eq&gt; \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) ... | ...) 是一个条件构造,用于检查是否已定义捕获组。由于捕获组的范围为每个递归级别,这很容易用作设置布尔标志(可以在任何地方使用(?&lt;cl&gt;) 设置)的机制,以后可以有条件地对其进行操作。

那么实际的加法就很简单了:

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

digitadd捕获组表示(使用a ^ b == (a &amp;&amp; !b) || (!a &amp;&amp; b),使用l1表示左操作数的数字是否等于1,r1等价于右数字:

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

给定的偏移量处检查添加

现在,我们可以验证,给定定义的crcll1r1,只需在该偏移处调用(?&amp;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 对这个问题的 solution — 此处稍作修改以适合示例)

首先,我们在当前偏移处存储有关数字的信息((?:1(?&lt;l1&gt;)|0) - 设置 标志(即,可以使用 (?(flag) ... | ...) 检查的捕获组)l1 当当前位数是 1。

然后我们使用(?=\d* + \d*(?&lt;r&gt;\d\k&lt;r&gt;) =) \d (?&amp;fundamentalrecursedigit)递归地在右操作数的搜索偏移的右侧构建字符串,它在每个递归级别上前进一位(在左操作数上)并且添加右操作数右侧多一位数:(?&lt;r&gt; \d \k&lt;r&gt;) 重新定义了r 捕获组,并将另一位数字添加到已经存在的捕获(引用\k&lt;r&gt;)。

因此,只要左操作数上有数字,并且每个递归级别将一个数字添加到 r 捕获组中,就会递归,该捕获组将包含与上数字一样多的字符左操作数。

现在,在递归结束时(即到达分隔符+ 时),我们可以通过\d*(?:1(?&lt;r1&gt;)|0)\k&lt;r&gt; 直接找到正确的偏移量,因为搜索到的数字现在将正好是之前的数字 捕获组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 匹配之前访问偏移量。

让我们添加对进位的支持,这实际上只是在左右操作数的当前数字之后通过(?=(?&lt;cr/cl&gt;1)?) 设置crcl 标志下一个数字是否为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(?&lt;r1&gt;)|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)
)

现在处理正确的操作数,就好像它是零填充的; r1cr 现在永远不会在那之后设置。这就是我们所需要的。

这里可能很容易混淆为什么我们可以在超过正确的运算符长度时立即设置ro标志并立即忽略进位;原因是 checkdigit 已经消耗了当前位置的第一个数字,因此我们实际上已经超过了右操作数末尾的一个数字。

右边的操作数恰好比左边的长

这现在有点难了。我们不能把它塞进recursedigit,因为它只会像左操作数中的数字一样频繁地迭代。因此,我们需要一个单独的匹配项。

现在有几种情况需要考虑:

    左操作数的最高有效位相加产生进位 没有进位。

对于前一种情况,我们要匹配10 + 110 = 100011 + 101 = 1000;对于后一种情况,我们希望匹配 1 + 10 = 111 + 1000 = 1001

为了简化我们的任务,我们现在将忽略前导零。然后我们知道最重要的数字是 1。现在只有 没有进位并且只有当:

右操作数中当前偏移的数字(即左操作数最高位的偏移)为0 并且前一个偏移量没有进位,这意味着结果中当前的数字是1。

这转化为以下断言:

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

在这种情况下,我们可以用(?&lt;remaining&gt;\d+)捕获第一个\d+,并要求它在\k&lt;f&gt;前面(结果当前偏移量右边的部分):

(?<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)
)

请注意,我在第一个分支的(?&amp;eq) 之后添加了一个(*PRUNE),以避免使用carryoverflow 回溯到第二个分支,以防碰巧没有进位并且结果不匹配。

注意:这里我们不对\k&lt;f&gt; 部分做任何检查,这是由上面的carrycheckdigit 捕获组管理的。

前导1的情况

我们肯定不希望匹配1 + 1 = 0。如果我们单独通过checkdigit,情况会是这样。因此,在不同的情况下,前导 1 是必要的(如果前一个右操作数较长的情况尚未涵盖):

两个操作数(不带前导零)长度相等(即,它们的最高有效位都有一个 1,加在一起会留下一个进位) 左操作数较长,最高有效位有进位,或者两个字符串都一样长。

前一种情况处理10 + 10 = 100之类的输入,第二种情况处理110 + 10 = 10001101 + 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&lt;f&gt;\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 = xx + 0 = x 仍未处理并将失败。

我们没有破解大型捕获组来丑陋地处理它,而是求助于手动处理它们:

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

注意:当与主分支交替使用时,我们需要在(?&amp;eq)之后放置一个(*PRUNE),以避免在任何操作数为零时跳转到该主分支并且匹配失败。

现在,为了简化表达式,我们也一直假设输入中没有前导零。如果您查看最初的正则表达式,您会发现0*0*+ 出现了很多次(为了避免回溯到它并且......发生意外的事情),以便跳过前导零,因为我们在某些地方假设最左边的数字是 1。

结论

就是这样。我们实现了只匹配正确的二进制数相加。

关于相对较新的J 标志的小说明:它允许重新定义捕获组。这首先很重要,以便将捕获组初始化为空值。此外,它简化了一些条件(如addone),因为我们只需要检查一个值而不是两个。比较 (?(a) ... | ...)(?(?=(?(a)|(?(b)|(*F)))) ... | ...)。此外,不能在 (?(DEFINE) ...) 构造中任意重新排序多个定义的捕获组。

最后说明:二进制加法不是乔姆斯基 3 型(即常规)语言。这是一个 PCRE 特定的答案,使用 PCRE 特定的功能。 [.NET 等其他正则表达式可能也能解决这个问题,但不是全部都可以。]

如果有任何问题,请发表评论,然后我会尝试在这个答案中澄清这一点。

【讨论】:

您的程序中存在错误:100 + 101 = 1001 不匹配。 @Qwerp-Derp 嗯,这很奇怪。在本地(php 7.1)它完美匹配...... @bwoebi 我猜这是 regex101.com 的问题,然后......因为我正在那里测试它。顺便说一句,这是什么正则表达式风格? 继续解决这个问题:101 + 100 = 1001 确实匹配,这使得问题变得越来越奇怪。由于某些奇怪的原因,1000 + 101 = 10001 匹配,而 1000 + 101 = 1101 不匹配。 @Qwerp-Derp 非常感谢您的关注,我已向 PCRE 错误跟踪器报告了一个错误:bugs.exim.org/show_bug.cgi?id=1887

以上是关于使用 PCRE 正则表达式匹配两个二进制数的正确加法的主要内容,如果未能解决你的问题,请参考以下文章

PHP 正则表达式(PCRE)

PHP 正则表达式(PCRE)

雷林鹏分享:PHP 正则表达式(PCRE)

使用正则表达式 (PCRE) 匹配 a^n b^n c^n(例如“aaabbbccc”)

pcre和正则表达式的误点

强烈推荐!Python 这个宝藏库 re 正则匹配