diff --git a/README.md b/README.md index 3e90e45..7f1a59d 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ const count = stringzy.analyze.wordCount('Hello world'); // 2 - [characterFrequency](#characterfrequency) - Analyzes character frequency in a string - [stringSimilarity](#stringsimilarity) - Calculates the percentage similarity between two strings - [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 + ### Formatting @@ -737,7 +739,7 @@ stringSimilarity('flaw', 'lawn', 'Damerau-Levenshtein'); // Returns: 50 | textB | string | required | The second text to compare. | | algorithm | string | 'Levenshtein' | The algorithm to use: 'Levenshtein' or 'Damerau-Levenshtein'. | ---- + #### `complexity(text)` @@ -766,6 +768,25 @@ complexity(''); - `uniqueness` (number): Measure of character uniqueness - `length` (number): Length of the input string + + +#### `patternCount(text, pattern)` + +Counts the number of times a substring (pattern) occurs in a string, including overlapping occurrences. +This function uses the **Knuth–Morris–Pratt (KMP)** algorithm for efficient matching. + +```javascript +patternCount('aaaa', 'aa'); // 3 +patternCount('abababa', 'aba'); // 3 +patternCount('hello world', 'o'); // 2 +patternCount('hello world', 'x'); // 0 +``` + +| Parameter | Type | Default | Description | +| --------- | ------ | -------- | ---------------------------------------------- | +| text | string | required | The input string to search in | +| pattern | string | required | The substring (pattern) to count (overlapping) | + --- ### 🎨 Formatting diff --git a/src/analyzing/index.ts b/src/analyzing/index.ts index 260a99e..2d9362f 100644 --- a/src/analyzing/index.ts +++ b/src/analyzing/index.ts @@ -4,6 +4,7 @@ export { complexity } from './complexity'; export { readingDuration } from './readingDuration'; export { wordCount } from './wordCount'; export { stringSimilarity } from './stringSimilarity'; +export { patternCount } from './patternCount'; import { characterCount } from './characterCount'; import { characterFrequency } from './characterFrequency'; @@ -11,6 +12,7 @@ import { complexity } from './complexity'; import { readingDuration } from './readingDuration'; import { wordCount } from './wordCount'; import { stringSimilarity } from './stringSimilarity'; +import { patternCount } from './patternCount'; export const analyzing = { characterCount, @@ -19,4 +21,5 @@ export const analyzing = { readingDuration, wordCount, stringSimilarity, + patternCount }; diff --git a/src/analyzing/patternCount.ts b/src/analyzing/patternCount.ts new file mode 100644 index 0000000..0ff927c --- /dev/null +++ b/src/analyzing/patternCount.ts @@ -0,0 +1,54 @@ +/** + * Calculates the number of times a specific pattern occurs in a given text, including overlapping occurrences + * + * The algorithm used here is based on the Knuth-Morris-Pratt (KMP) pattern matching algorithm for better performance + * + * @param {string} text - The text for which we want to count the occurrences of a specific pattern. + * @param {string} pattern - The pattern to search for within the text. + * @returns {number} - The number of times the pattern occurs in the text (overlapping). + */ +export function patternCount(text: string, pattern: string): number { + if (pattern.length === 0) { + return 0; // No pattern to search for + } + + const prefixFunction = computePrefixFunction(pattern); + + let count = 0; + let j = 0; // Index for pattern + + for (let i = 0; i < text.length; i++) { + while (j > 0 && text[i] !== pattern[j]) { + j = prefixFunction[j - 1]; + } + if (text[i] === pattern[j]) { + j++; + } + if (j === pattern.length) { + count++; + j = prefixFunction[j - 1]; // Allow for overlapping matches + } + } + + return count; +} + +/** + * Computes the prefix function (partial match table) for KMP algorithm. + * @param {string} pattern - The pattern string. + * @returns {number[]} - The prefix function array. + */ +function computePrefixFunction(pattern: string): number[] { + const prefixFunction = new Array(pattern.length).fill(0); + let j = 0; + for (let i = 1; i < pattern.length; i++) { + while (j > 0 && pattern[i] !== pattern[j]) { + j = prefixFunction[j - 1]; + } + if (pattern[i] === pattern[j]) { + j++; + } + prefixFunction[i] = j; + } + return prefixFunction; +} \ No newline at end of file diff --git a/src/tests/analyzing/patternCount.test.ts b/src/tests/analyzing/patternCount.test.ts new file mode 100644 index 0000000..da755bf --- /dev/null +++ b/src/tests/analyzing/patternCount.test.ts @@ -0,0 +1,30 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { patternCount } from '../../analyzing/patternCount'; + +describe('patternCount', () => { + it('returns 0 for empty string', () => { + assert.deepStrictEqual(patternCount('', 'aa'), 0); + }); + it('returns 0 for empty pattern', () => { + assert.deepStrictEqual(patternCount('abc', ''), 0); + }); + it('returns 0 for empty string and empty pattern', () => { + assert.deepStrictEqual(patternCount('', ''), 0); + }); + it('returns correct count for single character pattern', () => { + assert.strictEqual(patternCount('abcabcabc', 'a'), 3); + }); + it('returns correct count for multi-character pattern', () => { + assert.strictEqual(patternCount('abcabcabc', 'ab'), 3); + }); + it('returns correct count for overlapping patterns', () => { + assert.strictEqual(patternCount('ababababa', 'aba'), 4); + }); + it('returns correct count for non-overlapping patterns', () => { + assert.strictEqual(patternCount('abababab', 'ab'), 4); + }); + it('returns 0 for pattern not found', () => { + assert.strictEqual(patternCount('abcdefg', 'xyz'), 0); + }); +});