Skip to content

Instantly share code, notes, and snippets.

@lionel-rowe
Created July 17, 2024 07:39
Show Gist options
  • Save lionel-rowe/486a4838ce99971ac1a5ed17d74a4e8f to your computer and use it in GitHub Desktop.
Save lionel-rowe/486a4838ce99971ac1a5ed17d74a4e8f to your computer and use it in GitHub Desktop.
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