|
6 | 6 | GAMEDATA3, |
7 | 7 | GAMEDATAV, |
8 | 8 | } = require('../../../__gamedata'); |
9 | | -const { START_OF_LINE_FORMS, END_OF_LINE_FORMS, REVERSE_ORDER_ARRAY } = require('./nameConstants') |
| 9 | +const { |
| 10 | + START_OF_LINE_FORMS, |
| 11 | + END_OF_LINE_FORMS, |
| 12 | + REVERSE_ORDER_ARRAY, |
| 13 | + RE_SPECIAL, |
| 14 | + RE_ASCII_CHECK, |
| 15 | + RE_MULTI_SEP, |
| 16 | + RE_SPACE_OR_HYPHEN, |
| 17 | + START_MAP, |
| 18 | + START_REGEX, |
| 19 | + END_REGEX, |
| 20 | + END_MAP |
| 21 | +} = require('./nameConstants'); |
10 | 22 | const { FORM_MAP } = require('./functions'); |
11 | 23 |
|
| 24 | +// Cache normalized names to avoid redundant work |
| 25 | +const normalizeCache = new Map(); |
| 26 | + |
12 | 27 | const POKEMON_NAME_MAPV = PersonalTable[GAMEDATAV].Personal.reduce((pokemonNameMap, currentPokemon) => { |
13 | 28 | return createPokemonMap(pokemonNameMap, currentPokemon, GAMEDATAV); |
14 | 29 | }, {}); |
@@ -148,45 +163,74 @@ function getPokemonFormId(monsno = 0, id, mode = GAMEDATA2) { |
148 | 163 | } |
149 | 164 |
|
150 | 165 | function normalizePokemonName(value, mode = GAMEDATA2) { |
151 | | - // Converts to lowercase, removes non-word characters, |
152 | | - // converts spaces to hyphens, and strips leading/trailing whitespace. |
153 | | - let initialValue = value; |
154 | | - value = value.replace(/[!]/g, 'emark') |
155 | | - .replace(/[?]/g, 'qmark') |
156 | | - .replace(/[♀]/g, '-f') |
157 | | - .replace(/[♂]/g, '-m') |
158 | | - value = value.normalize('NFKD').replace(/[^\w\s-]/g, '').trim().toLowerCase(); |
159 | | - |
160 | | - if (mode === GAMEDATA2 ) { |
161 | | - return value.replace(/[-\s]+/g, '-'); |
162 | | - } |
| 166 | + if (!value) return ''; |
163 | 167 |
|
164 | | - if (value.includes(' ') || value.includes('-')) { |
165 | | - // Split the string at the last space |
166 | | - for (const badValue in START_OF_LINE_FORMS) { |
167 | | - if (value.includes(badValue)) { |
168 | | - value = value.replace(badValue, START_OF_LINE_FORMS[badValue]); |
169 | | - } |
170 | | - } |
| 168 | + const cacheKey = `${mode}:${value}`; |
| 169 | + if (normalizeCache.has(cacheKey)) return normalizeCache.get(cacheKey); |
171 | 170 |
|
172 | | - const lastWord = value.split(' ').pop(); |
173 | | - for (const badEndValue in END_OF_LINE_FORMS) { |
174 | | - if (lastWord === badEndValue) { |
175 | | - value = value.replace(` ${badEndValue}`, END_OF_LINE_FORMS[badEndValue]); |
176 | | - } |
| 171 | + // --- 1. Replace special symbols efficiently --- |
| 172 | + let cleaned = value.replace(RE_SPECIAL, m => { |
| 173 | + switch (m) { |
| 174 | + case '!': return 'emark'; |
| 175 | + case '?': return 'qmark'; |
| 176 | + case '♀': return '-f'; |
| 177 | + case '♂': return '-m'; |
| 178 | + default: return ''; // remove other non-word chars |
177 | 179 | } |
| 180 | + }); |
| 181 | + |
| 182 | + // --- 2. Normalize only if non-ASCII characters are found --- |
| 183 | + if (RE_ASCII_CHECK.test(cleaned)) { |
| 184 | + cleaned = cleaned.normalize('NFKD').replace(/[^\w\s-]/g, ''); |
| 185 | + } |
| 186 | + |
| 187 | + cleaned = cleaned.trim().toLowerCase(); |
| 188 | + |
| 189 | + if (START_REGEX) { |
| 190 | + cleaned = cleaned.replace(START_REGEX, (m) => { |
| 191 | + // we compiled START_MAP with lowercase keys, and cleaned is lowercase, so lookup succeeds |
| 192 | + return START_MAP[m] ?? m; |
| 193 | + }); |
| 194 | + } |
| 195 | + |
| 196 | + if (END_REGEX) { |
| 197 | + // END_REGEX captures the suffix as group 1; but because we used a group, |
| 198 | + // replace callback receives the full match as first arg and group as second. |
| 199 | + cleaned = cleaned.replace(END_REGEX, (fullMatch, group1) => { |
| 200 | + const key = group1.toLowerCase(); |
| 201 | + // return replacement (could be empty string) - we preserve any leading separator from fullMatch if needed |
| 202 | + return END_MAP[key] ?? ''; |
| 203 | + }); |
| 204 | + } |
| 205 | + |
| 206 | + // --- 3. Early return for common mode --- |
| 207 | + if (mode === GAMEDATA2) { |
| 208 | + const result = cleaned.replace(RE_MULTI_SEP, '-'); |
| 209 | + normalizeCache.set(cacheKey, result); |
| 210 | + return result; |
| 211 | + } |
| 212 | + |
| 213 | + // --- 4. Handle name reordering and special suffix/prefix cases --- |
| 214 | + if (RE_SPACE_OR_HYPHEN.test(cleaned)) { |
| 215 | + // Replace known bad prefixes/suffixes using precompiled regex |
| 216 | + cleaned = cleaned.replace(START_REGEX, m => START_OF_LINE_FORMS[m]); |
| 217 | + cleaned = cleaned.replace(END_REGEX, m => END_OF_LINE_FORMS[m]); |
178 | 218 |
|
179 | | - const parts = value.split(' ').reverse(); |
| 219 | + // Extract last word efficiently |
| 220 | + const lastSpace = cleaned.lastIndexOf(' '); |
| 221 | + const lastWord = lastSpace === -1 ? cleaned : cleaned.slice(lastSpace + 1); |
180 | 222 |
|
181 | | - // Check if the first part is "Mega" or "Gigantamax" |
182 | | - if (REVERSE_ORDER_ARRAY.includes(parts[0]) || lastWord === 'genesect') { |
183 | | - // Rearrange string and join with hyphen |
184 | | - value = [parts[1], parts[0]].join('-'); |
185 | | - return value; |
| 223 | + // Handle reverse-order names like "Mega" or "Gigantamax" |
| 224 | + if (REVERSE_ORDER_ARRAY.includes(lastWord) || lastWord === 'genesect') { |
| 225 | + const before = cleaned.slice(0, lastSpace).replace(RE_MULTI_SEP, '-'); |
| 226 | + cleaned = `${before}-${lastWord}`; |
186 | 227 | } |
187 | 228 | } |
188 | 229 |
|
189 | | - return value.replace(/[-\s]+/g, '-'); |
| 230 | + // --- 5. Final normalization for separators --- |
| 231 | + const result = cleaned.replace(RE_MULTI_SEP, '-'); |
| 232 | + normalizeCache.set(cacheKey, result); |
| 233 | + return result; |
190 | 234 | } |
191 | 235 |
|
192 | 236 | function getPokemonMonsNoAndFormNoFromPokemonId(pokemonId = 0, mode = GAMEDATA2) { |
|
0 commit comments