package.dist.NumberParser.mjs Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of number Show documentation
Show all versions of number Show documentation
Internationalized number formatting and parsing utilities
The newest version!
import {NumberFormatter as $488c6ddbf4ef74c2$export$cc77c4ff7e8673c5} from "./NumberFormatter.mjs";
/*
* 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 $6c7bd7858deea686$var$CURRENCY_SIGN_REGEX = new RegExp('^.*\\(.*\\).*$');
const $6c7bd7858deea686$var$NUMBERING_SYSTEMS = [
'latn',
'arab',
'hanidec'
];
class $6c7bd7858deea686$export$cd11ab140839f11d {
/**
* Parses the given string to a number. Returns NaN if a valid number could not be parsed.
*/ parse(value) {
return $6c7bd7858deea686$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 $6c7bd7858deea686$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 $6c7bd7858deea686$var$getNumberParserImpl(this.locale, this.options, value).options.numberingSystem;
}
constructor(locale, options = {}){
this.locale = locale;
this.options = options;
}
}
const $6c7bd7858deea686$var$numberParserCache = new Map();
function $6c7bd7858deea686$var$getNumberParserImpl(locale, options, value) {
// First try the default numbering system for the provided locale
let defaultParser = $6c7bd7858deea686$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 $6c7bd7858deea686$var$NUMBERING_SYSTEMS)if (numberingSystem !== defaultParser.options.numberingSystem) {
let parser = $6c7bd7858deea686$var$getCachedNumberParser(locale + (locale.includes('-u-') ? '-nu-' : '-u-nu-') + numberingSystem, options);
if (parser.isValidPartialNumber(value)) return parser;
}
}
return defaultParser;
}
function $6c7bd7858deea686$var$getCachedNumberParser(locale, options) {
let cacheKey = locale + (options ? Object.entries(options).sort((a, b)=>a[0] < b[0] ? -1 : 1).join() : '');
let parser = $6c7bd7858deea686$var$numberParserCache.get(cacheKey);
if (!parser) {
parser = new $6c7bd7858deea686$var$NumberParserImpl(locale, options);
$6c7bd7858deea686$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 $6c7bd7858deea686$var$NumberParserImpl {
parse(value) {
// 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);
if (this.symbols.group) // Remove group characters, and replace decimal points and numerals with ASCII values.
fullySanitizedValue = $6c7bd7858deea686$var$replaceAll(fullySanitizedValue, 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('-', '');
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') {
var _this_options_minimumFractionDigits, _this_options_maximumFractionDigits;
// extra step for rounding percents to what our formatter would output
let options = {
...this.options,
style: 'decimal',
minimumFractionDigits: Math.min(((_this_options_minimumFractionDigits = this.options.minimumFractionDigits) !== null && _this_options_minimumFractionDigits !== void 0 ? _this_options_minimumFractionDigits : 0) + 2, 20),
maximumFractionDigits: Math.min(((_this_options_maximumFractionDigits = this.options.maximumFractionDigits) !== null && _this_options_maximumFractionDigits !== void 0 ? _this_options_maximumFractionDigits : 0) + 2, 20)
};
return new $6c7bd7858deea686$export$cd11ab140839f11d(this.locale, options).parse(new (0, $488c6ddbf4ef74c2$export$cc77c4ff7e8673c5)(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' && $6c7bd7858deea686$var$CURRENCY_SIGN_REGEX.test(value)) newValue = -1 * newValue;
return newValue;
}
sanitize(value) {
// Remove literals and whitespace, which are allowed anywhere in the string
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 = value.replace(',', this.symbols.decimal);
value = value.replace(String.fromCharCode(1548), this.symbols.decimal);
}
if (this.symbols.group) value = $6c7bd7858deea686$var$replaceAll(value, '.', this.symbols.group);
}
// fr-FR group character is char code 8239, but that's not a key on the french keyboard,
// so allow 'period' as a group char and replace it with a space
if (this.options.locale === 'fr-FR') value = $6c7bd7858deea686$var$replaceAll(value, '.', String.fromCharCode(8239));
return value;
}
isValidPartialNumber(value, minValue = -Infinity, maxValue = Infinity) {
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 cannot start with a group separator
if (this.symbols.group && value.startsWith(this.symbols.group)) return false;
// 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) value = $6c7bd7858deea686$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;
}
constructor(locale, options = {}){
this.locale = locale;
this.formatter = new Intl.NumberFormat(locale, options);
this.options = this.formatter.resolvedOptions();
this.symbols = $6c7bd7858deea686$var$getSymbols(locale, this.formatter, this.options, options);
var _this_options_minimumFractionDigits, _this_options_maximumFractionDigits;
if (this.options.style === 'percent' && (((_this_options_minimumFractionDigits = this.options.minimumFractionDigits) !== null && _this_options_minimumFractionDigits !== void 0 ? _this_options_minimumFractionDigits : 0) > 18 || ((_this_options_maximumFractionDigits = this.options.maximumFractionDigits) !== null && _this_options_maximumFractionDigits !== void 0 ? _this_options_maximumFractionDigits : 0) > 18)) console.warn('NumberParser cannot handle percentages with greater than 18 decimal places, please reduce the number in your options.');
}
}
const $6c7bd7858deea686$var$nonLiteralParts = new Set([
'decimal',
'fraction',
'integer',
'minusSign',
'plusSign',
'group'
]);
// This list is derived from https://www.unicode.org/cldr/charts/43/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.
// See: https://github.com/adobe/react-spectrum/pull/5134/files#r1337037855 for used script
const $6c7bd7858deea686$var$pluralNumbers = [
0,
4,
2,
1,
11,
20,
3,
7,
100,
21,
0.1,
1.1
];
function $6c7bd7858deea686$var$getSymbols(locale, formatter, intlOptions, originalOptions) {
var _allParts_find, _posAllParts_find, _decimalParts_find, _allParts_find1;
// 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,
minimumSignificantDigits: 1,
maximumSignificantDigits: 21
});
// 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 = $6c7bd7858deea686$var$pluralNumbers.map((n)=>symbolFormatter.formatToParts(n));
var _allParts_find_value;
let minusSign = (_allParts_find_value = (_allParts_find = allParts.find((p)=>p.type === 'minusSign')) === null || _allParts_find === void 0 ? void 0 : _allParts_find.value) !== null && _allParts_find_value !== void 0 ? _allParts_find_value : '-';
let plusSign = (_posAllParts_find = posAllParts.find((p)=>p.type === 'plusSign')) === null || _posAllParts_find === void 0 ? void 0 : _posAllParts_find.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.
// @ts-ignore
if (!plusSign && ((originalOptions === null || originalOptions === void 0 ? void 0 : originalOptions.signDisplay) === 'exceptZero' || (originalOptions === null || originalOptions === void 0 ? void 0 : 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 = decimalParts.find((p)=>p.type === 'decimal')) === null || _decimalParts_find === void 0 ? void 0 : _decimalParts_find.value;
let group = (_allParts_find1 = allParts.find((p)=>p.type === 'group')) === null || _allParts_find1 === void 0 ? void 0 : _allParts_find1.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)=>!$6c7bd7858deea686$var$nonLiteralParts.has(p.type)).map((p)=>$6c7bd7858deea686$var$escapeRegex(p.value));
let pluralPartsLiterals = pluralParts.flatMap((p)=>p.filter((p)=>!$6c7bd7858deea686$var$nonLiteralParts.has(p.type)).map((p)=>$6c7bd7858deea686$var$escapeRegex(p.value)));
let sortedLiterals = [
...new Set([
...allPartsLiterals,
...pluralPartsLiterals
])
].sort((a, b)=>b.length - a.length);
let literals = sortedLiterals.length === 0 ? new RegExp('[\\p{White_Space}]', 'gu') : new RegExp(`${sortedLiterals.join('|')}|[\\p{White_Space}]`, '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,
index: index
};
}
function $6c7bd7858deea686$var$replaceAll(str, find, replace) {
// @ts-ignore
if (str.replaceAll) // @ts-ignore
return str.replaceAll(find, replace);
return str.split(find).join(replace);
}
function $6c7bd7858deea686$var$escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export {$6c7bd7858deea686$export$cd11ab140839f11d as NumberParser};
//# sourceMappingURL=NumberParser.module.js.map
© 2015 - 2025 Weber Informatics LLC | Privacy Policy