测试最少行数或令牌的最快方法

时间:2016-09-18 04:36:15

标签: javascript regex string count lines

我有两次现在发现自己想要知道Javascript字符串是否具有最小行数但不想浪费地拆分字符串以查找。两次都用正则表达式进行了过多的实验,以便意识到解决方案很简单。这个自我回答的帖子是为了阻止我(希望其他人)不得不重新解决这个问题。

更一般地说,我想有效地确定任何给定的字符串是否至少具有指定数量的标记。我不需要知道字符串有多少令牌。令牌可以是字符,子字符串,匹配正则表达式的子字符串,或分隔单位(如单词或行)。

Another SO question探讨了分割字符串或进行全局正则表达式匹配是否更快,以便计算字符串中的行数。据报道,拆分速度更快,至少记忆力充足。我们这里的问题是,如果我们只需要知道令牌的数量是等于还是超过最小值,那么在一般情况下我们能否比正则表达式更快地进行字符串拆分测试?

以下是我尝试匹配最少行数的一些错误 - 在这种情况下至少有42行:

/(^[\n]*){42}/m.test(stringToTest)
/(\n[^\n]*|[^\n]*){42}/.test(stringToTest)
/(\n[^\n]*|[^\n]*(?!\n)){42}/.test(stringToTest)

这些表达显然很乐意与42次匹配。它们对stringToTest = ''返回true。

1 个答案:

答案 0 :(得分:0)

解决方案是测试一系列令牌/非令牌单元,而不是尝试测试正确的分隔单元数或正确的令牌数。如果令牌是分隔符并且您想要最小数量的分隔单元,则需要令牌/非令牌单元的计数等于小于所需单元数的一个。正如我们所看到的,这种解决方案具有令人惊讶的性能。

最小行数

此函数检查最少行数,其中\n分隔行而不是严格结束行,允许空的最后一行:

function hasMinLineCount(text, minLineCount) {
    if (minLineCount <= 1)
        return true; // always 1+ lines, though perhaps empty
    var r = new RegExp('([^\n]*\n){' + (minLineCount-1) + '}');
    return r.test(text);
}

或者,\n可以假定为结束行,而不是纯粹分隔它们,使非空的最后一行成为例外。例如,"apple\npear\n"将是两行,而"apple\npear\ngrape"将是三行。以下函数以这种方式计算行:

function hasMinLineCount(text, minLineCount) {
    var r = new RegExp('([^\n]*\n|[^\n]+$){' + minLineCount + '}');
    return r.test(text);
}

字符串分隔符和标记

更一般地说,对于由字符串分隔符分隔的任何单位:

var _ = require('lodash');

function hasMinUnitCount(text, minUnitCount, unitDelim) {
    if (minUnitCount <= 1)
        return true; // always 1+ units, though perhaps empty
    var escDelim = _.escapeRegExp(unitDelim);
    var r = new RegExp('(.*?'+ escDelim +'){' + (minUnitCount-1) + '}');
    return r.test(text);
}

我们还可以测试是否存在最少数量的字符串标记:

var _ = require('lodash');

function hasMinTokenCount(text, minTokenCount, token) {
    var escToken = _.escapeRegExp(token);
    var r = new RegExp('(.*?'+ escToken +'){' + minTokenCount + '}');
    return r.test(text);
}

正则表达式分隔符和标记

我们可以通过允许单位分隔符和标记包含正则表达式字符来进一步概括。只需确保分隔符或标记可以明确地背靠背地发生。示例正则表达式分隔符包括"<br */>""[|,]"。这些是字符串,而不是RegExp个对象。

function hasMinUnitCount(text, minUnitCount, unitDelimRegexStr) {
    if (minUnitCount <= 1)
        return true; // always 1+ units, though perhaps empty
    var r = new RegExp(
              '(.*?'+ unitDelimRegexStr +'){' + (minUnitCount-1) + '}');
    return r.test(text);
}

function hasMinTokenCount(text, minTokenCount, tokenRegexStr) {
    var r = new RegExp('(.*?'+ tokenRegexStr +'){' + minTokenCount + '}');
    return r.test(text);
}

计算成本

泛型函数起作用,因为它们的正则表达式对字符进行非贪婪匹配(注意.*?)直到下一个分隔符或标记。这是一个计算成本昂贵的展望和回溯过程,因此相对于更加硬编码的表达式(例如上面hasMinLineCount()中找到的表达式而言,性能会受到影响。

让我们重温一下最初的问题,即我们是否可以通过正则表达式测试来胜过分裂字符串。回想一下,我们唯一的目标是测试最少的行数。我使用benchmark.js来测试,我假设我们知道需要多行。这是代码:

var Benchmark = require('benchmark');
var suite = new Benchmark.Suite;

var line = "Go faster faster faster!\n";
var text = line.repeat(100);
var MIN_LINE_COUNT = 50;
var preBuiltBackingRegex = new RegExp('(.*?\n){'+ MIN_LINE_COUNT +'}');
var preBuiltNoBackRegex = new RegExp('([^\n]*\n){'+ MIN_LINE_COUNT +'}');

suite.add('split string', function() {
    if (text.split("\n").length >= MIN_LINE_COUNT)
        'has minimum lines';
})
.add('backtracking on-the-fly regex', function() {
    if (new RegExp('(.*?\n){'+ MIN_LINE_COUNT +'}').test(text))
        'has minimum lines';
})
.add('backtracking pre-built regex', function() {
    if (preBuiltBackingRegex.test(text))
        'has minimum lines';
})
.add('no-backtrack on-the-fly regex', function() {
    if (new RegExp('([^\n]*\n){'+ MIN_LINE_COUNT +'}').test(text))
        'has minimum lines';
})
.add('no-backtrack pre-built regex', function() {
    if (preBuiltNoBackRegex.test(text))
        'has minimum lines';
})
.on('cycle', function(event) {
    console.log(String(event.target));
})
.on('complete', function() {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run({ 'async': true });

以下是三次运行的结果:

split string x 263,260 ops/sec ±0.68% (85 runs sampled)
backtracking on-the-fly regex x 492,671 ops/sec ±1.01% (82 runs sampled)
backtracking pre-built regex x 607,033 ops/sec ±0.72% (87 runs sampled)
no-backtrack on-the-fly regex x 581,681 ops/sec ±0.77% (84 runs sampled)
no-backtrack pre-built regex x 723,075 ops/sec ±0.72% (89 runs sampled)
Fastest is no-backtrack pre-built regex

split string x 260,962 ops/sec ±0.82% (85 runs sampled)
backtracking on-the-fly regex x 502,410 ops/sec ±0.79% (84 runs sampled)
backtracking pre-built regex x 606,220 ops/sec ±0.67% (88 runs sampled)
no-backtrack on-the-fly regex x 578,193 ops/sec ±0.83% (86 runs sampled)
no-backtrack pre-built regex x 741,864 ops/sec ±0.68% (84 runs sampled)
Fastest is no-backtrack pre-built regex

split string x 262,266 ops/sec ±0.76% (87 runs sampled)
backtracking on-the-fly regex x 495,697 ops/sec ±0.82% (87 runs sampled)
backtracking pre-built regex x 608,178 ops/sec ±0.72% (88 runs sampled)
no-backtrack on-the-fly regex x 574,640 ops/sec ±0.92% (87 runs sampled)
no-backtrack pre-built regex x 739,629 ops/sec ±0.72% (86 runs sampled)
Fastest is no-backtrack pre-built regex

所有正则表达式测试显然比分割字符串以检查行数更快,甚至是回溯测试。我想我会进行正则表达式测试。