Merge "Typo fix"
[mediawiki.git] / resources / mediawiki.libs / CLDRPluralRuleParser.js
blob441bc91f68027132af764baa68026c6489092d64
1 /* This is cldrpluralparser 1.0, ported to MediaWiki ResourceLoader */
3 /**
4 * cldrpluralparser.js
5 * A parser engine for CLDR plural rules.
7 * Copyright 2012 GPLV3+, Santhosh Thottingal
9 * @version 0.1.0-alpha
10 * @source https://github.com/santhoshtr/CLDRPluralRuleParser
11 * @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
12 * @author Timo Tijhof
13 * @author Amir Aharoni
16 /**
17  * Evaluates a plural rule in CLDR syntax for a number
18  * @param rule
19  * @param number
20  * @return true|false|null
21  */
22 ( function( mw ) {
24 function pluralRuleParser(rule, number) {
25         /*
26         Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
27         -----------------------------------------------------------------
29         condition     = and_condition ('or' and_condition)*
30         and_condition = relation ('and' relation)*
31         relation      = is_relation | in_relation | within_relation | 'n' <EOL>
32         is_relation   = expr 'is' ('not')? value
33         in_relation   = expr ('not')? 'in' range_list
34         within_relation = expr ('not')? 'within' range_list
35         expr          = 'n' ('mod' value)?
36         range_list    = (range | value) (',' range_list)*
37         value         = digit+
38         digit         = 0|1|2|3|4|5|6|7|8|9
39         range         = value'..'value
41         */
42         // Indicates current position in the rule as we parse through it.
43         // Shared among all parsing functions below.
44         var pos = 0;
46         var whitespace = makeRegexParser(/^\s+/);
47         var digits = makeRegexParser(/^\d+/);
49         var _n_ = makeStringParser('n');
50         var _is_ = makeStringParser('is');
51         var _mod_ = makeStringParser('mod');
52         var _not_ = makeStringParser('not');
53         var _in_ = makeStringParser('in');
54         var _within_ = makeStringParser('within');
55         var _range_ = makeStringParser('..');
56         var _comma_ = makeStringParser(',');
57         var _or_ = makeStringParser('or');
58         var _and_ = makeStringParser('and');
60         function debug() {
61                 /* console.log.apply(console, arguments);*/
62         }
64         debug('pluralRuleParser', rule, number);
66         // Try parsers until one works, if none work return null
67         function choice(parserSyntax) {
68                 return function () {
69                         for (var i = 0; i < parserSyntax.length; i++) {
70                                 var result = parserSyntax[i]();
71                                 if (result !== null) {
72                                         return result;
73                                 }
74                         }
75                         return null;
76                 };
77         }
79         // Try several parserSyntax-es in a row.
80         // All must succeed; otherwise, return null.
81         // This is the only eager one.
82         function sequence(parserSyntax) {
83                 var originalPos = pos;
84                 var result = [];
85                 for (var i = 0; i < parserSyntax.length; i++) {
86                         var res = parserSyntax[i]();
87                         if (res === null) {
88                                 pos = originalPos;
89                                 return null;
90                         }
91                         result.push(res);
92                 }
93                 return result;
94         }
96         // Run the same parser over and over until it fails.
97         // Must succeed a minimum of n times; otherwise, return null.
98         function nOrMore(n, p) {
99                 return function () {
100                         var originalPos = pos;
101                         var result = [];
102                         var parsed = p();
103                         while (parsed !== null) {
104                                 result.push(parsed);
105                                 parsed = p();
106                         }
107                         if (result.length < n) {
108                                 pos = originalPos;
109                                 return null;
110                         }
111                         return result;
112                 };
113         }
115         // Helpers -- just make parserSyntax out of simpler JS builtin types
117         function makeStringParser(s) {
118                 var len = s.length;
119                 return function () {
120                         var result = null;
121                         if (rule.substr(pos, len) === s) {
122                                 result = s;
123                                 pos += len;
124                         }
125                         return result;
126                 };
127         }
129         function makeRegexParser(regex) {
130                 return function () {
131                         var matches = rule.substr(pos).match(regex);
132                         if (matches === null) {
133                                 return null;
134                         }
135                         pos += matches[0].length;
136                         return matches[0];
137                 };
138         }
140         function n() {
141                 var result = _n_();
142                 if (result === null) {
143                         debug(" -- failed n");
144                         return result;
145                 }
146                 result = parseInt(number, 10);
147                 debug(" -- passed n ", result);
148                 return result;
149         }
151         var expression = choice([mod, n]);
153         function mod() {
154                 var result = sequence([n, whitespace, _mod_, whitespace, digits]);
155                 if (result === null) {
156                         debug(" -- failed mod");
157                         return null;
158                 }
159                 debug(" -- passed mod");
160                 return parseInt(result[0], 10) % parseInt(result[4], 10);
161         }
163         function not() {
164                 var result = sequence([whitespace, _not_]);
165                 if (result === null) {
166                         debug(" -- failed not");
167                         return null;
168                 } else {
169                         return result[1];
170                 }
171         }
173         function is() {
174                 var result = sequence([expression, whitespace, _is_, nOrMore(0, not), whitespace, digits]);
175                 if (result !== null) {
176                         debug(" -- passed is");
177                         if (result[3][0] === 'not') {
178                                 return result[0] !== parseInt(result[5], 10);
179                         } else {
180                                 return result[0] === parseInt(result[5], 10);
181                         }
182                 }
183                 debug(" -- failed is");
184                 return null;
185         }
187         function rangeList() {
188                 // range_list    = (range | value) (',' range_list)*
189                 var result = sequence([choice([range, digits]), nOrMore(0, rangeTail)]);
190                 var resultList = [];
191                 if (result !== null) {
192                         resultList = resultList.concat(result[0], result[1][0]);
193                         return resultList;
194                 }
195                 debug(" -- failed rangeList");
196                 return null;
197         }
199         function rangeTail() {
200                 // ',' range_list
201                 var result = sequence([_comma_, rangeList]);
202                 if (result !== null) {
203                         return result[1];
204                 }
205                 debug(" -- failed rangeTail");
206                 return null;
207         }
209         function range() {
210                 var i;
211                 var result = sequence([digits, _range_, digits]);
212                 if (result !== null) {
213                         debug(" -- passed range");
214                         var array = [];
215                         var left = parseInt(result[0], 10);
216                         var right = parseInt(result[2], 10);
217                         for ( i = left; i <= right; i++) {
218                                 array.push(i);
219                         }
220                         return array;
221                 }
222                 debug(" -- failed range");
223                 return null;
224         }
226         function _in() {
227                 // in_relation   = expr ('not')? 'in' range_list
228                 var result = sequence([expression, nOrMore(0, not), whitespace, _in_, whitespace, rangeList]);
229                 if (result !== null) {
230                         debug(" -- passed _in");
231                         var range_list = result[5];
232                         for (var i = 0; i < range_list.length; i++) {
233                                 if (parseInt(range_list[i], 10) === result[0]) {
234                                         return (result[1][0] !== 'not');
235                                 }
236                         }
237                         return (result[1][0] === 'not');
238                 }
239                 debug(" -- failed _in ");
240                 return null;
241         }
243         function within() {
244                 var result = sequence([expression, whitespace, _within_, whitespace, rangeList]);
245                 if (result !== null) {
246                         debug(" -- passed within ");
247                         var range_list = result[4];
248                         return (parseInt( range_list[0],10 )<= result[0] && result[0] <= parseInt( range_list[1], 10));
249                 }
250                 debug(" -- failed within ");
251                 return null;
252         }
255         var relation = choice([is, _in, within]);
257         function and() {
258                 var result = sequence([relation, whitespace, _and_, whitespace, condition]);
259                 if (result) {
260                         debug(" -- passed and");
261                         return result[0] && result[4];
262                 }
263                 debug(" -- failed and");
264                 return null;
265         }
267         function or() {
268                 var result = sequence([relation, whitespace, _or_, whitespace, condition]);
269                 if (result) {
270                         debug(" -- passed or");
271                         return result[0] || result[4];
272                 }
273                 debug(" -- failed or");
274                 return null;
275         }
277         var condition = choice([and, or, relation]);
279         function isInt(n) {
280                 return parseFloat(n) % 1 === 0;
281         }
284         function start() {
285                 if (!isInt(number)) {
286                         return false;
287                 }
288                 var result = condition();
289                 return result;
290         }
293         var result = start();
295         /*
296          * For success, the pos must have gotten to the end of the rule
297          * and returned a non-null.
298          * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
299          */
300         if (result === null || pos !== rule.length) {
301                 // throw new Error("Parse error at position " + pos.toString() + " in input: " + rule + " result is " + result);
302         }
304         return result;
307 /* For module loaders, e.g. NodeJS, NPM */
308 if (typeof module !== 'undefined' && module.exports) {
309         module.exports = pluralRuleParser;
312 /* pluralRuleParser ends here */
313 mw.libs.pluralRuleParser = pluralRuleParser;
315 } )( mediaWiki );