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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`. |

#### <a id="lexicographicalrank"></a>`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
Expand Down
5 changes: 4 additions & 1 deletion src/analyzing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -35,5 +37,6 @@ export const analyzing = {
vowelConsonantCount,
checkMultiplePatterns,
checkSubsequence,
checkStringRotations
checkStringRotations,
lexicographicalRank
};
71 changes: 71 additions & 0 deletions src/analyzing/lexicographicalRank.ts
Original file line number Diff line number Diff line change
@@ -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<string, number> = {};
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;
}
46 changes: 46 additions & 0 deletions src/tests/analyzing/lexicographicalRank.test.ts
Original file line number Diff line number Diff line change
@@ -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/);
});
});