UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

174 lines (173 loc) • 7.34 kB
/*! * All material copyright ESRI, All Rights Reserved, unless otherwise specified. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details. * v1.5.0-next.4 */ import { numberKeys } from "./key"; const unnecessaryDecimal = new RegExp(`\\${"."}(0+)?$`); const trailingZeros = new RegExp("0+$"); // adopted from https://stackoverflow.com/a/66939244 export class BigDecimal { 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) === "-"; } 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)); 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); } } // Configuration: constants BigDecimal.DECIMALS = 100; // number of decimals on all instances BigDecimal.ROUNDED = true; // numbers are truncated (false) or rounded (true) BigDecimal.SHIFT = BigInt("1" + "0".repeat(BigDecimal.DECIMALS)); // derived constant BigDecimal._divRound = (dividend, divisor) => BigDecimal.fromBigInt(dividend / divisor + (BigDecimal.ROUNDED ? ((dividend * BigInt(2)) / divisor) % BigInt(2) : BigInt(0))); BigDecimal.fromBigInt = (bigint) => Object.assign(Object.create(BigDecimal.prototype), { value: bigint, isNegative: bigint < BigInt(0) }); export function isValidNumber(numberString) { return !(!numberString || isNaN(Number(numberString))); } export 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); }) .reduce((string, part) => string + part); return isValidNumber(result) ? new BigDecimal(result).toString() : ""; }); } // regex for number sanitization const allLeadingZerosOptionallyNegative = /^([-0])0+(?=\d)/; const decimalOnlyAtEndOfString = /(?!^\.)\.$/; const allHyphensExceptTheStart = /(?!^-)-/g; const isNegativeDecimalOnlyZeros = /^-\b0\b\.?0*$/; export const sanitizeNumberString = (numberString) => sanitizeExponentialNumberString(numberString, (nonExpoNumString) => { const sanitizedValue = nonExpoNumString .replace(allHyphensExceptTheStart, "") .replace(decimalOnlyAtEndOfString, "") .replace(allLeadingZerosOptionallyNegative, "$1"); return isValidNumber(sanitizedValue) ? isNegativeDecimalOnlyZeros.test(sanitizedValue) ? sanitizedValue : new BigDecimal(sanitizedValue).toString() : nonExpoNumString; }); export 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"); } /** * Converts an exponential notation numberString into decimal notation. * BigInt doesn't support exponential notation, so this is required to maintain precision * * @param {string} numberString - pre-validated exponential or decimal number * @returns {string} numberString in decimal notation */ export 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 = (integers, magnitude) => { const magnitudeDelta = Math.abs(magnitude) - integers.length; const leftPaddedZeros = magnitudeDelta > 0 ? `${"0".repeat(magnitudeDelta)}${integers}` : integers; const shiftedDecimal = `${leftPaddedZeros.slice(0, magnitude)}${"."}${leftPaddedZeros.slice(magnitude)}`; return shiftedDecimal; }; const shiftDecimalRight = (decimals, magnitude) => { const rightPaddedZeros = magnitude > decimals.length ? `${decimals}${"0".repeat(magnitude - decimals.length)}` : decimals; const shiftedDecimal = `${rightPaddedZeros.slice(0, magnitude)}${"."}${rightPaddedZeros.slice(magnitude)}`; 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)); }