53

I was wondering if it is possible to format numbers in Javascript template strings, for example something like:

var n = 5.1234;
console.log(`This is a number: $.2d{n}`);
// -> 5.12

Or possibly

var n = 5.1234;
console.log(`This is a number: ${n.toString('.2d')}`);
// -> 5.12

That syntax obviously doesn't work, it is just an illustration of the type of thing I'm looking for.

I am aware of tools like sprintf from underscore.string, but this seems like something that JS should be able to do out the box, especially given the power of template strings.

EDIT

As stated above, I am already aware of 3rd party tools (e.g. sprintf) and customised functions to do this. Similar questions (e.g. JavaScript equivalent to printf/String.Format) don't mention template strings at all, probably because they were asked before the ES6 template strings were around. My question is specific to ES6, and is independent of implementation. I am quite happy to accept an answer of "No, this is not possible" if that is case, but what would be great is either info about a new ES6 feature that provides this, or some insight into whether such a feature is on its way.

Eliran Malka
  • 15,821
  • 6
  • 77
  • 100
aquavitae
  • 17,414
  • 11
  • 63
  • 106

7 Answers7

43

No, ES6 does not introduce any new number formatting functions, you will have to live with the existing .toExponential(fractionDigits), .toFixed(fractionDigits), .toPrecision(precision), .toString([radix]) and toLocaleString(…) (which has been updated to optionally support the ECMA-402 Standard, though).
Template strings have nothing to do with number formatting, they just desugar to a function call (if tagged) or string concatenation (default).

If those Number methods are not sufficient for you, you will have to roll your own. You can of course write your formatting function as a template string tag if you wish to do so.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
12

You should be able to use the toFixed() method of a number:

var num = 5.1234;
var n = num.toFixed(2); 
four43
  • 1,675
  • 2
  • 20
  • 33
userDEV
  • 495
  • 4
  • 18
2

If you want to use ES6 tag functions here's how such a tag function would look,

function d2(pieces) {
    var result = pieces[0];
    var substitutions = [].slice.call(arguments, 1);

    for (var i = 0; i < substitutions.length; ++i) {
        var n = substitutions[i];

        if (Number(n) == n) {
            result += Number(substitutions[i]).toFixed(2);
        } else {
            result += substitutions[i];
        }

        result += pieces[i + 1];
    }

    return result;
}

which can then be applied to a template string thusly,

d2`${some_float} (you can interpolate as many floats as you want) of ${some_string}`;

that will format the float and leave the string alone.

Filip Allberg
  • 3,941
  • 3
  • 20
  • 37
  • Nice! This should be the accepted answer. Here's a fully ES6 version with "rest" args: https://codepen.io/garyo/pen/gZYmNP – GaryO Dec 07 '18 at 16:40
2

While template-string interpolation formatting is not available as a built-in, you can get equivalent behavior with Intl.NumberFormat:

const format = (num, fraction = 2) => new Intl.NumberFormat([], {
  minimumFractionDigits: fraction,
  maximumFractionDigits: fraction,
}).format(num);

format(5.1234); // -> '5.12'

Note that regardless of your implementation of choice, you might get bitten by rounding errors:

(9.999).toFixed(2) // -> '10.00'

new Intl.NumberFormat([], {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2, // <- implicit rounding!
}).format(9.999) // -> '10.00'
Eliran Malka
  • 15,821
  • 6
  • 77
  • 100
2

Here's a fully ES6 version of Filip Allberg's solution above, using ES6 "rest" params. The only thing missing is being able to vary the precision; that could be done by making a factory function. Left as an exercise for the reader.

function d2(strs, ...args) {
    var result = strs[0];
    for (var i = 0; i < args.length; ++i) {
        var n = args[i];
        if (Number(n) == n) {
            result += Number(args[i]).toFixed(2);
        } else {
            result += args[i];
        }
        result += strs[i+1];
    }
    return result;
}

f=1.2345678;
s="a string";
console.log(d2`template: ${f} ${f*100} and ${s} (literal:${9.0001})`);
GaryO
  • 5,873
  • 1
  • 36
  • 61
1

based on ES6 Tagged Templates (credit to https://stackoverflow.com/a/51680250/711085), this will emulate typical template string syntax in other languages (this is loosely based on python f-strings; I avoid calling it f in case of name overlaps):

Demo:

> F`${(Math.sqrt(2))**2}{.0f}`  // normally 2.0000000000000004
"2"

> F`${1/3}{%} ~ ${1/3}{.2%} ~ ${1/3}{d} ~ ${1/3}{.2f} ~ ${1/3}"
"33% ~ 33.33% ~ 0 ~ 0.33 ~ 0.3333333333333333"

> F`${[1/3,1/3]}{.2f} ~ ${{a:1/3, b:1/3}}{.2f} ~ ${"someStr"}`
"[0.33,0.33] ~ {\"a\":\"0.33\",\"b\":\"0.33\"} ~ someStr

Fairly simple code using :

var FORMATTER = function(obj,fmt) {
    /* implements things using (Number).toFixed:
       ${1/3}{.2f} -> 0.33
       ${1/3}{.0f} -> 1
       ${1/3}{%} -> 33%
       ${1/3}{.3%} -> 33.333%
       ${1/3}{d} -> 0
       ${{a:1/3,b:1/3}}{.2f} -> {"a":0.33, "b":0.33}
       ${{a:1/3,b:1/3}}{*:'.2f',b:'%'} -> {"a":0.33, "b":'33%'}  //TODO not implemented
       ${[1/3,1/3]}{.2f} -> [0.33, 0.33]
       ${someObj} -> if the object/class defines a method [Symbol.FTemplate](){...}, 
                     it will be evaluated; alternatively if a method [Symbol.FTemplateKey](key){...} 
                     that can be evaluated to a fmt string; alternatively in the future 
                     once decorators exist, metadata may be appended to object properties to derive 
                     formats //TODO not implemented
    */
    try {
        let fracDigits=0,percent;
        if (fmt===undefined) {
            if (typeof obj === 'string')
                return obj;
            else
                return JSON.stringify(obj);
        } else if (obj instanceof Array)
            return '['+obj.map(x=> FORMATTER(x,fmt))+']'
        else if (typeof obj==='object' && obj!==null /*&&!Array.isArray(obj)*/)
            return JSON.stringify(Object.fromEntries(Object.entries(obj).map(([k,v])=> [k,FORMATTER(v,fmt)])));
        else if (matches = fmt.match(/^\.(\d+)f$/))
            [_,fracDigits] = matches;
        else if (matches = fmt.match(/^(?:\.(\d+))?(%)$/))
            [_,fracDigits,percent] = matches;
        else if (matches = fmt.match(/^d$/))
            fracDigits = 0;
        else
            throw 'format not recognized';
        
        if (obj===null)
            return 'null';
        if (obj===undefined) {
            // one might extend the above syntax to 
            // allow for example for .3f? -> "undefined"|"0.123"
            return 'undefined';
        }
        
        if (percent)
            obj *= 100;
        
        fracDigits = parseFloat(fracDigits);
        return obj.toFixed(fracDigits) + (percent? '%':'');
    } catch(err) {
        throw `error executing F\`$\{${someObj}\}{${fmt}}\` specification: ${err}`
    }
}

function F(strs, ...args) {
    /* usage: F`Demo: 1+1.5 = ${1+1.5}{.2f}` 
          --> "Demo: 1+1.5 = 2.50" 
    */
    let R = strs[0];
    args.forEach((arg,i)=> {
        let [_,fmt,str] = strs[i+1].match(/(?:\{(.*)(?<!\\)\})?(.*)/);
        R += FORMATTER(arg,fmt) + str;
    });
    return R;
}

sidenote: The core of the code is as follows. The heavy lifting is done by the formatter. The negative lookbehind is somewhat optional, and to let one escape actual curly braces.

    let R = strs[0];
    args.forEach((arg,i)=> {
        let [_,fmt,str] = strs[i+1].match(/(?:\{(.*)(?<!\\)\})?(.*)/);
        R += FORMATTER(arg,fmt) + str;
    });
ninjagecko
  • 88,546
  • 24
  • 137
  • 145
-3

You can use es6 tag functions. I don't know ready for use of that.

It might look like this:

num`This is a number: $.2d{n}`

Learn more:

Atombit
  • 953
  • 9
  • 12
  • This gives me a syntax error for first, num is not defined and also doesn't work if I remove "num" from the start of it. Any details here? – Colin D Jun 09 '18 at 16:05
  • A fully working example would have been nice. The MDN link does explain it quite well however. – jlh Nov 22 '18 at 12:47