-
Notifications
You must be signed in to change notification settings - Fork 46
/
Copy pathunique-name-finder.ts
167 lines (151 loc) · 5.15 KB
/
unique-name-finder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import {
reservedObjectPrototypeKeywords,
reservedVdmKeywords
} from './name-formatting-reserved-key-words';
type Separator = '-' | '_';
/**
* Holds state on already used names and provides new names if there are naming conflicts.
*/
export class UniqueNameFinder {
private static readonly MAXIMUM_NUMBER_OF_SUFFIX = 1000;
private static removeSuffixIfPresent(name: string): string {
return name.replace(new RegExp('_\\d+$'), '');
}
private static getNameForComparison(
name: string,
caseSensitive: boolean
): string {
return caseSensitive ? name : name.toLowerCase();
}
private usedNames: string[] = [];
/**
* Creates an instance of UniqueNameFinder.
* @param separator The separator to be used
* @param usedNames Sets the already used names considered in the finding process. Reserved TS keywords are always checked.
*/
public constructor(
private separator: Separator = '_',
usedNames: string[] = []
) {
this.addToUsedNames(...usedNames);
}
/**
* Adds the name(s) to the already used names.
* @param names Names to be added
* @returns The instance of the finder.
*/
public addToUsedNames(...names: string[]) {
this.usedNames.push(...names);
return this;
}
/**
* Find a unique name by appending a suffix of the form this.separator\d+ if necessary. If the name is already unique nothing is appended.
* @param name The name to get a unique name from.
* @param caseSensitive Whether to check the already used names in a case sensitive manner.
* @returns A unique name
*/
public findUniqueName(name: string, caseSensitive = true): string {
return this.findUniqueNameWithSuffixes(name, [], caseSensitive)[0];
}
/**
* Find a unique name by appending a suffix of the form this.separator\d+ if necessary. If the name is already unique nothing is appended.
* The suffixes are a list of strings appended to the name and these build names are also checked for uniqueness.
*
* @param name The name to get a unique name from
* @param suffixes Additional name suffixed to be considered
* @param caseSensitive Whether to check the already used names in a case sensitive manner.
* @returns A list of unique names. The length of this array is one plus the number of suffixes provided. The first entry corresponds to the given name.
*/
public findUniqueNameWithSuffixes(
name: string,
suffixes: string[],
caseSensitive = true
): string[] {
const relevantUsedNames = this.getUsedNamesStartingWith(
name,
caseSensitive
);
if (
!this.areNamesUsed(
this.getAllNames(name, suffixes),
relevantUsedNames,
caseSensitive
)
) {
return [name, ...this.getAllNames(name, suffixes)];
}
const suffix = this.getUniqueNameUsingSuffix(
name,
relevantUsedNames,
suffixes,
caseSensitive
);
return this.addSuffixes(name, suffix, suffixes);
}
private getUsedNamesForComparison(caseSensitive: boolean): string[] {
return this.usedNames.map(name =>
UniqueNameFinder.getNameForComparison(name, caseSensitive)
);
}
private areNamesUsed(
names: string[],
usedNames: string[],
caseSensitive: boolean
): boolean {
return names.some(
name =>
usedNames
.map(usedName =>
UniqueNameFinder.getNameForComparison(usedName, caseSensitive)
)
.includes(
UniqueNameFinder.getNameForComparison(name, caseSensitive)
) ||
reservedVdmKeywords.has(name) ||
reservedObjectPrototypeKeywords.has(name)
);
}
private addSuffixes(
name: string,
suffix: number,
nameSuffixes: string[]
): string[] {
const nameWithoutSuffix = UniqueNameFinder.removeSuffixIfPresent(name);
const numberSuffix = `${nameWithoutSuffix}${this.separator}${suffix}`;
return this.getAllNames(numberSuffix, nameSuffixes);
}
private getAllNames(name: string, suffixes: string[]): string[] {
return [name, ...suffixes.map(nameSuffix => `${name}${nameSuffix}`)];
}
private getUsedNamesStartingWith(
name: string,
caseSensitive: boolean
): string[] {
const modifiedName = UniqueNameFinder.removeSuffixIfPresent(name);
return this.getUsedNamesForComparison(caseSensitive).filter(used =>
used.startsWith(
UniqueNameFinder.getNameForComparison(modifiedName, caseSensitive)
)
);
}
private getUniqueNameUsingSuffix(
name: string,
usedNames: string[],
nameSuffixes: string[],
caseSensitive: boolean
): number {
let suffix = 1;
// This algorithm has order N**2 for N identical names. With a sort you could get it down to N*log(N)
// However with the related items in mind this is much easier and N should be small anyway.
while (suffix < UniqueNameFinder.MAXIMUM_NUMBER_OF_SUFFIX) {
const newNames = this.addSuffixes(name, suffix, nameSuffixes);
if (!this.areNamesUsed(newNames, usedNames, caseSensitive)) {
return suffix;
}
suffix++;
}
throw new Error(
`Unable to find a unique name for ${name} within the range of ${UniqueNameFinder.MAXIMUM_NUMBER_OF_SUFFIX} suffixes.`
);
}
}