import { ToRawFixed } from './ToRawFixed'; import { digitMapping } from './digit-mapping.generated'; import { S_UNICODE_REGEX } from '../regex.generated'; // This is from: unicode-12.1.0/General_Category/Symbol/regex.js // IE11 does not support unicode flag, otherwise this is just /\p{S}/u. // /^\p{S}/u var CARET_S_UNICODE_REGEX = new RegExp("^".concat(S_UNICODE_REGEX.source)); // /\p{S}$/u var S_DOLLAR_UNICODE_REGEX = new RegExp("".concat(S_UNICODE_REGEX.source, "$")); var CLDR_NUMBER_PATTERN = /[#0](?:[\.,][#0]+)*/g; export default function formatToParts(numberResult, data, pl, options) { var sign = numberResult.sign, exponent = numberResult.exponent, magnitude = numberResult.magnitude; var notation = options.notation, style = options.style, numberingSystem = options.numberingSystem; var defaultNumberingSystem = data.numbers.nu[0]; // #region Part 1: partition and interpolate the CLDR number pattern. // ---------------------------------------------------------- var compactNumberPattern = null; if (notation === 'compact' && magnitude) { compactNumberPattern = getCompactDisplayPattern(numberResult, pl, data, style, options.compactDisplay, options.currencyDisplay, numberingSystem); } // This is used multiple times var nonNameCurrencyPart; if (style === 'currency' && options.currencyDisplay !== 'name') { var byCurrencyDisplay = data.currencies[options.currency]; if (byCurrencyDisplay) { switch (options.currencyDisplay) { case 'code': nonNameCurrencyPart = options.currency; break; case 'symbol': nonNameCurrencyPart = byCurrencyDisplay.symbol; break; default: nonNameCurrencyPart = byCurrencyDisplay.narrow; break; } } else { // Fallback for unknown currency nonNameCurrencyPart = options.currency; } } var numberPattern; if (!compactNumberPattern) { // Note: if the style is unit, or is currency and the currency display is name, // its unit parts will be interpolated in part 2. So here we can fallback to decimal. if (style === 'decimal' || style === 'unit' || (style === 'currency' && options.currencyDisplay === 'name')) { // Shortcut for decimal var decimalData = data.numbers.decimal[numberingSystem] || data.numbers.decimal[defaultNumberingSystem]; numberPattern = getPatternForSign(decimalData.standard, sign); } else if (style === 'currency') { var currencyData = data.numbers.currency[numberingSystem] || data.numbers.currency[defaultNumberingSystem]; // We replace number pattern part with `0` for easier postprocessing. numberPattern = getPatternForSign(currencyData[options.currencySign], sign); } else { // percent var percentPattern = data.numbers.percent[numberingSystem] || data.numbers.percent[defaultNumberingSystem]; numberPattern = getPatternForSign(percentPattern, sign); } } else { numberPattern = compactNumberPattern; } // Extract the decimal number pattern string. It looks like "#,##0,00", which will later be // used to infer decimal group sizes. var decimalNumberPattern = CLDR_NUMBER_PATTERN.exec(numberPattern)[0]; // Now we start to substitute patterns // 1. replace strings like `0` and `#,##0.00` with `{0}` // 2. unquote characters (invariant: the quoted characters does not contain the special tokens) numberPattern = numberPattern .replace(CLDR_NUMBER_PATTERN, '{0}') .replace(/'(.)'/g, '$1'); // Handle currency spacing (both compact and non-compact). if (style === 'currency' && options.currencyDisplay !== 'name') { var currencyData = data.numbers.currency[numberingSystem] || data.numbers.currency[defaultNumberingSystem]; // See `currencySpacing` substitution rule in TR-35. // Here we always assume the currencyMatch is "[:^S:]" and surroundingMatch is "[:digit:]". // // Example 1: for pattern "#,##0.00¤" with symbol "US$", we replace "¤" with the symbol, // but insert an extra non-break space before the symbol, because "[:^S:]" matches "U" in // "US$" and "[:digit:]" matches the latn numbering system digits. // // Example 2: for pattern "¤#,##0.00" with symbol "US$", there is no spacing between symbol // and number, because `$` does not match "[:^S:]". // // Implementation note: here we do the best effort to infer the insertion. // We also assume that `beforeInsertBetween` and `afterInsertBetween` will never be `;`. var afterCurrency = currencyData.currencySpacing.afterInsertBetween; if (afterCurrency && !S_DOLLAR_UNICODE_REGEX.test(nonNameCurrencyPart)) { numberPattern = numberPattern.replace('¤{0}', "\u00A4".concat(afterCurrency, "{0}")); } var beforeCurrency = currencyData.currencySpacing.beforeInsertBetween; if (beforeCurrency && !CARET_S_UNICODE_REGEX.test(nonNameCurrencyPart)) { numberPattern = numberPattern.replace('{0}¤', "{0}".concat(beforeCurrency, "\u00A4")); } } // The following tokens are special: `{0}`, `¤`, `%`, `-`, `+`, `{c:...}. var numberPatternParts = numberPattern.split(/({c:[^}]+}|\{0\}|[¤%\-\+])/g); var numberParts = []; var symbols = data.numbers.symbols[numberingSystem] || data.numbers.symbols[defaultNumberingSystem]; for (var _i = 0, numberPatternParts_1 = numberPatternParts; _i < numberPatternParts_1.length; _i++) { var part = numberPatternParts_1[_i]; if (!part) { continue; } switch (part) { case '{0}': { // We only need to handle scientific and engineering notation here. numberParts.push.apply(numberParts, paritionNumberIntoParts(symbols, numberResult, notation, exponent, numberingSystem, // If compact number pattern exists, do not insert group separators. !compactNumberPattern && options.useGrouping, decimalNumberPattern)); break; } case '-': numberParts.push({ type: 'minusSign', value: symbols.minusSign }); break; case '+': numberParts.push({ type: 'plusSign', value: symbols.plusSign }); break; case '%': numberParts.push({ type: 'percentSign', value: symbols.percentSign }); break; case '¤': // Computed above when handling currency spacing. numberParts.push({ type: 'currency', value: nonNameCurrencyPart }); break; default: if (/^\{c:/.test(part)) { numberParts.push({ type: 'compact', value: part.substring(3, part.length - 1), }); } else { // literal numberParts.push({ type: 'literal', value: part }); } break; } } // #endregion // #region Part 2: interpolate unit pattern if necessary. // ---------------------------------------------- switch (style) { case 'currency': { // `currencyDisplay: 'name'` has similar pattern handling as units. if (options.currencyDisplay === 'name') { var unitPattern = (data.numbers.currency[numberingSystem] || data.numbers.currency[defaultNumberingSystem]).unitPattern; // Select plural var unitName = void 0; var currencyNameData = data.currencies[options.currency]; if (currencyNameData) { unitName = selectPlural(pl, numberResult.roundedNumber * Math.pow(10, exponent), currencyNameData.displayName); } else { // Fallback for unknown currency unitName = options.currency; } // Do {0} and {1} substitution var unitPatternParts = unitPattern.split(/(\{[01]\})/g); var result = []; for (var _a = 0, unitPatternParts_1 = unitPatternParts; _a < unitPatternParts_1.length; _a++) { var part = unitPatternParts_1[_a]; switch (part) { case '{0}': result.push.apply(result, numberParts); break; case '{1}': result.push({ type: 'currency', value: unitName }); break; default: if (part) { result.push({ type: 'literal', value: part }); } break; } } return result; } else { return numberParts; } } case 'unit': { var unit = options.unit, unitDisplay = options.unitDisplay; var unitData = data.units.simple[unit]; var unitPattern = void 0; if (unitData) { // Simple unit pattern unitPattern = selectPlural(pl, numberResult.roundedNumber * Math.pow(10, exponent), data.units.simple[unit][unitDisplay]); } else { // See: http://unicode.org/reports/tr35/tr35-general.html#perUnitPatterns // If cannot find unit in the simple pattern, it must be "per" compound pattern. // Implementation note: we are not following TR-35 here because we need to format to parts! var _b = unit.split('-per-'), numeratorUnit = _b[0], denominatorUnit = _b[1]; unitData = data.units.simple[numeratorUnit]; var numeratorUnitPattern = selectPlural(pl, numberResult.roundedNumber * Math.pow(10, exponent), data.units.simple[numeratorUnit][unitDisplay]); var perUnitPattern = data.units.simple[denominatorUnit].perUnit[unitDisplay]; if (perUnitPattern) { // perUnitPattern exists, combine it with numeratorUnitPattern unitPattern = perUnitPattern.replace('{0}', numeratorUnitPattern); } else { // get compoundUnit pattern (e.g. "{0} per {1}"), repalce {0} with numerator pattern and {1} with // the denominator pattern in singular form. var perPattern = data.units.compound.per[unitDisplay]; var denominatorPattern = selectPlural(pl, 1, data.units.simple[denominatorUnit][unitDisplay]); unitPattern = unitPattern = perPattern .replace('{0}', numeratorUnitPattern) .replace('{1}', denominatorPattern.replace('{0}', '')); } } var result = []; // We need spacing around "{0}" because they are not treated as "unit" parts, but "literal". for (var _c = 0, _d = unitPattern.split(/(\s*\{0\}\s*)/); _c < _d.length; _c++) { var part = _d[_c]; var interpolateMatch = /^(\s*)\{0\}(\s*)$/.exec(part); if (interpolateMatch) { // Space before "{0}" if (interpolateMatch[1]) { result.push({ type: 'literal', value: interpolateMatch[1] }); } // "{0}" itself result.push.apply(result, numberParts); // Space after "{0}" if (interpolateMatch[2]) { result.push({ type: 'literal', value: interpolateMatch[2] }); } } else if (part) { result.push({ type: 'unit', value: part }); } } return result; } default: return numberParts; } // #endregion } // A subset of https://tc39.es/ecma402/#sec-partitionnotationsubpattern // Plus the exponent parts handling. function paritionNumberIntoParts(symbols, numberResult, notation, exponent, numberingSystem, useGrouping, /** * This is the decimal number pattern without signs or symbols. * It is used to infer the group size when `useGrouping` is true. * * A typical value looks like "#,##0.00" (primary group size is 3). * Some locales like Hindi has secondary group size of 2 (e.g. "#,##,##0.00"). */ decimalNumberPattern) { var result = []; // eslint-disable-next-line prefer-const var n = numberResult.formattedString, x = numberResult.roundedNumber; if (isNaN(x)) { return [{ type: 'nan', value: n }]; } else if (!isFinite(x)) { return [{ type: 'infinity', value: n }]; } var digitReplacementTable = digitMapping[numberingSystem]; if (digitReplacementTable) { n = n.replace(/\d/g, function (digit) { return digitReplacementTable[+digit] || digit; }); } // TODO: Else use an implementation dependent algorithm to map n to the appropriate // representation of n in the given numbering system. var decimalSepIndex = n.indexOf('.'); var integer; var fraction; if (decimalSepIndex > 0) { integer = n.slice(0, decimalSepIndex); fraction = n.slice(decimalSepIndex + 1); } else { integer = n; } // #region Grouping integer digits // The weird compact and x >= 10000 check is to ensure consistency with Node.js and Chrome. // Note that `de` does not have compact form for thousands, but Node.js does not insert grouping separator // unless the rounded number is greater than 10000: // NumberFormat('de', {notation: 'compact', compactDisplay: 'short'}).format(1234) //=> "1234" // NumberFormat('de').format(1234) //=> "1.234" if (useGrouping && (notation !== 'compact' || x >= 10000)) { var groupSepSymbol = symbols.group; var groups = []; // > There may be two different grouping sizes: The primary grouping size used for the least // > significant integer group, and the secondary grouping size used for more significant groups. // > If a pattern contains multiple grouping separators, the interval between the last one and the // > end of the integer defines the primary grouping size, and the interval between the last two // > defines the secondary grouping size. All others are ignored. var integerNumberPattern = decimalNumberPattern.split('.')[0]; var patternGroups = integerNumberPattern.split(','); var primaryGroupingSize = 3; var secondaryGroupingSize = 3; if (patternGroups.length > 1) { primaryGroupingSize = patternGroups[patternGroups.length - 1].length; } if (patternGroups.length > 2) { secondaryGroupingSize = patternGroups[patternGroups.length - 2].length; } var i = integer.length - primaryGroupingSize; if (i > 0) { // Slice the least significant integer group groups.push(integer.slice(i, i + primaryGroupingSize)); // Then iteratively push the more signicant groups // TODO: handle surrogate pairs in some numbering system digits for (i -= secondaryGroupingSize; i > 0; i -= secondaryGroupingSize) { groups.push(integer.slice(i, i + secondaryGroupingSize)); } groups.push(integer.slice(0, i + secondaryGroupingSize)); } else { groups.push(integer); } while (groups.length > 0) { var integerGroup = groups.pop(); result.push({ type: 'integer', value: integerGroup }); if (groups.length > 0) { result.push({ type: 'group', value: groupSepSymbol }); } } } else { result.push({ type: 'integer', value: integer }); } // #endregion if (fraction !== undefined) { result.push({ type: 'decimal', value: symbols.decimal }, { type: 'fraction', value: fraction }); } if ((notation === 'scientific' || notation === 'engineering') && isFinite(x)) { result.push({ type: 'exponentSeparator', value: symbols.exponential }); if (exponent < 0) { result.push({ type: 'exponentMinusSign', value: symbols.minusSign }); exponent = -exponent; } var exponentResult = ToRawFixed(exponent, 0, 0); result.push({ type: 'exponentInteger', value: exponentResult.formattedString, }); } return result; } function getPatternForSign(pattern, sign) { if (pattern.indexOf(';') < 0) { pattern = "".concat(pattern, ";-").concat(pattern); } var _a = pattern.split(';'), zeroPattern = _a[0], negativePattern = _a[1]; switch (sign) { case 0: return zeroPattern; case -1: return negativePattern; default: return negativePattern.indexOf('-') >= 0 ? negativePattern.replace(/-/g, '+') : "+".concat(zeroPattern); } } // Find the CLDR pattern for compact notation based on the magnitude of data and style. // // Example return value: "¤ {c:laki}000;¤{c:laki} -0" (`sw` locale): // - Notice the `{c:...}` token that wraps the compact literal. // - The consecutive zeros are normalized to single zero to match CLDR_NUMBER_PATTERN. // // Returning null means the compact display pattern cannot be found. function getCompactDisplayPattern(numberResult, pl, data, style, compactDisplay, currencyDisplay, numberingSystem) { var _a; var roundedNumber = numberResult.roundedNumber, sign = numberResult.sign, magnitude = numberResult.magnitude; var magnitudeKey = String(Math.pow(10, magnitude)); var defaultNumberingSystem = data.numbers.nu[0]; var pattern; if (style === 'currency' && currencyDisplay !== 'name') { var byNumberingSystem = data.numbers.currency; var currencyData = byNumberingSystem[numberingSystem] || byNumberingSystem[defaultNumberingSystem]; // NOTE: compact notation ignores currencySign! var compactPluralRules = (_a = currencyData.short) === null || _a === void 0 ? void 0 : _a[magnitudeKey]; if (!compactPluralRules) { return null; } pattern = selectPlural(pl, roundedNumber, compactPluralRules); } else { var byNumberingSystem = data.numbers.decimal; var byCompactDisplay = byNumberingSystem[numberingSystem] || byNumberingSystem[defaultNumberingSystem]; var compactPlaralRule = byCompactDisplay[compactDisplay][magnitudeKey]; if (!compactPlaralRule) { return null; } pattern = selectPlural(pl, roundedNumber, compactPlaralRule); } // See https://unicode.org/reports/tr35/tr35-numbers.html#Compact_Number_Formats // > If the value is precisely “0”, either explicit or defaulted, then the normal number format // > pattern for that sort of object is supplied. if (pattern === '0') { return null; } pattern = getPatternForSign(pattern, sign) // Extract compact literal from the pattern .replace(/([^\s;\-\+\d¤]+)/g, '{c:$1}') // We replace one or more zeros with a single zero so it matches `CLDR_NUMBER_PATTERN`. .replace(/0+/, '0'); return pattern; } function selectPlural(pl, x, rules) { return rules[pl.select(x)] || rules.other; }