pretty-num
Version:
Lightweight module for formatting numbers to a human readable string
415 lines (378 loc) • 14.7 kB
JavaScript
(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 });
}));