Skip to content

Instantly share code, notes, and snippets.

@cho45
Last active November 12, 2022 04:18
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save cho45/9968462 to your computer and use it in GitHub Desktop.
Save cho45/9968462 to your computer and use it in GitHub Desktop.
function formatN (n) {
const unitList = ['y', 'z', 'a', 'f', 'p', 'n', 'u', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
const zeroIndex = 8;
const nn = n.toExponential(2).split(/e/);
let u = Math.floor(+nn[1] / 3) + zeroIndex;
if (u > unitList.length - 1) {
u = unitList.length - 1;
} else
if (u < 0) {
u = 0;
}
return nn[0] * Math.pow(10, +nn[1] - (u - zeroIndex) * 3) + unitList[u];
}
const array = [1e30, 1e9, 1e8, 1e7, 1e6, 2345, 100, 10, 1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 256e-9, 2.55e-12, 1e-13, -1e3, 1e-30];
for (let i = 0, len = array.length; i < len; i++) {
const n = array[i];
const formatted = formatN(n);
console.log(n, formatted);
}
1e+30 1000000Y
1000000000 1G
100000000 100M
10000000 10M
1000000 1M
2345 2.35k
100 100
10 10
1 1
0.01 10m
0.001 1m
0.0001 100u
0.00001 10u
0.000001 1u
2.56e-7 256n
2.55e-12 2.55p
1e-13 100f
-1000 -1k
1e-30 0.000001y
@bturner1273
Copy link

        var metricUnitStepsToPrefixSymbol = {
            1e-24: 'y', //yocto
            1e-21: 'z', //zepto
            1e-18: 'a', //atto
            1e-15: 'f', //femto
            1e-12: 'p', //pico
            1e-9:  'n',  //nano
            1e-6:  'μ',  //micro
            1e-3:  'm',  //milli
            1e-2:  'c',  //centi
            1e-1:  'd',  //deci
            1:      '',  //base 10 no prefix
            1e1:  'da',  //deca
            1e2:   'h',   //hecto
            1e3:   'k',   //kilo
            1e6:   'M',   //mega
            1e9:   'G',   //giga
            1e12:  'T',  //tera
            1e15:  'P',  //peta
            1e18:  'E',  //exa
            1e21:  'Z',  //zetta
            1e24:  'Y',  //yotta
        }

        let getSIPrefixSymbol = (/*:number*/num)/*:string*/ => {
            var strToReturn;
            Object.keys(metricUnitStepsToPrefixSymbol).forEach((key) => {
                if ((num / key) >= 1 && (num / key) <= 1000) {
                    strToReturn = (String(num / key) + metricUnitStepsToPrefixSymbol[key]);
                } 
            });
            return strToReturn;
        }

See anything wrong with this?

@yukulele
Copy link

Thanks @bturner1273 but it doesn't works as expected in some cases:

getSIPrefixSymbol(0); // get "", expected "0"
getSIPrefixSymbol(1); // get "10d", expected '1"
getSIPrefixSymbol(-1000); // get "", expected "-1k"
getSIPrefixSymbol(1e30); // get "", expected "1e30" or "1000000Y"
getSIPrefixSymbol(1e-4); // get  "100.00000000000001μ", expected "100µ"

@dirkgroenen
Copy link

dirkgroenen commented Nov 10, 2020

Note that this doesn't work well with cases as mentioned by @yukulele due to precision losses. The code below does work though (and is also way faster than @bturner1273 's):

const SI_PREFIXES_CENTER_INDEX = 8;

const siPrefixes: readonly string[] = [
  'y', 'z', 'a', 'f', 'p', 'n', 'μ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'
];

export const getSiPrefixedNumber = (number: number): string => {
  if (number === 0) return number.toString();
  const EXP_STEP_SIZE = 3;
  const base = Math.floor(Math.log10(Math.abs(number)));
  const siBase = (base < 0 ? Math.ceil : Math.floor)(base / EXP_STEP_SIZE);
  const prefix = siPrefixes[siBase + SI_PREFIXES_CENTER_INDEX];

  // return number as-is if no prefix is available
  if (siBase === 0) return number.toString();

  // We're left with a number which needs to be devided by the power of 10e[base]
  // This outcome is then rounded two decimals and parsed as float to make sure those
  // decimals only appear when they're actually requird (10.0 -> 10, 10.90 -> 19.9, 10.01 -> 10.01)
  const baseNumber = parseFloat((number / Math.pow(10, siBase * EXP_STEP_SIZE)).toFixed(2));
  return `${baseNumber}${prefix}`;
};

Which can obviously be shorted into the following Javascript:

function getSiPrefixedNumber(number) {
  if (number === 0) return number.toString();
  const base = Math.floor(Math.log10(Math.abs(number)));
  const siBase = (base < 0 ? Math.ceil : Math.floor)(base / 3);
  if (siBase === 0) return number.toString();
  const baseNumber = parseFloat((number / Math.pow(10, siBase * 3)).toFixed(2));
  const prefix = parseFloat((number / Math.pow(10, siBase * 3)).toFixed(2));
  return `${baseNumber}${prefix}`;
}
`

@KMurphs
Copy link

KMurphs commented Oct 26, 2021

Based on your comment @dirkgroenen.
A python implementation:

import math
SI_PREFIXES_CENTER_INDEX = 8
si_prefixes = ('y', 'z', 'a', 'f', 'p', 'n', 'μ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')


def to_si_notation(value: float, precision: int = 1):
    """Transforms a float number to a string (in SI Notation) using SI prefixes.
    ``e.g. 123456 => '123.46k'``

    Adapted from: https://gist.github.com/cho45/9968462 

    Args:
        value (float): The value to be converted
        precision (int, optional): The number of decimal places to display. Defaults to 1.

    Returns:
        str: String representing 'value' in SI Notation
    """

    value = float(value)
    if (value == 0): return str(value)

    exponent = math.floor(math.log10(abs(value)))
    exponent_of_1000 = (math.ceil if exponent < 0 else math.floor)(exponent / 3)
    if (exponent_of_1000 == 0): return "{0:.{1}f}".format(value, precision)
    
    mantissa = "{0:.{1}f}".format(float((value / math.pow(10, exponent_of_1000 * 3))), precision)
    si_prefix = si_prefixes[exponent_of_1000 + SI_PREFIXES_CENTER_INDEX]
    return f"{mantissa}{si_prefix}"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment