Merge "support for interwikis in experimental JS preview"
[lhc/web/wiklou.git] / resources / mediawiki.libs / CLDRPluralRuleParser.js
1 /* This is cldrpluralparser 1.0, ported to MediaWiki ResourceLoader */
2
3 /**
4 * cldrpluralparser.js
5 * A parser engine for CLDR plural rules.
6 *
7 * Copyright 2012 GPLV3+, Santhosh Thottingal
8 *
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
14 */
15
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 ) {
23
24 function pluralRuleParser(rule, number) {
25 /*
26 Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
27 -----------------------------------------------------------------
28
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
40
41 */
42 // Indicates current position in the rule as we parse through it.
43 // Shared among all parsing functions below.
44 var pos = 0;
45
46 var whitespace = makeRegexParser(/^\s+/);
47 var digits = makeRegexParser(/^\d+/);
48
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');
59
60 function debug() {
61 /* console.log.apply(console, arguments);*/
62 }
63
64 debug('pluralRuleParser', rule, number);
65
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 }
78
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 }
95
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 }
114
115 // Helpers -- just make parserSyntax out of simpler JS builtin types
116
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 }
128
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 }
139
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 }
150
151 var expression = choice([mod, n]);
152
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 }
162
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 }
172
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 }
186
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 }
198
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 }
208
209 function range() {
210 var result = sequence([digits, _range_, digits]);
211 if (result !== null) {
212 debug(" -- passed range");
213 var array = [];
214 var left = parseInt(result[0], 10);
215 var right = parseInt(result[2], 10);
216 for ( i = left; i <= right; i++) {
217 array.push(i);
218 }
219 return array;
220 }
221 debug(" -- failed range");
222 return null;
223 }
224
225 function _in() {
226 // in_relation = expr ('not')? 'in' range_list
227 var result = sequence([expression, nOrMore(0, not), whitespace, _in_, whitespace, rangeList]);
228 if (result !== null) {
229 debug(" -- passed _in");
230 var range_list = result[5];
231 for (var i = 0; i < range_list.length; i++) {
232 if (parseInt(range_list[i], 10) === result[0]) {
233 return (result[1][0] !== 'not');
234 }
235 }
236 return (result[1][0] === 'not');
237 }
238 debug(" -- failed _in ");
239 return null;
240 }
241
242 function within() {
243 var result = sequence([expression, whitespace, _within_, whitespace, rangeList]);
244 if (result !== null) {
245 debug(" -- passed within ");
246 var range_list = result[4];
247 return (parseInt( range_list[0],10 )<= result[0] && result[0] <= parseInt( range_list[1], 10));
248 }
249 debug(" -- failed within ");
250 return null;
251 }
252
253
254 var relation = choice([is, _in, within]);
255
256 function and() {
257 var result = sequence([relation, whitespace, _and_, whitespace, condition]);
258 if (result) {
259 debug(" -- passed and");
260 return result[0] && result[4];
261 }
262 debug(" -- failed and");
263 return null;
264 }
265
266 function or() {
267 var result = sequence([relation, whitespace, _or_, whitespace, condition]);
268 if (result) {
269 debug(" -- passed or");
270 return result[0] || result[4];
271 }
272 debug(" -- failed or");
273 return null;
274 }
275
276 var condition = choice([and, or, relation]);
277
278 function start() {
279 var result = condition();
280 return result;
281 }
282
283
284 var result = start();
285
286 /*
287 * For success, the pos must have gotten to the end of the rule
288 * and returned a non-null.
289 * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
290 */
291 if (result === null || pos !== rule.length) {
292 // throw new Error("Parse error at position " + pos.toString() + " in input: " + rule + " result is " + result);
293 }
294
295 return result;
296 }
297
298 /* For module loaders, e.g. NodeJS, NPM */
299 if (typeof module !== 'undefined' && module.exports) {
300 module.exports = pluralRuleParser;
301 }
302
303 /* pluralRuleParser ends here */
304 mw.libs.pluralRuleParser = pluralRuleParser;
305
306 } )( mediaWiki );