duplicate-name
v0.2.4
Published
duplicate name (1) duplicate name (2) duplicate name (3)
Maintainers
Readme
duplicate-name
Tiny utility for generating a unique name like "Foo (1)" when "Foo" already exists.
Install
npm i duplicate-nameExample
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
[email protected] test node test.js
uniqueNameForList Test Report
This report is auto-generated bytest.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)
