Skip to content

Instantly share code, notes, and snippets.

@panoply
Last active September 11, 2023 11:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save panoply/cf780499f0cd11c0dab6a73ca6945306 to your computer and use it in GitHub Desktop.
Save panoply/cf780499f0cd11c0dab6a73ca6945306 to your computer and use it in GitHub Desktop.
Pluralize

pluralize

String parser which enables pluralization. Extracted from an internal API logging system I maintain. It is fast, effective and an easy drop in solution. If you would like to see this in a module then please leave a comment.

Links

Usage

The function accepts a string as the first parameter which will contain the plural logic. The second parameter accepts an array or number. When you pass an array of numbers (eg: number[]) you can reference each index in that array to apply pluralized appenditures based on the position their indexes. If you pass an array or arrays (eg: any[][]) the function will fetch the lengths. It is hard to articulate in words, so see the below examples.

Note that N is sugar for 0 or the first items in the spread.

How it works

Below are a couple of example to help you understand how it works.

// Numbers
plural('#{N} #{0} #{1} #{2} #{3}', 1, 2, 3, 4) // => 1, 1, 2, 3, 4

// Arrays
plural('#{N} #{0} #{1} #{2} #{3}', ['x'], ['x','x'], ['x','x','x'], ['x','x','x','x'])  // => 1, 1, 2, 3, 4

// Pluralize cat and bird (first value is default)
plural('#{dog} #{cat}{1} #{bird}{2}', 1, 2, 3)  // => dog, cats, birds

// Pluralize dog (first value is default)
plural('#{dog} #{cat} #{bird}', 2, 1, 1)  // => dogs, cat, bird

// Pluralize birds based on 2nd index (it uses 0 based index when referencing)
plural('#{dog} #{cat} #{bird}{2}', 1, 2, 1)  // => dog, cat, birds

// Pluralize dogs and birds based on 2nd and 3rd index (it uses 0 based index when referencing)
plural('#{dog}{1} #{cat} #{bird}{2}', 1, 2, 2)  // => dogs, cat, birds

// Pluralize dog and use 2nd value (are) in the literal based on 2nd index in the spread
plural('The #{dog}{1} #{is|are}{1} chasing the #{cat}', 1, 2)  // => The dogs are chasing the cat

// Pluralize cat based on 2nd index in the spread
plural('The #{dog} #{is|are} chasing the #{cat}{1}', 1, 2)  // => The dog is chasing the cats

Number

Passing a number

Passing a spread of numbers and referencing them based on index position.

plural('#{N} #{person|people} #{is|are} boxing in #{N} #{fight}', 1)
// => 1 person is boxing in 1 fight

plural('#{N} #{person|people} #{is|are} boxing in #{N} #{fight}', 3)
// => 3 people are boxing in 3 fights

Spread of Numbers

Passing a spread of numbers and referencing them based on index position.

plural('#{N} #{person|people} #{is|are} boxing in #{1} #{fight}{1}', 2, 1)
// => 2 people are boxing in 1 fight

plural('#{N} #{person|people} #{is|are} boxing in #{1} #{fight}{1}', 1, 2)
// => 1 person is boxing in 2 fights

Spread of Arrays

Passing a spread of arrays, the function will convert these the index length.

const arr_1 = [{ foo: 'foo' }, { foo: 'bar'}, { foo: 'baz' }]
const arr_2 = ['random', 'values']

plural('#{N} #{person|people} #{is|are} boxing in #{0} #{fight}{1}', arr_1, arr_2)
// => 3 people are boxing in 2 fights

plural('#{N} #{person|people} #{is|are} boxing in #{N} #{fight}', arr_2)
// => 2 people are boxing in 2 fights
// HELPER FUNCTIONS
const { isArray } = Array
const isNumber =(value: any) => typeof value === 'number'
/**
* Get Plurals
*
* Parses string content messages returning pluralized versions.
* The Regex Expression looks for following string content.
*
* ---
*
* _Examples_
*
* - `#{N}`
*
* > Writes the _size_ number only. The `N` is replaced with the
* length of the array. For example, if the `size` (length of array)
* is `2` and the string is `#{N} #{product}` then `2 products` will
* be returned
*
* - `#{N}` or `#{1}` or `#{2}` etc
*
* > The `size` parameter is a spread and accepts multiple sizes.
* When multiple lengths are passed you can the reference its
* position. For example, if 2 sizes are passed to `size`, eg:
* `plural('#{N}, #{1} #{2}', 2, 5, 1)` can reference the length
* of the size by appending its zero offset number, eg: `#{N}`
* would return `2`, `#{1}` would return `5` and `#{2}` will return `1`.
*
* - `#{string}`
*
* > Returns plural when size more than `1`. The string surrounded by
* the parenthesis will be pluralized. For example if the `size`
* (length of array) is `2` and the string is `#{product}` then
* `products` will be returned
*
* - `#{string|other}`
*
* > Left is plural, right is singular plural. When size is more then `1` the
* right string is used, else the left side. This allows for different words,
* For example if the `size` (length of array) is `2` and the string is
* `#{quantity|quantities}` then `quantities` will be returned.
*
* - `#{string}{1}` or `#{string|other}{2}`
*
* > Allows a string to reference a size in the spread.
*
*/
export function plural (message: string, ...data: any[]): string {
let size: number | number[];
if (isNumber(data)) {
size = data;
} else if (isArray(data)) {
size = data.map((v, _, a) => (
isNumber(v) ? v : isArray(v) ? v.length : a.length
));
} else {
return message;
}
return message.replace(/#{[N0-9]{1}}|#{(?![N0-9])\w+(?:\|\w+)?}(?:{[N0-9]})?/g, text => {
if (text.charCodeAt(3) === 125) { // }=125
if (text.charCodeAt(2) === 78 || text.charCodeAt(2) === 48) {
return String(size[0] ?? text);
} else if (/[1-9]/.test(text[2])) {
return String(size[text[2]] ?? text);
}
}
const pipe = text.indexOf('|');
const nums = text.indexOf('}{');
const chars = text.length;
const length = /[1-9]/.test(text[chars - 2]) ? size[text[chars - 2]] : size[0];
return pipe > 2
? length > 1
? text.slice(pipe + 1, chars - (nums > 2 ? 4 : 1))
: text.slice(2, pipe)
: text.slice(2, chars - (nums > 2 ? 4 : 1)) + (length > 1 ? 's' : '');
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment