diff options
Diffstat (limited to 'resources/src/mediawiki.libs/CLDRPluralRuleParser.js')
-rw-r--r-- | resources/src/mediawiki.libs/CLDRPluralRuleParser.js | 475 |
1 files changed, 475 insertions, 0 deletions
diff --git a/resources/src/mediawiki.libs/CLDRPluralRuleParser.js b/resources/src/mediawiki.libs/CLDRPluralRuleParser.js new file mode 100644 index 00000000..83c25245 --- /dev/null +++ b/resources/src/mediawiki.libs/CLDRPluralRuleParser.js @@ -0,0 +1,475 @@ +/* This is CLDRPluralRuleParser v1.1, ported to MediaWiki ResourceLoader */ + +/** +* CLDRPluralRuleParser.js +* A parser engine for CLDR plural rules. +* +* Copyright 2012 GPLV3+, Santhosh Thottingal +* +* @version 0.1.0-alpha +* @source https://github.com/santhoshtr/CLDRPluralRuleParser +* @author Santhosh Thottingal <santhosh.thottingal@gmail.com> +* @author Timo Tijhof +* @author Amir Aharoni +*/ + +( function ( mw ) { +/** + * Evaluates a plural rule in CLDR syntax for a number + * @param {string} rule + * @param {integer} number + * @return {boolean} true if evaluation passed, false if evaluation failed. + */ + +function pluralRuleParser(rule, number) { + /* + Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules + ----------------------------------------------------------------- + condition = and_condition ('or' and_condition)* + ('@integer' samples)? + ('@decimal' samples)? + and_condition = relation ('and' relation)* + relation = is_relation | in_relation | within_relation + is_relation = expr 'is' ('not')? value + in_relation = expr (('not')? 'in' | '=' | '!=') range_list + within_relation = expr ('not')? 'within' range_list + expr = operand (('mod' | '%') value)? + operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w' + range_list = (range | value) (',' range_list)* + value = digit+ + digit = 0|1|2|3|4|5|6|7|8|9 + range = value'..'value + samples = sampleRange (',' sampleRange)* (',' ('…'|'...'))? + sampleRange = decimalValue '~' decimalValue + decimalValue = value ('.' value)? + */ + + // we don't evaluate the samples section of the rule. Ignore it. + rule = rule.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, ''); + + if (!rule.length) { + // empty rule or 'other' rule. + return true; + } + // Indicates current position in the rule as we parse through it. + // Shared among all parsing functions below. + var pos = 0, + operand, + expression, + relation, + result, + whitespace = makeRegexParser(/^\s+/), + value = makeRegexParser(/^\d+/), + _n_ = makeStringParser('n'), + _i_ = makeStringParser('i'), + _f_ = makeStringParser('f'), + _t_ = makeStringParser('t'), + _v_ = makeStringParser('v'), + _w_ = makeStringParser('w'), + _is_ = makeStringParser('is'), + _isnot_ = makeStringParser('is not'), + _isnot_sign_ = makeStringParser('!='), + _equal_ = makeStringParser('='), + _mod_ = makeStringParser('mod'), + _percent_ = makeStringParser('%'), + _not_ = makeStringParser('not'), + _in_ = makeStringParser('in'), + _within_ = makeStringParser('within'), + _range_ = makeStringParser('..'), + _comma_ = makeStringParser(','), + _or_ = makeStringParser('or'), + _and_ = makeStringParser('and'); + + function debug() { + // console.log.apply(console, arguments); + } + + debug('pluralRuleParser', rule, number); + + // Try parsers until one works, if none work return null + + function choice(parserSyntax) { + return function() { + for (var i = 0; i < parserSyntax.length; i++) { + var result = parserSyntax[i](); + if (result !== null) { + return result; + } + } + return null; + }; + } + + // Try several parserSyntax-es in a row. + // All must succeed; otherwise, return null. + // This is the only eager one. + + function sequence(parserSyntax) { + var originalPos = pos; + var result = []; + for (var i = 0; i < parserSyntax.length; i++) { + var res = parserSyntax[i](); + if (res === null) { + pos = originalPos; + return null; + } + result.push(res); + } + return result; + } + + // Run the same parser over and over until it fails. + // Must succeed a minimum of n times; otherwise, return null. + + function nOrMore(n, p) { + return function() { + var originalPos = pos; + var result = []; + var parsed = p(); + while (parsed !== null) { + result.push(parsed); + parsed = p(); + } + if (result.length < n) { + pos = originalPos; + return null; + } + return result; + }; + } + + // Helpers -- just make parserSyntax out of simpler JS builtin types + function makeStringParser(s) { + var len = s.length; + return function() { + var result = null; + if (rule.substr(pos, len) === s) { + result = s; + pos += len; + } + + return result; + }; + } + + function makeRegexParser(regex) { + return function() { + var matches = rule.substr(pos).match(regex); + if (matches === null) { + return null; + } + pos += matches[0].length; + return matches[0]; + }; + } + + /* + * integer digits of n. + */ + function i() { + var result = _i_(); + if (result === null) { + debug(' -- failed i', parseInt(number, 10)); + return result; + } + result = parseInt(number, 10); + debug(' -- passed i ', result); + return result; + } + + /* + * absolute value of the source number (integer and decimals). + */ + function n() { + var result = _n_(); + if (result === null) { + debug(' -- failed n ', number); + return result; + } + result = parseFloat(number, 10); + debug(' -- passed n ', result); + return result; + } + + /* + * visible fractional digits in n, with trailing zeros. + */ + function f() { + var result = _f_(); + if (result === null) { + debug(' -- failed f ', number); + return result; + } + result = (number + '.').split('.')[1] || 0; + debug(' -- passed f ', result); + return result; + } + + /* + * visible fractional digits in n, without trailing zeros. + */ + function t() { + var result = _t_(); + if (result === null) { + debug(' -- failed t ', number); + return result; + } + result = (number + '.').split('.')[1].replace(/0$/, '') || 0; + debug(' -- passed t ', result); + return result; + } + + /* + * number of visible fraction digits in n, with trailing zeros. + */ + function v() { + var result = _v_(); + if (result === null) { + debug(' -- failed v ', number); + return result; + } + result = (number + '.').split('.')[1].length || 0; + debug(' -- passed v ', result); + return result; + } + + /* + * number of visible fraction digits in n, without trailing zeros. + */ + function w() { + var result = _w_(); + if (result === null) { + debug(' -- failed w ', number); + return result; + } + result = (number + '.').split('.')[1].replace(/0$/, '').length || 0; + debug(' -- passed w ', result); + return result; + } + + // operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w' + operand = choice([n, i, f, t, v, w]); + + // expr = operand (('mod' | '%') value)? + expression = choice([mod, operand]); + + function mod() { + var result = sequence([operand, whitespace, choice([_mod_, _percent_]), whitespace, value]); + if (result === null) { + debug(' -- failed mod'); + return null; + } + debug(' -- passed ' + parseInt(result[0], 10) + ' ' + result[2] + ' ' + parseInt(result[4], 10)); + return parseInt(result[0], 10) % parseInt(result[4], 10); + } + + function not() { + var result = sequence([whitespace, _not_]); + if (result === null) { + debug(' -- failed not'); + return null; + } + + return result[1]; + } + + // is_relation = expr 'is' ('not')? value + function is() { + var result = sequence([expression, whitespace, choice([_is_]), whitespace, value]); + if (result !== null) { + debug(' -- passed is : ' + result[0] + ' == ' + parseInt(result[4], 10)); + return result[0] === parseInt(result[4], 10); + } + debug(' -- failed is'); + return null; + } + + // is_relation = expr 'is' ('not')? value + function isnot() { + var result = sequence([expression, whitespace, choice([_isnot_, _isnot_sign_]), whitespace, value]); + if (result !== null) { + debug(' -- passed isnot: ' + result[0] + ' != ' + parseInt(result[4], 10)); + return result[0] !== parseInt(result[4], 10); + } + debug(' -- failed isnot'); + return null; + } + + function not_in() { + var result = sequence([expression, whitespace, _isnot_sign_, whitespace, rangeList]); + if (result !== null) { + debug(' -- passed not_in: ' + result[0] + ' != ' + result[4]); + var range_list = result[4]; + for (var i = 0; i < range_list.length; i++) { + if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) { + return false; + } + } + return true; + } + debug(' -- failed not_in'); + return null; + } + + // range_list = (range | value) (',' range_list)* + function rangeList() { + var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]); + var resultList = []; + if (result !== null) { + resultList = resultList.concat(result[0]); + if (result[1][0]) { + resultList = resultList.concat(result[1][0]); + } + return resultList; + } + debug(' -- failed rangeList'); + return null; + } + + function rangeTail() { + // ',' range_list + var result = sequence([_comma_, rangeList]); + if (result !== null) { + return result[1]; + } + debug(' -- failed rangeTail'); + return null; + } + + // range = value'..'value + + function range() { + var i; + var result = sequence([value, _range_, value]); + if (result !== null) { + debug(' -- passed range'); + var array = []; + var left = parseInt(result[0], 10); + var right = parseInt(result[2], 10); + for (i = left; i <= right; i++) { + array.push(i); + } + return array; + } + debug(' -- failed range'); + return null; + } + + function _in() { + // in_relation = expr ('not')? 'in' range_list + var result = sequence([expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]); + if (result !== null) { + debug(' -- passed _in:' + result); + var range_list = result[5]; + for (var i = 0; i < range_list.length; i++) { + if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) { + return (result[1][0] !== 'not'); + } + } + return (result[1][0] === 'not'); + } + debug(' -- failed _in '); + return null; + } + + /* + * The difference between in and within is that in only includes integers in the specified range, + * while within includes all values. + */ + + function within() { + // within_relation = expr ('not')? 'within' range_list + var result = sequence([expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]); + if (result !== null) { + debug(' -- passed within'); + var range_list = result[5]; + if ((result[0] >= parseInt(range_list[0], 10)) && + (result[0] < parseInt(range_list[range_list.length - 1], 10))) { + return (result[1][0] !== 'not'); + } + return (result[1][0] === 'not'); + } + debug(' -- failed within '); + return null; + } + + // relation = is_relation | in_relation | within_relation + relation = choice([is, not_in, isnot, _in, within]); + + // and_condition = relation ('and' relation)* + function and() { + var result = sequence([relation, nOrMore(0, andTail)]); + if (result) { + if (!result[0]) { + return false; + } + for (var i = 0; i < result[1].length; i++) { + if (!result[1][i]) { + return false; + } + } + return true; + } + debug(' -- failed and'); + return null; + } + + // ('and' relation)* + function andTail() { + var result = sequence([whitespace, _and_, whitespace, relation]); + if (result !== null) { + debug(' -- passed andTail' + result); + return result[3]; + } + debug(' -- failed andTail'); + return null; + + } + // ('or' and_condition)* + function orTail() { + var result = sequence([whitespace, _or_, whitespace, and]); + if (result !== null) { + debug(' -- passed orTail: ' + result[3]); + return result[3]; + } + debug(' -- failed orTail'); + return null; + + } + + // condition = and_condition ('or' and_condition)* + function condition() { + var result = sequence([and, nOrMore(0, orTail)]); + if (result) { + for (var i = 0; i < result[1].length; i++) { + if (result[1][i]) { + return true; + } + } + return result[0]; + + } + return false; + } + + result = condition(); + /* + * For success, the pos must have gotten to the end of the rule + * and returned a non-null. + * n.b. This is part of language infrastructure, so we do not throw an internationalizable message. + */ + if (result === null) { + throw new Error('Parse error at position ' + pos.toString() + ' for rule: ' + rule); + } + + if (pos !== rule.length) { + debug('Warning: Rule not parsed completely. Parser stopped at ' + rule.substr(0, pos) + ' for rule: ' + rule); + } + + return result; +} + +/* pluralRuleParser ends here */ +mw.libs.pluralRuleParser = pluralRuleParser; + +} )( mediaWiki ); |