Skip to content

Commit

Permalink
Merge pull request #24 from eemeli/add-examples
Browse files Browse the repository at this point in the history
Add 'make-plural/examples'
  • Loading branch information
eemeli authored Aug 27, 2021
2 parents 1a2e5b4 + 7ec09c7 commit c1f01e9
Show file tree
Hide file tree
Showing 12 changed files with 801 additions and 78 deletions.
1 change: 1 addition & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ async function plurals(cb) {
ordinals: ['plurals', '--no-cardinals'],
plurals: ['plurals'],
pluralCategories: ['categories'],
examples: ['examples'],
ranges: ['ranges', '--max-repeat=3']
})) {
for (const [ext, extOpt] of Object.entries({
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import yargs from 'yargs'
import getCompiler from './get-compiler.js'
import printCategoriesModule from './print-categories.js'
import printExamplesModule from './print-examples.js'
import printPluralsModule from './print-plurals.js'
import printRangesModule from './print-ranges.js'
import printPluralTypes from './print-types.js'
Expand Down Expand Up @@ -92,6 +93,14 @@ yargs
process.stdout.write(printCategoriesModule(args))
}
})
.command({
command: 'examples [locale...]',
desc: 'Print the plural category examples as the source of a JS module',
builder: moduleCommandBuilder,
handler(args) {
process.stdout.write(printExamplesModule(args))
}
})
.help()
.wrap(Math.min(96, yargs.terminalWidth()))
.parse()
47 changes: 47 additions & 0 deletions packages/cli/src/print-examples.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { identifier } from 'safe-identifier'
import getCompiler from './get-compiler.js'
import printUMD from './print-umd.js'

export default function printExamplesModule(args) {
const MakePlural = getCompiler(args)
const { locale, dts, maxRepeat, umd } = args
const locales =
locale.length === 0 ? Object.keys(MakePlural.rules.cardinal) : locale.sort()

const localesByExample = {}
for (const lc of locales) {
const mpc = new MakePlural(lc)
mpc.compile()
mpc.test()
const ex = JSON.stringify(mpc.examples)
const id = identifier(lc)
const prev = localesByExample[ex]
if (prev) prev.push(id)
else localesByExample[ex] = [id]
}

let str = ''
let commonId = 'a'
const examples = []
for (const [ex, locales] of Object.entries(localesByExample)) {
if (!dts && locales.length > maxRepeat && commonId <= 'z') {
str += `const ${commonId} = ${ex};\n`
for (const lc of locales) examples.push({ lc, ex: commonId })
commonId = String.fromCharCode(commonId.charCodeAt(0) + 1)
} else {
for (const lc of locales) examples.push({ lc, ex })
}
}
examples.sort((a, b) => (a.lc < b.lc ? -1 : 1))
if (str) str += '\n'

if (dts) {
for (const { lc, ex } of examples) str += `export const ${lc}: ${ex};\n`
} else if (umd) {
const cm = examples.map(({ lc, ex }) => `${lc}: ${ex}`)
str += printUMD('pluralCategories', cm.join(',\n')) + '\n'
} else {
for (const { lc, ex } of examples) str += `export const ${lc} = ${ex};\n`
}
return str
}
25 changes: 21 additions & 4 deletions packages/compiler/src/compiler.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Parser } from './parser.js'
import { Tests } from './tests.js'
import { testCat } from './tests.js'

export class Compiler {
static cardinals = true
Expand Down Expand Up @@ -28,14 +28,27 @@ export class Compiler {
return null
}

/**
* @param {string} src
* @returns {string[]}
*/
static parseExamples(src) {
return src
.join(' ')
.replace(/^[ ,]+|[ ,]+$/g, '')
.replace(/(0\.[0-9])~(1\.[1-9])/g, '$1 1.0 $2')
.split(/[ ,~]+/)
.filter(n => !n.includes('c'))
}

constructor(lc, { cardinals, ordinals } = Compiler) {
if (!lc) throw new Error('A locale is required')
if (!cardinals && !ordinals)
throw new Error('At least one type of plural is required')
this.lc = lc
this.categories = { cardinal: [], ordinal: [] }
this.examples = { cardinal: {}, ordinal: {} }
this.parser = new Parser()
this.tests = new Tests(this)
this.types = { cardinals, ordinals }
}

Expand All @@ -52,7 +65,11 @@ export class Compiler {
(_, args) => `(${args.replace('/*``*/', '').trim()}) =>`
)
.replace(/{\s*return\s+([^{}]*);\s*}$/, '$1')
this.test = () => this.tests.testAll(this.fn)
this.test = () => {
for (const type of ['cardinal', 'ordinal'])
for (const [cat, values] of Object.entries(this.examples[type]))
testCat(this.lc, type, cat, values, this.fn)
}
}
return this.fn
}
Expand All @@ -69,7 +86,7 @@ export class Compiler {
const [cond, ...examples] = rules[r].trim().split(/\s*@\w*/)
const cat = r.replace('pluralRule-count-', '')
if (cond) cases.push([this.parser.parse(cond), cat])
this.tests.add(type, cat, examples)
this.examples[type][cat] = Compiler.parseExamples(examples)
}
this.categories[type] = cases.map(c => c[1]).concat('other')
if (cases.length === 1) {
Expand Down
70 changes: 19 additions & 51 deletions packages/compiler/src/tests.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,25 @@
export class Tests {
constructor(obj) {
this.lc = obj.lc
this.ordinal = {}
this.cardinal = {}
}

add(type, cat, src) {
this[type][cat] = { src, values: null }
}

error(n, type, msg) {
const lc = JSON.stringify(this.lc)
const val = JSON.stringify(n)
return new Error(
`Locale ${lc} ${type} rule self-test failed for ${val} (${msg})`
)
}
function errorMsg(lc, n, type, msg) {
const val = JSON.stringify(n)
return `Locale ${lc} ${type} rule self-test failed for ${val} (${msg})`
}

testCond(n, type, expResult, fn) {
try {
var r = fn(n, type === 'ordinal')
} catch (error) {
/* istanbul ignore next: should not happen unless CLDR data is broken */
throw this.error(n, type, error)
}
if (r !== expResult) {
const res = JSON.stringify(r)
const exp = JSON.stringify(expResult)
throw this.error(n, type, `was ${res}, expected ${exp}`)
}
return true
function testCond(lc, n, type, expResult, fn) {
try {
var r = fn(n, type === 'ordinal')
} catch (error) {
/* istanbul ignore next: should not happen unless CLDR data is broken */
throw new Error(errorMsg(lc, n, type, error))
}

testCat(type, cat, fn) {
const data = this[type][cat]
if (!data.values) {
data.values = data.src
.join(' ')
.replace(/^[ ,]+|[ ,]+$/g, '')
.replace(/(0\.[0-9])~(1\.[1-9])/g, '$1 1.0 $2')
.split(/[ ,~]+/)
.filter(n => !n.includes('c'))
}
data.values.forEach(n => {
this.testCond(n, type, cat, fn)
if (!/\.0+$/.test(n)) this.testCond(Number(n), type, cat, fn)
})
return true
if (r !== expResult) {
const res = JSON.stringify(r)
const exp = JSON.stringify(expResult)
throw new Error(errorMsg(lc, n, type, `was ${res}, expected ${exp}`))
}
}

testAll(fn) {
for (let cat in this.cardinal) this.testCat('cardinal', cat, fn)
for (let cat in this.ordinal) this.testCat('ordinal', cat, fn)
return true
export function testCat(lc, type, cat, values, fn) {
for (const n of values) {
testCond(lc, n, type, cat, fn)
if (!/\.0+$/.test(n)) testCond(lc, Number(n), type, cat, fn)
}
}
20 changes: 14 additions & 6 deletions packages/plurals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,26 @@ npm install make-plural
```js
import * as Plurals from 'make-plural/plurals' // or just 'make-plural'
import * as Cardinals from 'make-plural/cardinals'
import * as Examples from 'make-plural/examples'
import * as Ordinals from 'make-plural/ordinals'
import * as Categories from 'make-plural/pluralCategories'
import * as PluralRanges from 'make-plural/ranges'
```

Each of the endpoints is available with both UMD (.js) and ES (.mjs) packaging.
`Cardinals`, `Ordinals` and `Plurals` each export a set of functions keyed by locale code, returning the pluralization category for the input (either a number or a string representation of a number).
`Plurals` functions also accept a second boolean parameter to return the ordinal (`true`) rather than cardinal (`false`, default) plural category.
Note that `Ordinals` includes a slightly smaller subset of locales than `Cardinals` and `Plurals`, due to a lack of data in the CLDR.
`PluralRanges` provides a set of functions similarly keyed by locale code, but returning the pliralization category of a numerical range, given the corresponding categories of its start and end values as arguments.

`Categories` has a similar structure, but contains for each language an array of the pluralization categories the cardinal and ordinal rules that that language's pluralization function may output.
- `Cardinals`, `Ordinals` and `Plurals` each export a set of functions keyed by locale code,
returning the pluralization category for the input (either a number or a string representation of a number).
`Plurals` functions also accept a second boolean parameter to return
the ordinal (`true`) rather than cardinal (`false`, default) plural category.
Note that `Ordinals` includes a slightly smaller subset of locales than `Cardinals` and `Plurals`,
due to a lack of data in the CLDR.
- `PluralRanges` provides a set of functions similarly keyed by locale code,
but returning the pliralization category of a numerical range,
given the corresponding categories of its start and end values as arguments.
- `Categories` has a similar structure,
but contains for each language an array of the pluralization categories
the cardinal and ordinal rules that that language's pluralization function may output.
- `Examples` provide sample numeric values for each language's categories.

The object keys are named using the corresponding 2-3 character [language code].
Due to JavaScript identifier restrictions, there are two exceptions:
Expand Down
Loading

0 comments on commit c1f01e9

Please sign in to comment.