From cf210adfd3553a4f784e85950293f6129c82a2a4 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Tue, 18 Aug 2026 14:33:18 +0900 Subject: [PATCH] fix: singularize/pluralize regular -s nouns like gas/lens singularize() applies its rules first-match, front-to-back. For the small class of regular nouns whose singular already ends in -s and whose plural adds -es (gas/gases, lens/lenses), no -es rule matches, so the word falls through to the generic tail rule /s$/ -> '' and gets over-stripped (gases -> gase, lenses -> lense). The same generic rule also mangles the already-singular form (gas -> ga), breaking the singularize(pluralize(x)) === x round-trip. Add start-anchored, bidirectional rules for the safe -s -> -ses class gas|lens|bias|atlas|iris, following the existing summonses/nucleuses convention already in the file. Anchoring ^...$ keeps the bare protect rule from wrongly protecting words that merely end in the stem (sagas/omegas/cameras) and keeps houses -> house / buses -> bus working. Fixes #123. --- src/inflection.ts | 6 ++++++ test/inflection.test.ts | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/inflection.ts b/src/inflection.ts index 0cd03c9..86528a8 100644 --- a/src/inflection.ts +++ b/src/inflection.ts @@ -378,6 +378,7 @@ const regex = { genera: new RegExp('^genera$', 'gi'), nuclei: new RegExp('^(nucle)i$', 'gi'), nucleuses: new RegExp('^(nucle)uses$', 'gi'), + gases: new RegExp('^(gas|lens|bias|atlas|iris)es$', 'gi'), ss: new RegExp('ss$', 'gi'), s: new RegExp('s$', 'gi'), }, @@ -412,6 +413,7 @@ const regex = { criterion: new RegExp('^(criteri)on$', 'gi'), genus: new RegExp('^genus$', 'gi'), nucleus: new RegExp('^(nucle)us$', 'gi'), + gas: new RegExp('^(gas|lens|bias|atlas|iris)$', 'gi'), s: new RegExp('s$', 'gi'), common: new RegExp('$', 'gi'), }, @@ -453,6 +455,7 @@ const pluralRules: [RegExp, string?][] = [ [regex.plural.genera], [regex.plural.nuclei], [regex.plural.nucleuses], + [regex.plural.gases], // original rule [regex.singular.man, '$1en'], @@ -484,6 +487,7 @@ const pluralRules: [RegExp, string?][] = [ [regex.singular.criterion, '$1a'], [regex.singular.genus, 'genera'], [regex.singular.nucleus, '$1i'], + [regex.singular.gas, '$1es'], [regex.singular.s, 's'], [regex.singular.common, 's'], @@ -522,6 +526,7 @@ const singularRules: [RegExp, string?][] = [ [regex.singular.criterion], [regex.singular.genus], [regex.singular.nucleus], + [regex.singular.gas], // original rule [regex.plural.men, '$1an'], @@ -560,6 +565,7 @@ const singularRules: [RegExp, string?][] = [ [regex.plural.geese, 'goose'], [regex.plural.quizzes, '$1'], [regex.plural.whereases, '$1'], + [regex.plural.gases, '$1'], [regex.plural.ss, 'ss'], [regex.plural.s, ''], diff --git a/test/inflection.test.ts b/test/inflection.test.ts index 2e0a16c..7c1031e 100644 --- a/test/inflection.test.ts +++ b/test/inflection.test.ts @@ -54,6 +54,10 @@ describe('test .pluralize', function () { expect(inflection.pluralize('nucleus')).toEqual('nuclei'); expect(inflection.pluralize('nuclei')).toEqual('nuclei'); expect(inflection.pluralize('nucleuses')).toEqual('nucleuses'); + expect(inflection.pluralize('gas')).toEqual('gases'); + expect(inflection.pluralize('gases')).toEqual('gases'); + expect(inflection.pluralize('lens')).toEqual('lenses'); + expect(inflection.pluralize('lenses')).toEqual('lenses'); }); }); @@ -114,6 +118,21 @@ describe('test .singularize', function () { expect(inflection.singularize('analyses')).toEqual('analysis'); expect(inflection.singularize('bases')).toEqual('basis'); expect(inflection.singularize('diagnoses')).toEqual('diagnosis'); + // regular -s nouns whose plural ends in -ses (issue #123) + expect(inflection.singularize('gases')).toEqual('gas'); + expect(inflection.singularize('lenses')).toEqual('lens'); + expect(inflection.singularize('gas')).toEqual('gas'); + expect(inflection.singularize('lens')).toEqual('lens'); + // singularize(pluralize(x)) === x round-trip + expect(inflection.singularize(inflection.pluralize('gas'))).toEqual('gas'); + expect(inflection.singularize(inflection.pluralize('lens'))).toEqual('lens'); + // must not regress: -ses plurals that are not the anchored class + expect(inflection.singularize('houses')).toEqual('house'); + expect(inflection.singularize('buses')).toEqual('bus'); + // must not over-protect: bare -gas/-s words that merely end in the stem + expect(inflection.singularize('sagas')).toEqual('saga'); + expect(inflection.singularize('omegas')).toEqual('omega'); + expect(inflection.singularize('cameras')).toEqual('camera'); }); });