匹配正确添加两个二进制数与PCRE正则表达式

时间:2016-09-18 02:01:33

标签: regex 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 个答案:

答案 0 :(得分:63)

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

对于具有可能的前导和尾随空格的更具可读性的+=,有两个捕获组(?<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) )

给定偏移

处检查添加

现在,我们可以根据已定义的crcll1r1验证结果中的数字是否正确,只需调用{{1在那个偏移处。

......在那个偏移处。这是下一个挑战,找到了偏移量。

根本问题是,如果三个字符串之间有一个已知的分隔符,如何从右边找到正确的偏移

例如(?&digitadd)(此处的分隔符为1***+****0***=****1***+=表示任意数字。

甚至更基本的是:*

这可以与:

匹配
1***+****0***=1

(非常感谢nhahdth solution对这个问题的{{3}} - 在这里做了一些修改,以适应这个例子)

首先,我们在当前偏移量((?<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) ) - 存储有关数字的信息 - 设置标志(即捕获组,可以使用(?:1(?<l1>)|0)进行检查当前数字为1时,(?(flag) ... | ...)

然后我们用l1递归地在右侧操作数的搜索偏移量的右侧构建字符串,在递归和在右操作数的右侧添加一位数:(?=\d* + \d*(?<r>\d\k<r>) =) \d (?&fundamentalrecursedigit)重新定义(?<r> \d \k<r>)捕获组,并将另一个数字添加到已存在的捕获(用r引用)。 / p>

因此,只要左操作数上有数字并且每个递归级别的\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匹配之前访问偏移量。

让我们添加对进位的支持,这实际上只是设置fcr标志,当前数字后,下一位是否通过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)
)

即。递归地断言左操作数的每个数字,可以执行加法然后成功。

但显然,我们还没有完成。怎么样:

  1. 左操作数是否比右边长?
  2. 右操作数是否比左边长?
  3. 左操作数是否比右操作数长或长,结果有一个最高位数的进位(即需要前导1)?
  4. 处理左操作数长于右操作数

    那个是非常简单的,当我们完全捕获它时,停止尝试向\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,因为只会像左操作数中的数字那样经常迭代。因此,我们需要单独匹配。

    现在有几个案例要考虑:

    1. 添加左操作数的最高位数
    2. 有一个进位
    3. 没有随身携带。
    4. 对于前一种情况,我们希望匹配recursedigit10 + 110 = 1000;对于后一种情况,我们希望匹配11 + 101 = 10001 + 10 = 11

      为了简化我们的任务,我们暂时忽略前导零。然后我们知道最重要的数字是1.现在只有没有进位且仅在以下情况下:

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

      这转化为以下断言:

      1 + 1000 = 1001

      在这种情况下,我们可以使用\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 捕获组的工作原理是复制右操作数的左边部分,找到那里的最后一个零,并将所有不重于零的部分替换为0,将零替换为零。如果该部分中没有零,则这些部分全部被零替换,并且添加前导部分。

      或者用较少的字眼表达它(n是任意的,所以它适合):

      carryoverflow

      所以,让我们运用我们常用的技巧找出操作数右侧的部分:

        (?<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>捕获组管理的。

      领先1

      的情况

      我们确定不希望carrycheckdigit匹配。如果我们单独去1 + 1 = 0那就是它。因此,在前导1需要的情况下有不同的情况(如果前一个右操作数的情况没有覆盖,则更长):

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

      前一个案例处理checkdigit之类的输入,第二个案件处理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) ) 的情况下检查是否存在进位(即右操作数更短)。通常可以检查进位(因为最高有效数字始终为1,右操作数数字隐含为0),并且具有平凡ro - 如果有结果,则结果中当前偏移处的数字将为是0。

      这很容易实现,毕竟直到当前偏移的所有其他数字都将由(?:1(?=0))?\k<f>\b验证。因此,我们可以在这里检查当地的货物。

      因此我们可以将其添加到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*(占有欲以避免回溯并发生意外的事情),以便跳过我们假设的前导零点有些地方最左边的数字是1。

      结论

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

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

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

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