Getallen formatteren in JavaScript: toFixed en Intl.NumberFormat

Afronden op twee decimalen, 1 234 567,89 op zijn Frans weergeven, een valuta, een percentage of “1,4 k”: de native methodes om een getal te formatteren in JavaScript, hun valkuilen, en wat er nog rest van Numeral.js.

Getallen formatteren in JavaScript met de library Numeral
Kort antwoord

Gebruik voor het weergeven van een getal Intl.NumberFormat of de kortere vorm toLocaleString(): (1234.5).toLocaleString('fr-FR', { minimumFractionDigits: 2 }) geeft “1 234,50”, en de optie style: 'currency' voegt de valuta toe. Om af te ronden zonder weer te geven, Math.round(n * 100) / 100, toFixed(2) rondt ook af maar geeft een string terug.

Een getal wordt berekend als Number en weergegeven als string: “1 234,50 €”, “25,6 %”, “1,4 k weergaven”. Daartussen moet je afronden, een duizendtalscheiding plaatsen, de komma of de punt kiezen. JavaScript doet dit allemaal zonder library, dankzij toFixed, toLocaleString en vooral Intl.NumberFormat. Alle onderstaande uitvoer is uitgevoerd onder Node.js 22.

Een getal afronden

Math.round, Math.floor, Math.ceil, Math.trunc

javascript
Math.round(2.5);   // 3   : naar het dichtstbijzijnde, .5 rondt naar boven af
Math.round(-2.5);  // -2  : richting +Infinity, niet “weg van nul”
Math.floor(4.7);   // 4   : naar beneden
Math.floor(-4.2);  // -5
Math.ceil(4.2);    // 5   : naar boven
Math.trunc(-4.7);  // -4  : snijdt het decimale deel af, ongeacht het teken

Deze functies kennen alleen het gehele getal. Om op twee decimalen af te ronden, vermenigvuldig je, rond je af, en deel je:

javascript
Math.round(1.345 * 100) / 100;   // 1.35
Math.round(1.005 * 100) / 100;   // 1    : verwacht 1.01, zie hieronder

De valkuil van binaire decimalen

1.005 bestaat niet in binaire vorm: het opgeslagen getal is 1.00499999… en wordt dus afgerond tot 1.00. Dit is geen bug van JavaScript maar van het IEEE 754-formaat, gemeenschappelijk voor alle talen. Twee oplossingen:

javascript
// 1. De representatiefout corrigeren voor het afronden
Math.round((1.005 + Number.EPSILON) * 100) / 100;   // 1.01

// 2. Nooit decimalen opslaan: prijzen in hele centen
const prixCentimes = 1005;
(prixCentimes / 100).toFixed(2);                    // '10.05'

De tweede is de regel voor alles wat met geld te maken heeft: hele centen optellen geeft nooit 0.30000000000000004.

toFixed() : afronden om weer te geven

javascript
(1234.5678).toFixed(2);  // '1234.57'
(2.5).toFixed(0);        // '3'
(1.005).toFixed(2);      // '1.00'  : dezelfde binaire valkuil als Math.round
(0.1 + 0.2).toFixed(2);  // '0.30'  : daar dient het nu net voor

typeof (1.5).toFixed(2); // 'string'
(1.5).toFixed(2) + 1;    // '1.501' : concatenatie, geen optelling

toFixed geeft een string terug. Dat is perfect voor de weergave, gevaarlijk voor de berekening: converteer met Number() als je de waarde opnieuw moet gebruiken. En zijn decimaalteken is altijd een punt, voor de Franse komma gebruik je toLocaleString.

Duizendtalscheiding en Frans formaat: toLocaleString()

javascript
const n = 1234567.891;

n.toLocaleString('fr-FR');   // '1 234 567,891'
n.toLocaleString('en-US');   // '1,234,567.891'
n.toLocaleString('de-DE');   // '1.234.567,891'

(1234.5).toLocaleString('fr-FR', { minimumFractionDigits: 2 });   // '1 234,50'
(3.14159).toLocaleString('fr-FR', { maximumFractionDigits: 2 });  // '3,14'

Eén methode, drie nationale conventies, geen enkele regel formatteercode. Twee verduidelijkingen:

  • Het Franse duizendtalscheidingsteken is geen gewone spatie maar een smalle onderbrekingsspatie (U+202F). Ze voorkomt dat “1 234” aan het einde van een regel wordt afgebroken. Moet je ze verwijderen, filter dan met /\s/g, niet met ' '.
  • Zonder taalargument gebruikt toLocaleString() die van de browser of het systeem. Voor een stabiel resultaat geef je altijd 'fr-FR' op.

Intl.NumberFormat: valuta, percentage, eenheden

toLocaleString is een kortere vorm van Intl.NumberFormat. Het volledige object maak je één keer aan en hergebruik je, wat van belang is zodra je een lijst of tabel formatteert.

formats.js
const euros = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });
euros.format(1234.5);   // '1 234,50 €'

const dollars = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
dollars.format(1234.5); // '$1,234.50'

const pourcent = new Intl.NumberFormat('fr-FR', { style: 'percent', maximumFractionDigits: 1 });
pourcent.format(0.256); // '25,6 %'  : de waarde is een ratio, 0.256 en niet 25.6

const km = new Intl.NumberFormat('fr-FR', { style: 'unit', unit: 'kilometer' });
km.format(12.5);        // '12,5 km'

const go = new Intl.NumberFormat('fr-FR', { style: 'unit', unit: 'gigabyte', maximumFractionDigits: 2 });
go.format(2.35);        // '2,35 Go'

De valutaformatter plaatst het symbool op de juiste plek volgens de taal (na het bedrag in het Frans, ervoor in het Engels), kiest het aantal decimalen van de munt en voegt de onderbrekingsspatie in. Stuk voor stuk regels die je niet meer zelf hoeft te coderen.

Compacte notatie: 1,4 k en 1 M

“1 400 weergaven” tonen als “1,4 k” of “1 000 000” als “1 M” is een veelvoorkomende behoefte. De optie notation: 'compact' doet dat, in de gewenste taal.

javascript
const compact = new Intl.NumberFormat('fr-FR', { notation: 'compact' });
compact.format(1400);       // '1,4 k'
compact.format(1000000);    // '1 M'

new Intl.NumberFormat('en-US', { notation: 'compact' }).format(23000);   // '23K'
new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 3 }).format(1230974);   // '1.231M'

new Intl.NumberFormat('fr-FR', { notation: 'compact', compactDisplay: 'long' }).format(2500000);   // '2,5 millions'

Andere nuttige opties:

javascript
new Intl.NumberFormat('fr-FR', { minimumIntegerDigits: 2 }).format(7);          // '07'
new Intl.NumberFormat('fr-FR', { useGrouping: false }).format(1234567);        // '1234567'
new Intl.NumberFormat('fr-FR', { signDisplay: 'always' }).format(12);          // '+12'
new Intl.NumberFormat('en-US', { notation: 'scientific' }).format(676565765722); // '6.766E11'
new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 2, roundingMode: 'halfExpand' }).format(1.005); // '1,01'

De laatste regel verdient een woordje uitleg: Intl.NumberFormat rondt af op basis van de decimale representatie van het getal, niet van de binaire benadering ervan. 1.005 wordt terecht “1,01”, waar toFixed “1.00” gaf. Dat is nog een reden om met Intl te formatteren in plaats van met de hand.

Beginnullen en andere talstelsels

javascript
String(7).padStart(2, '0');   // '07'  : uren, minuten, factuurnummers
String(7).padStart(3, '0');   // '007'

(255).toString(16);   // 'ff'        : hexadecimaal, voor een CSS-kleur
(255).toString(2);    // '11111111'  : binair

(123.456).toPrecision(4);   // '123.5'     : totaal aantal significante cijfers
(1234.5678).toExponential(2); // '1.23e+3'

Twee gedragingen van toString() om te kennen: boven 1021 en onder 10−6 schakelt JavaScript over op wetenschappelijke notatie ((1e21).toString() geeft '1e+21', (0.0000001).toString() geeft '1e-7'). toFixed of Intl.NumberFormat met notation: 'standard' behouden de uitgeschreven vorm.

De omgekeerde bewerking

Een geformatteerde string laat zich niet zomaar terugzetten: Number('1 234,50') is NaN. Je moet de spaties verwijderen en de komma vervangen voor je converteert. De volledige aanpak staat in String naar getal omzetten in JavaScript.

En Numeral.js?

De eerste versie van dit artikel besprak Numeral.js, een library met een compact formaat: numeral(50000).format('0,0') voor “50,000”, '0a' voor “23k”, '0.00b' voor “2.35GB”, '0o' voor “23rd”. Ze wordt nog in veel projecten gebruikt en de syntax is aangenaam.

javascript
import numeral from 'numeral';

numeral(50000).format('0,0');       // 50,000
numeral(23000).format('0a');        // 23k
numeral(2348895676).format('0.00b'); // 2.35GB

Maar de laatste versie, 2.0.6, dateert van maart 2017 en het project wordt niet meer onderhouden: de locales liggen vast en het Franse scheidingsteken is niet de verwachte smalle spatie. Voor een nieuw project dekt Intl.NumberFormat dezelfde behoeften (duizendtallen, compact, valuta, percentage) zonder dependency, met de echte conventies van elke taal en geen enkel gewicht in de bundle. Behoud Numeral waar het al zit, voeg het niet meer toe.

Samenvatting

Behoefte Tool Voorbeeld
Afronden om te berekenen Math.round(n * 100) / 100 of hele centen 1.35
Afronden om weer te geven, decimale punt toFixed(2) '1234.57'
Frans formaat, duizendtallen en komma toLocaleString('fr-FR') '1 234 567,891'
Valuta, percentage, eenheid Intl.NumberFormat + style '1 234,50 €'
1,4 k / 1 M notation: 'compact' '1,4 k'
Beginnullen padStart(2, '0') '07'

Veelgemaakte fouten

Het resultaat van toFixed optellen (1.5).toFixed(2) + 1 geeft “1.501”: toFixed geeft een string terug, de optelling plakt aan elkaar. Converteer met Number() als je opnieuw moet rekenen.
Een “schoolse” afronding verwachten (1.005).toFixed(2) is “1.00” omdat 1.005 binair wordt opgeslagen als 1.00499999… Werk voor een financiële afronding met hele centen.
De spatie van toLocaleString('fr-FR') vervangen door een gewone spatie Het Franse duizendtalscheidingsteken is een smalle onderbrekingsspatie (U+202F). Een replace(' ', '') verwijdert ze niet, gebruik /\s/g.
Intl.NumberFormat opnieuw aanmaken in een lus Het opbouwen van de formatter is kostbaar. Maak hem één keer aan, en roep dan format() aan op elke waarde.

JavaScript

Damien Flandrin Webdeveloper sinds 2010, maker van Gekkode en Email Impact. Elk artikel wordt vóór publicatie getest op een echt project. Contact
Nieuwsbrief

Nieuwe tests, tutorials en projecten, per e-mail.

Reproduceerbare tests, geversioneerde code, gedateerde resultaten. Nooit spam.