3 * A parser engine for CLDR plural rules.
5 * Copyright 2012-2014 Santhosh Thottingal and other contributors
6 * Released under the MIT license
7 * http://opensource.org/licenses/MIT
9 * @source https://github.com/santhoshtr/CLDRPluralRuleParser
10 * @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
12 * @author Amir Aharoni
16 * Evaluates a plural rule in CLDR syntax for a number
17 * @param {string} rule
18 * @param {integer} number
19 * @return {boolean} true if evaluation passed, false if evaluation failed.
22 // UMD returnExports https://github.com/umdjs/umd/blob/master/returnExports.js
23 (function(root
, factory
) {
24 if (typeof define
=== 'function' && define
.amd
) {
25 // AMD. Register as an anonymous module.
27 } else if (typeof exports
=== 'object') {
29 // Node. Does not work with strict CommonJS, but
30 // only CommonJS-like environments that support module.exports,
32 module
.exports
= factory();
34 // Browser globals (root is window)
35 root
.pluralRuleParser
= factory();
39 function pluralRuleParser(rule
, number
) {
43 Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
44 -----------------------------------------------------------------
45 condition = and_condition ('or' and_condition)*
48 and_condition = relation ('and' relation)*
49 relation = is_relation | in_relation | within_relation
50 is_relation = expr 'is' ('not')? value
51 in_relation = expr (('not')? 'in' | '=' | '!=') range_list
52 within_relation = expr ('not')? 'within' range_list
53 expr = operand (('mod' | '%') value)?
54 operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
55 range_list = (range | value) (',' range_list)*
57 digit = 0|1|2|3|4|5|6|7|8|9
58 range = value'..'value
59 samples = sampleRange (',' sampleRange)* (',' ('…'|'...'))?
60 sampleRange = decimalValue '~' decimalValue
61 decimalValue = value ('.' value)?
64 // We don't evaluate the samples section of the rule. Ignore it.
65 rule
= rule
.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');
68 // Empty rule or 'other' rule.
72 // Indicates the current position in the rule as we parse through it.
73 // Shared among all parsing functions below.
79 whitespace
= makeRegexParser(/^\s+/),
80 value
= makeRegexParser(/^\d+/),
81 _n_
= makeStringParser('n'),
82 _i_
= makeStringParser('i'),
83 _f_
= makeStringParser('f'),
84 _t_
= makeStringParser('t'),
85 _v_
= makeStringParser('v'),
86 _w_
= makeStringParser('w'),
87 _is_
= makeStringParser('is'),
88 _isnot_
= makeStringParser('is not'),
89 _isnot_sign_
= makeStringParser('!='),
90 _equal_
= makeStringParser('='),
91 _mod_
= makeStringParser('mod'),
92 _percent_
= makeStringParser('%'),
93 _not_
= makeStringParser('not'),
94 _in_
= makeStringParser('in'),
95 _within_
= makeStringParser('within'),
96 _range_
= makeStringParser('..'),
97 _comma_
= makeStringParser(','),
98 _or_
= makeStringParser('or'),
99 _and_
= makeStringParser('and');
102 // console.log.apply(console, arguments);
105 debug('pluralRuleParser', rule
, number
);
107 // Try parsers until one works, if none work return null
108 function choice(parserSyntax
) {
112 for (i
= 0; i
< parserSyntax
.length
; i
++) {
113 result
= parserSyntax
[i
]();
115 if (result
!== null) {
124 // Try several parserSyntax-es in a row.
125 // All must succeed; otherwise, return null.
126 // This is the only eager one.
127 function sequence(parserSyntax
) {
132 for (i
= 0; i
< parserSyntax
.length
; i
++) {
133 parserRes
= parserSyntax
[i
]();
135 if (parserRes
=== null) {
141 result
.push(parserRes
);
147 // Run the same parser over and over until it fails.
148 // Must succeed a minimum of n times; otherwise, return null.
149 function nOrMore(n
, p
) {
151 var originalPos
= pos
,
155 while (parsed
!== null) {
160 if (result
.length
< n
) {
170 // Helpers - just make parserSyntax out of simpler JS builtin types
171 function makeStringParser(s
) {
177 if (rule
.substr(pos
, len
) === s
) {
186 function makeRegexParser(regex
) {
188 var matches
= rule
.substr(pos
).match(regex
);
190 if (matches
=== null) {
194 pos
+= matches
[0].length
;
201 * Integer digits of n.
206 if (result
=== null) {
207 debug(' -- failed i', parseInt(number
, 10));
212 result
= parseInt(number
, 10);
213 debug(' -- passed i ', result
);
219 * Absolute value of the source number (integer and decimals).
224 if (result
=== null) {
225 debug(' -- failed n ', number
);
230 result
= parseFloat(number
, 10);
231 debug(' -- passed n ', result
);
237 * Visible fractional digits in n, with trailing zeros.
242 if (result
=== null) {
243 debug(' -- failed f ', number
);
248 result
= (number
+ '.').split('.')[1] || 0;
249 debug(' -- passed f ', result
);
255 * Visible fractional digits in n, without trailing zeros.
260 if (result
=== null) {
261 debug(' -- failed t ', number
);
266 result
= (number
+ '.').split('.')[1].replace(/0$/, '') || 0;
267 debug(' -- passed t ', result
);
273 * Number of visible fraction digits in n, with trailing zeros.
278 if (result
=== null) {
279 debug(' -- failed v ', number
);
284 result
= (number
+ '.').split('.')[1].length
|| 0;
285 debug(' -- passed v ', result
);
291 * Number of visible fraction digits in n, without trailing zeros.
296 if (result
=== null) {
297 debug(' -- failed w ', number
);
302 result
= (number
+ '.').split('.')[1].replace(/0$/, '').length
|| 0;
303 debug(' -- passed w ', result
);
308 // operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
309 operand
= choice([n
, i
, f
, t
, v
, w
]);
311 // expr = operand (('mod' | '%') value)?
312 expression
= choice([mod
, operand
]);
315 var result
= sequence(
316 [operand
, whitespace
, choice([_mod_
, _percent_
]), whitespace
, value
]
319 if (result
=== null) {
320 debug(' -- failed mod');
325 debug(' -- passed ', parseInt(result
[0], 10), result
[2], parseInt(result
[4], 10));
327 return parseFloat(result
[0]) % parseInt(result
[4], 10);
331 var result
= sequence([whitespace
, _not_
]);
333 if (result
=== null) {
334 debug(' -- failed not');
342 // is_relation = expr 'is' ('not')? value
344 var result
= sequence([expression
, whitespace
, choice([_is_
]), whitespace
, value
]);
346 if (result
!== null) {
347 debug(' -- passed is :', result
[0], ' == ', parseInt(result
[4], 10));
349 return result
[0] === parseInt(result
[4], 10);
352 debug(' -- failed is');
357 // is_relation = expr 'is' ('not')? value
359 var result
= sequence(
360 [expression
, whitespace
, choice([_isnot_
, _isnot_sign_
]), whitespace
, value
]
363 if (result
!== null) {
364 debug(' -- passed isnot: ', result
[0], ' != ', parseInt(result
[4], 10));
366 return result
[0] !== parseInt(result
[4], 10);
369 debug(' -- failed isnot');
376 result
= sequence([expression
, whitespace
, _isnot_sign_
, whitespace
, rangeList
]);
378 if (result
!== null) {
379 debug(' -- passed not_in: ', result
[0], ' != ', result
[4]);
380 range_list
= result
[4];
382 for (i
= 0; i
< range_list
.length
; i
++) {
383 if (parseInt(range_list
[i
], 10) === parseInt(result
[0], 10)) {
391 debug(' -- failed not_in');
396 // range_list = (range | value) (',' range_list)*
397 function rangeList() {
398 var result
= sequence([choice([range
, value
]), nOrMore(0, rangeTail
)]),
401 if (result
!== null) {
402 resultList
= resultList
.concat(result
[0]);
405 resultList
= resultList
.concat(result
[1][0]);
411 debug(' -- failed rangeList');
416 function rangeTail() {
418 var result
= sequence([_comma_
, rangeList
]);
420 if (result
!== null) {
424 debug(' -- failed rangeTail');
429 // range = value'..'value
431 var i
, array
, left
, right
,
432 result
= sequence([value
, _range_
, value
]);
434 if (result
!== null) {
435 debug(' -- passed range');
438 left
= parseInt(result
[0], 10);
439 right
= parseInt(result
[2], 10);
441 for (i
= left
; i
<= right
; i
++) {
448 debug(' -- failed range');
454 var result
, range_list
, i
;
456 // in_relation = expr ('not')? 'in' range_list
458 [expression
, nOrMore(0, not
), whitespace
, choice([_in_
, _equal_
]), whitespace
, rangeList
]
461 if (result
!== null) {
462 debug(' -- passed _in:', result
);
464 range_list
= result
[5];
466 for (i
= 0; i
< range_list
.length
; i
++) {
467 if (parseInt(range_list
[i
], 10) === parseFloat(result
[0])) {
468 return (result
[1][0] !== 'not');
472 return (result
[1][0] === 'not');
475 debug(' -- failed _in ');
481 * The difference between "in" and "within" is that
482 * "in" only includes integers in the specified range,
483 * while "within" includes all values.
486 var range_list
, result
;
488 // within_relation = expr ('not')? 'within' range_list
490 [expression
, nOrMore(0, not
), whitespace
, _within_
, whitespace
, rangeList
]
493 if (result
!== null) {
494 debug(' -- passed within');
496 range_list
= result
[5];
498 if ((result
[0] >= parseInt(range_list
[0], 10)) &&
499 (result
[0] < parseInt(range_list
[range_list
.length
- 1], 10))) {
501 return (result
[1][0] !== 'not');
504 return (result
[1][0] === 'not');
507 debug(' -- failed within ');
512 // relation = is_relation | in_relation | within_relation
513 relation
= choice([is
, not_in
, isnot
, _in
, within
]);
515 // and_condition = relation ('and' relation)*
518 result
= sequence([relation
, nOrMore(0, andTail
)]);
525 for (i
= 0; i
< result
[1].length
; i
++) {
534 debug(' -- failed and');
541 var result
= sequence([whitespace
, _and_
, whitespace
, relation
]);
543 if (result
!== null) {
544 debug(' -- passed andTail', result
);
549 debug(' -- failed andTail');
554 // ('or' and_condition)*
556 var result
= sequence([whitespace
, _or_
, whitespace
, and
]);
558 if (result
!== null) {
559 debug(' -- passed orTail: ', result
[3]);
564 debug(' -- failed orTail');
569 // condition = and_condition ('or' and_condition)*
570 function condition() {
572 result
= sequence([and
, nOrMore(0, orTail
)]);
575 for (i
= 0; i
< result
[1].length
; i
++) {
587 result
= condition();
590 * For success, the pos must have gotten to the end of the rule
591 * and returned a non-null.
592 * n.b. This is part of language infrastructure,
593 * so we do not throw an internationalizable message.
595 if (result
=== null) {
596 throw new Error('Parse error at position ' + pos
.toString() + ' for rule: ' + rule
);
599 if (pos
!== rule
.length
) {
600 debug('Warning: Rule not parsed completely. Parser stopped at ', rule
.substr(0, pos
), ' for rule: ', rule
);
606 return pluralRuleParser
;