Skip to content

Commit

Permalink
change format behavior; add README
Browse files Browse the repository at this point in the history
  • Loading branch information
none23 committed Apr 29, 2019
1 parent fa532f9 commit fac9ce8
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 55 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# ru-plurals

[![npm version](http://img.shields.io/npm/v/ru-plurals.svg?style=flat)](https://npmjs.org/package/ru-plurals "View this project on npm")
[![flow coverage](https://img.shields.io/badge/flow%20coverage-99%25-brightgreen.svg)](https://flow.org)
[![jest coverage](https://img.shields.io/badge/jest%20coverage-92%25-brightgreen.svg)](https://jestjs.io)

Simple functional pluralization of Russian, Belarusian, and Ukrainian words.

## Install
```bash
npm install --save ru-plurals
# or
yarn add ru-plurals
```

## Usage
#### plural
```ts
import { plural } from 'ru-plurals';

const ruble = plural('рубль', 'рубля', 'рублей');
const work = plural('работает', 'работают'); // same as plural('работает', 'работают', 'работают');
const coffee = plural('кофе'); // same as plural('кофе', 'кофе', 'кофе')

ruble(101) // 'рубль'
ruble(500) // 'рублей'
work(21) // 'работает'
coffee(2) // 'кофе'
```
#### format
```ts
import { format } from 'ru-plurals';

const meters = format((count, word) => `${count} {word}`, 'метр', 'метра', 'метров');

meters(1) // '1 метр'
meters(200) // '200 метров'
```
with jsx:
```ts
const distance = format(
(count, word) => (
<div>
<div>{count}</div>
<span>{word}</span>
</div>
),
'метр',
'метра',
'метров',
);
```
12 changes: 8 additions & 4 deletions src/format.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// @flow strict

type Plural = (number: ?number) => string;
import plural from './plural';

// N - number parameter (e.g. or ?number)
// R - retyrn type
export type Formatter = <N, R>(number: N, word: string) => R;

function format(formatter: Formatter) {
return (_plural: Plural) => (number: ?number) =>
formatter(number, _plural(number));
function format(
formatter: Formatter,
one: string,
two?: string,
five?: string,
) {
return (number: ?number) => formatter(number, plural(one, two, five)(number));
}

export default format;
21 changes: 6 additions & 15 deletions src/format.test.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
/* eslint-disable sonarjs/no-duplicate-string */
import plural from './plural';
import format from './format';

// nouns
const ruble = plural('рубль', 'рубля', 'рублей');
const meter = plural('метр', 'метра', 'метров');
const seat = plural('место', 'места', 'мест');
const coffee = plural('кофе');

// formatting
const joinDash = (number, word) => `${number}-${word}`;
const joinSpace = (number, word) => `${number} ${word}`;
const skipZero = (number, word) => (number > 0 ? `${number} ${word}` : '-');

describe.each([
[joinDash, ruble, 0, '0-рублей'],
[joinSpace, meter, 1, '1 метр'],
[skipZero, coffee, 0, '-'],
[skipZero, seat, 5, '5 мест'],
])('formatPlural', (formatter, wordPlural, number, result) => {
[joinDash, ['рубль', 'рубля', 'рублей'], 0, '0-рублей'],
[joinSpace, ['метр', 'метра', 'метров'], 1, '1 метр'],
[skipZero, ['кофе'], 0, '-'],
[skipZero, ['место', 'места', 'мест'], 55, '55 мест'],
])('format', (formatter, words, number, result) => {
test('works', () => {
expect(format(formatter)(wordPlural)(number)).toBe(result);
expect(format(formatter, ...words)(number)).toBe(result);
});
});
/* eslint-enable sonarjs/no-duplicate-string */
24 changes: 14 additions & 10 deletions src/plural.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
// @flow strict

opaque type PositiveInt = number;
opaque type AbsInt = number;

function toPositiveInt(number: ?number): PositiveInt {
return Math.abs(parseInt(number, 10));
}
export type Numberlike = number | string | null | void;
export type Declensions = [string, string, string];
export type Plural = (number: ?number) => string;

type Declensions = [string, string, string];
function absInt(number: Numberlike): AbsInt {
const int = Math.abs(parseInt(number, 10) || 0);
if (Number.isNaN(int)) {
throw new TypeError('Invalid number');
}
return int;
}

function declensions(one: string, _two?: string, _five?: string): Declensions {
const two = typeof _two === 'undefined' ? one : _two;
const five = typeof _five === 'undefined' ? two : _five;
return [one, two, five];
}

function pluralize(int: PositiveInt, _declensions: Declensions) {
function pluralize(int: AbsInt, _declensions: Declensions) {
return _declensions[
int % 100 > 4 && int % 100 < 20
? 2
: [2, 0, 1, 1, 1, 2][int % 10 < 5 ? int % 10 : 5]
];
}

export type Plural = (number: ?number) => string;

function plural(one: string, two?: string, five?: string): Plural {
return (number: ?number) =>
pluralize(toPositiveInt(number), declensions(one, two, five));
return (number: Numberlike) =>
pluralize(absInt(number), declensions(one, two, five));
}

export default plural;
65 changes: 39 additions & 26 deletions src/plural.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,37 @@ import plural from './plural';

// nouns
const ruble = plural('рубль', 'рубля', 'рублей');
const meter = plural('метр', 'метра', 'метров');
const seat = plural('место', 'места', 'мест');
const purchase = plural('покупка', 'покупки', 'покупок');

// verbs/adjectives
const work = plural('работает', 'работают');
const matching = plural('подходящий', 'подходящиx');
const selected = plural('выбранной', 'выбранных');

// single word
const coffee = plural('кофе');

describe.each([
[ruble, 0, 'рублей'],
[ruble, 1, 'рубль'],
[ruble, 2, 'рубля'],
[ruble, 5, 'рублей'],
[ruble, 50, 'рублей'],
[ruble, 11, 'рублей'],
[ruble, 12, 'рублей'],
[ruble, 13, 'рублей'],
[ruble, 101, 'рубль'],
[ruble, 234, 'рубля'],
[ruble, 980, 'рублей'],
[ruble, 4000, 'рублей'],
[ruble, 5561, 'рубль'],
[meter, 0, 'метров'],
[meter, 1, 'метр'],
[meter, 2, 'метра'],
[meter, 5, 'метров'],
[meter, 50, 'метров'],
[meter, 101, 'метр'],
[meter, 234, 'метра'],
[meter, 980, 'метров'],
[meter, 4000, 'метров'],
[meter, 5561, 'метр'],
[seat, 0, 'мест'],
[seat, 1, 'место'],
[seat, 2, 'места'],
[seat, 5, 'мест'],
[seat, 50, 'мест'],
[seat, 11, 'мест'],
[seat, 12, 'мест'],
[seat, 13, 'мест'],
[seat, 101, 'место'],
[seat, 234, 'места'],
[seat, 980, 'мест'],
Expand All @@ -42,7 +43,9 @@ describe.each([
[purchase, 1, 'покупка'],
[purchase, 2, 'покупки'],
[purchase, 5, 'покупок'],
[purchase, 50, 'покупок'],
[purchase, 11, 'покупок'],
[purchase, 12, 'покупок'],
[purchase, 13, 'покупок'],
[purchase, 101, 'покупка'],
[purchase, 234, 'покупки'],
[purchase, 980, 'покупок'],
Expand All @@ -57,17 +60,14 @@ describe.each([
});
});

// verbs/adjectives
const work = plural('работает', 'работают');
const matching = plural('подходящий', 'подходящиx');
const selected = plural('выбранной', 'выбранных');

describe.each([
[work, 0, 'работают'],
[work, 1, 'работает'],
[work, 2, 'работают'],
[work, 5, 'работают'],
[work, 50, 'работают'],
[work, 11, 'работают'],
[work, 12, 'работают'],
[work, 13, 'работают'],
[work, 101, 'работает'],
[work, 234, 'работают'],
[work, 980, 'работают'],
Expand All @@ -77,7 +77,9 @@ describe.each([
[matching, 1, 'подходящий'],
[matching, 2, 'подходящиx'],
[matching, 5, 'подходящиx'],
[matching, 50, 'подходящиx'],
[matching, 11, 'подходящиx'],
[matching, 12, 'подходящиx'],
[matching, 13, 'подходящиx'],
[matching, 101, 'подходящий'],
[matching, 234, 'подходящиx'],
[matching, 980, 'подходящиx'],
Expand All @@ -87,7 +89,9 @@ describe.each([
[selected, 1, 'выбранной'],
[selected, 2, 'выбранных'],
[selected, 5, 'выбранных'],
[selected, 50, 'выбранных'],
[selected, 11, 'выбранных'],
[selected, 12, 'выбранных'],
[selected, 13, 'выбранных'],
[selected, 101, 'выбранной'],
[selected, 234, 'выбранных'],
[selected, 980, 'выбранных'],
Expand All @@ -102,15 +106,14 @@ describe.each([
});
});

// single word
const coffee = plural('кофе');

describe.each([
[coffee, 0, 'кофе'],
[coffee, 1, 'кофе'],
[coffee, 2, 'кофе'],
[coffee, 5, 'кофе'],
[coffee, 50, 'кофе'],
[coffee, 11, 'кофе'],
[coffee, 12, 'кофе'],
[coffee, 13, 'кофе'],
[coffee, 101, 'кофе'],
[coffee, 234, 'кофе'],
[coffee, 980, 'кофе'],
Expand All @@ -124,3 +127,13 @@ describe.each([
expect(wordPlural(number)).toBe(result);
});
});

describe.each(
['a', 'foo', {}, [], new Map(), new Date(), new Promise(() => 1)],
'plural throws when the number is invalid',
invalidArg => {
test(`(${String(invalidArg)}) throws`, () => {
expect(coffee(invalidArg)).toThrow();
});
},
);

0 comments on commit fac9ce8

Please sign in to comment.