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;