UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

403 lines (402 loc) 15.8 kB
/*! All material copyright ESRI, All Rights Reserved, unless otherwise specified. See https://github.com/Esri/calcite-design-system/blob/dev/LICENSE.md for details. v3.2.1 */ import { n as numberKeys } from "./key.js"; const unnecessaryDecimal = new RegExp(`\\${"."}(0+)?$`); const trailingZeros = new RegExp("0+$"); class BigDecimal { static { this.DECIMALS = 100; } static { this.ROUNDED = true; } static { this.SHIFT = BigInt("1" + "0".repeat(this.DECIMALS)); } // derived constant constructor(input) { if (input instanceof BigDecimal) { return input; } const [integers, decimals] = expandExponentialNumberString(input).split(".").concat(""); this.value = BigInt(integers + decimals.padEnd(BigDecimal.DECIMALS, "0").slice(0, BigDecimal.DECIMALS)) + BigInt(BigDecimal.ROUNDED && decimals[BigDecimal.DECIMALS] >= "5"); this.isNegative = input.charAt(0) === "-"; } static { this._divRound = (dividend, divisor) => BigDecimal.fromBigInt( dividend / divisor + (BigDecimal.ROUNDED ? dividend * BigInt(2) / divisor % BigInt(2) : BigInt(0)) ); } static { this.fromBigInt = (bigint) => Object.assign(Object.create(BigDecimal.prototype), { value: bigint, isNegative: bigint < BigInt(0) }); } getIntegersAndDecimals() { const s = this.value.toString().replace("-", "").padStart(BigDecimal.DECIMALS + 1, "0"); const integers = s.slice(0, -BigDecimal.DECIMALS); const decimals = s.slice(-BigDecimal.DECIMALS).replace(trailingZeros, ""); return { integers, decimals }; } toString() { const { integers, decimals } = this.getIntegersAndDecimals(); return `${this.isNegative ? "-" : ""}${integers}${decimals.length ? "." + decimals : ""}`; } formatToParts(formatter) { const { integers, decimals } = this.getIntegersAndDecimals(); const parts = formatter.numberFormatter.formatToParts(BigInt(integers)); if (this.isNegative) { parts.unshift({ type: "minusSign", value: formatter.minusSign }); } if (decimals.length) { parts.push({ type: "decimal", value: formatter.decimal }); decimals.split("").forEach((char) => parts.push({ type: "fraction", value: char })); } return parts; } format(formatter) { const { integers, decimals } = this.getIntegersAndDecimals(); const integersFormatted = `${this.isNegative ? formatter.minusSign : ""}${formatter.numberFormatter.format( BigInt(integers) )}`; const decimalsFormatted = decimals.length ? `${formatter.decimal}${decimals.split("").map((char) => formatter.numberFormatter.format(Number(char))).join("")}` : ""; return `${integersFormatted}${decimalsFormatted}`; } add(n) { return BigDecimal.fromBigInt(this.value + new BigDecimal(n).value); } subtract(n) { return BigDecimal.fromBigInt(this.value - new BigDecimal(n).value); } multiply(n) { return BigDecimal._divRound(this.value * new BigDecimal(n).value, BigDecimal.SHIFT); } divide(n) { return BigDecimal._divRound(this.value * BigDecimal.SHIFT, new BigDecimal(n).value); } } function isValidNumber(numberString) { return !(!numberString || isNaN(Number(numberString))); } function parseNumberString(numberString) { if (!numberString || !stringContainsNumbers(numberString)) { return ""; } return sanitizeExponentialNumberString(numberString, (nonExpoNumString) => { let containsDecimal = false; const result = nonExpoNumString.split("").filter((value, i) => { if (value.match(/\./g) && !containsDecimal) { containsDecimal = true; return true; } if (value.match(/-/g) && i === 0) { return true; } return numberKeys.includes(value); }).join(""); return isValidNumber(result) ? new BigDecimal(result).toString() : ""; }); } const allLeadingZerosOptionallyNegative = /^([-0])0+(?=\d)/; const decimalOnlyAtEndOfString = /(?!^\.)\.$/; const allHyphensExceptTheStart = /(?!^-)-/g; const isNegativeDecimalOnlyZeros = /^-\b0\b\.?0*$/; const hasTrailingDecimalZeros = /0*$/; const charAllowlist = /* @__PURE__ */ new Set(["e", "E", "-", ",", ".", ...numberKeys]); const sanitizeNumberString = (numberString) => { const strippedInvalidCharsValue = Array.from(numberString).filter((char) => charAllowlist.has(char)).join(""); return sanitizeExponentialNumberString(strippedInvalidCharsValue, (nonExpoNumString) => { const sanitizedValue = nonExpoNumString.replace(allHyphensExceptTheStart, "").replace(decimalOnlyAtEndOfString, "").replace(allLeadingZerosOptionallyNegative, "$1"); return isValidNumber(sanitizedValue) ? isNegativeDecimalOnlyZeros.test(sanitizedValue) ? sanitizedValue : getBigDecimalAsString(sanitizedValue) : nonExpoNumString; }); }; function getBigDecimalAsString(sanitizedValue) { const sanitizedValueDecimals = sanitizedValue.split(".")[1]; const value = new BigDecimal(sanitizedValue).toString(); const [bigDecimalValueInteger, bigDecimalValueDecimals] = value.split("."); return sanitizedValueDecimals && bigDecimalValueDecimals !== sanitizedValueDecimals ? `${bigDecimalValueInteger}.${sanitizedValueDecimals}` : value; } function sanitizeExponentialNumberString(numberString, func) { if (!numberString) { return numberString; } const firstE = numberString.toLowerCase().indexOf("e") + 1; if (!firstE) { return func(numberString); } return numberString.replace(/[eE]*$/g, "").substring(0, firstE).concat(numberString.slice(firstE).replace(/[eE]/g, "")).split(/[eE]/).map((section, i) => i === 1 ? func(section.replace(/\./g, "")) : func(section)).join("e").replace(/^e/, "1e"); } function expandExponentialNumberString(numberString) { const exponentialParts = numberString.split(/[eE]/); if (exponentialParts.length === 1) { return numberString; } const number = +numberString; if (Number.isSafeInteger(number)) { return `${number}`; } const isNegative = numberString.charAt(0) === "-"; const magnitude = +exponentialParts[1]; const decimalParts = exponentialParts[0].split("."); const integers = (isNegative ? decimalParts[0].substring(1) : decimalParts[0]) || ""; const decimals = decimalParts[1] || ""; const shiftDecimalLeft = (integers2, magnitude2) => { const magnitudeDelta = Math.abs(magnitude2) - integers2.length; const leftPaddedZeros = magnitudeDelta > 0 ? `${"0".repeat(magnitudeDelta)}${integers2}` : integers2; const shiftedDecimal = `${leftPaddedZeros.slice(0, magnitude2)}${"."}${leftPaddedZeros.slice(magnitude2)}`; return shiftedDecimal; }; const shiftDecimalRight = (decimals2, magnitude2) => { const rightPaddedZeros = magnitude2 > decimals2.length ? `${decimals2}${"0".repeat(magnitude2 - decimals2.length)}` : decimals2; const shiftedDecimal = `${rightPaddedZeros.slice(0, magnitude2)}${"."}${rightPaddedZeros.slice(magnitude2)}`; return shiftedDecimal; }; const expandedNumberString = magnitude > 0 ? `${integers}${shiftDecimalRight(decimals, magnitude)}` : `${shiftDecimalLeft(integers, magnitude)}${decimals}`; return `${isNegative ? "-" : ""}${expandedNumberString.charAt(0) === "." ? "0" : ""}${expandedNumberString.replace(unnecessaryDecimal, "").replace(allLeadingZerosOptionallyNegative, "")}`; } function stringContainsNumbers(string) { return numberKeys.some((number) => string.includes(number)); } function addLocalizedTrailingDecimalZeros(localizedValue, value, formatter) { const decimals = value.split(".")[1]; if (decimals) { const trailingDecimalZeros = decimals.match(hasTrailingDecimalZeros)[0]; if (trailingDecimalZeros && formatter.delocalize(localizedValue).length !== value.length && decimals.indexOf("e") === -1) { const decimalSeparator = formatter.decimal; localizedValue = !localizedValue.includes(decimalSeparator) ? `${localizedValue}${decimalSeparator}` : localizedValue; return localizedValue.padEnd(localizedValue.length + trailingDecimalZeros.length, formatter.localize("0")); } } return localizedValue; } const defaultLocale = "en"; const locales = [ "ar", "bg", "bs", "ca", "cs", "da", "de", "de-AT", "de-CH", "el", defaultLocale, "en-AU", "en-CA", "en-GB", "es", "es-MX", "et", "fi", "fr", "fr-CH", "he", "hi", "hr", "hu", "id", "it", "it-CH", "ja", "ko", "lt", "lv", "mk", "no", "nl", "pl", "pt", "pt-PT", "ro", "ru", "sk", "sl", "sr", "sv", "th", "tr", "uk", "vi", "zh-CN", "zh-HK", "zh-TW" ]; const localizedTwentyFourHourMeridiems = new Map( Object.entries({ bg: { am: "пр.об.", pm: "сл.об." }, bs: { am: "prijepodne", pm: "popodne" }, ca: { am: "a. m.", pm: "p. m." }, cs: { am: "dop.", pm: "odp." }, es: { am: "a. m.", pm: "p. m." }, "es-mx": { am: "a.m.", pm: "p.m." }, "es-MX": { am: "a.m.", pm: "p.m." }, fi: { am: "ap.", pm: "ip." }, he: { am: "לפנה״צ", pm: "אחה״צ" }, hu: { am: "de. ", pm: "du." }, lt: { am: "priešpiet", pm: "popiet" }, lv: { am: "priekšpusdienā", pm: "pēcpusdienā" }, mk: { am: "претпл.", pm: "попл." }, no: { am: "a.m.", pm: "p.m." }, nl: { am: "a.m.", pm: "p.m." }, "pt-pt": { am: "da manhã", pm: "da tarde" }, "pt-PT": { am: "da manhã", pm: "da tarde" }, ro: { am: "a.m.", pm: "p.m." }, sl: { am: "dop.", pm: "pop." }, sv: { am: "fm", pm: "em" }, th: { am: "ก่อนเที่ยง", pm: "หลังเที่ยง" }, tr: { am: "ÖÖ", pm: "ÖS" }, uk: { am: "дп", pm: "пп" }, vi: { am: "SA", pm: "CH" } }) ); const numberingSystems = ["arab", "arabext", "latn"]; const supportedLocales = [...locales]; const isNumberingSystemSupported = (numberingSystem) => numberingSystems.includes(numberingSystem); const browserNumberingSystem = new Intl.NumberFormat().resolvedOptions().numberingSystem; const defaultNumberingSystem = browserNumberingSystem === "arab" || !isNumberingSystemSupported(browserNumberingSystem) ? "latn" : browserNumberingSystem; const getSupportedNumberingSystem = (numberingSystem) => isNumberingSystemSupported(numberingSystem) ? numberingSystem : defaultNumberingSystem; function getSupportedLocale(locale) { if (!locale) { return defaultLocale; } if (supportedLocales.includes(locale)) { return locale; } locale = locale.toLowerCase(); if (locale.includes("-")) { locale = locale.replace(/(\w+)-(\w+)/, (_match, language, region) => `${language}-${region.toUpperCase()}`); if (!supportedLocales.includes(locale)) { locale = locale.split("-")[0]; } } if (locale === "nb" || locale === "nn") { return "no"; } if (locale === "zh") { return "zh-CN"; } if (!supportedLocales.includes(locale)) { console.warn( `Translations for the "${locale}" locale are not available and will fall back to the default, English (en).` ); return defaultLocale; } return locale; } function getDateFormatSupportedLocale(locale) { switch (locale) { case "it-CH": return "de-CH"; case "bs": return "bs-Cyrl"; default: return locale; } } class NumberStringFormat { constructor() { this.delocalize = (numberString) => ( // For performance, (de)localization is skipped if the formatter isn't initialized. // In order to localize/delocalize, e.g. when lang/numberingSystem props are not default values, // `numberFormatOptions` must be set in a component to create and cache the formatter. this._numberFormatOptions ? sanitizeExponentialNumberString( numberString, (nonExpoNumString) => nonExpoNumString.replace(new RegExp(`[${this._minusSign}]`, "g"), "-").replace(new RegExp(`[${this._group}]`, "g"), "").replace(new RegExp(`[${this._decimal}]`, "g"), ".").replace(new RegExp(`[${this._digits.join("")}]`, "g"), this._getDigitIndex) ) : numberString ); this.localize = (numberString) => this._numberFormatOptions ? sanitizeExponentialNumberString( numberString, (nonExpoNumString) => isValidNumber(nonExpoNumString.trim()) ? new BigDecimal(nonExpoNumString.trim()).format(this).replace(new RegExp(`[${this._actualGroup}]`, "g"), this._group) : nonExpoNumString ) : numberString; } get group() { return this._group; } get decimal() { return this._decimal; } get minusSign() { return this._minusSign; } get digits() { return this._digits; } get numberFormatter() { return this._numberFormatter; } get numberFormatOptions() { return this._numberFormatOptions; } /** numberFormatOptions needs to be set before localize/delocalize is called to ensure the options are up to date */ set numberFormatOptions(options) { options.locale = getSupportedLocale(options?.locale); options.numberingSystem = getSupportedNumberingSystem(options?.numberingSystem); if ( // No need to create the formatter if `locale` and `numberingSystem` // are the default values and `numberFormatOptions` has not been set !this._numberFormatOptions && options.locale === defaultLocale && options.numberingSystem === defaultNumberingSystem && // don't skip initialization if any options besides locale/numberingSystem are set Object.keys(options).length === 2 || // cache formatter by only recreating when options change JSON.stringify(this._numberFormatOptions) === JSON.stringify(options) ) { return; } this._numberFormatOptions = options; this._numberFormatter = new Intl.NumberFormat( this._numberFormatOptions.locale, this._numberFormatOptions ); this._digits = [ ...new Intl.NumberFormat(this._numberFormatOptions.locale, { useGrouping: false, numberingSystem: this._numberFormatOptions.numberingSystem }).format(9876543210) ].reverse(); const index = new Map(this._digits.map((d, i) => [d, i])); const parts = new Intl.NumberFormat(this._numberFormatOptions.locale, { numberingSystem: this._numberFormatOptions.numberingSystem }).formatToParts(-123456789e-1); this._actualGroup = parts.find((d) => d.type === "group").value; this._group = this._actualGroup.trim().length === 0 || this._actualGroup == " " ? " " : this._actualGroup; this._decimal = options.locale === "bs" || options.locale === "mk" ? "," : parts.find((d) => d.type === "decimal").value; this._minusSign = parts.find((d) => d.type === "minusSign").value; this._getDigitIndex = (d) => index.get(d); } } const numberStringFormatter = new NumberStringFormat(); let dateTimeFormatCache; let previousLocaleUsedForCaching; function buildDateTimeFormatCacheKey(options = {}) { return Object.entries(options).sort(([key1], [key2]) => key1.localeCompare(key2)).map((keyValue) => `${keyValue[0]}-${keyValue[1]}`).flat().join(":"); } function getDateTimeFormat(locale, options) { locale = getSupportedLocale(locale); if (!dateTimeFormatCache) { dateTimeFormatCache = /* @__PURE__ */ new Map(); } if (previousLocaleUsedForCaching !== locale) { dateTimeFormatCache.clear(); previousLocaleUsedForCaching = locale; } const key = buildDateTimeFormatCacheKey(options); const cached = dateTimeFormatCache.get(key); if (cached) { return cached; } const format = new Intl.DateTimeFormat(locale, options); dateTimeFormatCache.set(key, format); return format; } export { BigDecimal as B, NumberStringFormat as N, getSupportedNumberingSystem as a, getSupportedLocale as b, addLocalizedTrailingDecimalZeros as c, getDateFormatSupportedLocale as d, getDateTimeFormat as g, isValidNumber as i, localizedTwentyFourHourMeridiems as l, numberStringFormatter as n, parseNumberString as p, sanitizeNumberString as s };