diff --git a/README.md b/README.md index eb09b57..e8f3cf8 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,8 @@ const count = stringzy.analyze.wordCount('Hello world'); // 2 - [checkMultiplePatterns](#checkmultiplepatterns) - Finds occurrences of multiple patterns within a given text using Rabin–Karp algorithm (case sensitive) - [checkSubsequence](#checksubsequence) - Checks whether the second string is a subsequence of the first string (case sensitive) - [stringRotation](#stringrotation) - Checks if one string is a rotation of another (case sensitive). +- [lexicographicalRank](#lexicographicalrank) - Calculates the lexicographical rank of a string among all its unique permutations. + ### Formatting - [capitalize](#capitalize) - Capitalizes the first letter of each word @@ -1188,6 +1190,36 @@ isRotation('abcd', 'abc'); | str1 | string | required | The original string. | | str2 | string | required | The string to verify if it is a rotation of `str1`. | +#### `lexicographicalRank(str)` + +Calculates the lexicographic rank of a string among all its unique permutations sorted alphabetically. +The rank is **1-based** (first permutation has rank 1). +Handles duplicate characters by correctly adjusting ranks. + +```javascript +lexicographicRank("acb"); +// 2 → permutations: ["abc", "acb", "bac", "bca", "cab", "cba"] + +lexicographicRank("string"); +// 598 + +lexicographicRank("cba"); +// 6 → permutations: ["abc", "acb", "bac", "bca", "cab", "cba"] + +lexicographicRank("aba"); +// 2 → permutations: ["aab", "aba", "baa"] + +lexicographicRank("a"); +// 1 + +lexicographicRank(""); +// 1 → edge case, empty string considered rank 1 +``` + +| Parameter | Type | Default | Description | +| --------- | ------ | -------- | ----------------------------------------- | +| str | string | required | The input string to calculate the rank of | + --- ### 🎨 Formatting diff --git a/src/analyzing/index.ts b/src/analyzing/index.ts index 9130acd..20055ac 100644 --- a/src/analyzing/index.ts +++ b/src/analyzing/index.ts @@ -11,6 +11,7 @@ export { checkSubsequence } from './checkSubsequence'; export { functionWordCount } from './functionWordCount'; export { contentWordCount } from './contentWordCount'; export { checkStringRotations } from './stringRotation'; +export { lexicographicalRank } from './lexicographicalRank'; import { characterCount } from './characterCount'; import { characterFrequency } from './characterFrequency'; @@ -23,6 +24,7 @@ import { vowelConsonantCount } from './vowelConsonantCount'; import { checkMultiplePatterns } from './checkMultiplePatterns'; import { checkSubsequence } from './checkSubsequence'; import { checkStringRotations } from './stringRotation'; +import { lexicographicalRank } from './lexicographicalRank'; export const analyzing = { characterCount, @@ -35,5 +37,6 @@ export const analyzing = { vowelConsonantCount, checkMultiplePatterns, checkSubsequence, - checkStringRotations + checkStringRotations, + lexicographicalRank }; diff --git a/src/analyzing/lexicographicalRank.ts b/src/analyzing/lexicographicalRank.ts new file mode 100644 index 0000000..99d3e25 --- /dev/null +++ b/src/analyzing/lexicographicalRank.ts @@ -0,0 +1,71 @@ +/** + * Calculates the lexicographic rank of a string among all its unique permutations. + * + * The rank is 1-based (i.e., the first permutation has rank 1). + * Handles strings with duplicate characters correctly by adjusting for repetition. + * + * @param {string} str - The input string. + * @returns {number} The 1-based lexicographic rank of the string. + * @throws {TypeError} If the input is not a string. + * + * @example + * lexicographicRank("acb"); // 2 + * + * @example + * lexicographicRank("string"); // 598 + * + * @example + * lexicographicRank("cba"); // 6 + * + * @example + * lexicographicRank("aba"); // 2 + * + * @example + * lexicographicRank("a"); // 1 + */ +export function lexicographicalRank(str: string): number { + if (typeof str !== 'string') { + throw new TypeError('Input must be a string'); + } + if (str.length === 0) return 1; + + const factorial = (n: number): number => (n <= 1 ? 1 : n * factorial(n - 1)); + + const charCount: Record = {}; + for (const ch of str) { + charCount[ch] = (charCount[ch] || 0) + 1; + } + + const chars = Object.keys(charCount).sort(); + + let rank = 1; + for (let i = 0; i < str.length; i++) { + const ch = str[i]; + + for (const smaller of chars) { + if (smaller >= ch) break; + + if (charCount[smaller] > 0) { + charCount[smaller]--; + + let denom = 1; + const remaining = str.length - i - 1; + for (const count of Object.values(charCount)) { + denom *= factorial(count); + } + + rank += factorial(remaining) / denom; + + charCount[smaller]++; + } + } + + if (charCount[ch] > 0) { + charCount[ch]--; + } else { + break; // shouldn't happen unless str has invalid chars + } + } + + return rank; +} diff --git a/src/tests/analyzing/lexicographicalRank.test.ts b/src/tests/analyzing/lexicographicalRank.test.ts new file mode 100644 index 0000000..b7b1445 --- /dev/null +++ b/src/tests/analyzing/lexicographicalRank.test.ts @@ -0,0 +1,46 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { lexicographicalRank } from '../../analyzing/lexicographicalRank'; + +describe('lexicographicalRank', () => { + it('returns correct rank for small strings', () => { + assert.strictEqual(lexicographicalRank('acb'), 2); + assert.strictEqual(lexicographicalRank('cba'), 6); + assert.strictEqual(lexicographicalRank('abc'), 1); + }); + + it('handles strings with repeated characters', () => { + assert.strictEqual(lexicographicalRank('aba'), 2); + assert.strictEqual(lexicographicalRank('aab'), 1); + assert.strictEqual(lexicographicalRank('baa'), 3); + }); + + it('returns 1 for single character string', () => { + assert.strictEqual(lexicographicalRank('a'), 1); + assert.strictEqual(lexicographicalRank('Z'), 1); + }); + + it('handles larger examples correctly', () => { + assert.strictEqual(lexicographicalRank('string'), 598); + }); + + it('handles empty string', () => { + assert.strictEqual(lexicographicalRank(''), 1); + }); + + it('is case-sensitive', () => { + assert.strictEqual(lexicographicalRank('Abc'), 1); // 'A' < 'b' < 'c' + assert.strictEqual(lexicographicalRank('bAc'), 3); + }); + + it('handles special characters', () => { + assert.strictEqual(lexicographicalRank('!ab'), 1); // '!' comes first + assert.strictEqual(lexicographicalRank('ab!'), 4); // correct rank is 4 + }); + + it('throws an error if input is not a string', () => { + assert.throws(() => lexicographicalRank(123 as any), /Input must be a string/); + assert.throws(() => lexicographicalRank(null as any), /Input must be a string/); + assert.throws(() => lexicographicalRank(undefined as any), /Input must be a string/); + }); +});