Regex字符计数,但有些计数为3

Regex character count, but some count for three

本文关键字:字符 Regex      更新时间:2023-09-26

我试图构建一个限制输入长度的正则表达式,但并非所有字符在此长度中都相等。我将把基本原理放在问题的后面。作为一个简单的例子,让我们将最大长度限制为12,并且只允许ab,但b计数为3个字符。

可以是:

  • aa(小于12是可以的)
  • aaaaaaaaaaaa(正好12个就可以了)。
  • aaabaaab(6 + 2 * 3 = 12,这是好的)。
  • abaaaaab (still 6 + 2 * 3 = 12).

不允许:

  • aaaaaaaaaaaaa (13 a).
  • bbbba(1 + 4 * 3 = 13,这太大了)。
  • baaaaaaab (7 + 2 * 3 = 13).

我已经做了一个相当接近的尝试:

^(a{0,3}|b){0,4}$

最多匹配4个集群,可以由0-3个a或一个b组成。

然而,它不能匹配我的最后一个正示例:abaaaaab,因为这迫使第一个集群在开始时是单个a,为b消耗第二个集群,然后只剩下2个集群用于其余的aaaaab,这太长了。

<标题> 约束
  • 必须在JavaScript中运行。这个正则表达式提供给Qt,它显然使用JavaScript的语法。
  • 真的不需要这么快。最后,它只适用于最多40个字符的字符串。我希望在50毫秒左右的时间内验证,但稍微慢一点也是可以接受的。
<标题> 的基本原理

为什么我需要用正则表达式做这个?

这是一个用户界面在Qt通过PyQt和QML。用户可以在这里的文本字段中为概要文件输入名称。该概要名称是url编码的(特殊字符由%XX替换),然后保存在用户的文件系统中。当用户输入大量特殊字符(如中文)时,我们会遇到问题,这些字符随后会编码为非常长的文件名。结果是,在诸如17个字符的地方,这个文件名对于某些文件系统来说太长了。url编码编码为UTF-8,每个字符最多有4个字节,因此文件名中最多有12个字符(因为每个字符都得到百分比编码)。

16个字符对于配置文件名称来说太短了。甚至我们的一些默认名称也超过了这个值。我们需要一个基于这些特殊字符的变量限制。

Qt通常允许您指定一个Validator来确定文本框中哪些值是可接受的。我们尝试实现这样的验证器,但由于PyQt中的一个错误,导致了上游的段错误。目前,它似乎无法处理自定义Validator实现。然而,PyQt也公开了三个内置验证器。2只适用于数字。第三种是一个regex验证器,它允许您输入匹配所有有效字符串的正则表达式。因此需要这个正则表达式。

由于regexp的限制,没有真正直接的方法可以做到这一点。您必须测试所有组合,例如13个b和最多1个a, 12个b和最多4个a,等等。我们将构建一个小程序来为我们生成这些。测试最多四个a的基本格式为

/^(?=([^a]*a){0,4}[^a]*$)/

我们将编写一个小例程来为我们创建这些查找,给定一个字母和最小和最大出现次数:

function matchLetter(c, m, n) {
  return `(?=([^${c}]*${c}){${m},${n}}[^${c}]*$)`;
}
> matchLetter('a', 0, 4)
< "(?=([^a]*a){0,4}[^a]*$)"

我们可以将这些组合起来测试三个b和三个a:

/^(?=([^b]*b){3}[^b]*$)(?=([^a]*a){0,3}[^a]*$)/

我们将编写一个函数来创建这样的组合查找头,它精确匹配c1m次出现和c2n次出现:

function matchTwoLetters(c1, m, c2, n) {
  return matchLetter(c1, m, m) + matchLetter(c2, 0, n);
}

我们可以用它来匹配12个b和最多4个a,总共40个或更少:

> matchTwoLetters('b', 12, 'a', 1, 4)
< "(?=([^b]*b){12,12}[^b]*$)(?=([^a]*a){0,4}[^a]*$)"

只需为b的每个计数创建此版本,并将它们放在一起(对于最大计数为12的情况):

function makeRegExp() {
  const res = [];
  for (let bs = 0; bs <= 4; bs++)
    res.push(matchTwoLetters('b', bs, 'a', 12 - bs*3));
  return new RegExp(`^(${res.join('|')})`);
}
> makeRegExp()
< "^((?=([^b]*b){0,0}[^b]*$)(?=([^a]*a){0,12}[^a]*$)|(?=([^b]*b){1,1}[^b]*$)(?=([^a]*a){0,9}[^a]*$)|(?=([^b]*b){2,2}[^b]*$)(?=([^a]*a){0,6}[^a]*$)|(?=([^b]*b){3,3}[^b]*$)(?=([^a]*a){0,3}[^a]*$)|(?=([^b]*b){4,4}[^b]*$)(?=([^a]*a){0,0}[^a]*$))"

现在可以用

做测试了
makeRegExp().test("baabaaa");

对于length=40的情况,regxp长度为679个字符。一个非常粗略的基准测试显示,它的执行时间不到1微秒。

如果您想在多字节编码时计算字节数,您可以使用以下函数:

function bytesLength(str) {
  var s = str.length;
  for (var i = s-1; i > -1; i--) {
    var code = str.charCodeAt(i);
    if (code > 0x7f && code <= 0x7ff) {s++;}
    else if (code > 0x7ff && code <= 0xffff) {s+=2;}
    if (code >= 0xDC00 && code <= 0xDFFF) {i--;}
  }
  return s;
}
console.log(bytesLength('敗')); // length 3

试试这样写:

^((a{1,3}|b){1,4}|(a{1,4}|a?b|ba){1,3}|((a{2,3}|b){2}|aaba|abaa){2})$

示例:https://regex101.com/r/yTTiEX/6

这将其分解为逻辑可能性:

4个部分,每个部分的值不超过3。
3个部分,每个部分的值最高可达4。
2部分,每个部分的值不超过6。