UNPKG

cldr

Version:

Library for extracting data from CLDR (the Unicode Common Locale Data Repository)

704 lines (684 loc) 22.7 kB
function CldrRbnfRuleSet(config) { Object.assign(this, config); this.ruleByValue = {}; } CldrRbnfRuleSet.getSafeRendererName = (rendererName) => ('render-' + rendererName) .replace(/[^\w-]/g, '-') .replace(/[-_]+([0-9a-z])/gi, ($0, ch) => ch.toUpperCase()) .replace('GREEKNUMERALMAJUSCULES', 'GreekNumeralMajuscules'); function applyRenderer(expr, formatName) { return { type: 'CallExpression', callee: { type: 'MemberExpression', object: { type: 'ThisExpression', }, property: { type: 'Identifier', name: formatName, }, computed: false, }, arguments: [expr], }; } CldrRbnfRuleSet.prototype = { toFunctionAst() { const that = this; const isSeenByRuleSetType = {}; function ruleToExpressionAst(rule) { let expressionAsts = []; let rbnf = rule.rbnf; // "If a rule body begins with an apostrophe, the apostrophe is ignored, but all text after it becomes // significant (this is how you can have a rule's rule text begin with whitespace)." // -- http://www.icu-project.org/apiref/icu4c/classRuleBasedNumberFormat.html rbnf = rbnf.replace(/^'/, ''); rule.radix = rule.radix || 10; function getDivisor() { let divisor = 1; while (10 * divisor <= parseInt(rule.value, 10)) { // Inefficient, but won't suffer from Math.log rounding errors divisor *= 10; } return divisor; } // Replace is used for tokenization, the return value isn't used: rbnf.replace( /(?:([<>=])(?:(%%?[\w-]+)|([#,0.]+))?\1)|(?:\[([^\]]+)\])|([\x7f-\uffff:'.\s\w\d-]+|(?:\$\((cardinal|ordinal),([^)]+)\)))/gi, function ( $0, specialChar, otherFormat, decimalFormat, optional, literal, cardinalOrOrdinal, dollarRule, ) { // The meanings of the substitution token characters are as follows: if (dollarRule) { const callAst = { type: 'CallExpression', callee: { type: 'FunctionExpression', id: null, params: [ { type: 'Identifier', // The plural functions expect the parameter to be named `val`. name: 'val', }, ], body: { type: 'BlockStatement', body: cardinalOrOrdinal === 'cardinal' ? that.cardinalPluralRuleAst : that.ordinalPluralRuleAst, }, }, arguments: [ { type: 'Identifier', name: 'n', }, ], }; const objAst = { type: 'ObjectExpression', properties: [], }; dollarRule.split('}').forEach((fragment) => { const pluralCaseAndValue = fragment.split('{'); if (pluralCaseAndValue.length === 2) { objAst.properties.push({ type: 'Property', key: { type: 'Literal', value: pluralCaseAndValue[0], }, value: { type: 'Literal', value: pluralCaseAndValue[1], }, }); } }); expressionAsts.push({ type: 'MemberExpression', object: objAst, property: callAst, computed: true, }); } else if (specialChar) { let expr; let wrap = true; if (specialChar === '<') { // << if (/^\d+$/.test(rule.value)) { // In normal rule: Divide the number by the rule's divisor and format the quotient expr = { type: 'CallExpression', callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'Math', }, property: { type: 'Identifier', name: 'floor', }, computed: false, }, arguments: [ { type: 'BinaryExpression', operator: '/', left: { type: 'Identifier', name: 'n', }, right: { type: 'Literal', value: getDivisor(), }, }, ], }; } else if (rule.value === '-x') { throw new Error('<< not allowed in negative number rule'); } else { // In fraction or master rule: Isolate the number's integral part and format it. expr = { type: 'CallExpression', callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'Math', }, property: { type: 'Identifier', name: 'floor', }, computed: false, }, arguments: [ { type: 'Identifier', name: 'n', }, ], }; } } else if (specialChar === '>') { // >> if (/\./.test(rule.value)) { // Fraction or master rule => parseInt(String(n).replace(/\d*\./, ''), 10) wrap = false; expr = { type: 'CallExpression', callee: { type: 'MemberExpression', object: { type: 'CallExpression', callee: { type: 'MemberExpression', object: { type: 'CallExpression', callee: { type: 'MemberExpression', object: { type: 'CallExpression', callee: { type: 'MemberExpression', object: { type: 'CallExpression', callee: { type: 'Identifier', name: 'String', }, arguments: [ { type: 'Identifier', name: 'n', }, ], }, property: { type: 'Identifier', name: 'replace', }, computed: false, }, arguments: [ { type: 'Literal', value: /\d*\./, }, { type: 'Literal', value: '', }, ], }, property: { type: 'Identifier', name: 'split', }, computed: false, }, arguments: [ { type: 'Literal', value: /(?:)/, }, ], }, property: { type: 'Identifier', name: 'map', }, computed: false, }, arguments: [ { type: 'FunctionExpression', params: [ { type: 'Identifier', name: 'digit', }, ], body: { type: 'BlockStatement', body: [ { type: 'ReturnStatement', argument: applyRenderer( { type: 'CallExpression', callee: { type: 'Identifier', name: 'parseInt', }, arguments: [ { type: 'Identifier', name: 'digit', }, ], }, that.type, ), }, ], }, }, { type: 'ThisExpression', }, ], }, property: { type: 'Identifier', name: 'join', }, computed: false, }, arguments: [ { type: 'Literal', value: ' ', }, ], }; } else if (rule.value === '-x') { expr = { type: 'UnaryExpression', operator: '-', prefix: true, argument: { type: 'Identifier', name: 'n', }, }; } else { expr = { type: 'BinaryExpression', operator: '%', left: { type: 'Identifier', name: 'n', }, right: { type: 'Literal', value: getDivisor(), }, }; } } else if (specialChar === '=') { // == expr = { type: 'Identifier', name: 'n', }; } // FIXME: >>> not supported // The substitution descriptor (i.e., the text between the token characters) may take one of three forms: if (otherFormat) { // A rule set name: // Perform the mathematical operation on the number, and format the result using the named rule set. const otherFormatName = CldrRbnfRuleSet.getSafeRendererName(otherFormat); isSeenByRuleSetType[otherFormatName] = true; // Turn into this.<otherFormatName>(<expr>) expressionAsts.push( wrap ? applyRenderer(expr, otherFormatName) : expr, ); } else if (decimalFormat) { // A DecimalFormat pattern: // Perform the mathematical operation on the number, and format the result using a DecimalFormat // with the specified pattern. The pattern must begin with 0 or #. expressionAsts.push({ type: 'CallExpression', callee: { type: 'MemberExpression', object: { type: 'ThisExpression', }, property: { type: 'Identifier', name: 'renderNumber', }, computed: false, }, arguments: [ expr, { type: 'Literal', value: decimalFormat, }, ], }); } else { // Nothing: if (specialChar === '>') { // If you omit the substitution descriptor in a >> substitution in a fraction rule, format the result one digit at a time using the rule set containing the current rule. expressionAsts.push( wrap ? applyRenderer(expr, that.type) : expr, ); } else if (specialChar === '<') { // If you omit the substitution descriptor in a << substitution in a rule in a fraction rule set, format the result using the default rule set for this renderer. // FIXME: Should be the default rule set for this renderer! expressionAsts.push( wrap ? applyRenderer(expr, that.type) : expr, ); } else { throw new Error('== not supported!'); } } } else if (optional) { // [ ... ] const optionalRuleExpressionAst = ruleToExpressionAst({ radix: rule.radix, rbnf: optional, value: rule.value, }); let testAst; if (rule.value === '-x') { throw new Error('[ ... ] not allowed in negative number rule'); } else if (rule.value === '0.x') { throw new Error('[ ... ] not allowed in proper fraction rule'); } else if (rule.value === 'x.x') { // in improper-fraction rule: // Omit the optional text if the number is between 0 and 1 (same as specifying both an x.x rule and a 0.x rule) testAst = { type: 'BinaryExpression', operator: '&&', left: { type: 'BinaryExpression', operator: '>=', left: { type: 'Identifier', name: 'n', }, right: { type: 'Literal', value: 0, }, }, right: { type: 'BinaryExpression', operator: '<=', left: { type: 'Identifier', name: 'n', }, right: { type: 'Literal', value: 1, }, }, }; } else if (rule.value === 'x.0') { // in default rule: // Omit the optional text if the number is an integer (same as specifying both an x.x rule and an x.0 rule) testAst = { type: 'UnaryExpression', operator: '!', prefix: true, argument: { type: 'Identifier', name: 'isFractional', }, }; } else { // TODO: // in rule in fraction rule set: // Omit the optional text if multiplying the number by the rule's base value yields 1. // in normal rule: // Omit the optional text if the number is an even multiple of the rule's divisor testAst = { type: 'BinaryExpression', operator: '===', left: { type: 'BinaryExpression', operator: '%', left: { type: 'Identifier', name: 'n', }, right: { type: 'Literal', value: parseInt(rule.value, 10), }, }, right: { type: 'Literal', value: 0, }, }; } expressionAsts.push({ type: 'ConditionalExpression', test: testAst, consequent: { type: 'Literal', value: '', }, alternate: optionalRuleExpressionAst, }); } else if (literal) { expressionAsts.push({ type: 'Literal', value: literal, }); } else { throw new Error('Unknown token in ' + rule.rbnf); } }, ); if (expressionAsts.length === 0) { expressionAsts = [ { type: 'Literal', value: '', }, ]; } let expressionAst = expressionAsts.shift(); while (expressionAsts.length > 0) { expressionAst = { type: 'BinaryExpression', operator: '+', left: expressionAst, right: expressionAsts.shift(), }; } return expressionAst; } function conditionToStatementAst(conditionAst, rule) { return { type: 'IfStatement', test: conditionAst, consequent: { type: 'ReturnStatement', argument: ruleToExpressionAst(rule), }, }; } const statementAsts = []; if (this.ruleByValue['x.0'] || this.ruleByValue['x.x']) { // var isFractional = n !== Math.floor(n); statementAsts.push({ type: 'VariableDeclaration', kind: 'const', declarations: [ { type: 'VariableDeclarator', id: { type: 'Identifier', name: 'isFractional', }, init: { type: 'BinaryExpression', operator: '!==', left: { type: 'Identifier', name: 'n', }, right: { type: 'CallExpression', callee: { type: 'MemberExpression', computed: false, object: { type: 'Identifier', name: 'Math', }, property: { type: 'Identifier', name: 'floor', }, }, arguments: [ { type: 'Identifier', name: 'n', }, ], }, }, }, ], }); } if (this.ruleByValue['x.0']) { statementAsts.push( conditionToStatementAst( { type: 'Identifier', name: 'isFractional', }, this.ruleByValue['x.0'], ), ); } if (this.ruleByValue['-x']) { statementAsts.push( conditionToStatementAst( { type: 'BinaryExpression', operator: '<', left: { type: 'Identifier', name: 'n', }, right: { type: 'Literal', value: 0, }, }, this.ruleByValue['-x'], ), ); } if (this.ruleByValue['x.x']) { statementAsts.push( conditionToStatementAst( { type: 'Identifier', name: 'isFractional', }, this.ruleByValue['x.x'], ), ); } if (this.ruleByValue['0.x']) { statementAsts.push( conditionToStatementAst( { type: 'LogicalExpression', operator: '&&', left: { type: 'BinaryExpression', operator: '>', left: { type: 'Identifier', name: 'n', }, right: { type: 'Literal', value: 0, }, }, right: { type: 'BinaryExpression', operator: '<', left: { type: 'Identifier', name: 'n', }, right: { type: 'Literal', value: 1, }, }, }, this.ruleByValue['0.x'], ), ); } Object.keys(this.ruleByValue) .filter((value) => /^\d+$/.test(value)) .map((value) => parseInt(value, 10)) .sort((a, b) => b - a) .forEach(function (numericalValue) { if (numericalValue === 0) { statementAsts.push({ type: 'ReturnStatement', argument: ruleToExpressionAst(this.ruleByValue[numericalValue]), }); } else { statementAsts.push( conditionToStatementAst( { type: 'BinaryExpression', operator: '>=', left: { type: 'Identifier', name: 'n', }, right: { type: 'Literal', value: numericalValue, }, }, this.ruleByValue[numericalValue], ), ); } }, this); return { functionAst: { type: 'FunctionExpression', params: [ { type: 'Identifier', name: 'n', }, ], body: { type: 'BlockStatement', body: statementAsts, }, }, dependencies: Object.keys(isSeenByRuleSetType), }; }, }; module.exports = CldrRbnfRuleSet;