kopia lustrzana https://gitlab.com/gridtracker.org/gridtracker
686 wiersze
14 KiB
JavaScript
686 wiersze
14 KiB
JavaScript
/* eslint-disable no-tabs */
|
|
/**
|
|
* cldrpluralparser.js
|
|
* A parser engine for CLDR plural rules.
|
|
*
|
|
* Copyright 2012-2014 Santhosh Thottingal and other contributors
|
|
* Released under the MIT license
|
|
* http://opensource.org/licenses/MIT
|
|
*
|
|
* @source https://github.com/santhoshtr/CLDRPluralRuleParser
|
|
* @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
|
|
* @author Timo Tijhof
|
|
* @author Amir Aharoni
|
|
*/
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
|
|
// UMD returnExports https://github.com/umdjs/umd/blob/master/returnExports.js
|
|
(function(root, factory)
|
|
{
|
|
if (typeof define === "function" && define.amd)
|
|
{
|
|
// AMD. Register as an anonymous module.
|
|
define(factory);
|
|
}
|
|
else if (typeof exports === "object")
|
|
{
|
|
// Node. Does not work with strict CommonJS, but
|
|
// only CommonJS-like environments that support module.exports,
|
|
// like Node.
|
|
module.exports = factory();
|
|
}
|
|
else
|
|
{
|
|
// Browser globals (root is window)
|
|
root.pluralRuleParser = factory();
|
|
}
|
|
}(this, function()
|
|
{
|
|
function pluralRuleParser(rule, number)
|
|
{
|
|
"use strict";
|
|
|
|
/*
|
|
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 the 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()
|
|
{
|
|
var i, result;
|
|
|
|
for (i = 0; i < parserSyntax.length; i++)
|
|
{
|
|
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 i, parserRes,
|
|
originalPos = pos,
|
|
result = [];
|
|
|
|
for (i = 0; i < parserSyntax.length; i++)
|
|
{
|
|
parserRes = parserSyntax[i]();
|
|
|
|
if (parserRes === null)
|
|
{
|
|
pos = originalPos;
|
|
|
|
return null;
|
|
}
|
|
|
|
result.push(parserRes);
|
|
}
|
|
|
|
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,
|
|
result = [],
|
|
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 parseFloat(result[0]) % 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 i, range_list,
|
|
result = sequence([expression, whitespace, _isnot_sign_, whitespace, rangeList]);
|
|
|
|
if (result !== null)
|
|
{
|
|
debug(" -- passed not_in: " + result[0] + " != " + result[4]);
|
|
range_list = result[4];
|
|
|
|
for (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)]),
|
|
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, array, left, right,
|
|
result = sequence([value, _range_, value]);
|
|
|
|
if (result !== null)
|
|
{
|
|
debug(" -- passed range");
|
|
|
|
array = [];
|
|
left = parseInt(result[0], 10);
|
|
right = parseInt(result[2], 10);
|
|
|
|
for (i = left; i <= right; i++)
|
|
{
|
|
array.push(i);
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
debug(" -- failed range");
|
|
|
|
return null;
|
|
}
|
|
|
|
function _in()
|
|
{
|
|
var result, range_list, i;
|
|
|
|
// in_relation = expr ('not')? 'in' range_list
|
|
result = sequence(
|
|
[expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]
|
|
);
|
|
|
|
if (result !== null)
|
|
{
|
|
debug(" -- passed _in:" + result);
|
|
|
|
range_list = result[5];
|
|
|
|
for (i = 0; i < range_list.length; i++)
|
|
{
|
|
if (parseInt(range_list[i], 10) === parseFloat(result[0]))
|
|
{
|
|
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()
|
|
{
|
|
var range_list, result;
|
|
|
|
// within_relation = expr ('not')? 'within' range_list
|
|
result = sequence(
|
|
[expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]
|
|
);
|
|
|
|
if (result !== null)
|
|
{
|
|
debug(" -- passed within");
|
|
|
|
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 i,
|
|
result = sequence([relation, nOrMore(0, andTail)]);
|
|
|
|
if (result)
|
|
{
|
|
if (!result[0])
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (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 i,
|
|
result = sequence([and, nOrMore(0, orTail)]);
|
|
|
|
if (result)
|
|
{
|
|
for (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;
|
|
}
|
|
|
|
return pluralRuleParser;
|
|
}));
|