Nella versione moderna di JavaScript ci sono due differenti tipi di numeri:
-
I numeri regolari, che vengono memorizzati nel formato a 64 bit IEEE-754, conosciuti anche come “numeri in virgola mobile con doppia precisione”. Questi sono i numeri che utilizziamo la maggior parte del tempo, e sono quelli di cui parleremo in questo capitolo.
-
I BigInt, che vengono utilizzati per rappresentare numeri interi di lunghezza arbitraria. Talvolta possono tornare utili, poiché i numeri regolari non possono eccedere
253od essere inferiori di-253. Poiché i BigInt vengono utilizzati in alcune aree speciali, gli abbiamo dedicato un capitolo BigInt.
Quindi in questo capitolo parleremo dei numeri regolari.
Diversi modi di scrivere un numero
Immaginiamo di dover scrivere 1 milione. La via più ovvia è:
let billion = 1000000000;
Possiamo anche usare il carattere underscore _ come separatore:
let billion = 1_000_000_000;
Qui il carattere _ gioca il ruolo di “zucchero sintattico”, cioè rende il numero più leggibile. Il motore JavaScript semplicemente ignorerà i caratteri _ tra le cifre, quindi è equivalente al milione scritto sopra.
Nella vita reale però cerchiamo di evitare di scrivere lunghe file di zeri per evitare errori. E anche perché siamo pigri. Solitamente scriviamo qualcosa del tipo "1ml" per un milione o "7.3ml" 7 milioni e 300mila. Lo stesso vale per i numeri più grandi.
In JavaScript, possiamo abbreviare un numero inserendo la lettera "e" con il numero di zeri a seguire:
let billion = 1e9; // 1 miliardo, letteralmente: 1 e 9 zeri
alert( 7.3e9 ); // 7.3 miliardi (equivale a 7300000000 o 7_300_000_000)
In altre parole, "e" moltiplica il numero 1 seguito dal numero di zeri dati.
1e3 = 1 * 1000 // e3 significa *1000
1.23e6 = 1.23 * 1000000 // e6 significa *1000000
Ora proviamo a scrivere qualcosa di molto piccolo. Ad esempio, 1 microsecondo (un milionesimo di secondo):
let ms = 0.000001;
Come prima, l’utilizzo di "e" può aiutare. Se volessimo evitare di scrivere esplicitamente tutti gli “0”, potremmo scrivere:
let ms = 1e-6; // sei zeri alla sinistra di 1
Se contiamo gli zeri in 0.000001, ce ne sono 6. Quindi ovviamente 1e-6.
In altre parole, un numero negativo dopo "e" significa una divisione per 1 seguito dal numero di zeri dati:
// -3 divide 1 con 3 zeri
1e-3 = 1 / 1000; // 0.001
// -6 divide 1 con 6 zeri
1.23e-6 = 1.23 / 1000000; // 0.00000123
Numeri esadecimali, binari e ottali
I numeri esadecimali (hexadecimal o hex) sono largamente utilizzati in JavaScript per rappresentare colori, codifiche di caratteri, e molte altre cose. Quindi ovviamente esiste un modo per abbreviarli: 0x e poi il numero.
Ad esempio:
alert( 0xff ); // 255
alert( 0xFF ); // 255 (equivalente)
I sistemi binario e ottale sono utilizzati raramente, ma sono comunque supportati con l’utilizzo dei prefissi 0b e 0o:
let a = 0b11111111; // forma binaria di 255
let b = 0o377; // forma ottale di 255
alert( a == b ); // true, lo stesso numero 255 da entrambe le parti
Ci sono solo 3 sistemi di numerazione con questo livello di supporto. Per gli altri sistemi, dovremmo utilizzare la funzione parseInt (che vedremo più avanti in questo capitolo).
toString(base)
Il metodo num.toString(base) ritorna una rappresentazione in stringa del numero num con il sistema di numerazione fornito base.
Ad esempio:
let num = 255;
alert( num.toString(16) ); // ff
alert( num.toString(2) ); // 11111111
La base può variare da 2 a 36. Di default vale 10.
Altri casi di uso comune sono:
-
base=16 si utilizza per colori in esadecimale, codifiche di caratteri, i caratteri supportati sono
0..9oA..F. -
base=2 viene utilizzato per debugging o operazioni bit a bit, i caratteri accettati sono
0o1. -
base=36 la base massima, i caratteri possono andare da
0..9oA..Z. L’intero alfabeto latino viene utilizzato per rappresentare un numero. Un caso divertente di utilizzo per la base36è quando abbiamo bisogno che un identificatore molto lungo diventi qualcosa di più breve, come ad esempio per accorciare gli url. Possiamo semplicemente rappresentarlo in base36:alert( 123456..toString(36) ); // 2n9c
Da notare che i due punti in 123456..toString(36) non sono un errore. Se vogliamo chiamare un metodo direttamente da un numero, come toString nell’esempio sopra, abbiamo bisogno di inserire due punti ...
Se inseriamo un solo punto: 123456.toString(36), otterremo un errore, perché la sintassi JavaScript implica una parte decimale a seguire del primo punto. Se invece inseriamo un ulteriore punto, allora JavaScript capirà che la parte decimale è vuota e procederà nel chiamare il metodo.
Potremmo anche scrivere (123456).toString(36).
Arrotondare
Una delle operazioni più utilizzate quando lavoriamo con i numeri è l’arrotondamento.
Ci sono diverse funzioni integrate per eseguire questa operazione:
Math.floor- Arrotonda per difetto:
3.1diventa3, e-1.1diventa-2. Math.ceil- Arrotonda per eccesso:
3.1diventa4, e-1.1diventa-1. Math.round- Arrotonda all’intero più vicino:
3.1diventa3,3.6diventa4, e3.5viene arrotondato anch’esso a4. Math.trunc(non supportato da Internet Explorer)- Rimuove tutto dopo la virgola decimale senza arrotondare:
3.1diventa3,-1.1diventa-1.
Qui abbiamo una tabella che riassume le principali differenze:
Math.floor |
Math.ceil |
Math.round |
Math.trunc |
|
|---|---|---|---|---|
3.1 |
3 |
4 |
3 |
3 |
3.6 |
3 |
4 |
4 |
3 |
-1.1 |
-2 |
-1 |
-1 |
-1 |
-1.6 |
-2 |
-1 |
-2 |
-1 |
Queste funzioni coprono tutti i possibili casi quando trattiamo numeri con una parte decimale. Come potremmo fare se volessimo arrotondare il numero ad n cifre dopo la virgola?
Ad esempio, abbiamo 1.2345 e vogliamo arrotondarlo a due cifre dopo la virgola, tenendo solo 1.23.
Ci sono due modi per farlo:
-
Moltiplica e dividi.
Ad esempio, per arrotondare un numero alla seconda cifra decimale, possiamo moltiplicare il numero per
100, chiamare la funzione di arrotondamento e dividerlo nuovamente.let num = 1.23456; alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23 -
Il metodo toFixed(n) arrotonda il numero a
ncifre dopo la virgola e ritorna una rappresentazione in stringa del risultato.let num = 12.34; alert( num.toFixed(1) ); // "12.3"Questo metodo arrotonderà per difetto o per eccesso in base al valore più vicino, similmente a
Math.round:let num = 12.36; alert( num.toFixed(1) ); // "12.4"Da notare che il risultato di
toFixedè una stringa. Se la parte decimale è più breve di quanto richiesto, verranno aggiunti degli zeri:let num = 12.34; alert( num.toFixed(5) ); // "12.34000", aggiunti gli zeri per renderlo esattamente di 5 cifre decimaliPossiamo convertire il risultato al tipo numerico utilizzando la somma unaria o chiamando il metodo
Number():+num.toFixed(5).
Calcoli imprecisi
Internamente, un numero è rappresentato in formato 64-bit IEEE-754, quindi vengono utilizzati esattamente 64 bit per rappresentare un numero: 52 vengono utilizzati per rappresentare le cifre, 11 per la parte decimale, e infine 1 bit per il segno.
Se un numero è troppo grande, tale da superare i 64 bit disponibili, come ad esempio un numero potenzialmente infinito:
alert( 1e500 ); // Infinity
Potrebbe essere poco ovvio, ma quello che accade è la perdita di precisione.
Consideriamo questo test (falso!):
alert( 0.1 + 0.2 == 0.3 ); // false
Esatto, se provassimo a confrontare il risultato della somma tra 0.1 e 0.2 con 0.3, otterremmo false.
Strano! Quale può essere il risultato se non 0.3?
alert( 0.1 + 0.2 ); // 0.30000000000000004
Ouch! Un confronto errato di questo tipo può generare diverse conseguenze. Immaginate di progettare un sito di e-shop in cui i visitatori aggiungono al carrello articoli da $0.10 e $0.20. Poi come prezzo totale viene mostrato $0.30000000000000004. Questo risultato lascerebbe sorpreso chiunque.
Ma perché accade questo?
Un numero viene memorizzato nella sua forma binaria, una sequenza di “1” e “0”. I numeri con virgola come 0.1, 0.2 che visti nella loro forma decimale sembrano semplici, sono in realtà una sequenza infinita di cifre nella forma binaria.
In altre parole, cos’è 0.1? Vale 1 diviso 10 1/10, “un decimo”. Nel sistema decimale questi numeri sono facilmente rappresentabili. Prendiamo invece “un terzo”: 1/3. Diventa un numero con infiniti decimali 0.33333(3).
Quindi, le divisioni per potenze di 10 funzionano molto bene nel sistema decimale, non vale lo stesso con la divisione per 3. Per la stessa ragione, nel sistema binario le divisioni per potenze di 2 sono una garanzia, ma 1/10 diventa una sequenza infinita di cifre.
Non esiste alcun modo per rappresentare esattamente 0.1 o esattamente 0.2 usando il sistema binario, proprio come non è possibile memorizzare “un terzo” come decimale.
Il formato numerico IEEE-754 cerca di risolvere questo arrotondando al più vicino numero possibile. Questo tipo di arrotondamento non ci consente di vedere le “piccole perdite di precisione”, quindi il numero viene mostrato come 0.3. Ma comunque è presente una perdita.
Possiamo vedere un esempio:
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
E quando sommiamo due numeri, la loro “perdita di precisione” viene incrementata.
Questo è il motivo per cui 0.1 + 0.2 non vale esattamente 0.3.
Lo stesso problema esiste in molti altri linguaggi di programmazione.
PHP, Java, C, Perl, Ruby hanno lo stesso tipo di problema, poiché si basano sullo stesso formato numerico.
Possiamo risolvere questo problema? Certamente, ci sono diverse soluzioni:
- Possiamo arrotondare il risultato con un metodo toFixed(n):
let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // 0.30
Da notare che toFixed ritorna sempre una stringa. Viene cosi garantito che ci siano almeno due cifre dopo la virgola decimale. Questo ci torna molto utile se abbiamo un e-shopping e vogliamo mostrare $0.30. Per tutti gli altri casi possiamo semplicemente chiamare la conversione con l’operatore di somma unaria:
let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3
- Possiamo temporaneamente convertire i numeri ad interi per eseguire le operazioni e poi riconvertirli. In questo modo:
alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001
Questo funziona perché quando facciamo 0.1 * 10 = 1 e 0.2 * 10 = 2 entrambi diventano interi, non vi è quindi perdita di precisione.
- Se abbiamo a che fare con dei prezzi, la miglior soluzione rimane quella di memorizzare tutti i prezzi in centesimi, evitando quindi di utilizzare i numeri con virgola. Ma cosa succede se proviamo ad applicare uno sconto del 30%? Nella pratica, evitare completamente questo problema è difficile, in alcuni casi possono tornare utili entrambe le soluzioni viste sopra.
Quindi, l’approccio moltiplicazione/divisione riduce gli errori, ma non li elimina completamente.
Talvolta possiamo evitare le frazioni. Ad esempio se abbiamo a che fare con un negozio, allora possiamo memorizzare i prezzi in centesimi piuttosto che in euro.
Provate ad eseguire questo:
// Ciao! Sono un numero autoincrementante!
alert( 9999999999999999 ); // mostra 10000000000000000
Questo esempio ha lo stesso problema: perdita di precisione. Per la rappresentazione di un numero sono disponibili 64bit, ne vengono utilizzati 52 per le cifre, questi potrebbero non essere sufficienti. Quindi le cifre meno significative vengono perse.
JavaScript non mostra errori in questi casi. Semplicemente fa del suo meglio per “farci stare” il numero, anche se il formato non è “grande” abbastanza.
Un’altra conseguenza divertente della rappresentazione interna è l’esistenza di due zeri: 0 e -0.
Questo perché il segno viene rappresentato con un solo bit, in questo modo ogni numero può essere positivo o negativo, lo stesso vale per lo zero.
Nella maggior parte dei casi questa differenza è impercettibile, poiché gli operatori sono studiati per trattarli allo stesso modo.
Test: isFinite e isNaN
Ricordate questi due valori numerici speciali?
Infinity(e-Infinity) è uno speciale valore che è più grande (o più piccolo) di qualsiasi altro numero.NaNrappresenta un errore.
Questi appartengono al tipo number, ma non sono dei numeri “normali”, esistono quindi delle funzioni dedicate per la loro verifica:
-
isNaN(value)converte l’argomento al tipo numerico e successivamente verifica se è unNaN:alert( isNaN(NaN) ); // true alert( isNaN("str") ); // trueMa abbiamo veramente bisogno di questa funzione? Non possiamo semplicemente usare il confronto
=== NaN? Purtroppo la risposta è no. Il valoreNaNè unico in questo aspetto, non è uguale a niente, nemmeno a se stesso:alert( NaN === NaN ); // false -
isFinite(value)converte l’argomento al tipo numerico e ritornatruese questo è un numero diverso daNaN/Infinity/-Infinity:alert( isFinite("15") ); // true alert( isFinite("str") ); // false, perché rappresenta un valore speciale: NaN alert( isFinite(Infinity) ); // false, perché rappresenta un valore speciale: Infinity
In alcuni casi isFinite viene utilizzato per verificare se una stringa qualunque è un numero: