Numeral-js icon indicating copy to clipboard operation
Numeral-js copied to clipboard

Specifying a number of significant digits?

Open rickhuizinga opened this issue 10 years ago • 15 comments

First, this has been a very handy library.

I would like to limit the number of significant digits to 3 such that: $12600 -> $12.6k $342622 -> $343k $1242050 -> $1.24m

I haven't found a way to specify this style of formatting. It only allows me to specify the number of decimal points, which can be too much information.

For example, specifying 2 decimal points works for $1242050 -> $1.24m, but is not appropriate for $342622 -> $342.62k. Its just includes too much unnecessary information for displaying prices to users.

Is there a way to limit the total number of digits, rather than the number of decimal places? If not, could it be added?

rickhuizinga avatar Jul 18 '14 00:07 rickhuizinga

This is exactly what I want. Just found this library and is exactly the kind of thing I needed.

nbauernfeind avatar Oct 18 '14 05:10 nbauernfeind

I don't care about negative numbers, so I didn't even bother testing them. However, I accomplished this feat with a method that wraps the numeraljs formatting.

function(num) {
  if (num < 1000) return num;
  var r = Math.ceil(Math.log(num) / Math.LN10) % 3;
  return numeral(num).format(r == 0 ? "0a" : r == 1 ? "0.00a" : "0.0a");
};

nbauernfeind avatar Oct 18 '14 06:10 nbauernfeind

i need this as well. @nbauernfeind why dont you make a PR PS your function seems to make numbers that are mostly 0s limit to 4 rather than 3... for ex yourFunc(10000) === "10.00k"

cellvia avatar Oct 31 '14 19:10 cellvia

This is a big issue for me as well. Is this being worked on?

joyt avatar Apr 01 '15 21:04 joyt

I'm running into this too, (e.g. I want 200, not 200.0). I'll hop into the lib today and see if it's an easy fix or not.

therebelrobot avatar Apr 27 '15 17:04 therebelrobot

Digging into the tests, it looks like you can set some of the format parameters as optional by wrapping it in [], so in my example, I could do

numeral(200).format('0[.]0') // 200
numeral(200.345).format('0[.]0') // 200.3

@rickhuizinga you should be able to achieve what you want by wrapping the 0 in [], like this:

numeral(12600).format('$0[.]0[0]a') // $12.6k
numeral(1242050).format('$0[.]0[0]a') // $1.24m

numeral(342622).format('$0a') // $343k = > this one is a bit trickier, since there is no way to determine if you find the following numbers significant. the previous two examples with '$0[.]0[0]' above returns "$342.62k"

I hope that helped a bit. Digging into the tests is sometimes the best way to determine usage for a lot of libraries. It is shown as an example on the doc website but isn't specifically called out, so that's what made it more difficult for me.

@adamwdraper, would it be alright if I chipped into the docs a bit, make some of the usage cases a bit more explicit?

therebelrobot avatar Apr 27 '15 17:04 therebelrobot

@therebelrobot I'm not sure you understand the feature request. It doesn't make sense to say "there is no way to determine if you find the following numbers significant." By definition significant figures are the most significant. $342.62 has five significant figures, not three. It sounds like you want to solve a different problem than the original issue.

@cellvia You're right; it looks like my rounding is wrong on powers of 10. Here is the fixed formula:

function(num) {
  if (num < 1000) return num;
  var r = (Math.floor(Math.log(num) / Math.LN10) + 1) % 3;
  return numeral(num).format(r == 0 ? "0a" : r == 1 ? "0.00a" : "0.0a");
};

nbauernfeind avatar Apr 27 '15 22:04 nbauernfeind

Also, the reason that my solution isn't appropriate for a PR is because it would be useful to give an arbitrary number of significant digits; my method only solves for 3 significant digits.

nbauernfeind avatar Apr 27 '15 22:04 nbauernfeind

@nbauernfeind I was responding to @rickhuizinga's original request. I'm aware that your solution is only for 3 digits, I was hoping that in finding a solution to what I needed (200 => '0.0' => 200) I could fix what was needed here (the use cases outlined originally). I found that that wasn't not the case. By saying "there is no way to determine if you find the following numbers significant.", I meant not as the library currently does, which you are correct in stating, but rather that there is no way to determine if the digits are significant to the end user besides how the library currently does it, so it may be a useful addition to put in a method to arbitrarily specify the number of significant digits. I did, however, try to address the other two use cases originally posted, even if it didn't cover the last use case.

Besides that, additional documentation on the current API would be beneficial, but I can move that discussion to another Github issue.

therebelrobot avatar Apr 28 '15 01:04 therebelrobot

@therebelrobot Yeah, that makes sense. I think the useful API that the request suggests is to have some format specifier that says you want n-significant figures. To that end, it's not up to the end-user but rather the application (a.k.a. API user). For example, it would be nice to just say numeral(num).format("0a-3") and your format will always contain 3 significant digits.

Per your suggestion, note that it really doesn't solve the original user's request:

numeral(12650).format('$0[.]0[0]a')
> "$12.65k"

12650 with three significant figures is "$12.7k"

nbauernfeind avatar Apr 28 '15 17:04 nbauernfeind

I also just ran into this as well. The library is very nice, but dealing with significant digits is pretty useful and important. I agree with @nbauernfeind that having it in the format string would be really useful.

Here is my solution as a wrapper function. Note, I wrote it in TypeScript but if you strip the type annotations, it will work in Javascript too. This allows for an arbitrary number of significant digits. I tried to cover all the corner cases, but I could have missed some (all the more reason it should be in the library where these same corner cases need to be dealt with).

function sig(num: number, n: number) {
    let a = Math.abs(num); // Absolute value of number
    let la = Math.log(a)/Math.LN10; // Log10 of absolute value
    let ld = Math.floor(la) % 3 + 1; // Digits to the left of .
    if (ld>=n) return numeral(num).format("0a");
    let rd = n-ld; // Digital to the right of .

    // Build format string... (TypeScript doesn't allow string*number)
    let fmt = "0.";
    for(let i=0;i<rd;i++) {
        fmt = fmt + "0";
    }
    fmt = fmt + "a";

    return numeral(num).format(fmt);
};

xogeny avatar Jan 20 '16 16:01 xogeny

I wrote a library which formats numbers to use S.I. prefixes with 3 sig. figs. The code might be useful as a reference if someone wants to work on a PR for this.

format-si-prefix

ThomWright avatar Jan 26 '16 12:01 ThomWright

JavaScript's native 'number' class has a toPrecision() member function. So to get something very close to 3 significant figures for variable 'num' in a much easier way than the examples above you can do:

numeral(num.toPrecision(3)).format('$0[.][0][0]a')

The only place this fails is when there are significant trailing zeroes so e.g. 2.10 becomes 2.1.

keefmarshall avatar Apr 10 '17 16:04 keefmarshall

numeral(num.toPrecision(3)).format('$0[.][00]a') is what worked for us. Note the bracket placement.

rajivraman avatar Aug 27 '18 20:08 rajivraman

Just for reference this is how my team is handling significant digits and all after studying some other solutions people came up with in here, thanks:

    export const formatNumber = (num: number): string => {
      const numAbs = Math.abs(num);
      if (numAbs >= 1) {
        return numeral(num.toPrecision(3)).format('0[.][00]a').toUpperCase();
      } else if (numAbs >= 0.01) {
        return numeral(num).format('0.0[000]');
      } else {
        return numeral(num).format('0.00e+0').toUpperCase();
      }
    };

With passing tests for these cases:

describe('formatNumber', () => {
  test('should format numbers with an absolute value >= 1 with a max of 3 significant figures and size symbol if needed', () => {
    expect(formatNumber(1)).toBe('1');
    expect(formatNumber(10)).toBe('10');
    expect(formatNumber(100)).toBe('100');
    expect(formatNumber(1000)).toBe('1K');
    expect(formatNumber(10000)).toBe('10K');
    expect(formatNumber(100000)).toBe('100K');
    expect(formatNumber(1000000)).toBe('1M');
    expect(formatNumber(10000000)).toBe('10M');
    expect(formatNumber(100000000)).toBe('100M');
    expect(formatNumber(1000000000)).toBe('1B');
    expect(formatNumber(1200000000)).toBe('1.2B');
    expect(formatNumber(1230000000)).toBe('1.23B');
    expect(formatNumber(1234000000)).toBe('1.23B');
    expect(formatNumber(-1)).toBe('-1');
    expect(formatNumber(-10)).toBe('-10');
    expect(formatNumber(-100)).toBe('-100');
    expect(formatNumber(-1000)).toBe('-1K');
    expect(formatNumber(-10000)).toBe('-10K');
    expect(formatNumber(-100000)).toBe('-100K');
    expect(formatNumber(-1000000)).toBe('-1M');
    expect(formatNumber(-10000000)).toBe('-10M');
    expect(formatNumber(-100000000)).toBe('-100M');
    expect(formatNumber(-1000000000)).toBe('-1B');
    expect(formatNumber(-1200000000)).toBe('-1.2B');
    expect(formatNumber(-1230000000)).toBe('-1.23B');
    expect(formatNumber(-1234000000)).toBe('-1.23B');
  });
  test('should format numbers with an absolute value < 1 and >= 0.01 with a max of 3 significant figures', () => {
    expect(formatNumber(0.1)).toBe('0.1');
    expect(formatNumber(0.01)).toBe('0.01');
    expect(formatNumber(0.011)).toBe('0.011');
    expect(formatNumber(0.0111)).toBe('0.0111');
    expect(formatNumber(0.0111111)).toBe('0.0111');
    expect(formatNumber(-0.1)).toBe('-0.1');
    expect(formatNumber(-0.01)).toBe('-0.01');
    expect(formatNumber(-0.011)).toBe('-0.011');
    expect(formatNumber(-0.0111)).toBe('-0.0111');
    expect(formatNumber(-0.0111111)).toBe('-0.0111');
  });
  test('should format numbers with an absolute value < 0.01 with scientific notation', () => {
    expect(formatNumber(0.001)).toBe('1.00E-3');
    expect(formatNumber(0.0012)).toBe('1.20E-3');
    expect(formatNumber(0.00123)).toBe('1.23E-3');
    expect(formatNumber(0.00123456)).toBe('1.23E-3');
    expect(formatNumber(0.000123456)).toBe('1.23E-4');
    expect(formatNumber(-0.001)).toBe('-1.00E-3');
    expect(formatNumber(-0.0012)).toBe('-1.20E-3');
    expect(formatNumber(-0.00123)).toBe('-1.23E-3');
    expect(formatNumber(-0.00123456)).toBe('-1.23E-3');
    expect(formatNumber(-0.000123456)).toBe('-1.23E-4');
  });
});

Hopefully didn't miss any edge cases but wouldn't be surprised if a few slipped by

DaltheCow avatar Oct 03 '23 21:10 DaltheCow