422 lines
20 KiB
Plaintext
Executable File
422 lines
20 KiB
Plaintext
Executable File
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;
|
||
}
|