Created
July 17, 2024 07:39
-
-
Save lionel-rowe/486a4838ce99971ac1a5ed17d74a4e8f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { unreachable } from '@std/assert' | |
/** | |
* Sibilant sounds that are pluralized with -es instead of -s. | |
* | |
* Include double ss/zz but not single s/z, as the latter are more likely to be part of -se/-ze suffix. | |
* Same reasoning for -tch vs -ch. | |
* | |
* For example: | |
* - basses <-> bass (not "basse") | |
* - bases <-> base (not "bas") | |
* - buzzes <-> buzz (not "buzze") | |
* - hazes <-> haze (not "haz") | |
* - caches <-> cache (not "cach") | |
* - catches <-> catch (not "catche") | |
*/ | |
type Sibilant = typeof sibilants[number] | |
// deno-fmt-ignore | |
const sibilants = ['x', 'sh', 'ss', 'zz', 'tch'] as const | |
/** | |
* Consonant before y affects pluralization. | |
* | |
* For example: | |
* - ray <-> rays (not "raies") | |
* - city <-> cities (not "citys") | |
*/ | |
type Consonant = typeof consonants[number] | |
// deno-fmt-ignore | |
const consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'z'] as const | |
type PluralizeExceptions = typeof pluralizeExceptions | |
type SingularizeExceptions = typeof singularizeExceptions | |
const pluralizeExceptions = { | |
child: 'children', | |
person: 'people', | |
ox: 'oxen', | |
mouse: 'mice', | |
louse: 'lice', | |
tooth: 'teeth', | |
goose: 'geese', | |
foot: 'feet', | |
die: 'dice', | |
man: 'men', | |
woman: 'women', | |
} as const | |
const singularizeExceptions = Object.fromEntries(Object.entries(pluralizeExceptions).map(([sg, pl]) => [pl, sg])) as { | |
[K in keyof PluralizeExceptions as PluralizeExceptions[K]]: K | |
} | |
export type Singular<Pl extends string> = Pl extends keyof SingularizeExceptions ? SingularizeExceptions[Pl] | |
: Pl extends `${infer Prefix extends `${string}${Consonant}`}ies` ? `${Prefix}y` | |
: Pl extends `${infer Prefix extends `${string}${Sibilant}`}es` ? Prefix | |
: Pl extends `${infer Prefix extends string}ives` ? `${Prefix}ife` | |
: Pl extends `${infer Prefix extends string}ves` ? `${Prefix}f` | |
: Pl extends `${infer Prefix extends string}s` ? Prefix | |
: Pl | |
export type Plural<Sg extends string> = Sg extends keyof PluralizeExceptions ? PluralizeExceptions[Sg] | |
: Sg extends `${infer Prefix extends `${string}${Consonant}`}y` ? `${Prefix}ies` | |
: Sg extends `${string}${Sibilant}` ? `${Sg}es` | |
: Sg extends `${infer Prefix extends string}ife` ? `${Prefix}ives` | |
: Sg extends `${infer Prefix extends string}f` ? `${Prefix}ves` | |
: `${Sg}s` | |
const pluralizationRules = [ | |
[new RegExp(String.raw`(?<=${consonants.join('|')})y$`), 'ies'], | |
[new RegExp(String.raw`(?<=${sibilants.join('|')})$`), 'es'], | |
[/ife$/, 'ives'], | |
[/f$/, 'ves'], | |
[/$/, 's'], | |
] as const | |
const singularizationRules = [ | |
[new RegExp(String.raw`(?<=${consonants.join('|')})ies$`), 'y'], | |
[new RegExp(String.raw`(?<=${sibilants.join('|')})es$`), ''], | |
[/ives$/, 'ife'], | |
[/ves$/, 'f'], | |
[/s$/, ''], | |
] as const | |
export function pluralize<Sg extends string>(sg: Sg): Plural<Sg> { | |
if (Object.hasOwn(pluralizeExceptions, sg)) { | |
return pluralizeExceptions[sg as keyof PluralizeExceptions] as Plural<Sg> | |
} | |
for (const [matcher, replacer] of pluralizationRules) { | |
if (matcher.test(sg)) return sg.replace(matcher, replacer) as Plural<Sg> | |
} | |
unreachable('Every string matches /$/') | |
} | |
export function singularize<Pl extends string>(pl: Pl): Singular<Pl> { | |
if (Object.hasOwn(singularizeExceptions, pl)) { | |
return singularizeExceptions[pl as keyof SingularizeExceptions] as Singular<Pl> | |
} | |
for (const [matcher, replacer] of singularizationRules) { | |
if (matcher.test(pl)) return pl.replace(matcher, replacer) as Singular<Pl> | |
} | |
return pl as Singular<Pl> | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment