是否可以匹配(?<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
最佳答案
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处于 Activity 状态的所有情况;感谢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
为了使可读性更高的
+
和=
带有可能的前导和尾随空格,有两个捕获组(?<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) ... | ...)
是一个条件构造,用于检查是否已定义捕获组。由于捕获组的作用域是递归的每个级别,因此它很容易用作设置 bool(boolean) 标志(可以在任何地方使用(?<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对此问题的solution,在此做了一些修改以适合示例)
首先,我们在当前偏移量处存储有关位数的信息(
(?:1(?<l1>)|0)
-设置当前位数为1时的标志(即可以使用(?(flag) ... | ...)
进行检查的捕获组)l1
。然后,我们使用
(?=\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
和(?=(?<cr/cl>1)?)
标志是否下一位为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
即,对左操作数的每个数字递归地断言可以执行加法,然后成功。
但是显然,我们还没有完成。关于什么:
处理左操作数要长于右操作数
那是非常琐碎的,只要在我们完全捕获了它之后就停止尝试将数字添加到
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。现在只有在以下情况下才没有进位:
这转化为以下断言:
\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
捕获组的工作方式是,复制右操作数的左侧部分,在该位置找到最后一个零,然后将比该零低有效的所有零替换为零,并用零替换零。如果该部分中没有零,则将它们全部替换为零,然后添加前导1。或用较少的语言来表达(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是必需的(如果前面的右操作数较长的情况还没有覆盖):前一种情况处理诸如
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))?\k<f>\b
来检查进位(因为最高有效位始终为1,而右边的操作数位则隐式为0)-如果存在进位,则当前偏移量处的位将为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) ...)
构造内对重新定义的多个捕获组进行重新排序。最后说明:二进制加法不是Chomsky 3类(即常规)语言。这是使用PCRE特定功能的PCRE特定答案。 [.NET等其他正则表达式也可以解决它,但并非所有人都可以。]
如有任何疑问,请发表评论,然后我将尝试在此答案中进行澄清。
关于regex - 使用PCRE正则表达式匹配两个二进制数的正确加法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39553372/