Merge "BotPasswords: Indicate when a password needs reset"
[lhc/web/wiklou.git] / resources / lib / CLDRPluralRuleParser / CLDRPluralRuleParser.js
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 * @version 0.1.0
10 * @source https://github.com/santhoshtr/CLDRPluralRuleParser
11 * @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
12 * @author Timo Tijhof
13 * @author Amir Aharoni
14 */
15
16 /**
17 * Evaluates a plural rule in CLDR syntax for a number
18 * @param {string} rule
19 * @param {integer} number
20 * @return {boolean} true if evaluation passed, false if evaluation failed.
21 */
22
23 // UMD returnExports https://github.com/umdjs/umd/blob/master/returnExports.js
24 (function(root, factory) {
25 if (typeof define === 'function' && define.amd) {
26 // AMD. Register as an anonymous module.
27 define(factory);
28 } else if (typeof exports === 'object') {
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() {
38
39 function pluralRuleParser(rule, number) {
40 'use strict';
41
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 */
63
64 // We don't evaluate the samples section of the rule. Ignore it.
65 rule = rule.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');
66
67 if (!rule.length) {
68 // Empty rule or 'other' rule.
69 return true;
70 }
71
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');
100
101 function debug() {
102 // console.log.apply(console, arguments);
103 }
104
105 debug('pluralRuleParser', rule, number);
106
107 // Try parsers until one works, if none work return null
108 function choice(parserSyntax) {
109 return function() {
110 var i, result;
111
112 for (i = 0; i < parserSyntax.length; i++) {
113 result = parserSyntax[i]();
114
115 if (result !== null) {
116 return result;
117 }
118 }
119
120 return null;
121 };
122 }
123
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 = [];
131
132 for (i = 0; i < parserSyntax.length; i++) {
133 parserRes = parserSyntax[i]();
134
135 if (parserRes === null) {
136 pos = originalPos;
137
138 return null;
139 }
140
141 result.push(parserRes);
142 }
143
144 return result;
145 }
146
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();
154
155 while (parsed !== null) {
156 result.push(parsed);
157 parsed = p();
158 }
159
160 if (result.length < n) {
161 pos = originalPos;
162
163 return null;
164 }
165
166 return result;
167 };
168 }
169
170 // Helpers - just make parserSyntax out of simpler JS builtin types
171 function makeStringParser(s) {
172 var len = s.length;
173
174 return function() {
175 var result = null;
176
177 if (rule.substr(pos, len) === s) {
178 result = s;
179 pos += len;
180 }
181
182 return result;
183 };
184 }
185
186 function makeRegexParser(regex) {
187 return function() {
188 var matches = rule.substr(pos).match(regex);
189
190 if (matches === null) {
191 return null;
192 }
193
194 pos += matches[0].length;
195
196 return matches[0];
197 };
198 }
199
200 /**
201 * Integer digits of n.
202 */
203 function i() {
204 var result = _i_();
205
206 if (result === null) {
207 debug(' -- failed i', parseInt(number, 10));
208
209 return result;
210 }
211
212 result = parseInt(number, 10);
213 debug(' -- passed i ', result);
214
215 return result;
216 }
217
218 /**
219 * Absolute value of the source number (integer and decimals).
220 */
221 function n() {
222 var result = _n_();
223
224 if (result === null) {
225 debug(' -- failed n ', number);
226
227 return result;
228 }
229
230 result = parseFloat(number, 10);
231 debug(' -- passed n ', result);
232
233 return result;
234 }
235
236 /**
237 * Visible fractional digits in n, with trailing zeros.
238 */
239 function f() {
240 var result = _f_();
241
242 if (result === null) {
243 debug(' -- failed f ', number);
244
245 return result;
246 }
247
248 result = (number + '.').split('.')[1] || 0;
249 debug(' -- passed f ', result);
250
251 return result;
252 }
253
254 /**
255 * Visible fractional digits in n, without trailing zeros.
256 */
257 function t() {
258 var result = _t_();
259
260 if (result === null) {
261 debug(' -- failed t ', number);
262
263 return result;
264 }
265
266 result = (number + '.').split('.')[1].replace(/0$/, '') || 0;
267 debug(' -- passed t ', result);
268
269 return result;
270 }
271
272 /**
273 * Number of visible fraction digits in n, with trailing zeros.
274 */
275 function v() {
276 var result = _v_();
277
278 if (result === null) {
279 debug(' -- failed v ', number);
280
281 return result;
282 }
283
284 result = (number + '.').split('.')[1].length || 0;
285 debug(' -- passed v ', result);
286
287 return result;
288 }
289
290 /**
291 * Number of visible fraction digits in n, without trailing zeros.
292 */
293 function w() {
294 var result = _w_();
295
296 if (result === null) {
297 debug(' -- failed w ', number);
298
299 return result;
300 }
301
302 result = (number + '.').split('.')[1].replace(/0$/, '').length || 0;
303 debug(' -- passed w ', result);
304
305 return result;
306 }
307
308 // operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
309 operand = choice([n, i, f, t, v, w]);
310
311 // expr = operand (('mod' | '%') value)?
312 expression = choice([mod, operand]);
313
314 function mod() {
315 var result = sequence(
316 [operand, whitespace, choice([_mod_, _percent_]), whitespace, value]
317 );
318
319 if (result === null) {
320 debug(' -- failed mod');
321
322 return null;
323 }
324
325 debug(' -- passed ' + parseInt(result[0], 10) + ' ' + result[2] + ' ' + parseInt(result[4], 10));
326
327 return parseInt(result[0], 10) % parseInt(result[4], 10);
328 }
329
330 function not() {
331 var result = sequence([whitespace, _not_]);
332
333 if (result === null) {
334 debug(' -- failed not');
335
336 return null;
337 }
338
339 return result[1];
340 }
341
342 // is_relation = expr 'is' ('not')? value
343 function is() {
344 var result = sequence([expression, whitespace, choice([_is_]), whitespace, value]);
345
346 if (result !== null) {
347 debug(' -- passed is : ' + result[0] + ' == ' + parseInt(result[4], 10));
348
349 return result[0] === parseInt(result[4], 10);
350 }
351
352 debug(' -- failed is');
353
354 return null;
355 }
356
357 // is_relation = expr 'is' ('not')? value
358 function isnot() {
359 var result = sequence(
360 [expression, whitespace, choice([_isnot_, _isnot_sign_]), whitespace, value]
361 );
362
363 if (result !== null) {
364 debug(' -- passed isnot: ' + result[0] + ' != ' + parseInt(result[4], 10));
365
366 return result[0] !== parseInt(result[4], 10);
367 }
368
369 debug(' -- failed isnot');
370
371 return null;
372 }
373
374 function not_in() {
375 var i, range_list,
376 result = sequence([expression, whitespace, _isnot_sign_, whitespace, rangeList]);
377
378 if (result !== null) {
379 debug(' -- passed not_in: ' + result[0] + ' != ' + result[4]);
380 range_list = result[4];
381
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 }
387
388 return true;
389 }
390
391 debug(' -- failed not_in');
392
393 return null;
394 }
395
396 // range_list = (range | value) (',' range_list)*
397 function rangeList() {
398 var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]),
399 resultList = [];
400
401 if (result !== null) {
402 resultList = resultList.concat(result[0]);
403
404 if (result[1][0]) {
405 resultList = resultList.concat(result[1][0]);
406 }
407
408 return resultList;
409 }
410
411 debug(' -- failed rangeList');
412
413 return null;
414 }
415
416 function rangeTail() {
417 // ',' range_list
418 var result = sequence([_comma_, rangeList]);
419
420 if (result !== null) {
421 return result[1];
422 }
423
424 debug(' -- failed rangeTail');
425
426 return null;
427 }
428
429 // range = value'..'value
430 function range() {
431 var i, array, left, right,
432 result = sequence([value, _range_, value]);
433
434 if (result !== null) {
435 debug(' -- passed range');
436
437 array = [];
438 left = parseInt(result[0], 10);
439 right = parseInt(result[2], 10);
440
441 for (i = left; i <= right; i++) {
442 array.push(i);
443 }
444
445 return array;
446 }
447
448 debug(' -- failed range');
449
450 return null;
451 }
452
453 function _in() {
454 var result, range_list, i;
455
456 // in_relation = expr ('not')? 'in' range_list
457 result = sequence(
458 [expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]
459 );
460
461 if (result !== null) {
462 debug(' -- passed _in:' + result);
463
464 range_list = result[5];
465
466 for (i = 0; i < range_list.length; i++) {
467 if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
468 return (result[1][0] !== 'not');
469 }
470 }
471
472 return (result[1][0] === 'not');
473 }
474
475 debug(' -- failed _in ');
476
477 return null;
478 }
479
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;
487
488 // within_relation = expr ('not')? 'within' range_list
489 result = sequence(
490 [expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]
491 );
492
493 if (result !== null) {
494 debug(' -- passed within');
495
496 range_list = result[5];
497
498 if ((result[0] >= parseInt(range_list[0], 10)) &&
499 (result[0] < parseInt(range_list[range_list.length - 1], 10))) {
500
501 return (result[1][0] !== 'not');
502 }
503
504 return (result[1][0] === 'not');
505 }
506
507 debug(' -- failed within ');
508
509 return null;
510 }
511
512 // relation = is_relation | in_relation | within_relation
513 relation = choice([is, not_in, isnot, _in, within]);
514
515 // and_condition = relation ('and' relation)*
516 function and() {
517 var i,
518 result = sequence([relation, nOrMore(0, andTail)]);
519
520 if (result) {
521 if (!result[0]) {
522 return false;
523 }
524
525 for (i = 0; i < result[1].length; i++) {
526 if (!result[1][i]) {
527 return false;
528 }
529 }
530
531 return true;
532 }
533
534 debug(' -- failed and');
535
536 return null;
537 }
538
539 // ('and' relation)*
540 function andTail() {
541 var result = sequence([whitespace, _and_, whitespace, relation]);
542
543 if (result !== null) {
544 debug(' -- passed andTail' + result);
545
546 return result[3];
547 }
548
549 debug(' -- failed andTail');
550
551 return null;
552
553 }
554 // ('or' and_condition)*
555 function orTail() {
556 var result = sequence([whitespace, _or_, whitespace, and]);
557
558 if (result !== null) {
559 debug(' -- passed orTail: ' + result[3]);
560
561 return result[3];
562 }
563
564 debug(' -- failed orTail');
565
566 return null;
567 }
568
569 // condition = and_condition ('or' and_condition)*
570 function condition() {
571 var i,
572 result = sequence([and, nOrMore(0, orTail)]);
573
574 if (result) {
575 for (i = 0; i < result[1].length; i++) {
576 if (result[1][i]) {
577 return true;
578 }
579 }
580
581 return result[0];
582 }
583
584 return false;
585 }
586
587 result = condition();
588
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 }
598
599 if (pos !== rule.length) {
600 debug('Warning: Rule not parsed completely. Parser stopped at ' + rule.substr(0, pos) + ' for rule: ' + rule);
601 }
602
603 return result;
604 }
605
606 return pluralRuleParser;
607
608 }));