Skip to content

Commit 20103a3

Browse files
hanslalexeagle
authored andcommitted
fix(@angular-devkit/build-angular): fix countOccurences
And add testing for it.
1 parent 600b021 commit 20103a3

2 files changed

Lines changed: 107 additions & 3 deletions

File tree

packages/angular_devkit/build_angular/plugins/webpack/analytics.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,23 @@ export function countOccurrences(source: string, match: string, wordBreak = fals
4242
// We condition here so branch prediction happens out of the loop, not in it.
4343
if (wordBreak) {
4444
const re = /\w/;
45-
for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos - 1)) {
46-
if (!(re.test(source[pos - 1]) || re.test(source[pos + match.length]))) {
45+
for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) {
46+
if (!(re.test(source[pos - 1] || '') || re.test(source[pos + match.length] || ''))) {
4747
count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH!
4848
}
49+
50+
pos -= match.length;
51+
if (pos < 0) {
52+
break;
53+
}
4954
}
5055
} else {
51-
for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos - 1)) {
56+
for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) {
5257
count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH!
58+
pos -= match.length;
59+
if (pos < 0) {
60+
break;
61+
}
5362
}
5463
}
5564

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { countOccurrences } from './analytics';
9+
10+
11+
function _randomString(len: number) {
12+
const charSpace = `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
13+
14+
let s = '';
15+
for (let i = 0; i < len; i++) {
16+
s += charSpace[Math.floor(Math.random() * charSpace.length)];
17+
}
18+
19+
return s;
20+
}
21+
22+
describe('countOccurrences', () => {
23+
// Every use cases is a text, search, word break or not, and expected result.
24+
const useCases: [string, string, boolean, number][] = [
25+
['abc1def1ghi1jkl1mno1pqrs1tuvw1xyz', '1', false, 7], // 0
26+
['abc1def12ghi1jkl1mno12pqrs12tuvw1xyz12', '12', false, 4], // 1
27+
['abc', 'abc', false, 1], // 2
28+
['abc', 'abc', true, 1], // 3
29+
['aaaaa', 'aaa', false, 1], // 4
30+
['aa aaa', 'aaa', true, 1], // 5
31+
['aaaaaa', 'aaa', false, 2], // 6
32+
['aaa aaa', 'aaa', true, 2], // 7
33+
['a', 'a', false, 1], // 8
34+
['a', 'a', true, 1], // 9
35+
];
36+
37+
useCases.forEach(([text, search, wordBreak, expected], i) => {
38+
it(`works (${i})`, () => {
39+
expect(countOccurrences(text, search, wordBreak)).toBe(expected);
40+
});
41+
});
42+
43+
// Random testing.
44+
it('can count (random, wordBreak=false)', () => {
45+
// Generate a random string with injected search strings in it.
46+
let text = _randomString(10000);
47+
const search = _randomString(100).toLowerCase();
48+
const nb = Math.floor(Math.random() * 200 + 100);
49+
50+
// Insert nb search string in.
51+
new Array(nb).fill(0)
52+
// Map it with a random position.
53+
.map(() => Math.floor(Math.random() * text.length))
54+
// Sort from highest to lowest.
55+
.sort((a, b) => b - a)
56+
// Insert the search string for each position created this way.
57+
.forEach(pos => {
58+
text = text.slice(0, pos) + search + text.slice(pos);
59+
});
60+
61+
expect(countOccurrences(text, search, false)).toBe(nb);
62+
expect(countOccurrences(text, search, true)).toBe(0);
63+
});
64+
65+
it('can count (random, wordBreak=true)', () => {
66+
// Generate a random string with injected search strings in it.
67+
let text = _randomString(10000);
68+
const search = _randomString(100).toLowerCase();
69+
let nb = Math.floor(Math.random() * 200 + 100);
70+
71+
// Insert nb search string in.
72+
new Array(nb).fill(0)
73+
// Map it with a random position.
74+
.map(() => Math.floor(Math.random() * text.length))
75+
// Sort from highest to lowest.
76+
.sort((a, b) => b - a)
77+
// Insert the search string for each position created this way.
78+
.forEach(pos => {
79+
switch (Math.floor(Math.random() * 5)) {
80+
case 0:
81+
// Do not insert a wordbreak.
82+
text = text.slice(0, pos) + search + text.slice(pos);
83+
nb--;
84+
break;
85+
86+
case 1: text = text.slice(0, pos) + ' ' + search + ' ' + text.slice(pos); break;
87+
case 2: text = text.slice(0, pos) + '(' + search + '$' + text.slice(pos); break;
88+
case 3: text = text.slice(0, pos) + '|' + search + ')' + text.slice(pos); break;
89+
case 4: text = text.slice(0, pos) + '-' + search + '.' + text.slice(pos); break;
90+
}
91+
});
92+
93+
expect(countOccurrences(text, search, true)).toBe(nb);
94+
});
95+
});

0 commit comments

Comments
 (0)