Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format to at least N decimal places #260

Open
jackc opened this issue Apr 24, 2020 · 4 comments
Open

Format to at least N decimal places #260

jackc opened this issue Apr 24, 2020 · 4 comments

Comments

@jackc
Copy link

jackc commented Apr 24, 2020

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.

@MikeMcl
Copy link
Owner

MikeMcl commented Apr 25, 2020

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);
}

@jackc
Copy link
Author

jackc commented Apr 26, 2020

Thanks! That's great!

@ghost
Copy link

ghost commented Apr 7, 2022

@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.

@piecler
Copy link

piecler commented Jul 27, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants