nhb-toolbox
Version:
A versatile collection of smart, efficient, and reusable utility functions and classes for everyday development needs.
289 lines (288 loc) • 11.4 kB
JavaScript
import { isNonEmptyString } from '../guards/primitives.js';
import { normalizeNumber } from '../number/utilities.js';
import { irregularRules, pluralRules, singularRules, uncountables, } from './rules.js';
/**
* * Handles English word pluralization and singularization with support for irregular forms and uncountable nouns.
*
* - Provides methods to convert words between singular and plural forms, check if a word is plural or singular, and manage custom pluralization rules.
* - Supports adding custom pluralization and singularization rules, as well as uncountable nouns.
* - Automatically handles common irregular forms like "child" to "children"
* - Automatically loads common irregular forms and uncountable nouns.
* - Supports options for count-based pluralization, allowing for inclusive formatting.
* - This class is useful for applications that need to handle natural language processing, such as chatbots, content management systems, or any text processing tasks that require accurate pluralization.
*
* @remarks For simpler pluralization (plural with only 's'), please refer to {@link https://nhb-toolbox.vercel.app/docs/utilities/string/formatUnitWithPlural formatUnitWithPlural} instead.
*
* @remarks For ready to use instance, please refer to {@link https://nhb-toolbox.vercel.app/docs/utilities/string/pluralizer pluralizer} instead.
*
* @example
* const pluralizer = new Pluralizer();
* pluralizer.pluralize('child'); // "children"
* pluralizer.toSingular('geese'); // "goose"
* pluralizer.isPlural('fish'); // false (uncountable)
*/
export class Pluralizer {
#pluralRules = [];
#singularRules = [];
#uncountables = new Set();
#irregularSingles = {};
#irregularPlurals = {};
/**
* Initializes the Pluralizer with default rules and exceptions.
* Automatically loads irregular, pluralization and singular rules along with pre-defined uncountable nouns.
*/
constructor() {
this.#loadRules();
}
#loadRules() {
// ! Load irregular rules
irregularRules.forEach(([single, plural]) => {
this.addIrregular(single, plural);
});
// ! Load plural rules
pluralRules.forEach(([rule, replacement]) => {
this.addPluralRule(rule, replacement);
});
// ! Load singular rules
singularRules.forEach(([rule, replacement]) => {
this.addSingularRule(rule, replacement);
});
// ! Load uncountables
uncountables.forEach((word) => {
this.addUncountable(word);
});
}
#restoreCase(original, transformed) {
// Exact match
if (original === transformed)
return transformed;
// Entire original is lowercase
if (original === original.toLowerCase()) {
return transformed.toLowerCase();
}
// Entire original is uppercase
if (original === original.toUpperCase()) {
return transformed.toUpperCase();
}
// Title case (first letter uppercase, rest lowercase)
if (original[0] === original[0].toUpperCase() &&
original.slice(1) === original.slice(1).toLowerCase()) {
return (transformed.charAt(0).toUpperCase() +
transformed.slice(1).toLowerCase());
}
// Mixed case: per-character casing
let result = '';
for (let i = 0; i < transformed.length; i++) {
const origChar = original[i];
if (origChar &&
origChar === origChar.toUpperCase() &&
origChar !== origChar.toLowerCase()) {
result += transformed[i].toUpperCase();
}
else {
result += transformed[i].toLowerCase();
}
}
return result;
}
#sanitizeWord(word, rules) {
if (!isNonEmptyString(word))
return '';
if (this.#isUncountable(word)) {
return word;
}
for (let i = rules.length - 1; i >= 0; i--) {
const [rule, replacement] = rules[i];
if (rule.test(word)) {
return word.replace(rule, replacement);
}
}
return word;
}
/**
* Check if a word is uncountable.
* Supports both string and RegExp entries.
*/
#isUncountable(word) {
const lower = word?.toLowerCase();
for (const entry of this.#uncountables) {
if (typeof entry === 'string') {
if (entry === lower)
return true;
}
else {
if (entry.test(word))
return true;
}
}
return false;
}
/**
* * Add a new pluralization rule.
* @param rule Pattern to match singular words.
* @param replacement Replacement pattern for plural form.
* @example
* pluralizer.addPluralRule(/(quiz)$/i, '$1zes');
*/
addPluralRule(rule, replacement) {
this.#pluralRules.push([rule, replacement]);
}
/**
* * Add a new singularization rule.
* @param rule Pattern to match plural words.
* @param replacement Replacement pattern for singular form.
* @example
* pluralizer.addSingularRule(/(matr)ices$/i, '$1ix');
*/
addSingularRule(rule, replacement) {
this.#singularRules.push([rule, replacement]);
}
/**
* * Add a word or pattern that should never change between singular and plural.
* @param word A word or regex pattern.
* @example
* pluralizer.addUncountable('fish');
* pluralizer.addUncountable(/pok[eé]mon$/i);
*/
addUncountable(word) {
this.#uncountables.add(typeof word === 'string' ? word?.toLowerCase() : word);
}
/**
* * Add a custom irregular form.
* @param single Singular word.
* @param plural Plural word.
* @example
* pluralizer.addIrregular('person', 'people');
*/
addIrregular(single, plural) {
const singleLower = single?.toLowerCase();
const pluralLower = plural?.toLowerCase();
this.#irregularSingles[singleLower] = pluralLower;
this.#irregularPlurals[pluralLower] = singleLower;
}
/**
* * Get the proper singular or plural form based on optional count.
* @param word Target word to pluralize or singularize.
* @param options Optional count and inclusive formatting.
* @returns The transformed word.
* @example
* pluralizer.pluralize('category', { count: 3 }); // "categories"
* pluralizer.pluralize('child', { count: 1, inclusive: true }); // "1 child"
*/
pluralize(word, options = {}) {
const count = normalizeNumber(options?.count);
if (typeof count === 'number') {
const pluralized = count > 1 ? this.toPlural(word) : this.toSingular(word);
return options.inclusive ? `${count} ${pluralized}` : pluralized;
}
return this.toPlural(word);
}
/**
* * Convert a word to its plural form.
* @param word Singular form of the word.
* @returns Plural form of the word.
* @example
* pluralizer.toPlural('analysis'); // "analyses"
*/
toPlural(word) {
if (!isNonEmptyString(word))
return '';
const lower = word.toLowerCase();
if (this.#isUncountable(word))
return word;
if (this.#irregularSingles[lower]) {
return this.#restoreCase(word, this.#irregularSingles[lower]);
}
return this.#restoreCase(word, this.#sanitizeWord(lower, this.#pluralRules));
}
/**
* * Convert a word to its singular form.
* @param word Plural form of the word.
* @returns Singular form of the word.
* @example
* pluralizer.toSingular('geese'); // "goose"
*/
toSingular(word) {
if (!isNonEmptyString(word))
return '';
const lower = word.toLowerCase();
if (this.#isUncountable(word))
return word;
if (this.#irregularPlurals[lower]) {
return this.#restoreCase(word, this.#irregularPlurals[lower]);
}
return this.#restoreCase(word, this.#sanitizeWord(lower, this.#singularRules));
}
/**
* * Check if a given word is plural.
* @param word Word to check.
* @returns `true` if the word is plural, otherwise `false`.
* @remarks Always returns `true` for uncountable nouns.
* @example
* pluralizer.isPlural('children'); // true
* pluralizer.isPlural('water'); // true
*/
isPlural(word) {
if (!isNonEmptyString(word))
return false;
const lower = word.toLowerCase();
// if uncountable return true
if (this.#isUncountable(lower))
return true;
// directly known as plural
if (this.#irregularPlurals[lower])
return true;
// directly known as singular
if (this.#irregularSingles[lower])
return false;
return this.toSingular(lower) !== lower;
}
/**
* * Check if a given word is singular.
* @param word Word to check.
* @returns True if the word is singular, otherwise false.
* @remarks Always returns `true` for uncountable nouns.
* @example
* pluralizer.isSingular('child'); // true
* pluralizer.isPlural('water'); // true
*/
isSingular(word) {
if (!isNonEmptyString(word))
return false;
const lower = word.toLowerCase();
// if uncountable return true
if (this.#isUncountable(lower))
return true;
// directly known as singular
if (this.#irregularSingles[lower])
return true;
// directly known as plural
if (this.#irregularPlurals[lower])
return false;
return this.toSingular(lower) === lower;
}
}
/**
* Default shared instance of {@link https://nhb-toolbox.vercel.app/docs/classes/Pluralizer Pluralizer}.
*
* - _Use this when you don’t need multiple configurations._
* - _It comes preloaded with standard pluralization rules, irregular forms, and uncountable nouns._
*
* @remarks For simpler pluralization (plural with only 's'), please refer to {@link https://nhb-toolbox.vercel.app/docs/utilities/string/formatUnitWithPlural formatUnitWithPlural} instead.
*
* * Handles English word pluralization and singularization with support for irregular forms and uncountable nouns.
*
* - Provides methods to convert words between singular and plural forms, check if a word is plural or singular, and manage custom pluralization rules.
* - Supports adding custom pluralization and singularization rules, as well as uncountable nouns.
* - Automatically handles common irregular forms like "child" to "children"
* - Automatically loads common irregular forms and uncountable nouns.
* - Supports options for count-based pluralization, allowing for inclusive formatting.
* - This is useful for applications that need to handle natural language processing, such as chatbots, content management systems, or any text processing tasks that require accurate pluralization.
* @example
* import { pluralizer } from 'nhb-toolbox';
*
* pluralizer.pluralize('child'); // "children"
* pluralizer.toSingular('geese'); // "goose"
* pluralizer.isPlural('fish'); // false (uncountable)
*/
export const pluralizer = new Pluralizer();