UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

315 lines (314 loc) • 11.8 kB
/** * DevExtreme (esm/common/core/localization/number.js) * Version: 24.2.6 * Build date: Mon Mar 17 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import dependencyInjector from "../../../core/utils/dependency_injector"; import { escapeRegExp } from "../../../core/utils/common"; import { each } from "../../../core/utils/iterator"; import { isPlainObject } from "../../../core/utils/type"; import { getFormatter } from "./ldml/number"; import config from "../../../core/config"; import errors from "../../../core/errors"; import { toFixed } from "./utils"; import currencyLocalization from "./currency"; import intlNumberLocalization from "./intl/number"; const hasIntl = "undefined" !== typeof Intl; const MAX_LARGE_NUMBER_POWER = 4; const DECIMAL_BASE = 10; const NUMERIC_FORMATS = ["currency", "fixedpoint", "exponential", "percent", "decimal"]; const LargeNumberFormatPostfixes = { 1: "K", 2: "M", 3: "B", 4: "T" }; const LargeNumberFormatPowers = { largenumber: "auto", thousands: 1, millions: 2, billions: 3, trillions: 4 }; const numberLocalization = dependencyInjector({ engine: function() { return "base" }, numericFormats: NUMERIC_FORMATS, defaultLargeNumberFormatPostfixes: LargeNumberFormatPostfixes, _parseNumberFormatString: function(formatType) { const formatObject = {}; if (!formatType || "string" !== typeof formatType) { return } const formatList = formatType.toLowerCase().split(" "); each(formatList, ((index, value) => { if (NUMERIC_FORMATS.includes(value)) { formatObject.formatType = value } else if (value in LargeNumberFormatPowers) { formatObject.power = LargeNumberFormatPowers[value] } })); if (formatObject.power && !formatObject.formatType) { formatObject.formatType = "fixedpoint" } if (formatObject.formatType) { return formatObject } }, _calculateNumberPower: function(value, base, minPower, maxPower) { let number = Math.abs(value); let power = 0; if (number > 1) { while (number && number >= base && (void 0 === maxPower || power < maxPower)) { power++; number /= base } } else if (number > 0 && number < 1) { while (number < 1 && (void 0 === minPower || power > minPower)) { power--; number *= base } } return power }, _getNumberByPower: function(number, power, base) { let result = number; while (power > 0) { result /= base; power-- } while (power < 0) { result *= base; power++ } return result }, _formatNumber: function(value, formatObject, formatConfig) { if ("auto" === formatObject.power) { formatObject.power = this._calculateNumberPower(value, 1e3, 0, 4) } if (formatObject.power) { value = this._getNumberByPower(value, formatObject.power, 1e3) } const powerPostfix = this.defaultLargeNumberFormatPostfixes[formatObject.power] || ""; let result = this._formatNumberCore(value, formatObject.formatType, formatConfig); result = result.replace(/(\d|.$)(\D*)$/, "$1" + powerPostfix + "$2"); return result }, _formatNumberExponential: function(value, formatConfig) { let power = this._calculateNumberPower(value, 10); let number = this._getNumberByPower(value, power, 10); if (void 0 === formatConfig.precision) { formatConfig.precision = 1 } if (number.toFixed(formatConfig.precision || 0) >= 10) { power++; number /= 10 } const powString = (power >= 0 ? "+" : "") + power.toString(); return this._formatNumberCore(number, "fixedpoint", formatConfig) + "E" + powString }, _addZeroes: function(value, precision) { const multiplier = Math.pow(10, precision); const sign = value < 0 ? "-" : ""; value = (Math.abs(value) * multiplier >>> 0) / multiplier; let result = value.toString(); while (result.length < precision) { result = "0" + result } return sign + result }, _addGroupSeparators: function(value) { const parts = value.toString().split("."); return parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, config().thousandsSeparator) + (parts[1] ? config().decimalSeparator + parts[1] : "") }, _formatNumberCore: function(value, format, formatConfig) { if ("exponential" === format) { return this._formatNumberExponential(value, formatConfig) } if ("decimal" !== format && null !== formatConfig.precision) { formatConfig.precision = formatConfig.precision || 0 } if ("percent" === format) { value *= 100 } if (void 0 !== formatConfig.precision) { if ("decimal" === format) { value = this._addZeroes(value, formatConfig.precision) } else { value = null === formatConfig.precision ? value.toPrecision() : toFixed(value, formatConfig.precision) } } if ("decimal" !== format) { value = this._addGroupSeparators(value) } else { value = value.toString().replace(".", config().decimalSeparator) } if ("percent" === format) { value += "%" } return value }, _normalizeFormat: function(format) { if (!format) { return {} } if ("function" === typeof format) { return format } if (!isPlainObject(format)) { format = { type: format } } return format }, _getSeparators: function() { return { decimalSeparator: this.getDecimalSeparator(), thousandsSeparator: this.getThousandsSeparator() } }, getThousandsSeparator: function() { return this.format(1e4, "fixedPoint")[2] }, getDecimalSeparator: function() { return this.format(1.2, { type: "fixedPoint", precision: 1 })[1] }, convertDigits: function(value, toStandard) { const digits = this.format(90, "decimal"); if ("string" !== typeof value || "0" === digits[1]) { return value } const fromFirstDigit = toStandard ? digits[1] : "0"; const toFirstDigit = toStandard ? "0" : digits[1]; const fromLastDigit = toStandard ? digits[0] : "9"; const regExp = new RegExp("[" + fromFirstDigit + "-" + fromLastDigit + "]", "g"); return value.replace(regExp, (char => String.fromCharCode(char.charCodeAt(0) + (toFirstDigit.charCodeAt(0) - fromFirstDigit.charCodeAt(0))))) }, getNegativeEtalonRegExp: function(format) { const separators = this._getSeparators(); const digitalRegExp = new RegExp("[0-9" + escapeRegExp(separators.decimalSeparator + separators.thousandsSeparator) + "]+", "g"); let negativeEtalon = this.format(-1, format).replace(digitalRegExp, "1"); ["\\", "(", ")", "[", "]", "*", "+", "$", "^", "?", "|", "{", "}"].forEach((char => { negativeEtalon = negativeEtalon.replace(new RegExp(`\\${char}`, "g"), `\\${char}`) })); negativeEtalon = negativeEtalon.replace(/ /g, "\\s"); negativeEtalon = negativeEtalon.replace(/1/g, ".*"); return new RegExp(negativeEtalon, "g") }, getSign: function(text, format) { if (!format) { if ("-" === text.replace(/[^0-9-]/g, "").charAt(0)) { return -1 } return 1 } const negativeEtalon = this.getNegativeEtalonRegExp(format); return text.match(negativeEtalon) ? -1 : 1 }, format: function(value, format) { if ("number" !== typeof value) { return value } if ("number" === typeof format) { return value } format = format && format.formatter || format; if ("function" === typeof format) { return format(value) } format = this._normalizeFormat(format); if (!format.type) { format.type = "decimal" } const numberConfig = this._parseNumberFormatString(format.type); if (!numberConfig) { const formatterConfig = this._getSeparators(); formatterConfig.unlimitedIntegerDigits = format.unlimitedIntegerDigits; return this.convertDigits(getFormatter(format.type, formatterConfig)(value)) } return this._formatNumber(value, numberConfig, format) }, parse: function(text, format) { if (!text) { return } if (format && format.parser) { return format.parser(text) } text = this.convertDigits(text, true); if (format && "string" !== typeof format) { errors.log("W0011") } const decimalSeparator = this.getDecimalSeparator(); const regExp = new RegExp("[^0-9" + escapeRegExp(decimalSeparator) + "]", "g"); const cleanedText = text.replace(regExp, "").replace(decimalSeparator, ".").replace(/\.$/g, ""); if ("." === cleanedText || "" === cleanedText) { return null } if (this._calcSignificantDigits(cleanedText) > 15) { return NaN } let parsed = +cleanedText * this.getSign(text, format); format = this._normalizeFormat(format); const formatConfig = this._parseNumberFormatString(format.type); let power = null === formatConfig || void 0 === formatConfig ? void 0 : formatConfig.power; if (power) { if ("auto" === power) { const match = text.match(/\d(K|M|B|T)/); if (match) { power = Object.keys(LargeNumberFormatPostfixes).find((power => LargeNumberFormatPostfixes[power] === match[1])) } } parsed *= Math.pow(10, 3 * power) } if ("percent" === (null === formatConfig || void 0 === formatConfig ? void 0 : formatConfig.formatType)) { parsed /= 100 } return parsed }, _calcSignificantDigits: function(text) { const [integer, fractional] = text.split("."); const calcDigitsAfterLeadingZeros = digits => { let index = -1; for (let i = 0; i < digits.length; i++) { if ("0" !== digits[i]) { index = i; break } } return index > -1 ? digits.length - index : 0 }; let result = 0; if (integer) { result += calcDigitsAfterLeadingZeros(integer.split("")) } if (fractional) { result += calcDigitsAfterLeadingZeros(fractional.split("").reverse()) } return result } }); numberLocalization.inject(currencyLocalization); if (hasIntl) { numberLocalization.inject(intlNumberLocalization) } export default numberLocalization;