Merge "Make $wgMWLoggerDefaultSpi more expressive"
[mediawiki.git] / resources / src / mediawiki.libs / CLDRPluralRuleParser.js
blob31c8fef92aa7516ab28b55390be144be2daaa87f
1 /* This is CLDRPluralRuleParser v1.1.3, ported to MediaWiki ResourceLoader */
3 /**
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>
13 * @author Timo Tijhof
14 * @author Amir Aharoni
17 ( function ( mw ) {
18 /**
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.
23  */
25 function pluralRuleParser(rule, number) {
26         'use strict';
28         /*
29         Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
30         -----------------------------------------------------------------
31         condition     = and_condition ('or' and_condition)*
32                 ('@integer' samples)?
33                 ('@decimal' samples)?
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)*
42         value         = digit+
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)?
48         */
50         // We don't evaluate the samples section of the rule. Ignore it.
51         rule = rule.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');
53         if (!rule.length) {
54                 // Empty rule or 'other' rule.
55                 return true;
56         }
58         // Indicates the current position in the rule as we parse through it.
59         // Shared among all parsing functions below.
60         var pos = 0,
61                 operand,
62                 expression,
63                 relation,
64                 result,
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');
87         function debug() {
88                 // console.log.apply(console, arguments);
89         }
91         debug('pluralRuleParser', rule, number);
93         // Try parsers until one works, if none work return null
94         function choice(parserSyntax) {
95                 return function() {
96                         var i, result;
98                         for (i = 0; i < parserSyntax.length; i++) {
99                                 result = parserSyntax[i]();
101                                 if (result !== null) {
102                                         return result;
103                                 }
104                         }
106                         return null;
107                 };
108         }
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) {
114                 var i, parserRes,
115                         originalPos = pos,
116                         result = [];
118                 for (i = 0; i < parserSyntax.length; i++) {
119                         parserRes = parserSyntax[i]();
121                         if (parserRes === null) {
122                                 pos = originalPos;
124                                 return null;
125                         }
127                         result.push(parserRes);
128                 }
130                 return result;
131         }
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) {
136                 return function() {
137                         var originalPos = pos,
138                                 result = [],
139                                 parsed = p();
141                         while (parsed !== null) {
142                                 result.push(parsed);
143                                 parsed = p();
144                         }
146                         if (result.length < n) {
147                                 pos = originalPos;
149                                 return null;
150                         }
152                         return result;
153                 };
154         }
156         // Helpers - just make parserSyntax out of simpler JS builtin types
157         function makeStringParser(s) {
158                 var len = s.length;
160                 return function() {
161                         var result = null;
163                         if (rule.substr(pos, len) === s) {
164                                 result = s;
165                                 pos += len;
166                         }
168                         return result;
169                 };
170         }
172         function makeRegexParser(regex) {
173                 return function() {
174                         var matches = rule.substr(pos).match(regex);
176                         if (matches === null) {
177                                 return null;
178                         }
180                         pos += matches[0].length;
182                         return matches[0];
183                 };
184         }
186         /**
187          * Integer digits of n.
188          */
189         function i() {
190                 var result = _i_();
192                 if (result === null) {
193                         debug(' -- failed i', parseInt(number, 10));
195                         return result;
196                 }
198                 result = parseInt(number, 10);
199                 debug(' -- passed i ', result);
201                 return result;
202         }
204         /**
205          * Absolute value of the source number (integer and decimals).
206          */
207         function n() {
208                 var result = _n_();
210                 if (result === null) {
211                         debug(' -- failed n ', number);
213                         return result;
214                 }
216                 result = parseFloat(number, 10);
217                 debug(' -- passed n ', result);
219                 return result;
220         }
222         /**
223          * Visible fractional digits in n, with trailing zeros.
224          */
225         function f() {
226                 var result = _f_();
228                 if (result === null) {
229                         debug(' -- failed f ', number);
231                         return result;
232                 }
234                 result = (number + '.').split('.')[1] || 0;
235                 debug(' -- passed f ', result);
237                 return result;
238         }
240         /**
241          * Visible fractional digits in n, without trailing zeros.
242          */
243         function t() {
244                 var result = _t_();
246                 if (result === null) {
247                         debug(' -- failed t ', number);
249                         return result;
250                 }
252                 result = (number + '.').split('.')[1].replace(/0$/, '') || 0;
253                 debug(' -- passed t ', result);
255                 return result;
256         }
258         /**
259          * Number of visible fraction digits in n, with trailing zeros.
260          */
261         function v() {
262                 var result = _v_();
264                 if (result === null) {
265                         debug(' -- failed v ', number);
267                         return result;
268                 }
270                 result = (number + '.').split('.')[1].length || 0;
271                 debug(' -- passed v ', result);
273                 return result;
274         }
276         /**
277          * Number of visible fraction digits in n, without trailing zeros.
278          */
279         function w() {
280                 var result = _w_();
282                 if (result === null) {
283                         debug(' -- failed w ', number);
285                         return result;
286                 }
288                 result = (number + '.').split('.')[1].replace(/0$/, '').length || 0;
289                 debug(' -- passed w ', result);
291                 return result;
292         }
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]);
300         function mod() {
301                 var result = sequence(
302                         [operand, whitespace, choice([_mod_, _percent_]), whitespace, value]
303                 );
305                 if (result === null) {
306                         debug(' -- failed mod');
308                         return null;
309                 }
311                 debug(' -- passed ' + parseInt(result[0], 10) + ' ' + result[2] + ' ' + parseInt(result[4], 10));
313                 return parseInt(result[0], 10) % parseInt(result[4], 10);
314         }
316         function not() {
317                 var result = sequence([whitespace, _not_]);
319                 if (result === null) {
320                         debug(' -- failed not');
322                         return null;
323                 }
325                 return result[1];
326         }
328         // is_relation   = expr 'is' ('not')? value
329         function is() {
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);
336                 }
338                 debug(' -- failed is');
340                 return null;
341         }
343         // is_relation   = expr 'is' ('not')? value
344         function isnot() {
345                 var result = sequence(
346                         [expression, whitespace, choice([_isnot_, _isnot_sign_]), whitespace, value]
347                 );
349                 if (result !== null) {
350                         debug(' -- passed isnot: ' + result[0] + ' != ' + parseInt(result[4], 10));
352                         return result[0] !== parseInt(result[4], 10);
353                 }
355                 debug(' -- failed isnot');
357                 return null;
358         }
360         function not_in() {
361                 var i, range_list,
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)) {
370                                         return false;
371                                 }
372                         }
374                         return true;
375                 }
377                 debug(' -- failed not_in');
379                 return null;
380         }
382         // range_list    = (range | value) (',' range_list)*
383         function rangeList() {
384                 var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]),
385                         resultList = [];
387                 if (result !== null) {
388                         resultList = resultList.concat(result[0]);
390                         if (result[1][0]) {
391                                 resultList = resultList.concat(result[1][0]);
392                         }
394                         return resultList;
395                 }
397                 debug(' -- failed rangeList');
399                 return null;
400         }
402         function rangeTail() {
403                 // ',' range_list
404                 var result = sequence([_comma_, rangeList]);
406                 if (result !== null) {
407                         return result[1];
408                 }
410                 debug(' -- failed rangeTail');
412                 return null;
413         }
415         // range         = value'..'value
416         function range() {
417                 var i, array, left, right,
418                         result = sequence([value, _range_, value]);
420                 if (result !== null) {
421                         debug(' -- passed range');
423                         array = [];
424                         left = parseInt(result[0], 10);
425                         right = parseInt(result[2], 10);
427                         for (i = left; i <= right; i++) {
428                                 array.push(i);
429                         }
431                         return array;
432                 }
434                 debug(' -- failed range');
436                 return null;
437         }
439         function _in() {
440                 var result, range_list, i;
442                 // in_relation   = expr ('not')? 'in' range_list
443                 result = sequence(
444                         [expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]
445                 );
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');
455                                 }
456                         }
458                         return (result[1][0] === 'not');
459                 }
461                 debug(' -- failed _in ');
463                 return null;
464         }
466         /**
467          * The difference between "in" and "within" is that
468          * "in" only includes integers in the specified range,
469          * while "within" includes all values.
470          */
471         function within() {
472                 var range_list, result;
474                 // within_relation = expr ('not')? 'within' range_list
475                 result = sequence(
476                         [expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]
477                 );
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');
488                         }
490                         return (result[1][0] === 'not');
491                 }
493                 debug(' -- failed within ');
495                 return null;
496         }
498         // relation      = is_relation | in_relation | within_relation
499         relation = choice([is, not_in, isnot, _in, within]);
501         // and_condition = relation ('and' relation)*
502         function and() {
503                 var i,
504                         result = sequence([relation, nOrMore(0, andTail)]);
506                 if (result) {
507                         if (!result[0]) {
508                                 return false;
509                         }
511                         for (i = 0; i < result[1].length; i++) {
512                                 if (!result[1][i]) {
513                                         return false;
514                                 }
515                         }
517                         return true;
518                 }
520                 debug(' -- failed and');
522                 return null;
523         }
525         // ('and' relation)*
526         function andTail() {
527                 var result = sequence([whitespace, _and_, whitespace, relation]);
529                 if (result !== null) {
530                         debug(' -- passed andTail' + result);
532                         return result[3];
533                 }
535                 debug(' -- failed andTail');
537                 return null;
539         }
540         //  ('or' and_condition)*
541         function orTail() {
542                 var result = sequence([whitespace, _or_, whitespace, and]);
544                 if (result !== null) {
545                         debug(' -- passed orTail: ' + result[3]);
547                         return result[3];
548                 }
550                 debug(' -- failed orTail');
552                 return null;
553         }
555         // condition     = and_condition ('or' and_condition)*
556         function condition() {
557                 var i,
558                         result = sequence([and, nOrMore(0, orTail)]);
560                 if (result) {
561                         for (i = 0; i < result[1].length; i++) {
562                                 if (result[1][i]) {
563                                         return true;
564                                 }
565                         }
567                         return result[0];
568                 }
570                 return false;
571         }
573         result = condition();
575         /**
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.
580          */
581         if (result === null) {
582                 throw new Error('Parse error at position ' + pos.toString() + ' for rule: ' + rule);
583         }
585         if (pos !== rule.length) {
586                 debug('Warning: Rule not parsed completely. Parser stopped at ' + rule.substr(0, pos) + ' for rule: ' + rule);
587         }
589         return result;
592 /* pluralRuleParser ends here */
593 mw.libs.pluralRuleParser = pluralRuleParser;
595 } )( mediaWiki );