Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ const count = stringzy.analyze.wordCount('Hello world'); // 2
- [complexity](#complexity) - Analyzes string complexity including score, uniqueness, and length
- [patternCount](#patterncount) - calculates the number of times a specific pattern occurs in a given text
- [vowelConsonantCount](#vowelconsonantcount) - Counts the number of vowels and consonants in a given string
- [checkMultiplePatterns](#checkmultiplepatterns) - Finds occurrences of multiple patterns within a given text using Rabin–Karp algorithm (case sensitive)

### Formatting

Expand Down Expand Up @@ -907,6 +908,29 @@ vowelConsonantCount('');
| --------- | ------ | -------- | -------------------------------------------------- |
| str | string | required | The input string to count vowels and consonants in |

#### <a id="checkmultiplepatterns"></a>checkMultiplePatterns(text, patterns)

Finds occurrences of multiple patterns within a given text using the Rabin–Karp algorithm. <br>
Accepts an array of patterns.<br>
Returns all matches of each pattern along with starting indices.<br>
Handles hash collisions by verifying actual substrings.<br>
Pattern matching is case sensitive.

```javascript
checkMultiplePatterns('abracadabra', ['abra', 'cad']);
// { abra: [0, 7], cad: [4] }

checkMultiplePatterns('aaaa', ['aa', 'aaa']);
// { aa: [0, 1, 2], aaa: [0, 1] }

checkMultiplePatterns('hello world', ['xyz', '123']);
// { xyz: [], 123: [] }
```
| Parameter | Type | Default | Description |
| --------- | --------- | -------- | ----------------------------------------------------------- |
| text | string | required | The text to search within. |
| patterns | string\[ ] | required | An array of patterns to search for (each must be a string). |

---

### 🎨 Formatting
Expand Down
81 changes: 81 additions & 0 deletions src/analyzing/checkMultiplePatterns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Finds occurrences of multiple patterns within a given text using Rabin–Karp algorithm.
*
* - Accepts an array of patterns.
* - Returns all matches of each pattern along with starting indices.
* - Handles hash collisions by verifying actual substrings.
* - Is case sensitive
*
* @param {string} text - The text to search within.
* @param {string[]} patterns - The array of patterns to search for.
* @returns {Record<string, number[]>} An object mapping each pattern to an array of match indices.
* @throws {TypeError} If input types are invalid.
*/

export function checkMultiplePatterns(
text: string,
patterns: string[]
): Record<string, number[]> {
if (typeof text !== 'string') {
throw new TypeError('Text must be a string');
}
if (!Array.isArray(patterns) || !patterns.every(p => typeof p === 'string')) {
throw new TypeError('Patterns must be an array of strings');
}

const result: Record<string, number[]> = {};
if (text.length === 0 || patterns.length === 0) {
return result;
}

const prime = 101; // A prime base for hashing

const getHash = (str: string, m: number): number => {
let h = 0;
for (let i = 0; i < m; i++) {
h = (h * 256 + str.charCodeAt(i)) % prime;
}
return h;
};

const recomputeHash = (
oldHash: number,
dropped: string,
added: string,
m: number
): number => {
let h = (oldHash - dropped.charCodeAt(0) * Math.pow(256, m - 1)) % prime;
h = (h * 256 + added.charCodeAt(0)) % prime;
if (h < 0) h += prime;
return h;
};

for (const pattern of patterns) {
const m = pattern.length;
result[pattern] = [];
if (m === 0 || m > text.length) continue;

const patternHash = getHash(pattern, m);
let windowHash = getHash(text, m);

for (let i = 0; i <= text.length - m; i++) {
if (patternHash === windowHash) {
// Verify to avoid collision false positives
if (text.slice(i, i + m) === pattern) {
result[pattern].push(i);
}
}
if (i < text.length - m) {
windowHash = recomputeHash(
windowHash,
text[i],
text[i + m],
m
);
}
}
}

return result;
}

5 changes: 4 additions & 1 deletion src/analyzing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { wordCount } from './wordCount';
export { stringSimilarity } from './stringSimilarity';
export { patternCount } from './patternCount';
export { vowelConsonantCount } from './vowelConsonantCount';
export { checkMultiplePatterns } from './checkMultiplePatterns';

import { characterCount } from './characterCount';
import { characterFrequency } from './characterFrequency';
Expand All @@ -15,6 +16,7 @@ import { wordCount } from './wordCount';
import { stringSimilarity } from './stringSimilarity';
import { patternCount } from './patternCount';
import { vowelConsonantCount } from './vowelConsonantCount';
import { checkMultiplePatterns } from './checkMultiplePatterns';

export const analyzing = {
characterCount,
Expand All @@ -24,5 +26,6 @@ export const analyzing = {
wordCount,
stringSimilarity,
patternCount,
vowelConsonantCount
vowelConsonantCount,
checkMultiplePatterns
};
101 changes: 101 additions & 0 deletions src/tests/analyzing/checkMultiplePatterns.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { checkMultiplePatterns } from '../../analyzing/checkMultiplePatterns';

describe('checkMultiplePatterns', () => {
it('finds multiple valid matches', () => {
const text = 'abracadabra';
const patterns = ['abra', 'cad'];
const result = checkMultiplePatterns(text, patterns);

// "abra" occurs at index 0: "abra...cadabra"
// and again at index 7: "abracad...abra"
assert.deepStrictEqual(result['abra'], [0, 7]);

// "cad" occurs once starting at index 4: "abraCADabra"
assert.deepStrictEqual(result['cad'], [4]);
});

it('handles overlapping patterns', () => {
const text = 'aaaa';
const patterns = ['aa', 'aaa'];
const result = checkMultiplePatterns(text, patterns);

// "aa" occurs at indices 0 ("aa.."), 1 (".aa."), 2 ("..aa")
assert.deepStrictEqual(result['aa'], [0, 1, 2]);

// "aaa" occurs at indices 0 ("aaa.") and 1 (".aaa")
assert.deepStrictEqual(result['aaa'], [0, 1]);
});

it('returns empty arrays when no matches found', () => {
const text = 'hello world';
const patterns = ['xyz', '123'];
const result = checkMultiplePatterns(text, patterns);

// Neither "xyz" nor "123" exist in "hello world"
assert.deepStrictEqual(result['xyz'], []);
assert.deepStrictEqual(result['123'], []);
});

it('returns empty object when text is empty', () => {
// No text to search → nothing to return
const result = checkMultiplePatterns('', ['a', 'b']);
assert.deepStrictEqual(result, {});
});

it('returns empty object when patterns array is empty', () => {
// No patterns given → nothing to search for
const result = checkMultiplePatterns('hello', []);
assert.deepStrictEqual(result, {});
});

it('skips patterns longer than the text', () => {
const result = checkMultiplePatterns('hi', ['longpattern']);
// pattern is longer than text → no match possible
assert.deepStrictEqual(result['longpattern'], []);
});

it('is case-sensitive by default', () => {
const text = 'Hello hello';
const patterns = ['Hello', 'hello'];
const result = checkMultiplePatterns(text, patterns);

assert.deepStrictEqual(result['Hello'], [0]);
assert.deepStrictEqual(result['hello'], [6]);
});

it('handles spaces and special characters as part of patterns', () => {
const text = 'hi there!';
const patterns = [' ', '!'];
const result = checkMultiplePatterns(text, patterns);

// space occurs at index 2, "!" occurs at the end
assert.deepStrictEqual(result[' '], [2]);
assert.deepStrictEqual(result['!'], [8]);
});

it('does not match mixed case unless exact', () => {
const text = 'RabinKarp';
const patterns = ['rabinkarp'];
const result = checkMultiplePatterns(text, patterns);

// "rabinkarp" does not match "RabinKarp" because of case
assert.deepStrictEqual(result['rabinkarp'], []);
});

it('throws if text is not a string', () => {
assert.throws(() => checkMultiplePatterns(123 as any, ['a']), /Text must be a string/);
});

it('throws if patterns is not an array of strings', () => {
assert.throws(
() => checkMultiplePatterns('abc', 'a' as any),
/Patterns must be an array of strings/
);
assert.throws(
() => checkMultiplePatterns('abc', [123] as any),
/Patterns must be an array of strings/
);
});
});