UNPKG

pretty-num

Version:

Lightweight module for formatting numbers to a human readable string

415 lines (378 loc) 14.7 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.prettyNum = {})); })(this, (function (exports) { 'use strict'; /** * Return two parts array of exponential number * @param {number|string|Array} num * @return {string[]} */ function getExponentialParts(num) { return Array.isArray(num) ? num : String(num).split(/[eE]/); } /** * * @param {number|string|Array} num - number or array of its parts */ function isExponential(num) { var eParts = getExponentialParts(num); return !Number.isNaN(Number(eParts[1])); } /** * Converts exponential notation to a human readable string * @param {number|string|Array} num - number or array of its parts * @return {string} */ function fromExponential(num) { var eParts = getExponentialParts(num); if (!isExponential(eParts)) { return eParts[0]; } var sign = eParts[0][0] === '-' ? '-' : ''; var digits = eParts[0].replace(/^-/, ''); var digitsParts = digits.split('.'); var wholeDigits = digitsParts[0]; var fractionDigits = digitsParts[1] || ''; var e = Number(eParts[1]); if (e === 0) { return "".concat(sign + wholeDigits, ".").concat(fractionDigits); } else if (e < 0) { // move dot to the left var countWholeAfterTransform = wholeDigits.length + e; if (countWholeAfterTransform > 0) { // transform whole to fraction var wholeDigitsAfterTransform = wholeDigits.substr(0, countWholeAfterTransform); var wholeDigitsTransformedToFraction = wholeDigits.substr(countWholeAfterTransform); return "".concat(sign + wholeDigitsAfterTransform, ".").concat(wholeDigitsTransformedToFraction).concat(fractionDigits); } else { // not enough whole digits: prepend with fractional zeros // first e goes to dotted zero var zeros = '0.'; e = countWholeAfterTransform; while (e) { zeros += '0'; e += 1; } return sign + zeros + wholeDigits + fractionDigits; } } else { // move dot to the right var countFractionAfterTransform = fractionDigits.length - e; if (countFractionAfterTransform > 0) { // transform fraction to whole // countTransformedFractionToWhole = e var fractionDigitsAfterTransform = fractionDigits.substr(e); var fractionDigitsTransformedToWhole = fractionDigits.substr(0, e); return "".concat(sign + wholeDigits + fractionDigitsTransformedToWhole, ".").concat(fractionDigitsAfterTransform); } else { // not enough fractions: append whole zeros var zerosCount = -countFractionAfterTransform; var _zeros = ''; while (zerosCount) { _zeros += '0'; zerosCount -= 1; } return sign + wholeDigits + fractionDigits + _zeros; } } } var thousands = function thousands(number, separator) { var parts = ((number || number === 0 ? number : '') + '').split('.'); if (parts.length) { parts[0] = parts[0].replace(/(\d)(?=(\d{3})+\b)/g, '$1' + (separator || ',')); } return parts.join('.'); }; /** * @overload * @param {string} num * @param {boolean} [keepEnding] * @return {string} */ /** * @overload * @param {number} num * @param {boolean} [keepEnding] * @return {number} */ /** * Strip unnecessary last zeros after dot * @param {string|number} num * @param {boolean} [keepEnding] - not strip ending zeros * @return {string|number} */ function stripZeros(num, keepEnding) { if (typeof num === 'string') { if (!keepEnding && num.indexOf('.') !== -1) { // eslint-disable-next-line unicorn/no-negated-condition if (!/[eE]/.test(num)) { // strip ending zeros num = num.replace(/\.?0*$/, ''); } else { // strip ending zeros in exponential notation num = num.replace(/\.?0*(?=[eE])/, ''); } } // strip leading zeros num = num.replace(/^0+(?!\.)(?!$)/, ''); } return num; } /** * Pad number string with zeros to fixed precision * Don't work with exponential notation, use `from-exponential` if necessary * @param {string|number} numString * @param {number} precision * @return {string} */ function padZerosToFixed(numString, precision) { if (typeof numString !== 'string') { numString = numString.toString(); } if (!(precision > 0)) { return numString; } // leave exponential untouched if (numString.toLowerCase().indexOf('e') !== -1) { return numString; } var decimalStart = numString.indexOf('.'); var hasDot = decimalStart !== -1; var decimalEnd = numString.length; var countDecimals = hasDot ? decimalEnd - decimalStart - 1 : 0; var countZerosToPad = precision - countDecimals; var zeros = hasDot ? '' : '.'; for (var index = 0; index < countZerosToPad; index += 1) { zeros += '0'; } // insert zeros between number and exponential part return numString.slice(0, Math.max(0, decimalEnd)) + zeros + numString.slice(decimalEnd); } /** * @enum {number} */ var PRECISION_SETTING = { REDUCE: 1, REDUCE_SIGNIFICANT: 2, FIXED: 3, INCREASE: 4 }; /** * @enum {number} */ var ROUNDING_MODE = { UP: 1, DOWN: 2, CEIL: 3, FLOOR: 4, HALF_UP: 5, HALF_DOWN: 6, HALF_EVEN: 7 }; /** * Reduce precision, accept precision settings: * - 'reduce', 'default' - reduce precision to specified number of decimal digits, strip unnecessary ending zeros. * - 'reduceSignificant', 'significant' - reduce precision to specified number of significant decimal digits, strip unnecessary ending zeros. * - 'fixed' - reduce precision to specified number of decimal digits, pad with ending zeros. * - 'increase' - pad with ending zeros to increase precision to specified number of decimal digits. * Don't work with exponential notation, use `from-exponential` if necessary * @param {string|number} num * @param {number} [precision] * @param {object} [options] * @param {PRECISION_SETTING} [options.precisionSetting = PRECISION_SETTING.REDUCE] * @param {ROUNDING_MODE} [options.roundingMode = ROUNDING_MODE.HALF_EVEN] * @return {string} */ function toPrecision(num, precision) { var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; num = num.toString(); // leave exponential untouched if (num.toLowerCase().indexOf('e') !== -1) { return num; } if (!options.precisionSetting) { options.precisionSetting = PRECISION_SETTING.REDUCE; } if (options.precisionSetting === PRECISION_SETTING.FIXED) { var result = _reducePrecision(num, precision, { precisionSetting: PRECISION_SETTING.REDUCE, roundingMode: options.roundingMode }); result = stripZeros(result, true); result = padZerosToFixed(result, precision); return result; } else if (options.precisionSetting === PRECISION_SETTING.INCREASE) { var _result = stripZeros(num); return padZerosToFixed(_result, precision); } else { return stripZeros(_reducePrecision(num, precision, options)); } } /** * Reduce precision with `reduce` or `reduceSignificant` precisionSetting, can produce ending dot or zeros * @param {string} numString * @param {number} precision * @param {object} [options] * @param {PRECISION_SETTING} [options.precisionSetting = PRECISION_SETTING.REDUCE] * @param {ROUNDING_MODE} [options.roundingMode] * @return {string} */ function _reducePrecision(numString, precision) { var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, _ref$precisionSetting = _ref.precisionSetting, precisionSetting = _ref$precisionSetting === void 0 ? PRECISION_SETTING.REDUCE : _ref$precisionSetting, _ref$roundingMode = _ref.roundingMode, roundingMode = _ref$roundingMode === void 0 ? ROUNDING_MODE.HALF_UP : _ref$roundingMode; // do not proceed falsy precision, except `0` if (!precision && precision !== 0) { return numString; } precision = Number(precision); var parts = numString.split('.'); var partWhole = parts[0]; var partDecimal = parts[1]; // nothing to reduce if (!partDecimal) { return numString; } // save negation and remove from string var negation = false; if (partWhole[0] === '-') { negation = true; partWhole = partWhole.slice(1); } // remove dot from string (it's easier to work with single integer value), dot position will be restored from parts length numString = partWhole + partDecimal; // if precision setting is `reduceSignificant` then start discount from zeros, ex. `.0000`, otherwise discount starting from dot `.` if (precisionSetting === PRECISION_SETTING.REDUCE_SIGNIFICANT) { var discountStartMatch = partDecimal.match(/^0*/); if (discountStartMatch) { precision += discountStartMatch[0].length; } } // index from which to start erasing var discountStartIndex = partWhole.length + precision; // length of decimal part after erasing var discountedDecimalPartLength = Math.min(partDecimal.length, precision); // erased part var remainder = numString.slice(discountStartIndex); // string after erasing numString = numString.slice(0, discountStartIndex); // increment if needed by rounding mode if (remainder && greaterThanFive(remainder, numString, negation, roundingMode)) { numString = increment(numString); } // restore dot position from string end if (discountedDecimalPartLength) { var integerPartLength = numString.length - discountedDecimalPartLength; numString = "".concat(numString.slice(0, integerPartLength), ".").concat(numString.slice(integerPartLength)); } // restore negation return (negation ? '-' : '') + numString; } /** * * @param {string} part * @param {string} pre * @param {boolean} neg * @param {ROUNDING_MODE} [mode=ROUNDING_MODE.HALF_EVEN] * @return {boolean} */ function greaterThanFive(part, pre, neg, mode) { if (!part || part === new Array(part.length + 1).join('0')) return false; // #region UP, DOWN, CEILING, FLOOR if (mode === ROUNDING_MODE.DOWN || !neg && mode === ROUNDING_MODE.FLOOR || neg && mode === ROUNDING_MODE.CEIL) return false; if (mode === ROUNDING_MODE.UP || neg && mode === ROUNDING_MODE.FLOOR || !neg && mode === ROUNDING_MODE.CEIL) return true; // #endregion // case when part !== five var five = "5".concat(new Array(part.length).join('0')); if (part > five) return true;else if (part < five) return false; // case when part === five switch (mode) { case ROUNDING_MODE.HALF_DOWN: { return false; } case ROUNDING_MODE.HALF_UP: { return true; } // case ROUNDING_MODE.HALF_EVEN: default: { return Number.parseInt(pre.at(-1), 10) % 2 === 1; } } } /** * * @param {string} part * @param {number} [value = 1] * @return {string} */ function increment(part) { var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; var str = ''; // traverse string backward for (var index = part.length - 1; index >= 0; index -= 1) { var digit = Number.parseInt(part[index], 10) + value; if (digit === 10) { value = 1; digit = 0; } else { value = 0; } str += digit; } if (value) { str += value; } return str.split('').reverse().join(''); } /** * @import {PRECISION_SETTING} from './to-precision.js'; * @import {ROUNDING_MODE} from './to-precision.js'; */ /** * @param {number|string} value * @param {object} [options] * @param {number} [options.precision] * @param {PRECISION_SETTING} [options.precisionSetting = PRECISION_SETTING.REDUCE] * @param {ROUNDING_MODE} [options.roundingMode = ROUNDING_MODE.HALF_EVEN] * @param {string} [options.thousandsSeparator] * @param {string} [options.decimalSeparator = '.'] * @param {boolean} [options.separateOneDigit = true] * @return {string} */ function prettyNum(value) { var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, precision = _ref.precision, precisionSetting = _ref.precisionSetting, roundingMode = _ref.roundingMode, thousandsSeparator = _ref.thousandsSeparator, decimalSeparator = _ref.decimalSeparator, _ref$separateOneDigit = _ref.separateOneDigit, separateOneDigit = _ref$separateOneDigit === void 0 ? true : _ref$separateOneDigit; // remove exponential notation var num = fromExponential(value); // reduce precision num = toPrecision(num, precision, { precisionSetting: precisionSetting, roundingMode: roundingMode }); // skip separation if `!separateOneDigit && num < 10000` if (thousandsSeparator && (separateOneDigit || Number(num) >= 10000)) { num = thousands(num, thousandsSeparator); } if (decimalSeparator && decimalSeparator !== '.') { num = num.replace('.', decimalSeparator); } return num; } exports.PRECISION_SETTING = PRECISION_SETTING; exports.ROUNDING_MODE = ROUNDING_MODE; exports["default"] = prettyNum; exports.prettyNum = prettyNum; Object.defineProperty(exports, '__esModule', { value: true }); }));