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/);
+ });
+});