bignumber.js
bignumber.js copied to clipboard
Format to at least N decimal places
It is sometimes convenient when working with money to always format to at least N decimal places, but not to round if there is more precision present. For example, US currency generally should be formatted to two decimal places, but if there are fractions of a cent they should be included not rounded:
(new BigNumber("1")).toFormat(2) // 1.00 - toFormat(2) will work correctly without fractions of a cent
(new BigNumber("1.234")).toFormat(2) // 1.23 - toFormat(2) loses fractional cents
I currently work around this as follows:
export const formatMinDP = function(n, dp) {
const a = n.toFormat(dp)
const b = n.toFormat()
if (a.length > b.length) {
return a
} else {
return b
}
}
But it seems this would a common enough use that it might be useful to have built-in. Not sure what sort of interface would make sense. Maybe a no-op rounding mode or a new format key to skip rounding.
Thanks for your input. Yes, I agree.
One option is to change toFormat so a maximum or minimum number of decimal places can be specified via an options object (which could also allow all the other format options to be passed in as well).
In the meantime, the following overwrites the toFormat method so it can take an object specifying either decimalPlaces, maximumDecimalPlaces or minimumDecimalPlaces.
BigNumber.prototype.toFormat = (function (u) {
const format = BigNumber.prototype.toFormat;
return function (dp, rm) {
if (typeof dp === 'object' && dp) {
let t = dp.minimumDecimalPlaces;
if (t !== u) return format.call(this, this.dp() < t ? t : u);
rm = dp.roundingMode;
t = dp.maximumDecimalPlaces;
if (t !== u) return format.call(this.dp(t, rm));
t = dp.decimalPlaces;
if (t !== u) return format.call(this, t, rm);
}
return format.call(this, dp, rm);
}
})();
Usage:
const x = new BigNumber('1');
x.toFormat(2); // '1.00'
x.toFormat({ decimalPlaces: 2 }); // '1.00'
x.toFormat({ maximumDecimalPlaces: 2 }); // '1'
x.toFormat({ minimumDecimalPlaces: 2 }); // '1.00'
const y = new BigNumber('1.234');
y.toFormat(2); // '1.23'
y.toFormat({ decimalPlaces: 2 }); // '1.23'
y.toFormat({ decimalPlaces: 2, roundingMode: 0 }); // '1.24'
y.toFormat({ maximumDecimalPlaces: 2 }); // '1.23'
y.toFormat({ minimumDecimalPlaces: 2 }); // '1.234'
By the way, using the dp() method would make your workaround more efficient:
const formatMinDP = function(n, dp) {
return n.dp() < dp ? n.toFormat() : n.toFormat(dp);
}
Thanks! That's great!
@MikeMcl Thanks. This is really helpful.
I would just suggest to add a preserveTrailingZeroes key which automatically preserves all trailing zeroes 0s from the original value.
Edit: @MikeMcl decimalSeparator & groupSeparator options doesn't work when decimalPlaces, maximumDecimalPlaces or minimumDecimalPlaces options are provided.
I had the same use case and added a .toFormat2 method:
FORMAT = {
minDP: 0,
maxDP: DECIMAL_PLACES,
roundingMode: ROUNDING_MODE,
prefix: '',
groupSize: 3,
secondaryGroupSize: 0,
groupSeparator: ',',
decimalSeparator: '.',
fractionGroupSize: 0,
fractionGroupSeparator: '\xA0', // non-breaking space
suffix: ''
}
....
P.toFormat2 = function ( format ) {
var str,i,
x = this;
if (format == null ) {
format = FORMAT;
} else if (typeof format != 'object') {
throw Error
(bignumberError + 'Argument not an object: ' + format);
}
for ( i in FORMAT ) {
if ( typeof format[i] === 'undefined' ) {
format[i] = FORMAT[i];
}
}
str = x.toFixed( format.maxDP, format.roundingMode );
if (x.c) {
var arr = str.split('.'),
g1 = +format.groupSize,
g2 = +format.secondaryGroupSize,
groupSeparator = format.groupSeparator || '',
intPart = arr[0],
fractionPart = arr[1],
isNeg = x.s < 0,
intDigits = isNeg ? intPart.slice(1) : intPart,
len = intDigits.length;
if (g2) {
i = g1;
g1 = g2;
g2 = i;
len -= i;
}
// intPart
if (g1 > 0 && len > 0) {
i = len % g1 || g1;
intPart = intDigits.substr(0, i);
for (; i < len; i += g1) intPart += groupSeparator + intDigits.substr(i, g1);
if (g2 > 0) intPart += groupSeparator + intDigits.slice(i);
if (isNeg) intPart = '-' + intPart;
}
// fractionPart
if ( fractionPart && fractionPart.length > format.minDP ) {
// trim trailing zeros
while ( fractionPart.length > format.minDP && fractionPart.slice(-1) === '0' ) {
fractionPart = fractionPart.slice(0, -1);
}
}
str = fractionPart
? intPart + (format.decimalSeparator || '') + ((g2 = +format.fractionGroupSize)
? fractionPart.replace(new RegExp('\\d{' + g2 + '}\\B', 'g'),
'$&' + (format.fractionGroupSeparator || ''))
: fractionPart)
: intPart;
}
return (format.prefix || '') + str + (format.suffix || '');
};
I merged the dp und rm parameters into the format object, since these are as important as any other format property imho. I'd appreciate it if you could add this or something equally usable to your code. Minimal decimal places are often important, even if it's zeros.