diff --git a/README.md b/README.md index 36e69c6..8979d43 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,15 @@ const isValid = stringzy.validate.isEmail('user@example.com'); // true const count = stringzy.analyze.wordCount('Hello world'); // 2 ``` +## ✨ What’s new (perf update) + +This release contains a performance-focused update to the permutations utilities: + +- `stringPermutations(input: string)` — rewritten to use an optimized iterative approach to reduce recursion overhead and peak memory usage for longer strings. +- `stringPermutationsGenerator(input: string)` — a new generator-based API that yields permutations lazily so you can iterate large permutation sets without allocating the full result array in memory. + +These changes improve throughput and reduce memory pressure when working with larger inputs. Note: complexity remains O(n!) — this update optimizes allocation and recursion overhead. If your code relied on a precise ordering from a previous implementation, run your test-suite as ordering may differ in edge cases. + ## 📋 Table of Contents ### Transformations @@ -80,8 +89,9 @@ const count = stringzy.analyze.wordCount('Hello world'); // 2 - [splitChunks](#splitchunks) - Breaks a string down into chunks of specified length. - [numberToText](#numbertotext) - Converts a number to its text representation in specified language - [reverseWordsInString](#reversewordsinstring) - Reverses the order of words in a given string -- [stringPermutations](#stringpermutations) - Generates all unique permutations of a given string. -- [stringCombinations](#stringcombinations) - Generates all unique combinations of a given string. +- [stringPermutations](#stringpermutations) - Generates all unique permutations of a given string. +- [stringPermutationsGenerator](#stringpermutationsgenerator) - Generator-based permutations API for lazy iteration. +- [stringCombinations](#stringcombinations) - Generates all unique combinations of a given string. ### Validations @@ -519,13 +529,16 @@ reverseWordsInString('single-word'); | --------- | ------ | -------- | --------------------------- | | str | string | required | The input string to reverse | -#### `stringPermutations(str)` +#### `stringPermutations(input: string): string[]` + +Generates all unique permutations of the given string. Repeated characters are handled by ensuring only unique permutations are included in the returned array. -Generates all unique permutations of a given string. -Repeated characters are handled by ensuring only unique permutations are included in the output array. -The order of permutations is not guaranteed. +- Uses an optimized iterative algorithm under the hood to reduce recursion depth and intermediate allocations (lower peak memory usage and faster runtime for many practical inputs). +- Note: complexity remains O(n!) — this is an optimization of allocation/recursion overhead, not the factorial growth. ```javascript +import { stringPermutations } from 'stringzy'; + stringPermutations('ab'); // ['ab', 'ba'] @@ -534,20 +547,34 @@ stringPermutations('abc'); stringPermutations('aab'); // ['aab', 'aba', 'baa'] +``` + +| Parameter | Type | Default | Description | +| --------- | ------ | -------- | ----------------------------------------------------- | +| input | string | required | The input string to generate all unique permutations. | -stringPermutations(''); -// [''] +#### `stringPermutationsGenerator(input: string): Generator` -stringPermutations('a'); -// ['a'] +Generator-based API that yields permutations one-by-one. Use this when you only need to process permutations sequentially or when the full result set would not fit in memory. -stringPermutations('a1!'); -// ['a1!', 'a!1', '1a!', '1!a', '!a1', '!1a'] +- Lazily produces permutations; does not allocate the entire permutation set in memory. +- Particularly useful for memory-sensitive workflows or streaming processing. + +```javascript +import { stringPermutationsGenerator } from 'stringzy'; + +for (const p of stringPermutationsGenerator('abcd')) { + console.log(p); + // process each permutation without building a giant array in memory +} + +// If you must collect them all: +const perms = Array.from(stringPermutationsGenerator('abcd')); ``` -| Parameter | Type | Default | Description | -| --------- | ------ | -------- | ----------------------------------------------------- | -| str | string | required | The input string to generate all unique permutations. | +| Parameter | Type | Default | Description | +| --------- | ------ | -------- | ------------------------------------------------------ | +| input | string | required | The input string to generate permutations from. | #### `stringCombinations(str)` @@ -869,7 +896,7 @@ isMacAddress("aa:bb:cc:dd:ee:ff"); // true isMacAddress("FF-FF-FF-FF-FF-FF"); // true isMacAddress("00:1G:2B:3C:4D:5E"); // false (invalid hex digit) -isMacAddress("00:1A-2B:3C-4D:5E"); // false (mixed separators) +isMacAddress("00:1A-2B:3C:4D:5E"); // false (mixed separators) isMacAddress("001A:2B:3C:4D:5E"); // false (wrong group length) isMacAddress("hello-world-mac"); // false (invalid format) isMacAddress(""); // false (empty string) diff --git a/package-lock.json b/package-lock.json index ff51338..6066faf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,4 +70,4 @@ "license": "MIT" } } -} +} \ No newline at end of file diff --git a/src/tests/transformations/stringPermutations.test.ts b/src/tests/transformations/stringPermutations.test.ts index 39a8d88..1988681 100644 --- a/src/tests/transformations/stringPermutations.test.ts +++ b/src/tests/transformations/stringPermutations.test.ts @@ -1,6 +1,6 @@ import { describe, it } from 'node:test'; import assert from 'node:assert'; -import { stringPermutations } from '../../transformations/stringPermutations'; +import { stringPermutations, stringPermutationsGenerator } from '../../transformations/stringPermutations'; describe('stringPermutations', () => { it('returns correct permutations for small strings', () => { @@ -55,4 +55,90 @@ describe('stringPermutations', () => { assert.throws(() => stringPermutations(null as any), /Input must be a string/); assert.throws(() => stringPermutations(undefined as any), /Input must be a string/); }); + + it('respects limit parameter', () => { + const result = stringPermutations('abcdef', 3); + assert.strictEqual(result.length, 3); + + // All results should be valid permutations + for (const perm of result) { + assert.strictEqual(perm.length, 6); + assert.strictEqual([...perm].sort().join(''), 'abcdef'); + } + }); + + it('throws error for negative limit', () => { + assert.throws(() => stringPermutations('abc', -1), /Limit must be non-negative/); + }); + + it('handles limit of 0', () => { + const result = stringPermutations('abc', 0); + assert.strictEqual(result.length, 0); + }); + + it('handles limit larger than total permutations', () => { + const result = stringPermutations('abc', 100); + assert.strictEqual(result.length, 6); // 3! = 6 + }); +}); + +describe('stringPermutationsGenerator', () => { + it('yields correct permutations for small strings', () => { + const result = Array.from(stringPermutationsGenerator('ab')).sort(); + assert.deepStrictEqual(result, ['ab', 'ba']); + }); + + it('yields all permutations for 3 unique characters', () => { + const result = Array.from(stringPermutationsGenerator('abc')).sort(); + const expected = ['abc', 'acb', 'bac', 'bca', 'cab', 'cba']; + assert.deepStrictEqual(result, expected); + }); + + it('handles repeated characters correctly', () => { + const result = Array.from(stringPermutationsGenerator('aab')).sort(); + const expected = ['aab', 'aba', 'baa']; + assert.deepStrictEqual(result, expected); + }); + + it('handles single character input', () => { + const result = Array.from(stringPermutationsGenerator('a')); + assert.deepStrictEqual(result, ['a']); + }); + + it('handles empty string input', () => { + const result = Array.from(stringPermutationsGenerator('')); + assert.deepStrictEqual(result, ['']); + }); + + it('is case-sensitive', () => { + const result = Array.from(stringPermutationsGenerator('Ab')).sort(); + const expected = ['Ab', 'bA']; + assert.deepStrictEqual(result, expected); + }); + + it('handles special characters', () => { + const result = Array.from(stringPermutationsGenerator('!@')).sort(); + const expected = ['!@', '@!']; + assert.deepStrictEqual(result, expected); + }); + + it('throws an error if input is not a string', () => { + // Generator functions throw when first consumed, not when created + const gen1 = stringPermutationsGenerator(123 as any); + assert.throws(() => gen1.next(), /Input must be a string/); + + const gen2 = stringPermutationsGenerator(null as any); + assert.throws(() => gen2.next(), /Input must be a string/); + + const gen3 = stringPermutationsGenerator(undefined as any); + assert.throws(() => gen3.next(), /Input must be a string/); + }); + + it('can be used with for...of loop', () => { + const results: string[] = []; + for (const perm of stringPermutationsGenerator('abc')) { + results.push(perm); + } + assert.strictEqual(results.length, 6); + }); }); diff --git a/src/transformations/index.ts b/src/transformations/index.ts index 311c538..187d8db 100644 --- a/src/transformations/index.ts +++ b/src/transformations/index.ts @@ -15,7 +15,7 @@ export { escapeHtml } from './escapeHTML'; export { maskSegment } from './maskSegment'; export { numberToText } from './numberToText/main'; export { reverseWordsInString } from './reverseWordsInString '; -export { stringPermutations } from './stringPermutations'; +export { stringPermutations, stringPermutationsGenerator } from './stringPermutations'; export { stringCombinations } from './stringCombinations'; import { camelCase } from './camelCase'; @@ -36,7 +36,7 @@ import { maskSegment } from './maskSegment'; import { deburr } from './deburr'; import { numberToText } from './numberToText/main'; import { reverseWordsInString } from './reverseWordsInString '; -import { stringPermutations } from './stringPermutations'; +import { stringPermutations, stringPermutationsGenerator } from './stringPermutations'; import { stringCombinations } from './stringCombinations'; export const transformations = { @@ -59,5 +59,6 @@ export const transformations = { numberToText, reverseWordsInString, stringPermutations, + stringPermutationsGenerator, stringCombinations }; diff --git a/src/transformations/stringPermutations.ts b/src/transformations/stringPermutations.ts index c6dae16..b176cdd 100644 --- a/src/transformations/stringPermutations.ts +++ b/src/transformations/stringPermutations.ts @@ -3,10 +3,15 @@ * * Handles repeated characters by ensuring only unique permutations * are included in the result. The order of permutations is not guaranteed. + * + * Performance optimized version using iterative approach and efficient + * character frequency tracking to avoid duplicate permutations. * * @param {string} str - The input string to generate permutations for. + * @param {number} [limit] - Optional limit on number of permutations to generate. * @returns {string[]} An array of unique permutations of the input string. * @throws {TypeError} If the input is not a string. + * @throws {RangeError} If limit is negative. * * @example * stringPermutations("ab"); @@ -21,6 +26,10 @@ * // ["aab", "aba", "baa"] * * @example + * stringPermutations("abcdef", 10); + * // Returns first 10 permutations + * + * @example * stringPermutations(""); * // [""] * @@ -28,26 +37,168 @@ * stringPermutations("a"); * // ["a"] */ -export function stringPermutations(str: string): string[] { +export function stringPermutations(str: string, limit?: number): string[] { if (typeof str !== 'string') { throw new TypeError('Input must be a string'); } + if (limit !== undefined && limit < 0) { + throw new RangeError('Limit must be non-negative'); + } + if (str.length === 0) return ['']; + // For small strings, use the original approach for simplicity + if (str.length <= 6) { + return generatePermutationsSmall(str, limit); + } + + // For larger strings, use optimized approach + return generatePermutationsOptimized(str, limit); +} + +/** + * Generates permutations for small strings (≤6 characters) using recursive approach. + * This is more memory efficient for small inputs. + */ +function generatePermutationsSmall(str: string, limit?: number): string[] { const results = new Set(); + let count = 0; const permute = (prefix: string, remaining: string) => { + if (limit !== undefined && count >= limit) return; + if (remaining.length === 0) { results.add(prefix); + count++; } else { for (let i = 0; i < remaining.length; i++) { + if (limit !== undefined && count >= limit) break; permute(prefix + remaining[i], remaining.slice(0, i) + remaining.slice(i + 1)); } } }; permute('', str); - return Array.from(results); } + +/** + * Generates permutations for larger strings using optimized iterative approach. + * Uses character frequency tracking to avoid duplicate permutations efficiently. + */ +function generatePermutationsOptimized(str: string, limit?: number): string[] { + // Count character frequencies + const charCount = new Map(); + for (const char of str) { + charCount.set(char, (charCount.get(char) || 0) + 1); + } + + const results: string[] = []; + const chars = Array.from(charCount.keys()); + const counts = chars.map(char => charCount.get(char)!); + + // Use iterative approach with stack to avoid recursion + const stack: Array<{ + permutation: string; + remainingCounts: number[]; + depth: number; + }> = [{ + permutation: '', + remainingCounts: [...counts], + depth: 0 + }]; + + while (stack.length > 0) { + if (limit !== undefined && results.length >= limit) break; + + const current = stack.pop()!; + + if (current.depth === str.length) { + results.push(current.permutation); + continue; + } + + // Generate next level permutations + for (let i = 0; i < chars.length; i++) { + if (current.remainingCounts[i] > 0) { + const newCounts = [...current.remainingCounts]; + newCounts[i]--; + + stack.push({ + permutation: current.permutation + chars[i], + remainingCounts: newCounts, + depth: current.depth + 1 + }); + } + } + } + + return results; +} + +/** + * Generator function for permutations - yields permutations one at a time. + * This is the most memory-efficient approach for very large strings. + * + * @param {string} str - The input string to generate permutations for. + * @yields {string} Each unique permutation. + * + * @example + * for (const perm of stringPermutationsGenerator("abc")) { + * console.log(perm); + * } + */ +export function* stringPermutationsGenerator(str: string): Generator { + if (typeof str !== 'string') { + throw new TypeError('Input must be a string'); + } + + if (str.length === 0) { + yield ''; + return; + } + + // Count character frequencies + const charCount = new Map(); + for (const char of str) { + charCount.set(char, (charCount.get(char) || 0) + 1); + } + + const chars = Array.from(charCount.keys()); + const counts = chars.map(char => charCount.get(char)!); + + // Use iterative approach with stack + const stack: Array<{ + permutation: string; + remainingCounts: number[]; + depth: number; + }> = [{ + permutation: '', + remainingCounts: [...counts], + depth: 0 + }]; + + while (stack.length > 0) { + const current = stack.pop()!; + + if (current.depth === str.length) { + yield current.permutation; + continue; + } + + // Generate next level permutations + for (let i = 0; i < chars.length; i++) { + if (current.remainingCounts[i] > 0) { + const newCounts = [...current.remainingCounts]; + newCounts[i]--; + + stack.push({ + permutation: current.permutation + chars[i], + remainingCounts: newCounts, + depth: current.depth + 1 + }); + } + } + } +}