duplicate-name
Version:
duplicate name (1) duplicate name (2) duplicate name (3)
273 lines (242 loc) • 8.41 kB
Markdown
# duplicate-name
Tiny utility for generating a unique name like `"Foo (1)"` when `"Foo"` already exists.
## Install
```bash
npm i duplicate-name
```
## Example
```ts
import { uniqueNameForList } from "duplicate-name";
const name = uniqueNameForList(["A", "A (2)"], "A", { strategy: "end" });
// "A (3)"
```
## Behavior & docs
The exact behavior is defined by the test suite. Treat the test output as the source of truth and defer to it for documentation. (This README stays minimal on purpose.)
# Test docs
<!-- Do not remove -->
<!-- TEST_OUTPUT:START -->
> duplicate-name@0.2.4 test
> node test.js
> uniqueNameForList Test Report \
> This report is auto-generated by `test.js`.
## Compliance
### ✅ Free name returns as-is
- **Parameters**: \
*existingNames*: `[]`
*desiredName*: `"B"`
*opts*: `{}`
- **Expected**: `B`
- **Result**: `B`
### ✅ Strategy "end" → pick max+1
- **Parameters**: \
*existingNames*: `["A","A (2)"]`
*desiredName*: `"A"`
*opts*: `{"strategy":"end"}`
- **Expected**: `A (3)`
- **Result**: `A (3)`
### ✅ Strategy "firstEmpty" → smallest gap ≥ 1
- **Parameters**: \
*existingNames*: `["A","A (2)"]`
*desiredName*: `"A"`
*opts*: `{"strategy":"firstEmpty"}`
- **Expected**: `A (1)`
- **Result**: `A (1)`
### ✅ Keep provided number if free
- **Parameters**: \
*existingNames*: `["A","A (2)"]`
*desiredName*: `"A (5)"`
*opts*: `{"keepProvidedNumber":true}`
- **Expected**: `A (5)`
- **Result**: `A (5)`
### ✅ Provided number taken → fallback to strategy (end)
- **Parameters**: \
*existingNames*: `["A (1)","A (2)"]`
*desiredName*: `"A (1)"`
*opts*: `{"strategy":"end","keepProvidedNumber":true}`
- **Expected**: `A (3)`
- **Result**: `A (3)`
### ✅ Bare base taken → start numbering at 1
- **Parameters**: \
*existingNames*: `["A"]`
*desiredName*: `"A"`
*opts*: `{"strategy":"end"}`
- **Expected**: `A (1)`
- **Result**: `A (1)`
### ✅ firstEmpty fills gap (A,1,3 → 2)
- **Parameters**: \
*existingNames*: `["A","A (1)","A (3)"]`
*desiredName*: `"A"`
*opts*: `{"strategy":"firstEmpty"}`
- **Expected**: `A (2)`
- **Result**: `A (2)`
### ✅ end after gap (A,1,3 → 4)
- **Parameters**: \
*existingNames*: `["A","A (1)","A (3)"]`
*desiredName*: `"A"`
*opts*: `{"strategy":"end"}`
- **Expected**: `A (4)`
- **Result**: `A (4)`
## Case Sensitivity
### ✅ caseSensitive=false merges cases
- **Parameters**: \
*existingNames*: `["Doc","doc (1)"]`
*desiredName*: `"Doc"`
*opts*: `{"caseSensitive":false}`
- **Expected**: `Doc (2)`
- **Result**: `Doc (2)`
### ✅ caseSensitive=true keeps separate
- **Parameters**: \
*existingNames*: `["Doc","Doc (1)"]`
*desiredName*: `"doc"`
*opts*: `{"caseSensitive":true}`
- **Expected**: `doc`
- **Result**: `doc`
## Non-numeric & Formatting Oddities
### ✅ Non-numeric tag treated as literal base
- **Parameters**: \
*existingNames*: `["A","A (1)"]`
*desiredName*: `"A (draft)"`
*opts*: `{"strategy":"end"}`
- **Expected**: `A (draft)`
- **Result**: `A (draft)`
### ✅ No-space tag "A(3)" ignored as tag
- **Parameters**: \
*existingNames*: `["A","A(3)"]`
*desiredName*: `"A"`
*opts*: `{"strategy":"firstEmpty"}`
- **Expected**: `A (1)`
- **Result**: `A (1)`
### ✅ Extra spaces "A ( 3 )" ignored as tag
- **Parameters**: \
*existingNames*: `["A ( 3 )"]`
*desiredName*: `"A"`
*opts*: `{"strategy":"firstEmpty"}`
- **Expected**: `A`
- **Result**: `A`
### ✅ Invalid tags can be part of atomic name
- **Parameters**: \
*existingNames*: `["A ( 3 )"]`
*desiredName*: `"A ( 3 )"`
*opts*: `{"strategy":"firstEmpty"}`
- **Expected**: `A ( 3 ) (1)`
- **Result**: `A ( 3 ) (1)`
### ✅ Trailing spaces collapse bare base ("A ", "A\t")
- **Parameters**: \
*existingNames*: `["A ","A (1) ","A\t"]`
*desiredName*: `"A"`
*opts*: `{"strategy":"end"}`
- **Expected**: `A (2)`
- **Result**: `A (2)`
## Leading Zeros
### ✅ Desired "A (01)" is literal when free (no parsing)
- **Parameters**: \
*existingNames*: `[]`
*desiredName*: `"A (01)"`
*opts*: `{"strategy":"end","keepProvidedNumber":true}`
- **Expected**: `A (01)`
- **Result**: `A (01)`
### ✅ Existing "A (001)" does NOT occupy slot "A"
- **Parameters**: \
*existingNames*: `["A (001)"]`
*desiredName*: `"A"`
*opts*: `{"strategy":"firstEmpty"}`
- **Expected**: `A`
- **Result**: `A`
### ✅ Existing "A (001)" occupies slot "A (001)"
- **Parameters**: \
*existingNames*: `["A (001)"]`
*desiredName*: `"A (001)"`
*opts*: `{"strategy":"firstEmpty"}`
- **Expected**: `A (001) (1)`
- **Result**: `A (001) (1)`
## Weird List Entries
### ✅ Weird entries don’t affect "A"
- **Parameters**: \
*existingNames*: `["()","(3)","(#sf3)",""]`
*desiredName*: `"A"`
*opts*: `{"strategy":"end"}`
- **Expected**: `A`
- **Result**: `A`
### ✅ "(3)" alone doesn’t affect numbering for "A"
- **Parameters**: \
*existingNames*: `["A (1)","(3)"]`
*desiredName*: `"A"`
*opts*: `{"strategy":"firstEmpty"}`
- **Expected**: `A (2)`
- **Result**: `A (2)`
## Empty String as Desired Base
### ✅ Desired empty, no empty in list → ""
- **Parameters**: \
*existingNames*: `["()","(3)","(#sf3)"]`
*desiredName*: `""`
*opts*: `{"strategy":"end"}`
- **Expected**: ``
- **Result**: ``
### ✅ Desired empty, empty exists → " (1)"
- **Parameters**: \
*existingNames*: `[""]`
*desiredName*: `""`
*opts*: `{"strategy":"end"}`
- **Expected**: ` (1)`
- **Result**: ` (1)`
### ✅ Desired empty, empty + (1) + (2) → " (3)"
- **Parameters**: \
*existingNames*: `[""," (1)"," (2)"]`
*desiredName*: `""`
*opts*: `{"strategy":"end"}`
- **Expected**: ` (3)`
- **Result**: ` (3)`
### ✅ Desired empty, only " (1)"/" (2)" → "" (bare free)
- **Parameters**: \
*existingNames*: `[" (1)"," (2)"]`
*desiredName*: `""`
*opts*: `{"strategy":"firstEmpty"}`
- **Expected**: ``
- **Result**: ``
## Crowded Ranges
### ✅ firstEmpty finds smallest gap in crowded range (→ 4)
- **Parameters**: \
*existingNames*: `["Item","Item (1)","Item (2)","Item (3)","Item (5)"]`
*desiredName*: `"Item"`
*opts*: `{"strategy":"firstEmpty"}`
- **Expected**: `Item (4)`
- **Result**: `Item (4)`
### ✅ end goes to max+1 in crowded range (→ 6)
- **Parameters**: \
*existingNames*: `["Item","Item (1)","Item (2)","Item (3)","Item (5)"]`
*desiredName*: `"Item"`
*opts*: `{"strategy":"end"}`
- **Expected**: `Item (6)`
- **Result**: `Item (6)`
## Desired Literal Already Exists
### ✅ Desired "A (3)" exists → strategy decides next (end → 4)
- **Parameters**: \
*existingNames*: `["A (3)"]`
*desiredName*: `"A (3)"`
*opts*: `{"strategy":"end"}`
- **Expected**: `A (4)`
- **Result**: `A (4)`
## Provided Tagged Name (High Numbers)
### ✅ Provided "Report (7)" is free → keep it
- **Parameters**: \
*existingNames*: `["Report","Report (1)","Report (6)"]`
*desiredName*: `"Report (7)"`
*opts*: `{"keepProvidedNumber":true}`
- **Expected**: `Report (7)`
- **Result**: `Report (7)`
### ✅ Provided "Report (1)" taken → firstEmpty → 2
- **Parameters**: \
*existingNames*: `["Report","Report (1)","Report (6)"]`
*desiredName*: `"Report (1)"`
*opts*: `{"strategy":"firstEmpty","keepProvidedNumber":true}`
- **Expected**: `Report (2)`
- **Result**: `Report (2)`
### ✅ Provided "Report (1)" taken → end → 7
- **Parameters**: \
*existingNames*: `["Report","Report (1)","Report (6)"]`
*desiredName*: `"Report (1)"`
*opts*: `{"strategy":"end","keepProvidedNumber":true}`
- **Expected**: `Report (7)`
- **Result**: `Report (7)`
<!-- TEST_OUTPUT:END -->