Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / lib / CLDRPluralRuleParser / CLDRPluralRuleParser.js
blob73268d443e3f367f7d6b3f6595490e222f80d074
1 /**
2  * cldrpluralparser.js
3  * A parser engine for CLDR plural rules.
4  *
5  * Copyright 2012-2014 Santhosh Thottingal and other contributors
6  * Released under the MIT license
7  * http://opensource.org/licenses/MIT
8  *
9  * @source https://github.com/santhoshtr/CLDRPluralRuleParser
10  * @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
11  * @author Timo Tijhof
12  * @author Amir Aharoni
13  */
15 /**
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.
20  */
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.
26                 define(factory);
27         } else if (typeof exports === 'object') {
28                 /* global module */
29                 // Node. Does not work with strict CommonJS, but
30                 // only CommonJS-like environments that support module.exports,
31                 // like Node.
32                 module.exports = factory();
33         } else {
34                 // Browser globals (root is window)
35                 root.pluralRuleParser = factory();
36         }
37 }(this, function() {
39 function pluralRuleParser(rule, number) {
40         'use strict';
42         /*
43         Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
44         -----------------------------------------------------------------
45         condition     = and_condition ('or' and_condition)*
46                 ('@integer' samples)?
47                 ('@decimal' samples)?
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)*
56         value         = digit+
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)?
62         */
64         // We don't evaluate the samples section of the rule. Ignore it.
65         rule = rule.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');
67         if (!rule.length) {
68                 // Empty rule or 'other' rule.
69                 return true;
70         }
72         // Indicates the current position in the rule as we parse through it.
73         // Shared among all parsing functions below.
74         var pos = 0,
75                 operand,
76                 expression,
77                 relation,
78                 result,
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');
101         function debug() {
102                 // console.log.apply(console, arguments);
103         }
105         debug('pluralRuleParser', rule, number);
107         // Try parsers until one works, if none work return null
108         function choice(parserSyntax) {
109                 return function() {
110                         var i, result;
112                         for (i = 0; i < parserSyntax.length; i++) {
113                                 result = parserSyntax[i]();
115                                 if (result !== null) {
116                                         return result;
117                                 }
118                         }
120                         return null;
121                 };
122         }
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) {
128                 var i, parserRes,
129                         originalPos = pos,
130                         result = [];
132                 for (i = 0; i < parserSyntax.length; i++) {
133                         parserRes = parserSyntax[i]();
135                         if (parserRes === null) {
136                                 pos = originalPos;
138                                 return null;
139                         }
141                         result.push(parserRes);
142                 }
144                 return result;
145         }
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) {
150                 return function() {
151                         var originalPos = pos,
152                                 result = [],
153                                 parsed = p();
155                         while (parsed !== null) {
156                                 result.push(parsed);
157                                 parsed = p();
158                         }
160                         if (result.length < n) {
161                                 pos = originalPos;
163                                 return null;
164                         }
166                         return result;
167                 };
168         }
170         // Helpers - just make parserSyntax out of simpler JS builtin types
171         function makeStringParser(s) {
172                 var len = s.length;
174                 return function() {
175                         var result = null;
177                         if (rule.substr(pos, len) === s) {
178                                 result = s;
179                                 pos += len;
180                         }
182                         return result;
183                 };
184         }
186         function makeRegexParser(regex) {
187                 return function() {
188                         var matches = rule.substr(pos).match(regex);
190                         if (matches === null) {
191                                 return null;
192                         }
194                         pos += matches[0].length;
196                         return matches[0];
197                 };
198         }
200         /**
201          * Integer digits of n.
202          */
203         function i() {
204                 var result = _i_();
206                 if (result === null) {
207                         debug(' -- failed i', parseInt(number, 10));
209                         return result;
210                 }
212                 result = parseInt(number, 10);
213                 debug(' -- passed i ', result);
215                 return result;
216         }
218         /**
219          * Absolute value of the source number (integer and decimals).
220          */
221         function n() {
222                 var result = _n_();
224                 if (result === null) {
225                         debug(' -- failed n ', number);
227                         return result;
228                 }
230                 result = parseFloat(number, 10);
231                 debug(' -- passed n ', result);
233                 return result;
234         }
236         /**
237          * Visible fractional digits in n, with trailing zeros.
238          */
239         function f() {
240                 var result = _f_();
242                 if (result === null) {
243                         debug(' -- failed f ', number);
245                         return result;
246                 }
248                 result = (number + '.').split('.')[1] || 0;
249                 debug(' -- passed f ', result);
251                 return result;
252         }
254         /**
255          * Visible fractional digits in n, without trailing zeros.
256          */
257         function t() {
258                 var result = _t_();
260                 if (result === null) {
261                         debug(' -- failed t ', number);
263                         return result;
264                 }
266                 result = (number + '.').split('.')[1].replace(/0$/, '') || 0;
267                 debug(' -- passed t ', result);
269                 return result;
270         }
272         /**
273          * Number of visible fraction digits in n, with trailing zeros.
274          */
275         function v() {
276                 var result = _v_();
278                 if (result === null) {
279                         debug(' -- failed v ', number);
281                         return result;
282                 }
284                 result = (number + '.').split('.')[1].length || 0;
285                 debug(' -- passed v ', result);
287                 return result;
288         }
290         /**
291          * Number of visible fraction digits in n, without trailing zeros.
292          */
293         function w() {
294                 var result = _w_();
296                 if (result === null) {
297                         debug(' -- failed w ', number);
299                         return result;
300                 }
302                 result = (number + '.').split('.')[1].replace(/0$/, '').length || 0;
303                 debug(' -- passed w ', result);
305                 return result;
306         }
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]);
314         function mod() {
315                 var result = sequence(
316                         [operand, whitespace, choice([_mod_, _percent_]), whitespace, value]
317                 );
319                 if (result === null) {
320                         debug(' -- failed mod');
322                         return null;
323                 }
325                 debug(' -- passed ', parseInt(result[0], 10), result[2], parseInt(result[4], 10));
327                 return parseFloat(result[0]) % parseInt(result[4], 10);
328         }
330         function not() {
331                 var result = sequence([whitespace, _not_]);
333                 if (result === null) {
334                         debug(' -- failed not');
336                         return null;
337                 }
339                 return result[1];
340         }
342         // is_relation   = expr 'is' ('not')? value
343         function is() {
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);
350                 }
352                 debug(' -- failed is');
354                 return null;
355         }
357         // is_relation   = expr 'is' ('not')? value
358         function isnot() {
359                 var result = sequence(
360                         [expression, whitespace, choice([_isnot_, _isnot_sign_]), whitespace, value]
361                 );
363                 if (result !== null) {
364                         debug(' -- passed isnot: ', result[0], ' != ', parseInt(result[4], 10));
366                         return result[0] !== parseInt(result[4], 10);
367                 }
369                 debug(' -- failed isnot');
371                 return null;
372         }
374         function not_in() {
375                 var i, range_list,
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)) {
384                                         return false;
385                                 }
386                         }
388                         return true;
389                 }
391                 debug(' -- failed not_in');
393                 return null;
394         }
396         // range_list    = (range | value) (',' range_list)*
397         function rangeList() {
398                 var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]),
399                         resultList = [];
401                 if (result !== null) {
402                         resultList = resultList.concat(result[0]);
404                         if (result[1][0]) {
405                                 resultList = resultList.concat(result[1][0]);
406                         }
408                         return resultList;
409                 }
411                 debug(' -- failed rangeList');
413                 return null;
414         }
416         function rangeTail() {
417                 // ',' range_list
418                 var result = sequence([_comma_, rangeList]);
420                 if (result !== null) {
421                         return result[1];
422                 }
424                 debug(' -- failed rangeTail');
426                 return null;
427         }
429         // range         = value'..'value
430         function range() {
431                 var i, array, left, right,
432                         result = sequence([value, _range_, value]);
434                 if (result !== null) {
435                         debug(' -- passed range');
437                         array = [];
438                         left = parseInt(result[0], 10);
439                         right = parseInt(result[2], 10);
441                         for (i = left; i <= right; i++) {
442                                 array.push(i);
443                         }
445                         return array;
446                 }
448                 debug(' -- failed range');
450                 return null;
451         }
453         function _in() {
454                 var result, range_list, i;
456                 // in_relation   = expr ('not')? 'in' range_list
457                 result = sequence(
458                         [expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]
459                 );
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');
469                                 }
470                         }
472                         return (result[1][0] === 'not');
473                 }
475                 debug(' -- failed _in ');
477                 return null;
478         }
480         /**
481          * The difference between "in" and "within" is that
482          * "in" only includes integers in the specified range,
483          * while "within" includes all values.
484          */
485         function within() {
486                 var range_list, result;
488                 // within_relation = expr ('not')? 'within' range_list
489                 result = sequence(
490                         [expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]
491                 );
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');
502                         }
504                         return (result[1][0] === 'not');
505                 }
507                 debug(' -- failed within ');
509                 return null;
510         }
512         // relation      = is_relation | in_relation | within_relation
513         relation = choice([is, not_in, isnot, _in, within]);
515         // and_condition = relation ('and' relation)*
516         function and() {
517                 var i,
518                         result = sequence([relation, nOrMore(0, andTail)]);
520                 if (result) {
521                         if (!result[0]) {
522                                 return false;
523                         }
525                         for (i = 0; i < result[1].length; i++) {
526                                 if (!result[1][i]) {
527                                         return false;
528                                 }
529                         }
531                         return true;
532                 }
534                 debug(' -- failed and');
536                 return null;
537         }
539         // ('and' relation)*
540         function andTail() {
541                 var result = sequence([whitespace, _and_, whitespace, relation]);
543                 if (result !== null) {
544                         debug(' -- passed andTail', result);
546                         return result[3];
547                 }
549                 debug(' -- failed andTail');
551                 return null;
553         }
554         //  ('or' and_condition)*
555         function orTail() {
556                 var result = sequence([whitespace, _or_, whitespace, and]);
558                 if (result !== null) {
559                         debug(' -- passed orTail: ', result[3]);
561                         return result[3];
562                 }
564                 debug(' -- failed orTail');
566                 return null;
567         }
569         // condition     = and_condition ('or' and_condition)*
570         function condition() {
571                 var i,
572                         result = sequence([and, nOrMore(0, orTail)]);
574                 if (result) {
575                         for (i = 0; i < result[1].length; i++) {
576                                 if (result[1][i]) {
577                                         return true;
578                                 }
579                         }
581                         return result[0];
582                 }
584                 return false;
585         }
587         result = condition();
589         /**
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.
594          */
595         if (result === null) {
596                 throw new Error('Parse error at position ' + pos.toString() + ' for rule: ' + rule);
597         }
599         if (pos !== rule.length) {
600                 debug('Warning: Rule not parsed completely. Parser stopped at ', rule.substr(0, pos), ' for rule: ', rule);
601         }
603         return result;
606 return pluralRuleParser;
608 }));