1 /* This is CLDRPluralRuleParser v1.1.3, ported to MediaWiki ResourceLoader */
4 * CLDRPluralRuleParser.js
5 * A parser engine for CLDR plural rules.
7 * Copyright 2012-2014 Santhosh Thottingal and other contributors
8 * Released under the MIT license
9 * http://opensource.org/licenses/MIT
11 * @source https://github.com/santhoshtr/CLDRPluralRuleParser
12 * @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
14 * @author Amir Aharoni
19 * Evaluates a plural rule in CLDR syntax for a number
20 * @param {string} rule
21 * @param {integer} number
22 * @return {boolean} true if evaluation passed, false if evaluation failed.
25 function pluralRuleParser(rule
, number
) {
29 Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
30 -----------------------------------------------------------------
31 condition = and_condition ('or' and_condition)*
34 and_condition = relation ('and' relation)*
35 relation = is_relation | in_relation | within_relation
36 is_relation = expr 'is' ('not')? value
37 in_relation = expr (('not')? 'in' | '=' | '!=') range_list
38 within_relation = expr ('not')? 'within' range_list
39 expr = operand (('mod' | '%') value)?
40 operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
41 range_list = (range | value) (',' range_list)*
43 digit = 0|1|2|3|4|5|6|7|8|9
44 range = value'..'value
45 samples = sampleRange (',' sampleRange)* (',' ('…'|'...'))?
46 sampleRange = decimalValue '~' decimalValue
47 decimalValue = value ('.' value)?
50 // We don't evaluate the samples section of the rule. Ignore it.
51 rule
= rule
.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');
54 // Empty rule or 'other' rule.
58 // Indicates the current position in the rule as we parse through it.
59 // Shared among all parsing functions below.
65 whitespace
= makeRegexParser(/^\s+/),
66 value
= makeRegexParser(/^\d+/),
67 _n_
= makeStringParser('n'),
68 _i_
= makeStringParser('i'),
69 _f_
= makeStringParser('f'),
70 _t_
= makeStringParser('t'),
71 _v_
= makeStringParser('v'),
72 _w_
= makeStringParser('w'),
73 _is_
= makeStringParser('is'),
74 _isnot_
= makeStringParser('is not'),
75 _isnot_sign_
= makeStringParser('!='),
76 _equal_
= makeStringParser('='),
77 _mod_
= makeStringParser('mod'),
78 _percent_
= makeStringParser('%'),
79 _not_
= makeStringParser('not'),
80 _in_
= makeStringParser('in'),
81 _within_
= makeStringParser('within'),
82 _range_
= makeStringParser('..'),
83 _comma_
= makeStringParser(','),
84 _or_
= makeStringParser('or'),
85 _and_
= makeStringParser('and');
88 // console.log.apply(console, arguments);
91 debug('pluralRuleParser', rule
, number
);
93 // Try parsers until one works, if none work return null
94 function choice(parserSyntax
) {
98 for (i
= 0; i
< parserSyntax
.length
; i
++) {
99 result
= parserSyntax
[i
]();
101 if (result
!== null) {
110 // Try several parserSyntax-es in a row.
111 // All must succeed; otherwise, return null.
112 // This is the only eager one.
113 function sequence(parserSyntax
) {
118 for (i
= 0; i
< parserSyntax
.length
; i
++) {
119 parserRes
= parserSyntax
[i
]();
121 if (parserRes
=== null) {
127 result
.push(parserRes
);
133 // Run the same parser over and over until it fails.
134 // Must succeed a minimum of n times; otherwise, return null.
135 function nOrMore(n
, p
) {
137 var originalPos
= pos
,
141 while (parsed
!== null) {
146 if (result
.length
< n
) {
156 // Helpers - just make parserSyntax out of simpler JS builtin types
157 function makeStringParser(s
) {
163 if (rule
.substr(pos
, len
) === s
) {
172 function makeRegexParser(regex
) {
174 var matches
= rule
.substr(pos
).match(regex
);
176 if (matches
=== null) {
180 pos
+= matches
[0].length
;
187 * Integer digits of n.
192 if (result
=== null) {
193 debug(' -- failed i', parseInt(number
, 10));
198 result
= parseInt(number
, 10);
199 debug(' -- passed i ', result
);
205 * Absolute value of the source number (integer and decimals).
210 if (result
=== null) {
211 debug(' -- failed n ', number
);
216 result
= parseFloat(number
, 10);
217 debug(' -- passed n ', result
);
223 * Visible fractional digits in n, with trailing zeros.
228 if (result
=== null) {
229 debug(' -- failed f ', number
);
234 result
= (number
+ '.').split('.')[1] || 0;
235 debug(' -- passed f ', result
);
241 * Visible fractional digits in n, without trailing zeros.
246 if (result
=== null) {
247 debug(' -- failed t ', number
);
252 result
= (number
+ '.').split('.')[1].replace(/0$/, '') || 0;
253 debug(' -- passed t ', result
);
259 * Number of visible fraction digits in n, with trailing zeros.
264 if (result
=== null) {
265 debug(' -- failed v ', number
);
270 result
= (number
+ '.').split('.')[1].length
|| 0;
271 debug(' -- passed v ', result
);
277 * Number of visible fraction digits in n, without trailing zeros.
282 if (result
=== null) {
283 debug(' -- failed w ', number
);
288 result
= (number
+ '.').split('.')[1].replace(/0$/, '').length
|| 0;
289 debug(' -- passed w ', result
);
294 // operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
295 operand
= choice([n
, i
, f
, t
, v
, w
]);
297 // expr = operand (('mod' | '%') value)?
298 expression
= choice([mod
, operand
]);
301 var result
= sequence(
302 [operand
, whitespace
, choice([_mod_
, _percent_
]), whitespace
, value
]
305 if (result
=== null) {
306 debug(' -- failed mod');
311 debug(' -- passed ' + parseInt(result
[0], 10) + ' ' + result
[2] + ' ' + parseInt(result
[4], 10));
313 return parseInt(result
[0], 10) % parseInt(result
[4], 10);
317 var result
= sequence([whitespace
, _not_
]);
319 if (result
=== null) {
320 debug(' -- failed not');
328 // is_relation = expr 'is' ('not')? value
330 var result
= sequence([expression
, whitespace
, choice([_is_
]), whitespace
, value
]);
332 if (result
!== null) {
333 debug(' -- passed is : ' + result
[0] + ' == ' + parseInt(result
[4], 10));
335 return result
[0] === parseInt(result
[4], 10);
338 debug(' -- failed is');
343 // is_relation = expr 'is' ('not')? value
345 var result
= sequence(
346 [expression
, whitespace
, choice([_isnot_
, _isnot_sign_
]), whitespace
, value
]
349 if (result
!== null) {
350 debug(' -- passed isnot: ' + result
[0] + ' != ' + parseInt(result
[4], 10));
352 return result
[0] !== parseInt(result
[4], 10);
355 debug(' -- failed isnot');
362 result
= sequence([expression
, whitespace
, _isnot_sign_
, whitespace
, rangeList
]);
364 if (result
!== null) {
365 debug(' -- passed not_in: ' + result
[0] + ' != ' + result
[4]);
366 range_list
= result
[4];
368 for (i
= 0; i
< range_list
.length
; i
++) {
369 if (parseInt(range_list
[i
], 10) === parseInt(result
[0], 10)) {
377 debug(' -- failed not_in');
382 // range_list = (range | value) (',' range_list)*
383 function rangeList() {
384 var result
= sequence([choice([range
, value
]), nOrMore(0, rangeTail
)]),
387 if (result
!== null) {
388 resultList
= resultList
.concat(result
[0]);
391 resultList
= resultList
.concat(result
[1][0]);
397 debug(' -- failed rangeList');
402 function rangeTail() {
404 var result
= sequence([_comma_
, rangeList
]);
406 if (result
!== null) {
410 debug(' -- failed rangeTail');
415 // range = value'..'value
417 var i
, array
, left
, right
,
418 result
= sequence([value
, _range_
, value
]);
420 if (result
!== null) {
421 debug(' -- passed range');
424 left
= parseInt(result
[0], 10);
425 right
= parseInt(result
[2], 10);
427 for (i
= left
; i
<= right
; i
++) {
434 debug(' -- failed range');
440 var result
, range_list
, i
;
442 // in_relation = expr ('not')? 'in' range_list
444 [expression
, nOrMore(0, not
), whitespace
, choice([_in_
, _equal_
]), whitespace
, rangeList
]
447 if (result
!== null) {
448 debug(' -- passed _in:' + result
);
450 range_list
= result
[5];
452 for (i
= 0; i
< range_list
.length
; i
++) {
453 if (parseInt(range_list
[i
], 10) === parseInt(result
[0], 10)) {
454 return (result
[1][0] !== 'not');
458 return (result
[1][0] === 'not');
461 debug(' -- failed _in ');
467 * The difference between "in" and "within" is that
468 * "in" only includes integers in the specified range,
469 * while "within" includes all values.
472 var range_list
, result
;
474 // within_relation = expr ('not')? 'within' range_list
476 [expression
, nOrMore(0, not
), whitespace
, _within_
, whitespace
, rangeList
]
479 if (result
!== null) {
480 debug(' -- passed within');
482 range_list
= result
[5];
484 if ((result
[0] >= parseInt(range_list
[0], 10)) &&
485 (result
[0] < parseInt(range_list
[range_list
.length
- 1], 10))) {
487 return (result
[1][0] !== 'not');
490 return (result
[1][0] === 'not');
493 debug(' -- failed within ');
498 // relation = is_relation | in_relation | within_relation
499 relation
= choice([is
, not_in
, isnot
, _in
, within
]);
501 // and_condition = relation ('and' relation)*
504 result
= sequence([relation
, nOrMore(0, andTail
)]);
511 for (i
= 0; i
< result
[1].length
; i
++) {
520 debug(' -- failed and');
527 var result
= sequence([whitespace
, _and_
, whitespace
, relation
]);
529 if (result
!== null) {
530 debug(' -- passed andTail' + result
);
535 debug(' -- failed andTail');
540 // ('or' and_condition)*
542 var result
= sequence([whitespace
, _or_
, whitespace
, and
]);
544 if (result
!== null) {
545 debug(' -- passed orTail: ' + result
[3]);
550 debug(' -- failed orTail');
555 // condition = and_condition ('or' and_condition)*
556 function condition() {
558 result
= sequence([and
, nOrMore(0, orTail
)]);
561 for (i
= 0; i
< result
[1].length
; i
++) {
573 result
= condition();
576 * For success, the pos must have gotten to the end of the rule
577 * and returned a non-null.
578 * n.b. This is part of language infrastructure,
579 * so we do not throw an internationalizable message.
581 if (result
=== null) {
582 throw new Error('Parse error at position ' + pos
.toString() + ' for rule: ' + rule
);
585 if (pos
!== rule
.length
) {
586 debug('Warning: Rule not parsed completely. Parser stopped at ' + rule
.substr(0, pos
) + ' for rule: ' + rule
);
592 /* pluralRuleParser ends here */
593 mw
.libs
.pluralRuleParser
= pluralRuleParser
;
594 module
.exports
= pluralRuleParser
;