1 /* This is CLDRPluralRuleParser v1.1, ported to MediaWiki ResourceLoader */
4 * CLDRPluralRuleParser.js
5 * A parser engine for CLDR plural rules.
7 * Copyright 2012 GPLV3+, Santhosh Thottingal
10 * @source https://github.com/santhoshtr/CLDRPluralRuleParser
11 * @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
13 * @author Amir Aharoni
18 * Evaluates a plural rule in CLDR syntax for a number
19 * @param {string} rule
20 * @param {integer} number
21 * @return {boolean} true if evaluation passed, false if evaluation failed.
24 function pluralRuleParser(rule
, number
) {
26 Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
27 -----------------------------------------------------------------
28 condition = and_condition ('or' and_condition)*
31 and_condition = relation ('and' relation)*
32 relation = is_relation | in_relation | within_relation
33 is_relation = expr 'is' ('not')? value
34 in_relation = expr (('not')? 'in' | '=' | '!=') range_list
35 within_relation = expr ('not')? 'within' range_list
36 expr = operand (('mod' | '%') value)?
37 operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
38 range_list = (range | value) (',' range_list)*
40 digit = 0|1|2|3|4|5|6|7|8|9
41 range = value'..'value
42 samples = sampleRange (',' sampleRange)* (',' ('…'|'...'))?
43 sampleRange = decimalValue '~' decimalValue
44 decimalValue = value ('.' value)?
47 // we don't evaluate the samples section of the rule. Ignore it.
48 rule
= rule
.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');
51 // empty rule or 'other' rule.
54 // Indicates current position in the rule as we parse through it.
55 // Shared among all parsing functions below.
61 whitespace
= makeRegexParser(/^\s+/),
62 value
= makeRegexParser(/^\d+/),
63 _n_
= makeStringParser('n'),
64 _i_
= makeStringParser('i'),
65 _f_
= makeStringParser('f'),
66 _t_
= makeStringParser('t'),
67 _v_
= makeStringParser('v'),
68 _w_
= makeStringParser('w'),
69 _is_
= makeStringParser('is'),
70 _isnot_
= makeStringParser('is not'),
71 _isnot_sign_
= makeStringParser('!='),
72 _equal_
= makeStringParser('='),
73 _mod_
= makeStringParser('mod'),
74 _percent_
= makeStringParser('%'),
75 _not_
= makeStringParser('not'),
76 _in_
= makeStringParser('in'),
77 _within_
= makeStringParser('within'),
78 _range_
= makeStringParser('..'),
79 _comma_
= makeStringParser(','),
80 _or_
= makeStringParser('or'),
81 _and_
= makeStringParser('and');
84 // console.log.apply(console, arguments);
87 debug('pluralRuleParser', rule
, number
);
89 // Try parsers until one works, if none work return null
91 function choice(parserSyntax
) {
93 for (var i
= 0; i
< parserSyntax
.length
; i
++) {
94 var result
= parserSyntax
[i
]();
95 if (result
!== null) {
103 // Try several parserSyntax-es in a row.
104 // All must succeed; otherwise, return null.
105 // This is the only eager one.
107 function sequence(parserSyntax
) {
108 var originalPos
= pos
;
110 for (var i
= 0; i
< parserSyntax
.length
; i
++) {
111 var res
= parserSyntax
[i
]();
121 // Run the same parser over and over until it fails.
122 // Must succeed a minimum of n times; otherwise, return null.
124 function nOrMore(n
, p
) {
126 var originalPos
= pos
;
129 while (parsed
!== null) {
133 if (result
.length
< n
) {
141 // Helpers -- just make parserSyntax out of simpler JS builtin types
142 function makeStringParser(s
) {
146 if (rule
.substr(pos
, len
) === s
) {
155 function makeRegexParser(regex
) {
157 var matches
= rule
.substr(pos
).match(regex
);
158 if (matches
=== null) {
161 pos
+= matches
[0].length
;
167 * integer digits of n.
171 if (result
=== null) {
172 debug(' -- failed i', parseInt(number
, 10));
175 result
= parseInt(number
, 10);
176 debug(' -- passed i ', result
);
181 * absolute value of the source number (integer and decimals).
185 if (result
=== null) {
186 debug(' -- failed n ', number
);
189 result
= parseFloat(number
, 10);
190 debug(' -- passed n ', result
);
195 * visible fractional digits in n, with trailing zeros.
199 if (result
=== null) {
200 debug(' -- failed f ', number
);
203 result
= (number
+ '.').split('.')[1] || 0;
204 debug(' -- passed f ', result
);
209 * visible fractional digits in n, without trailing zeros.
213 if (result
=== null) {
214 debug(' -- failed t ', number
);
217 result
= (number
+ '.').split('.')[1].replace(/0$/, '') || 0;
218 debug(' -- passed t ', result
);
223 * number of visible fraction digits in n, with trailing zeros.
227 if (result
=== null) {
228 debug(' -- failed v ', number
);
231 result
= (number
+ '.').split('.')[1].length
|| 0;
232 debug(' -- passed v ', result
);
237 * number of visible fraction digits in n, without trailing zeros.
241 if (result
=== null) {
242 debug(' -- failed w ', number
);
245 result
= (number
+ '.').split('.')[1].replace(/0$/, '').length
|| 0;
246 debug(' -- passed w ', result
);
250 // operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
251 operand
= choice([n
, i
, f
, t
, v
, w
]);
253 // expr = operand (('mod' | '%') value)?
254 expression
= choice([mod
, operand
]);
257 var result
= sequence([operand
, whitespace
, choice([_mod_
, _percent_
]), whitespace
, value
]);
258 if (result
=== null) {
259 debug(' -- failed mod');
262 debug(' -- passed ' + parseInt(result
[0], 10) + ' ' + result
[2] + ' ' + parseInt(result
[4], 10));
263 return parseInt(result
[0], 10) % parseInt(result
[4], 10);
267 var result
= sequence([whitespace
, _not_
]);
268 if (result
=== null) {
269 debug(' -- failed not');
276 // is_relation = expr 'is' ('not')? value
278 var result
= sequence([expression
, whitespace
, choice([_is_
]), whitespace
, value
]);
279 if (result
!== null) {
280 debug(' -- passed is : ' + result
[0] + ' == ' + parseInt(result
[4], 10));
281 return result
[0] === parseInt(result
[4], 10);
283 debug(' -- failed is');
287 // is_relation = expr 'is' ('not')? value
289 var result
= sequence([expression
, whitespace
, choice([_isnot_
, _isnot_sign_
]), whitespace
, value
]);
290 if (result
!== null) {
291 debug(' -- passed isnot: ' + result
[0] + ' != ' + parseInt(result
[4], 10));
292 return result
[0] !== parseInt(result
[4], 10);
294 debug(' -- failed isnot');
299 var result
= sequence([expression
, whitespace
, _isnot_sign_
, whitespace
, rangeList
]);
300 if (result
!== null) {
301 debug(' -- passed not_in: ' + result
[0] + ' != ' + result
[4]);
302 var range_list
= result
[4];
303 for (var i
= 0; i
< range_list
.length
; i
++) {
304 if (parseInt(range_list
[i
], 10) === parseInt(result
[0], 10)) {
310 debug(' -- failed not_in');
314 // range_list = (range | value) (',' range_list)*
315 function rangeList() {
316 var result
= sequence([choice([range
, value
]), nOrMore(0, rangeTail
)]);
318 if (result
!== null) {
319 resultList
= resultList
.concat(result
[0]);
321 resultList
= resultList
.concat(result
[1][0]);
325 debug(' -- failed rangeList');
329 function rangeTail() {
331 var result
= sequence([_comma_
, rangeList
]);
332 if (result
!== null) {
335 debug(' -- failed rangeTail');
339 // range = value'..'value
343 var result
= sequence([value
, _range_
, value
]);
344 if (result
!== null) {
345 debug(' -- passed range');
347 var left
= parseInt(result
[0], 10);
348 var right
= parseInt(result
[2], 10);
349 for (i
= left
; i
<= right
; i
++) {
354 debug(' -- failed range');
359 // in_relation = expr ('not')? 'in' range_list
360 var result
= sequence([expression
, nOrMore(0, not
), whitespace
, choice([_in_
, _equal_
]), whitespace
, rangeList
]);
361 if (result
!== null) {
362 debug(' -- passed _in:' + result
);
363 var range_list
= result
[5];
364 for (var i
= 0; i
< range_list
.length
; i
++) {
365 if (parseInt(range_list
[i
], 10) === parseInt(result
[0], 10)) {
366 return (result
[1][0] !== 'not');
369 return (result
[1][0] === 'not');
371 debug(' -- failed _in ');
376 * The difference between in and within is that in only includes integers in the specified range,
377 * while within includes all values.
381 // within_relation = expr ('not')? 'within' range_list
382 var result
= sequence([expression
, nOrMore(0, not
), whitespace
, _within_
, whitespace
, rangeList
]);
383 if (result
!== null) {
384 debug(' -- passed within');
385 var range_list
= result
[5];
386 if ((result
[0] >= parseInt(range_list
[0], 10)) &&
387 (result
[0] < parseInt(range_list
[range_list
.length
- 1], 10))) {
388 return (result
[1][0] !== 'not');
390 return (result
[1][0] === 'not');
392 debug(' -- failed within ');
396 // relation = is_relation | in_relation | within_relation
397 relation
= choice([is
, not_in
, isnot
, _in
, within
]);
399 // and_condition = relation ('and' relation)*
401 var result
= sequence([relation
, nOrMore(0, andTail
)]);
406 for (var i
= 0; i
< result
[1].length
; i
++) {
413 debug(' -- failed and');
419 var result
= sequence([whitespace
, _and_
, whitespace
, relation
]);
420 if (result
!== null) {
421 debug(' -- passed andTail' + result
);
424 debug(' -- failed andTail');
428 // ('or' and_condition)*
430 var result
= sequence([whitespace
, _or_
, whitespace
, and
]);
431 if (result
!== null) {
432 debug(' -- passed orTail: ' + result
[3]);
435 debug(' -- failed orTail');
440 // condition = and_condition ('or' and_condition)*
441 function condition() {
442 var result
= sequence([and
, nOrMore(0, orTail
)]);
444 for (var i
= 0; i
< result
[1].length
; i
++) {
455 result
= condition();
457 * For success, the pos must have gotten to the end of the rule
458 * and returned a non-null.
459 * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
461 if (result
=== null) {
462 throw new Error('Parse error at position ' + pos
.toString() + ' for rule: ' + rule
);
465 if (pos
!== rule
.length
) {
466 debug('Warning: Rule not parsed completely. Parser stopped at ' + rule
.substr(0, pos
) + ' for rule: ' + rule
);
472 /* pluralRuleParser ends here */
473 mw
.libs
.pluralRuleParser
= pluralRuleParser
;