nhb-toolbox 4.13.4 → 4.13.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,15 @@ All notable changes to the package will be documented here.
6
6
 
7
7
  ---
8
8
 
9
+ ## [4.13.7] - 2025-07-23
10
+
11
+ - **Updated** `isPlural` and `isSingular` methods in `Pluralizer` class to handle more cases.
12
+ - **Ran full test** on `pluralizer` and fixed some known issues.
13
+
14
+ ## [4.13.3-6] - 2025-07-22
15
+
16
+ - **Reordered** rules for `pluralizer` and fixed other issues.
17
+
9
18
  ## [4.13.3] - 2025-07-22
10
19
 
11
20
  - **Updated** pluralization/uncountable rules, case restoration method and fixed other bugs in `pluralizer`.
@@ -56,17 +56,31 @@ class Pluralizer {
56
56
  });
57
57
  }
58
58
  #restoreCase(original, transformed) {
59
+ // Exact match
60
+ if (original === transformed)
61
+ return transformed;
62
+ // Entire original is lowercase
63
+ if (original === original.toLowerCase()) {
64
+ return transformed.toLowerCase();
65
+ }
66
+ // Entire original is uppercase
67
+ if (original === original.toUpperCase()) {
68
+ return transformed.toUpperCase();
69
+ }
70
+ // Title case (first letter uppercase, rest lowercase)
71
+ if (original[0] === original[0].toUpperCase() &&
72
+ original.slice(1) === original.slice(1).toLowerCase()) {
73
+ return (transformed.charAt(0).toUpperCase() +
74
+ transformed.slice(1).toLowerCase());
75
+ }
76
+ // Mixed case: per-character casing
59
77
  let result = '';
60
78
  for (let i = 0; i < transformed.length; i++) {
61
79
  const origChar = original[i];
62
- if (origChar) {
63
- if (origChar.toUpperCase() === origChar &&
64
- origChar.toLowerCase() !== origChar) {
65
- result += transformed[i].toUpperCase();
66
- }
67
- else {
68
- result += transformed[i].toLowerCase();
69
- }
80
+ if (origChar &&
81
+ origChar === origChar.toUpperCase() &&
82
+ origChar !== origChar.toLowerCase()) {
83
+ result += transformed[i].toUpperCase();
70
84
  }
71
85
  else {
72
86
  result += transformed[i].toLowerCase();
@@ -210,11 +224,18 @@ class Pluralizer {
210
224
  * pluralizer.isPlural('children'); // true
211
225
  */
212
226
  isPlural(word) {
213
- const lower = word?.toLowerCase();
214
- if (this.#isUncountable(word))
227
+ if (!(0, primitives_1.isNonEmptyString)(word))
215
228
  return false;
216
- if (this.#irregularPlurals?.[lower])
229
+ const lower = word.toLowerCase();
230
+ // if uncountable return true
231
+ if (this.#isUncountable(lower))
232
+ return true;
233
+ // directly known as plural
234
+ if (this.#irregularPlurals[lower])
217
235
  return true;
236
+ // directly known as singular
237
+ if (this.#irregularSingles[lower])
238
+ return false;
218
239
  return this.toSingular(lower) !== lower;
219
240
  }
220
241
  /**
@@ -225,12 +246,19 @@ class Pluralizer {
225
246
  * pluralizer.isSingular('child'); // true
226
247
  */
227
248
  isSingular(word) {
228
- const lower = word?.toLowerCase();
229
- if (this.#isUncountable(word))
249
+ if (!(0, primitives_1.isNonEmptyString)(word))
230
250
  return false;
231
- if (this.#irregularSingles?.[lower])
251
+ const lower = word.toLowerCase();
252
+ // if uncountable return true
253
+ if (this.#isUncountable(lower))
254
+ return true;
255
+ // directly known as singular
256
+ if (this.#irregularSingles[lower])
232
257
  return true;
233
- return this.toPlural(lower) !== lower;
258
+ // directly known as plural
259
+ if (this.#irregularPlurals[lower])
260
+ return false;
261
+ return this.toSingular(lower) === lower;
234
262
  }
235
263
  }
236
264
  exports.Pluralizer = Pluralizer;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.singularRules = exports.uncountables = exports.pluralRules = exports.irregularRules = void 0;
4
- /** Irregular rules and replacements */
3
+ exports.uncountables = exports.singularRules = exports.pluralRules = exports.irregularRules = void 0;
4
+ /** Irregular singular plural mappings */
5
5
  exports.irregularRules =
6
6
  /* @__PURE__ */ Object.freeze([
7
7
  // Pronouns
@@ -26,70 +26,79 @@ exports.irregularRules =
26
26
  ['his', 'their'],
27
27
  ['her', 'their'],
28
28
  // Common irregulars
29
+ ['analysis', 'analyses'],
30
+ ['anathema', 'anathemata'],
31
+ ['appendix', 'appendices'],
32
+ ['automaton', 'automata'],
33
+ ['basis', 'bases'],
34
+ ['calf', 'calves'],
35
+ ['carve', 'carves'],
29
36
  ['child', 'children'],
30
- ['person', 'people'],
31
- ['man', 'men'],
32
- ['woman', 'women'],
33
- ['tooth', 'teeth'],
37
+ ['codex', 'codices'],
38
+ ['criterion', 'criteria'],
39
+ ['crisis', 'crises'],
40
+ ['datum', 'data'],
41
+ ['diagnosis', 'diagnoses'],
42
+ ['die', 'dice'],
43
+ ['dogma', 'dogmata'],
44
+ ['echo', 'echoes'],
45
+ ['elf', 'elves'],
34
46
  ['foot', 'feet'],
35
- ['mouse', 'mice'],
47
+ ['genus', 'genera'],
36
48
  ['goose', 'geese'],
37
- ['ox', 'oxen'],
49
+ ['groove', 'grooves'],
50
+ ['half', 'halves'],
51
+ ['hedron', 'hedra'],
52
+ ['hero', 'heroes'],
53
+ ['hoof', 'hooves'],
54
+ ['human', 'humans'],
55
+ ['honey', 'honeys'],
56
+ ['index', 'indices'],
38
57
  ['leaf', 'leaves'],
39
- ['datum', 'data'],
58
+ ['lemma', 'lemmata'],
59
+ ['loaf', 'loaves'],
60
+ ['looey', 'looies'],
61
+ ['man', 'men'],
62
+ ['mango', 'mangoes'],
63
+ ['matrix', 'matrices'],
40
64
  ['medium', 'media'],
41
- ['analysis', 'analyses'],
42
- ['diagnosis', 'diagnoses'],
43
- ['basis', 'bases'],
44
- ['thesis', 'theses'],
45
- ['crisis', 'crises'],
65
+ ['mouse', 'mice'],
66
+ ['neurosis', 'neuroses'],
67
+ ['noumenon', 'noumena'],
68
+ ['organon', 'organa'],
69
+ ['ox', 'oxen'],
70
+ ['passerby', 'passersby'],
46
71
  ['phenomenon', 'phenomena'],
47
- ['criterion', 'criteria'],
48
- ['index', 'indices'],
49
- ['matrix', 'matrices'],
50
- ['vertex', 'vertices'],
51
- ['quiz', 'quizzes'],
52
- ['die', 'dice'],
53
- ['yes', 'yeses'],
54
- ['human', 'humans'],
72
+ ['pickaxe', 'pickaxes'],
73
+ ['potato', 'potatoes'],
74
+ ['prolegomenon', 'prolegomena'],
55
75
  ['proof', 'proofs'],
56
- ['carve', 'carves'],
57
- ['valve', 'valves'],
58
- ['looey', 'looies'],
76
+ ['quiz', 'quizzes'],
77
+ ['schema', 'schemata'],
78
+ ['self', 'selves'],
79
+ ['shelf', 'shelves'],
80
+ ['stigma', 'stigmata'],
81
+ ['stoma', 'stomata'],
59
82
  ['thief', 'thieves'],
60
- ['groove', 'grooves'],
61
- ['pickaxe', 'pickaxes'],
62
- ['passerby', 'passersby'],
63
- ['honey', 'honeys'],
64
- // Words ending in with a consonant and `o`.
65
- ['echo', 'echoes'],
66
- ['dingo', 'dingoes'],
67
- ['mango', 'mangoes'],
68
- ['volcano', 'volcanoes'],
69
- ['tornado', 'tornadoes'],
83
+ ['tooth', 'teeth'],
84
+ ['tomato', 'tomatoes'],
70
85
  ['torpedo', 'torpedoes'],
71
- // Ends with `us`.
72
- ['genus', 'genera'],
86
+ ['tornado', 'tornadoes'],
87
+ ['valve', 'valves'],
88
+ ['vertex', 'vertices'],
89
+ ['virus', 'viruses'],
73
90
  ['viscus', 'viscera'],
74
- // Ends with `ma`.
75
- ['stigma', 'stigmata'],
76
- ['stoma', 'stomata'],
77
- ['dogma', 'dogmata'],
78
- ['lemma', 'lemmata'],
79
- ['schema', 'schemata'],
80
- ['anathema', 'anathemata'],
91
+ ['volcano', 'volcanoes'],
92
+ ['woman', 'women'],
93
+ ['wolf', 'wolves'],
94
+ ['yes', 'yeses'],
81
95
  ]);
82
- /** Plural rules and replacements */
96
+ /** Pluralization rules with regex and replacements */
83
97
  exports.pluralRules =
84
98
  /* @__PURE__ */ Object.freeze([
85
- [/(\P{ASCII})$/u, '$1'],
99
+ [/s?$/i, 's'],
86
100
  [/(pe)(rson|ople)$/i, '$1ople'],
87
101
  [/(child)(?:ren)?$/i, '$1ren'],
88
- [/(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$/i, '$1ices'],
89
- [
90
- /(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i,
91
- '$1i',
92
- ],
93
102
  [/(alumn|alg|vertebr)(?:a|ae)$/i, '$1ae'],
94
103
  [
95
104
  /(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i,
@@ -108,18 +117,94 @@ exports.pluralRules =
108
117
  [/(x|ch|ss|sh|zz)$/i, '$1es'],
109
118
  [/(her|at|gr)o$/i, '$1oes'],
110
119
  [/sis$/i, 'ses'],
111
- [/(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$/i, '$1$2ves'],
120
+ [/^(chief|chef|belief|roof|cliff|reef)$/i, '$1s'],
121
+ [/(seraph|cherub)$/i, '$1im'],
122
+ [/(kni|wi|li)fe$/i, '$1ves'],
123
+ [/(ar|l|ea|eo|oa|hoo)f$/i, '$1ves'],
112
124
  [/([^aeiouy]|qu)y$/i, '$1ies'],
113
125
  [/(tive)$/i, '$1s'],
114
126
  [/(hive)$/i, '$1s'],
115
127
  [/(quiz)$/i, '$1zes'],
116
128
  [/m[ae]n$/i, 'men'],
117
129
  [/eaux$/i, '$0'],
118
- // fallback
119
- [/s$/i, 's'], // <--- final catch-all
130
+ [
131
+ /(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i,
132
+ '$1i',
133
+ ],
120
134
  ]);
121
- /** Uncountable constants */
135
+ /** Singularization rules with regex and replacements */
136
+ exports.singularRules =
137
+ /* @__PURE__ */ Object.freeze([
138
+ [/s$/i, ''],
139
+ [/(\P{ASCII})$/u, '$1'],
140
+ [/(pe)(rson|ople)$/i, '$1rson'],
141
+ [/(child)ren$/i, '$1'],
142
+ [/(eau)x?$/i, '$1'],
143
+ [/men$/i, 'man'],
144
+ [/(matr|append)ices$/i, '$1ix'],
145
+ [/(cod|mur|sil|vert|ind)ices$/i, '$1ex'],
146
+ [/(alumn|alg|vertebr)ae$/i, '$1a'],
147
+ [
148
+ /(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i,
149
+ '$1on',
150
+ ],
151
+ [
152
+ /(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$/i,
153
+ '$1um',
154
+ ],
155
+ [/(test)(?:is|es)$/i, '$1is'],
156
+ [/(movie|twelve|abuse|e[mn]u)s$/i, '$1'],
157
+ [
158
+ /(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$/i,
159
+ '$1sis',
160
+ ],
161
+ [
162
+ /(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$/i,
163
+ '$1',
164
+ ],
165
+ [/^(chief|chef|belief|roof|cliff|reef)s$/i, '$1'],
166
+ [/(seraph|cherub)im$/i, '$1'],
167
+ [/\b((?:tit)?m|l)ice$/i, '$1ouse'],
168
+ [/\b(mon|smil)ies$/i, '$1ey'],
169
+ [
170
+ /\b([pl]|zomb|(?:neck|cross)?t|coll|faer|food|gen|goon|group|lass|talk|goal|cut)ies$/i,
171
+ '$1ie',
172
+ ],
173
+ [/ies$/i, 'y'],
174
+ [/(kni|wi|li)ves$/i, '$1fe'],
175
+ [/(ar|l|ea|eo|oa|hoo)ves$/i, '$1f'],
176
+ [/(ar|(?:wo|[ae])l|[eo][ao])ves$/i, '$1f'],
177
+ [
178
+ /(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$/i,
179
+ '$1fe',
180
+ ],
181
+ [/(ss)$/i, '$1'],
182
+ [/(quiz)zes$/i, '$1'],
183
+ [/(vert|ind)ices$/i, '$1ex'],
184
+ [/^(ox)en$/i, '$1'],
185
+ [/(alias|status)es$/i, '$1'],
186
+ [/(octop|vir)i$/i, '$1us'],
187
+ [/(cris|ax|test)es$/i, '$1is'],
188
+ [/(shoe)s$/i, '$1'],
189
+ [/(her|at|gr)oes$/i, '$1o'],
190
+ [/oes$/i, 'o'],
191
+ [/(bus)es$/i, '$1'],
192
+ [/ices$/i, 'ex'],
193
+ [/(hive)s$/i, '$1'],
194
+ [/(tive)s$/i, '$1'],
195
+ [/([^f])ves$/i, '$1fe'],
196
+ [/([lr])ves$/i, '$1f'],
197
+ [/(^analy)ses$/i, '$1sis'],
198
+ [/([ti])a$/i, '$1um'],
199
+ [/(n)ews$/i, '$1ews'],
200
+ [
201
+ /(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i,
202
+ '$1us',
203
+ ],
204
+ ]);
205
+ /** Uncountable words */
122
206
  exports.uncountables = Object.freeze(new Set([
207
+ // common
123
208
  'aircraft',
124
209
  'alcohol',
125
210
  'ammo',
@@ -170,6 +255,7 @@ exports.uncountables = Object.freeze(new Set([
170
255
  'weather',
171
256
  'wildlife',
172
257
  'you',
258
+ // abstract
173
259
  'adulthood',
174
260
  'advertising',
175
261
  'anger',
@@ -181,9 +267,9 @@ exports.uncountables = Object.freeze(new Set([
181
267
  'botany',
182
268
  'carbon',
183
269
  'chaos',
184
- 'coffee',
185
270
  'cheese',
186
271
  'childhood',
272
+ 'coffee',
187
273
  'compassion',
188
274
  'cotton',
189
275
  'dancing',
@@ -358,7 +444,7 @@ exports.uncountables = Object.freeze(new Set([
358
444
  'youth',
359
445
  'zinc',
360
446
  'zoology',
361
- // RegEx(es)
447
+ // regex-based uncountables
362
448
  /pok[eé]mon$/i,
363
449
  /[^aeiou]ese$/i,
364
450
  /deer$/i,
@@ -373,71 +459,3 @@ exports.uncountables = Object.freeze(new Set([
373
459
  /trousers$/i,
374
460
  /jeans$/i,
375
461
  ]));
376
- /** Singular rules and replacements */
377
- exports.singularRules =
378
- /* @__PURE__ */ Object.freeze([
379
- [/(\P{ASCII})$/u, '$1'],
380
- [/(pe)(rson|ople)$/i, '$1rson'],
381
- [/(child)ren$/i, '$1'],
382
- [/(eau)x?$/i, '$1'],
383
- [/men$/i, 'man'],
384
- [/(matr|append)ices$/i, '$1ix'],
385
- [/(cod|mur|sil|vert|ind)ices$/i, '$1ex'],
386
- [/(alumn|alg|vertebr)ae$/i, '$1a'],
387
- [
388
- /(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i,
389
- '$1on',
390
- ],
391
- [
392
- /(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$/i,
393
- '$1um',
394
- ],
395
- [
396
- /(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i,
397
- '$1us',
398
- ],
399
- [/(test)(?:is|es)$/i, '$1is'],
400
- [/(movie|twelve|abuse|e[mn]u)s$/i, '$1'],
401
- [
402
- /(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$/i,
403
- '$1sis',
404
- ],
405
- [
406
- /(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$/i,
407
- '$1',
408
- ],
409
- [/(seraph|cherub)im$/i, '$1'],
410
- [/\b((?:tit)?m|l)ice$/i, '$1ouse'],
411
- [/\b(mon|smil)ies$/i, '$1ey'],
412
- [
413
- /\b([pl]|zomb|(?:neck|cross)?t|coll|faer|food|gen|goon|group|lass|talk|goal|cut)ies$/i,
414
- '$1ie',
415
- ],
416
- [/ies$/i, 'y'],
417
- [/(ar|(?:wo|[ae])l|[eo][ao])ves$/i, '$1f'],
418
- [
419
- /(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$/i,
420
- '$1fe',
421
- ],
422
- [/(ss)$/i, '$1'],
423
- [/(quiz)zes$/i, '$1'],
424
- [/(matr)ices$/i, '$1ix'],
425
- [/(vert|ind)ices$/i, '$1ex'],
426
- [/^(ox)en$/i, '$1'],
427
- [/(alias|status)es$/i, '$1'],
428
- [/(octop|vir)i$/i, '$1us'],
429
- [/(cris|ax|test)es$/i, '$1is'],
430
- [/(shoe)s$/i, '$1'],
431
- [/(o)es$/i, '$1'],
432
- [/(bus)es$/i, '$1'],
433
- [/ices$/i, 'ex'],
434
- [/(hive)s$/i, '$1'],
435
- [/(tive)s$/i, '$1'],
436
- [/([^f])ves$/i, '$1fe'],
437
- [/([lr])ves$/i, '$1f'],
438
- [/(^analy)ses$/i, '$1sis'],
439
- [/([ti])a$/i, '$1um'],
440
- [/(n)ews$/i, '$1ews'],
441
- // <-- put generic catch-all last
442
- [/s$/i, ''],
443
- ]);
@@ -1 +1 @@
1
- {"version":3,"file":"Pluralizer.d.ts","sourceRoot":"","sources":["../../../src/pluralize/Pluralizer.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAgB,gBAAgB,EAAiB,MAAM,SAAS,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,UAAU;;IAOtB;;;OAGG;;IAoFH;;;;;;OAMG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAItD;;;;;;OAMG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAIxD;;;;;;OAMG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAM3C;;;;;;OAMG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAOlD;;;;;;;;OAQG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM;IAY/D;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAiB9B;;;;;;OAMG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAiBhC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAO/B;;;;;;OAMG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAMjC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,UAAU,YAAmB,CAAC"}
1
+ {"version":3,"file":"Pluralizer.d.ts","sourceRoot":"","sources":["../../../src/pluralize/Pluralizer.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAgB,gBAAgB,EAAiB,MAAM,SAAS,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,UAAU;;IAOtB;;;OAGG;;IAuGH;;;;;;OAMG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAItD;;;;;;OAMG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAIxD;;;;;;OAMG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAM3C;;;;;;OAMG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAOlD;;;;;;;;OAQG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM;IAY/D;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAiB9B;;;;;;OAMG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAiBhC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAc/B;;;;;;OAMG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAajC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,UAAU,YAAmB,CAAC"}
@@ -1,10 +1,10 @@
1
1
  import type { PluralizeRule } from './types';
2
- /** Irregular rules and replacements */
2
+ /** Irregular singular plural mappings */
3
3
  export declare const irregularRules: readonly [string, string][];
4
- /** Plural rules and replacements */
4
+ /** Pluralization rules with regex and replacements */
5
5
  export declare const pluralRules: readonly PluralizeRule[];
6
- /** Uncountable constants */
7
- export declare const uncountables: Readonly<Set<string | RegExp>>;
8
- /** Singular rules and replacements */
6
+ /** Singularization rules with regex and replacements */
9
7
  export declare const singularRules: readonly PluralizeRule[];
8
+ /** Uncountable words */
9
+ export declare const uncountables: Readonly<Set<string | RegExp>>;
10
10
  //# sourceMappingURL=rules.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/pluralize/rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,uCAAuC;AACvC,eAAO,MAAM,cAAc,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,EA+EnD,CAAC;AAEJ,oCAAoC;AACpC,eAAO,MAAM,WAAW,EAAE,SAAS,aAAa,EAqC7C,CAAC;AAEJ,4BAA4B;AAC5B,eAAO,MAAM,YAAY,gCAiQxB,CAAC;AAEF,sCAAsC;AACtC,eAAO,MAAM,aAAa,EAAE,SAAS,aAAa,EAkE/C,CAAC"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/pluralize/rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,2CAA2C;AAC3C,eAAO,MAAM,cAAc,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,EA2FnD,CAAC;AAEJ,sDAAsD;AACtD,eAAO,MAAM,WAAW,EAAE,SAAS,aAAa,EAqC7C,CAAC;AAEJ,wDAAwD;AACxD,eAAO,MAAM,aAAa,EAAE,SAAS,aAAa,EAoE/C,CAAC;AAEJ,wBAAwB;AACxB,eAAO,MAAM,YAAY,gCAmQxB,CAAC"}
@@ -53,17 +53,31 @@ export class Pluralizer {
53
53
  });
54
54
  }
55
55
  #restoreCase(original, transformed) {
56
+ // Exact match
57
+ if (original === transformed)
58
+ return transformed;
59
+ // Entire original is lowercase
60
+ if (original === original.toLowerCase()) {
61
+ return transformed.toLowerCase();
62
+ }
63
+ // Entire original is uppercase
64
+ if (original === original.toUpperCase()) {
65
+ return transformed.toUpperCase();
66
+ }
67
+ // Title case (first letter uppercase, rest lowercase)
68
+ if (original[0] === original[0].toUpperCase() &&
69
+ original.slice(1) === original.slice(1).toLowerCase()) {
70
+ return (transformed.charAt(0).toUpperCase() +
71
+ transformed.slice(1).toLowerCase());
72
+ }
73
+ // Mixed case: per-character casing
56
74
  let result = '';
57
75
  for (let i = 0; i < transformed.length; i++) {
58
76
  const origChar = original[i];
59
- if (origChar) {
60
- if (origChar.toUpperCase() === origChar &&
61
- origChar.toLowerCase() !== origChar) {
62
- result += transformed[i].toUpperCase();
63
- }
64
- else {
65
- result += transformed[i].toLowerCase();
66
- }
77
+ if (origChar &&
78
+ origChar === origChar.toUpperCase() &&
79
+ origChar !== origChar.toLowerCase()) {
80
+ result += transformed[i].toUpperCase();
67
81
  }
68
82
  else {
69
83
  result += transformed[i].toLowerCase();
@@ -207,11 +221,18 @@ export class Pluralizer {
207
221
  * pluralizer.isPlural('children'); // true
208
222
  */
209
223
  isPlural(word) {
210
- const lower = word?.toLowerCase();
211
- if (this.#isUncountable(word))
224
+ if (!isNonEmptyString(word))
212
225
  return false;
213
- if (this.#irregularPlurals?.[lower])
226
+ const lower = word.toLowerCase();
227
+ // if uncountable return true
228
+ if (this.#isUncountable(lower))
229
+ return true;
230
+ // directly known as plural
231
+ if (this.#irregularPlurals[lower])
214
232
  return true;
233
+ // directly known as singular
234
+ if (this.#irregularSingles[lower])
235
+ return false;
215
236
  return this.toSingular(lower) !== lower;
216
237
  }
217
238
  /**
@@ -222,12 +243,19 @@ export class Pluralizer {
222
243
  * pluralizer.isSingular('child'); // true
223
244
  */
224
245
  isSingular(word) {
225
- const lower = word?.toLowerCase();
226
- if (this.#isUncountable(word))
246
+ if (!isNonEmptyString(word))
227
247
  return false;
228
- if (this.#irregularSingles?.[lower])
248
+ const lower = word.toLowerCase();
249
+ // if uncountable return true
250
+ if (this.#isUncountable(lower))
251
+ return true;
252
+ // directly known as singular
253
+ if (this.#irregularSingles[lower])
229
254
  return true;
230
- return this.toPlural(lower) !== lower;
255
+ // directly known as plural
256
+ if (this.#irregularPlurals[lower])
257
+ return false;
258
+ return this.toSingular(lower) === lower;
231
259
  }
232
260
  }
233
261
  /**
@@ -1,4 +1,4 @@
1
- /** Irregular rules and replacements */
1
+ /** Irregular singular plural mappings */
2
2
  export const irregularRules =
3
3
  /* @__PURE__ */ Object.freeze([
4
4
  // Pronouns
@@ -23,70 +23,79 @@ export const irregularRules =
23
23
  ['his', 'their'],
24
24
  ['her', 'their'],
25
25
  // Common irregulars
26
+ ['analysis', 'analyses'],
27
+ ['anathema', 'anathemata'],
28
+ ['appendix', 'appendices'],
29
+ ['automaton', 'automata'],
30
+ ['basis', 'bases'],
31
+ ['calf', 'calves'],
32
+ ['carve', 'carves'],
26
33
  ['child', 'children'],
27
- ['person', 'people'],
28
- ['man', 'men'],
29
- ['woman', 'women'],
30
- ['tooth', 'teeth'],
34
+ ['codex', 'codices'],
35
+ ['criterion', 'criteria'],
36
+ ['crisis', 'crises'],
37
+ ['datum', 'data'],
38
+ ['diagnosis', 'diagnoses'],
39
+ ['die', 'dice'],
40
+ ['dogma', 'dogmata'],
41
+ ['echo', 'echoes'],
42
+ ['elf', 'elves'],
31
43
  ['foot', 'feet'],
32
- ['mouse', 'mice'],
44
+ ['genus', 'genera'],
33
45
  ['goose', 'geese'],
34
- ['ox', 'oxen'],
46
+ ['groove', 'grooves'],
47
+ ['half', 'halves'],
48
+ ['hedron', 'hedra'],
49
+ ['hero', 'heroes'],
50
+ ['hoof', 'hooves'],
51
+ ['human', 'humans'],
52
+ ['honey', 'honeys'],
53
+ ['index', 'indices'],
35
54
  ['leaf', 'leaves'],
36
- ['datum', 'data'],
55
+ ['lemma', 'lemmata'],
56
+ ['loaf', 'loaves'],
57
+ ['looey', 'looies'],
58
+ ['man', 'men'],
59
+ ['mango', 'mangoes'],
60
+ ['matrix', 'matrices'],
37
61
  ['medium', 'media'],
38
- ['analysis', 'analyses'],
39
- ['diagnosis', 'diagnoses'],
40
- ['basis', 'bases'],
41
- ['thesis', 'theses'],
42
- ['crisis', 'crises'],
62
+ ['mouse', 'mice'],
63
+ ['neurosis', 'neuroses'],
64
+ ['noumenon', 'noumena'],
65
+ ['organon', 'organa'],
66
+ ['ox', 'oxen'],
67
+ ['passerby', 'passersby'],
43
68
  ['phenomenon', 'phenomena'],
44
- ['criterion', 'criteria'],
45
- ['index', 'indices'],
46
- ['matrix', 'matrices'],
47
- ['vertex', 'vertices'],
48
- ['quiz', 'quizzes'],
49
- ['die', 'dice'],
50
- ['yes', 'yeses'],
51
- ['human', 'humans'],
69
+ ['pickaxe', 'pickaxes'],
70
+ ['potato', 'potatoes'],
71
+ ['prolegomenon', 'prolegomena'],
52
72
  ['proof', 'proofs'],
53
- ['carve', 'carves'],
54
- ['valve', 'valves'],
55
- ['looey', 'looies'],
73
+ ['quiz', 'quizzes'],
74
+ ['schema', 'schemata'],
75
+ ['self', 'selves'],
76
+ ['shelf', 'shelves'],
77
+ ['stigma', 'stigmata'],
78
+ ['stoma', 'stomata'],
56
79
  ['thief', 'thieves'],
57
- ['groove', 'grooves'],
58
- ['pickaxe', 'pickaxes'],
59
- ['passerby', 'passersby'],
60
- ['honey', 'honeys'],
61
- // Words ending in with a consonant and `o`.
62
- ['echo', 'echoes'],
63
- ['dingo', 'dingoes'],
64
- ['mango', 'mangoes'],
65
- ['volcano', 'volcanoes'],
66
- ['tornado', 'tornadoes'],
80
+ ['tooth', 'teeth'],
81
+ ['tomato', 'tomatoes'],
67
82
  ['torpedo', 'torpedoes'],
68
- // Ends with `us`.
69
- ['genus', 'genera'],
83
+ ['tornado', 'tornadoes'],
84
+ ['valve', 'valves'],
85
+ ['vertex', 'vertices'],
86
+ ['virus', 'viruses'],
70
87
  ['viscus', 'viscera'],
71
- // Ends with `ma`.
72
- ['stigma', 'stigmata'],
73
- ['stoma', 'stomata'],
74
- ['dogma', 'dogmata'],
75
- ['lemma', 'lemmata'],
76
- ['schema', 'schemata'],
77
- ['anathema', 'anathemata'],
88
+ ['volcano', 'volcanoes'],
89
+ ['woman', 'women'],
90
+ ['wolf', 'wolves'],
91
+ ['yes', 'yeses'],
78
92
  ]);
79
- /** Plural rules and replacements */
93
+ /** Pluralization rules with regex and replacements */
80
94
  export const pluralRules =
81
95
  /* @__PURE__ */ Object.freeze([
82
- [/(\P{ASCII})$/u, '$1'],
96
+ [/s?$/i, 's'],
83
97
  [/(pe)(rson|ople)$/i, '$1ople'],
84
98
  [/(child)(?:ren)?$/i, '$1ren'],
85
- [/(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$/i, '$1ices'],
86
- [
87
- /(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i,
88
- '$1i',
89
- ],
90
99
  [/(alumn|alg|vertebr)(?:a|ae)$/i, '$1ae'],
91
100
  [
92
101
  /(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i,
@@ -105,18 +114,94 @@ export const pluralRules =
105
114
  [/(x|ch|ss|sh|zz)$/i, '$1es'],
106
115
  [/(her|at|gr)o$/i, '$1oes'],
107
116
  [/sis$/i, 'ses'],
108
- [/(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$/i, '$1$2ves'],
117
+ [/^(chief|chef|belief|roof|cliff|reef)$/i, '$1s'],
118
+ [/(seraph|cherub)$/i, '$1im'],
119
+ [/(kni|wi|li)fe$/i, '$1ves'],
120
+ [/(ar|l|ea|eo|oa|hoo)f$/i, '$1ves'],
109
121
  [/([^aeiouy]|qu)y$/i, '$1ies'],
110
122
  [/(tive)$/i, '$1s'],
111
123
  [/(hive)$/i, '$1s'],
112
124
  [/(quiz)$/i, '$1zes'],
113
125
  [/m[ae]n$/i, 'men'],
114
126
  [/eaux$/i, '$0'],
115
- // fallback
116
- [/s$/i, 's'], // <--- final catch-all
127
+ [
128
+ /(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i,
129
+ '$1i',
130
+ ],
117
131
  ]);
118
- /** Uncountable constants */
132
+ /** Singularization rules with regex and replacements */
133
+ export const singularRules =
134
+ /* @__PURE__ */ Object.freeze([
135
+ [/s$/i, ''],
136
+ [/(\P{ASCII})$/u, '$1'],
137
+ [/(pe)(rson|ople)$/i, '$1rson'],
138
+ [/(child)ren$/i, '$1'],
139
+ [/(eau)x?$/i, '$1'],
140
+ [/men$/i, 'man'],
141
+ [/(matr|append)ices$/i, '$1ix'],
142
+ [/(cod|mur|sil|vert|ind)ices$/i, '$1ex'],
143
+ [/(alumn|alg|vertebr)ae$/i, '$1a'],
144
+ [
145
+ /(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i,
146
+ '$1on',
147
+ ],
148
+ [
149
+ /(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$/i,
150
+ '$1um',
151
+ ],
152
+ [/(test)(?:is|es)$/i, '$1is'],
153
+ [/(movie|twelve|abuse|e[mn]u)s$/i, '$1'],
154
+ [
155
+ /(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$/i,
156
+ '$1sis',
157
+ ],
158
+ [
159
+ /(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$/i,
160
+ '$1',
161
+ ],
162
+ [/^(chief|chef|belief|roof|cliff|reef)s$/i, '$1'],
163
+ [/(seraph|cherub)im$/i, '$1'],
164
+ [/\b((?:tit)?m|l)ice$/i, '$1ouse'],
165
+ [/\b(mon|smil)ies$/i, '$1ey'],
166
+ [
167
+ /\b([pl]|zomb|(?:neck|cross)?t|coll|faer|food|gen|goon|group|lass|talk|goal|cut)ies$/i,
168
+ '$1ie',
169
+ ],
170
+ [/ies$/i, 'y'],
171
+ [/(kni|wi|li)ves$/i, '$1fe'],
172
+ [/(ar|l|ea|eo|oa|hoo)ves$/i, '$1f'],
173
+ [/(ar|(?:wo|[ae])l|[eo][ao])ves$/i, '$1f'],
174
+ [
175
+ /(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$/i,
176
+ '$1fe',
177
+ ],
178
+ [/(ss)$/i, '$1'],
179
+ [/(quiz)zes$/i, '$1'],
180
+ [/(vert|ind)ices$/i, '$1ex'],
181
+ [/^(ox)en$/i, '$1'],
182
+ [/(alias|status)es$/i, '$1'],
183
+ [/(octop|vir)i$/i, '$1us'],
184
+ [/(cris|ax|test)es$/i, '$1is'],
185
+ [/(shoe)s$/i, '$1'],
186
+ [/(her|at|gr)oes$/i, '$1o'],
187
+ [/oes$/i, 'o'],
188
+ [/(bus)es$/i, '$1'],
189
+ [/ices$/i, 'ex'],
190
+ [/(hive)s$/i, '$1'],
191
+ [/(tive)s$/i, '$1'],
192
+ [/([^f])ves$/i, '$1fe'],
193
+ [/([lr])ves$/i, '$1f'],
194
+ [/(^analy)ses$/i, '$1sis'],
195
+ [/([ti])a$/i, '$1um'],
196
+ [/(n)ews$/i, '$1ews'],
197
+ [
198
+ /(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i,
199
+ '$1us',
200
+ ],
201
+ ]);
202
+ /** Uncountable words */
119
203
  export const uncountables = /* @__PURE__ */ Object.freeze(new Set([
204
+ // common
120
205
  'aircraft',
121
206
  'alcohol',
122
207
  'ammo',
@@ -167,6 +252,7 @@ export const uncountables = /* @__PURE__ */ Object.freeze(new Set([
167
252
  'weather',
168
253
  'wildlife',
169
254
  'you',
255
+ // abstract
170
256
  'adulthood',
171
257
  'advertising',
172
258
  'anger',
@@ -178,9 +264,9 @@ export const uncountables = /* @__PURE__ */ Object.freeze(new Set([
178
264
  'botany',
179
265
  'carbon',
180
266
  'chaos',
181
- 'coffee',
182
267
  'cheese',
183
268
  'childhood',
269
+ 'coffee',
184
270
  'compassion',
185
271
  'cotton',
186
272
  'dancing',
@@ -355,7 +441,7 @@ export const uncountables = /* @__PURE__ */ Object.freeze(new Set([
355
441
  'youth',
356
442
  'zinc',
357
443
  'zoology',
358
- // RegEx(es)
444
+ // regex-based uncountables
359
445
  /pok[eé]mon$/i,
360
446
  /[^aeiou]ese$/i,
361
447
  /deer$/i,
@@ -370,71 +456,3 @@ export const uncountables = /* @__PURE__ */ Object.freeze(new Set([
370
456
  /trousers$/i,
371
457
  /jeans$/i,
372
458
  ]));
373
- /** Singular rules and replacements */
374
- export const singularRules =
375
- /* @__PURE__ */ Object.freeze([
376
- [/(\P{ASCII})$/u, '$1'],
377
- [/(pe)(rson|ople)$/i, '$1rson'],
378
- [/(child)ren$/i, '$1'],
379
- [/(eau)x?$/i, '$1'],
380
- [/men$/i, 'man'],
381
- [/(matr|append)ices$/i, '$1ix'],
382
- [/(cod|mur|sil|vert|ind)ices$/i, '$1ex'],
383
- [/(alumn|alg|vertebr)ae$/i, '$1a'],
384
- [
385
- /(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i,
386
- '$1on',
387
- ],
388
- [
389
- /(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$/i,
390
- '$1um',
391
- ],
392
- [
393
- /(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i,
394
- '$1us',
395
- ],
396
- [/(test)(?:is|es)$/i, '$1is'],
397
- [/(movie|twelve|abuse|e[mn]u)s$/i, '$1'],
398
- [
399
- /(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$/i,
400
- '$1sis',
401
- ],
402
- [
403
- /(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$/i,
404
- '$1',
405
- ],
406
- [/(seraph|cherub)im$/i, '$1'],
407
- [/\b((?:tit)?m|l)ice$/i, '$1ouse'],
408
- [/\b(mon|smil)ies$/i, '$1ey'],
409
- [
410
- /\b([pl]|zomb|(?:neck|cross)?t|coll|faer|food|gen|goon|group|lass|talk|goal|cut)ies$/i,
411
- '$1ie',
412
- ],
413
- [/ies$/i, 'y'],
414
- [/(ar|(?:wo|[ae])l|[eo][ao])ves$/i, '$1f'],
415
- [
416
- /(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$/i,
417
- '$1fe',
418
- ],
419
- [/(ss)$/i, '$1'],
420
- [/(quiz)zes$/i, '$1'],
421
- [/(matr)ices$/i, '$1ix'],
422
- [/(vert|ind)ices$/i, '$1ex'],
423
- [/^(ox)en$/i, '$1'],
424
- [/(alias|status)es$/i, '$1'],
425
- [/(octop|vir)i$/i, '$1us'],
426
- [/(cris|ax|test)es$/i, '$1is'],
427
- [/(shoe)s$/i, '$1'],
428
- [/(o)es$/i, '$1'],
429
- [/(bus)es$/i, '$1'],
430
- [/ices$/i, 'ex'],
431
- [/(hive)s$/i, '$1'],
432
- [/(tive)s$/i, '$1'],
433
- [/([^f])ves$/i, '$1fe'],
434
- [/([lr])ves$/i, '$1f'],
435
- [/(^analy)ses$/i, '$1sis'],
436
- [/([ti])a$/i, '$1um'],
437
- [/(n)ews$/i, '$1ews'],
438
- // <-- put generic catch-all last
439
- [/s$/i, ''],
440
- ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nhb-toolbox",
3
- "version": "4.13.4",
3
+ "version": "4.13.7",
4
4
  "description": "A versatile collection of smart, efficient, and reusable utility functions and classes for everyday development needs.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -243,6 +243,7 @@
243
243
  },
244
244
  "scripts": {
245
245
  "test": "jest --coverage --verbose",
246
+ "start": "start coverage/lcov-report/index.html",
246
247
  "format": "nhb-format",
247
248
  "lint": "nhb-lint",
248
249
  "fix": "nhb-fix",