var $7fe67f77592add0a$exports = require("./NumberFormatter.cjs"); function $parcel$export(e, n, v, s) { Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true}); } $parcel$export(module.exports, "NumberParser", function () { return $52ebab2a875d5a2c$export$cd11ab140839f11d; }); /* * Copyright 2020 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ const $52ebab2a875d5a2c$var$CURRENCY_SIGN_REGEX = new RegExp('^.*\\(.*\\).*$'); const $52ebab2a875d5a2c$var$NUMBERING_SYSTEMS = [ 'latn', 'arab', 'hanidec', 'deva', 'beng', 'fullwide' ]; class $52ebab2a875d5a2c$export$cd11ab140839f11d { constructor(locale, options = {}){ this.locale = locale; this.options = options; } /** * Parses the given string to a number. Returns NaN if a valid number could not be parsed. */ parse(value) { return $52ebab2a875d5a2c$var$getNumberParserImpl(this.locale, this.options, value).parse(value); } /** * Returns whether the given string could potentially be a valid number. This should be used to * validate user input as the user types. If a `minValue` or `maxValue` is provided, the validity * of the minus/plus sign characters can be checked. */ isValidPartialNumber(value, minValue, maxValue) { return $52ebab2a875d5a2c$var$getNumberParserImpl(this.locale, this.options, value).isValidPartialNumber(value, minValue, maxValue); } /** * Returns a numbering system for which the given string is valid in the current locale. * If no numbering system could be detected, the default numbering system for the current * locale is returned. */ getNumberingSystem(value) { return $52ebab2a875d5a2c$var$getNumberParserImpl(this.locale, this.options, value).options.numberingSystem; } } const $52ebab2a875d5a2c$var$numberParserCache = new Map(); function $52ebab2a875d5a2c$var$getNumberParserImpl(locale, options, value) { // First try the default numbering system for the provided locale let defaultParser = $52ebab2a875d5a2c$var$getCachedNumberParser(locale, options); // If that doesn't match, and the locale doesn't include a hard coded numbering system, // try each of the other supported numbering systems until we find one that matches. if (!locale.includes('-nu-') && !defaultParser.isValidPartialNumber(value)) { for (let numberingSystem of $52ebab2a875d5a2c$var$NUMBERING_SYSTEMS)if (numberingSystem !== defaultParser.options.numberingSystem) { let parser = $52ebab2a875d5a2c$var$getCachedNumberParser(locale + (locale.includes('-u-') ? '-nu-' : '-u-nu-') + numberingSystem, options); if (parser.isValidPartialNumber(value)) return parser; } } return defaultParser; } function $52ebab2a875d5a2c$var$getCachedNumberParser(locale, options) { let cacheKey = locale + (options ? Object.entries(options).sort((a, b)=>a[0] < b[0] ? -1 : 1).join() : ''); let parser = $52ebab2a875d5a2c$var$numberParserCache.get(cacheKey); if (!parser) { parser = new $52ebab2a875d5a2c$var$NumberParserImpl(locale, options); $52ebab2a875d5a2c$var$numberParserCache.set(cacheKey, parser); } return parser; } // The actual number parser implementation. Instances of this class are cached // based on the locale, options, and detected numbering system. class $52ebab2a875d5a2c$var$NumberParserImpl { constructor(locale, options = {}){ this.locale = locale; // see https://tc39.es/ecma402/#sec-setnfdigitoptions, when using roundingIncrement, the maximumFractionDigits and minimumFractionDigits must be equal // by default, they are 0 and 3 respectively, so we set them to 0 if neither are set if (options.roundingIncrement !== 1 && options.roundingIncrement != null) { if (options.maximumFractionDigits == null && options.minimumFractionDigits == null) { options.maximumFractionDigits = 0; options.minimumFractionDigits = 0; } else if (options.maximumFractionDigits == null) options.maximumFractionDigits = options.minimumFractionDigits; else if (options.minimumFractionDigits == null) options.minimumFractionDigits = options.maximumFractionDigits; // if both are specified, let the normal Range Error be thrown } this.formatter = new Intl.NumberFormat(locale, options); this.options = this.formatter.resolvedOptions(); this.symbols = $52ebab2a875d5a2c$var$getSymbols(locale, this.formatter, this.options, options); if (this.options.style === 'percent' && ((this.options.minimumFractionDigits ?? 0) > 18 || (this.options.maximumFractionDigits ?? 0) > 18)) console.warn('NumberParser cannot handle percentages with greater than 18 decimal places, please reduce the number in your options.'); } parse(value) { let isGroupSymbolAllowed = this.formatter.resolvedOptions().useGrouping; // to parse the number, we need to remove anything that isn't actually part of the number, for example we want '-10.40' not '-10.40 USD' let fullySanitizedValue = this.sanitize(value); // Return NaN if there is a group symbol but useGrouping is false if (!isGroupSymbolAllowed && this.symbols.group && fullySanitizedValue.includes(this.symbols.group)) return NaN; else if (this.symbols.group) fullySanitizedValue = fullySanitizedValue.replaceAll(this.symbols.group, ''); if (this.symbols.decimal) fullySanitizedValue = fullySanitizedValue.replace(this.symbols.decimal, '.'); if (this.symbols.minusSign) fullySanitizedValue = fullySanitizedValue.replace(this.symbols.minusSign, '-'); fullySanitizedValue = fullySanitizedValue.replace(this.symbols.numeral, this.symbols.index); if (this.options.style === 'percent') { // javascript is bad at dividing by 100 and maintaining the same significant figures, so perform it on the string before parsing let isNegative = fullySanitizedValue.indexOf('-'); fullySanitizedValue = fullySanitizedValue.replace('-', ''); fullySanitizedValue = fullySanitizedValue.replace('+', ''); let index = fullySanitizedValue.indexOf('.'); if (index === -1) index = fullySanitizedValue.length; fullySanitizedValue = fullySanitizedValue.replace('.', ''); if (index - 2 === 0) fullySanitizedValue = `0.${fullySanitizedValue}`; else if (index - 2 === -1) fullySanitizedValue = `0.0${fullySanitizedValue}`; else if (index - 2 === -2) fullySanitizedValue = '0.00'; else fullySanitizedValue = `${fullySanitizedValue.slice(0, index - 2)}.${fullySanitizedValue.slice(index - 2)}`; if (isNegative > -1) fullySanitizedValue = `-${fullySanitizedValue}`; } let newValue = fullySanitizedValue ? +fullySanitizedValue : NaN; if (isNaN(newValue)) return NaN; if (this.options.style === 'percent') { // extra step for rounding percents to what our formatter would output let options = { ...this.options, style: 'decimal', minimumFractionDigits: Math.min((this.options.minimumFractionDigits ?? 0) + 2, 20), maximumFractionDigits: Math.min((this.options.maximumFractionDigits ?? 0) + 2, 20) }; return new $52ebab2a875d5a2c$export$cd11ab140839f11d(this.locale, options).parse(new (0, $7fe67f77592add0a$exports.NumberFormatter)(this.locale, options).format(newValue)); } // accounting will always be stripped to a positive number, so if it's accounting and has a () around everything, then we need to make it negative again if (this.options.currencySign === 'accounting' && $52ebab2a875d5a2c$var$CURRENCY_SIGN_REGEX.test(value)) newValue = -1 * newValue; return newValue; } sanitize(value) { let isGroupSymbolAllowed = this.formatter.resolvedOptions().useGrouping; // If the value is only a unit and it matches one of the formatted numbers where the value is part of the unit and doesn't have any numerals, then // return the known value for that case. if (this.symbols.noNumeralUnits.length > 0 && this.symbols.noNumeralUnits.find((obj)=>obj.unit === value)) return this.symbols.noNumeralUnits.find((obj)=>obj.unit === value).value.toString(); value = value.replace(this.symbols.literals, ''); // Replace the ASCII minus sign with the minus sign used in the current locale // so that both are allowed in case the user's keyboard doesn't have the locale's minus sign. if (this.symbols.minusSign) value = value.replace('-', this.symbols.minusSign); // In arab numeral system, their decimal character is 1643, but most keyboards don't type that // instead they use the , (44) character or apparently the (1548) character. if (this.options.numberingSystem === 'arab') { if (this.symbols.decimal) { value = $52ebab2a875d5a2c$var$replaceAll(value, ',', this.symbols.decimal); value = $52ebab2a875d5a2c$var$replaceAll(value, String.fromCharCode(1548), this.symbols.decimal); } if (this.symbols.group && isGroupSymbolAllowed) value = $52ebab2a875d5a2c$var$replaceAll(value, '.', this.symbols.group); } // In some locale styles, such as swiss currency, the group character can be a special single quote // that keyboards don't typically have. This expands the character to include the easier to type single quote. if (this.symbols.group === "\u2019" && value.includes("'") && isGroupSymbolAllowed) value = $52ebab2a875d5a2c$var$replaceAll(value, "'", this.symbols.group); // On newer ICU versions, the special single quote has been normalized, so we need to backport. if (this.symbols.group === "'" && value.includes("\u2019") && isGroupSymbolAllowed) value = $52ebab2a875d5a2c$var$replaceAll(value, "\u2019", this.symbols.group); // fr-FR group character is narrow non-breaking space, char code 8239 (U+202F), but that's not a key on the french keyboard, // so allow space and non-breaking space as a group char as well if (this.options.locale === 'fr-FR' && this.symbols.group && isGroupSymbolAllowed) { value = $52ebab2a875d5a2c$var$replaceAll(value, ' ', this.symbols.group); value = $52ebab2a875d5a2c$var$replaceAll(value, /\u00A0/g, this.symbols.group); } return value; } isValidPartialNumber(value, minValue = -Infinity, maxValue = Infinity) { let isGroupSymbolAllowed = this.formatter.resolvedOptions().useGrouping; value = this.sanitize(value); // Remove minus or plus sign, which must be at the start of the string. if (this.symbols.minusSign && value.startsWith(this.symbols.minusSign) && minValue < 0) value = value.slice(this.symbols.minusSign.length); else if (this.symbols.plusSign && value.startsWith(this.symbols.plusSign) && maxValue > 0) value = value.slice(this.symbols.plusSign.length); // Numbers that can't have any decimal values fail if a decimal character is typed if (this.symbols.decimal && value.indexOf(this.symbols.decimal) > -1 && this.options.maximumFractionDigits === 0) return false; // Remove numerals, groups, and decimals if (this.symbols.group && isGroupSymbolAllowed) value = $52ebab2a875d5a2c$var$replaceAll(value, this.symbols.group, ''); value = value.replace(this.symbols.numeral, ''); if (this.symbols.decimal) value = value.replace(this.symbols.decimal, ''); // The number is valid if there are no remaining characters return value.length === 0; } } const $52ebab2a875d5a2c$var$nonLiteralParts = new Set([ 'decimal', 'fraction', 'integer', 'minusSign', 'plusSign', 'group' ]); // This list is derived from https://www.unicode.org/cldr/charts/49/supplemental/language_plural_rules.html#comparison and includes // all unique numbers which we need to check in order to determine all the plural forms for a given locale. // Run scripts/generateAllPlurals.mjs to generate this list. const $52ebab2a875d5a2c$var$pluralNumbers = [ 0, 4, 2, 1, 11, 20, 3, 7, 100, 21, 0.1, 1.1 ]; function $52ebab2a875d5a2c$var$getSymbols(locale, formatter, intlOptions, originalOptions) { // formatter needs access to all decimal places in order to generate the correct literal strings for the plural set let symbolFormatter = new Intl.NumberFormat(locale, { ...intlOptions, // Resets so we get the full range of symbols minimumSignificantDigits: 1, maximumSignificantDigits: 21, roundingIncrement: 1, roundingPriority: 'auto', roundingMode: 'halfExpand', useGrouping: true }); // Note: some locale's don't add a group symbol until there is a ten thousands place let allParts = symbolFormatter.formatToParts(-10000.111); let posAllParts = symbolFormatter.formatToParts(10000.111); let pluralParts = $52ebab2a875d5a2c$var$pluralNumbers.map((n)=>symbolFormatter.formatToParts(n)); // if the plural parts include a unit but no integer or fraction, then we need to add the unit to the special set let noNumeralUnits = pluralParts.map((p, i)=>{ let unit = p.find((p)=>p.type === 'unit'); if (unit && !p.some((p)=>p.type === 'integer' || p.type === 'fraction')) return { unit: unit.value, value: $52ebab2a875d5a2c$var$pluralNumbers[i] }; return null; }).filter((p)=>!!p); let minusSign = allParts.find((p)=>p.type === 'minusSign')?.value ?? '-'; let plusSign = posAllParts.find((p)=>p.type === 'plusSign')?.value; // Safari does not support the signDisplay option, but our number parser polyfills it. // If no plus sign was returned, but the original options contained signDisplay, default to the '+' character. if (!plusSign && (originalOptions?.signDisplay === 'exceptZero' || originalOptions?.signDisplay === 'always')) plusSign = '+'; // If maximumSignificantDigits is 1 (the minimum) then we won't get decimal characters out of the above formatters // Percent also defaults to 0 fractionDigits, so we need to make a new one that isn't percent to get an accurate decimal let decimalParts = new Intl.NumberFormat(locale, { ...intlOptions, minimumFractionDigits: 2, maximumFractionDigits: 2 }).formatToParts(0.001); let decimal = decimalParts.find((p)=>p.type === 'decimal')?.value; let group = allParts.find((p)=>p.type === 'group')?.value; // this set is also for a regex, it's all literals that might be in the string we want to eventually parse that // don't contribute to the numerical value let allPartsLiterals = allParts.filter((p)=>!$52ebab2a875d5a2c$var$nonLiteralParts.has(p.type)).map((p)=>$52ebab2a875d5a2c$var$escapeRegex(p.value)); let pluralPartsLiterals = pluralParts.flatMap((p)=>p.filter((p)=>!$52ebab2a875d5a2c$var$nonLiteralParts.has(p.type)).map((p)=>$52ebab2a875d5a2c$var$escapeRegex(p.value))); let sortedLiterals = [ ...new Set([ ...allPartsLiterals, ...pluralPartsLiterals ]) ].sort((a, b)=>b.length - a.length); // Match both whitespace and formatting characters let literals = sortedLiterals.length === 0 ? new RegExp('\\p{White_Space}|\\p{Cf}', 'gu') : new RegExp(`${sortedLiterals.join('|')}|\\p{White_Space}|\\p{Cf}`, 'gu'); // These are for replacing non-latn characters with the latn equivalent let numerals = [ ...new Intl.NumberFormat(intlOptions.locale, { useGrouping: false }).format(9876543210) ].reverse(); let indexes = new Map(numerals.map((d, i)=>[ d, i ])); let numeral = new RegExp(`[${numerals.join('')}]`, 'g'); let index = (d)=>String(indexes.get(d)); return { minusSign: minusSign, plusSign: plusSign, decimal: decimal, group: group, literals: literals, numeral: numeral, numerals: numerals, index: index, noNumeralUnits: noNumeralUnits }; } function $52ebab2a875d5a2c$var$replaceAll(str, find, replace) { if (str.replaceAll) return str.replaceAll(find, replace); return str.split(find).join(replace); } function $52ebab2a875d5a2c$var$escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } //# sourceMappingURL=NumberParser.cjs.map