Javascript bietet drei verschiedene globale Funktionen an, um beliebige Eingaben zu Nummern zu konvertieren: Number()
, parseFloat()
und parseInt()
. Auf den ersten Blick wirken die Ergebnisse willkürlich.
parseFloat(value)
und parseInt(value)
sind so definiert, dass sie einen Eingabestring value
vom Typ string
zu einem Wert vom Typ number
konvertieren. Beide Funktionen lesen aus dem Eingabestring solange Zeichen, bis sie auf das erste nicht-verarbeitbare Zeichen stoßen und brechen die Konvertierung an der Stelle ab. Leerzeichen, Tabs oder Zeilenumbrüche werden ignoriert. Daraus erklärt sich, dass parseFloat("7.2Garbage")
7.2
, parseInt("7.2Garbage")
hingegen nur eine 7
zurückgibt. Sollte bereits das erste Zeichen unlesbar bzw. der Eingabewert ungültig sein, brechen beide Funktionen die Konvertierung mit NaN
ab (z.B. für die Eingaben abcdef
, null
oder undefined
). Einen Unterschied gibt es allerdings zwischen der Funktionsweise von parseFloat
und parseInt
: parseInt
verarbeitet auch Hexadezimal,- Oktal- sowie Binärliterale mit den jeweiligen Präfixen 0x
, 0o
sowie 0b
korrekt, während parseFloat
diese Präfixe nicht erkennt und das Konvertieren nach der führenden 0
abbricht und nur diese zurückgibt.
Sollte der übergebene Wert nicht bereits vom Typ string
sein, rufen beide Funktionen zunächst die Methode value.toString()
auf, um die Eingabe zum Typ string
zu konvertieren und das Ergebnis nach oben genannten regeln zu konvertieren. Daurch lassen sich Besonderheiten erklären wie parseFloat(["17.3", 10])
, was 17.3
zurückgibt. Ruft man auf einem Array toString()
auf, ruft das Array wiederum auf jedem seiner Elemente die Methode toString()
auf und konkateniert die Ergebnisse Komma-separiert. ["17.3", 10].toString()
ergibt demnach die Zeichenkette "17.3,10"
die nach den oben genannten Regeln von parseInt zu 17
, von parseFloat
zu 17.3
konvertiert wird. Für beliebige Objekte gilt das analog, so würde parseInt({ toString: () => "100" })
100
zurückgeben, es zählt ausschließlich der Wert, den toString
zurückgibt.
Die Funktion Number ist ein komplexerer Fall, auch wenn die Grundregeln gleich sind. Auch Number
verarbeitet bevorzugt strings
, macht allerdings einige Ausnahmen:
- Gemischte Zeichenketten wie
7.2abcde
werden im Gegensatz zuparseFloat
undparseInt
nicht konvertiert - Eine leere Zeichenkette wird zu
0
(daher auch[] === 0
) null
ẁird zu0
true
wird zu1
,false
zu0
- Date-Objekte werden in ihren unix-timestamp konvertiert
Auf allen anderen Objekten wird analog zu parseFloat
und parseInt
die toString
Methode aufgerufen. Auch Number
gibt NaN
zurück, wenn die Konvertierung fehlschlägt. Alle drei Funktionen können zudem den infiniten Wert Infinity
bzw. -Infinity
zurückgeben.
Nun stellt sich die Frage: Welche der drei Funktionen sollten Sie nutzen, um Eingaben zu konvertieren und mit den Ergebnissen zu rechnen? Sollten sowohl Gleitkomma- als auch Ganzzahlen möglich sein (was in der Regel der Fall ist), fällt parseInt
weg. Sollten allerdings gleichzeitig bspw. Hexadezimal-Literale erlaubt sein, fällt auch parseFloat
weg. Verwenden Sie Number
, erhalten Sie hingegen eventuell überraschende Ergebnisse, wenn die Eingabe ein Date
, ein boolean
oder null
war. Zudem kann das Ergebnis mit NaN
oder Infinity
infinit sein und damit nicht zum Rechnen verwendet werden. Es wird also ein Vorverarbeitungsschritt benötigt.
Verschiedene Frameworks haben sich demselben Problem bereits angenommen und bieten direkt oder indirekt eine Funktion mit einer Signatur wie isNumeric
an, die eine beliebige Eingabe darauf prüft, ob sie sicher in eine finiten Wert vom Typ number
konvertiert werden kann; die Konvertierung selbst wird dann durch die Funktion Number
durchgeführt, für diesen Vergleich allerdings ausgeblendet. Alle Codebeispiele wurden so angepasst, dass sie ohne umgebenden Framework-Kontext oder Kompatiblitätsanpassungen verständlich sind und so restrukturiert, dass sie gut vergleichbar sind, ohne die ursprüngliche Semantik zu verfälschen.
JQuery bietet ein gutes Beispiel, wie eine Implementierung von isNumeric
aussehen kann und wie sie sich über die Zeit verändert hat. Vergleichen Sie die Implementierungen der Versionen 1.9.1, 2.2.4 und 3.3.1:
// https://code.jquery.com/jquery-1.9.1.js
static isNumeric(value) {
return !isNaN(parseFloat(value)) && isFinite(value);
}
// https://code.jquery.com/jquery-2.2.4.js
static isNumeric(value) {
var realStringObj = value && value.toString();
return !Array.isArray(value) && (value - parseFloat(value) + 1) >= 0;
}
// https://code.jquery.com/jquery-3.3.1.js
static isNumeric(value) {
return (typeof value === "number" || typeof value === "string") && !isNaN(value - parseFloat(value));
}
Die erste Version überprüft, ob die Eingabe per parseFloat
konvertierbar (also nicht NaN
) sowie finit ist. Dadurch werden alle Werte als numerisch erkannt, deren string
Repräsentation von parseFloat
konvertiert werden kann, also auch beliebige Objekte wie z.B. Arrays. Version 2.2.4 exkludiert Arrays explizit und überarbeitet die Überprüfung für finite Werte durch einen eher kryptisches Trick mit impliziter Typumwandlung, lässt aber weiterhin alle anderen Objekte zu, die eine toString
Methode besitzen. Erst in Version 3 werden ausschließlich Werte vom Typ string
oder number
akzeptiert, alle anderen Objekte also kategorisch abgelehnt, wodurch Version 3 die restriktivste ist. Die Übersicht in Tabelle T1 veranschaulicht, wie die verschiedenen Implementierungen sich auswirken.
RxJS folgt der Implementierung von jQuery 2.2.4 weitestgehend, weshalb die Ergebnisse von RxJS und jQuery 2.2.4 identisch sind (s. T1). RxJS schlüsselt zudem den kryptischen Code in einem Kommentar auf:
https://github.com/ReactiveX/rxjs/blob/d3e7e3f299e277b077602d26c59dab40ef0e1dba/src/internal/util/isNumeric.ts#L8
// parseFloat NaNs numeric-cast false positives (null|true|false|"")
// ...but misinterprets leading-number strings, particularly hex literals ("0x...")
// subtraction forces infinities to NaN
// adding 1 corrects loss of precision from parseFloat (#15100)
function isNumeric(value) {
return !Array.isArray(value) && (value - parseFloat(value) + 1) >= 0;
}
Durch geschicktes Ausnutzen automatischer Typumwandlung sowie dem Verhalten infiniter Werte in mathematischen Operationen kann durch den knappen Ausdruck (value - parseFloat(value) + 1) >= 0
sowohl überprüft werden, ob der Wert erfolgreich konvertiert werden kann und ob das Ergebnis auch finit ist.
Angular besitzt keine reine isNumeric
Funktion, stellt allerdings eine NumberPipe
bereit, um Eingabewerte vom Typ string
in einer Nummer zu konvertieren:
https://github.com/angular/angular/blob/f8096d499324cf0961f092944bbaedd05364eea1/packages/common/src/pipes/number_pipe.ts#L235
if (typeof value === 'string' && !isNaN(Number(value) - parseFloat(value))) {
return Number(value);
}
Hier findet sich technisch der Grundgedanke aus jQuery 3.3.1 wieder, allerdings beschränkt auf Zeichenketten, wodurch Angular und jQuery 1.9.1 identische Funktionsweisen besitzen (s. Tabelle T1).
Das npm-Modul is-numeric
von Jon Schlinkert hat die selbe strikte Spezifikation wie jQuery 3.3.1, verzichtet allerdings auf kryptischen Code und ist etwas lesbarer gehalten [schlinkert]:
function isNumeric(value) {
if (typeof value === 'number') {
return value - value === 0;
}
if (typeof value === 'string' && value.trim() !== '') {
return Number.isFinite(+v);
}
return false;
}
Das npm-Modul enthält ausschließlich diese Funktion ist daher besonders gut geeignet, losgelöst von einem Framework verwendet zu werden.
Der Vollständig halber sind in der Tabelle T1 noch die Implementierungen von plotly und crossley aufgelistet. plotly bietet eine auf Geschwindigkeit optimierte Variante mit der strikten Spezifikation von jQuery 3.3.1 bzw. is-numeric mit einem schwer verständlichen Algorithmus, crossley bietet einen eher unschönen Algorithmus mit der Spezifikation von jQuery 2.2.4 oder RxJS.
Common misconception: Array wird gesondert behandelt, dabei interessiert nur object.toString()
!
???
v | Number(v) / +v | parseFloat | parseInt |
---|---|---|---|
0 | 0 | 0 | 0 |
-0 | 0 | 0 | 0 |
100 | 100 | 100 | 100 |
-100 | -100 | -100 | -100 |
1e6 | 1000000 | 1000000 | 1000000 |
-1e6 | -1000000 | -1000000 | -1000000 |
0b10 | 2 | 2 | 2 |
0o10 | 8 | 8 | 8 |
010 | 8 | 8 | 8 |
0x10 | 16 | 16 | 16 |
"1" | 1 | 1 | 1 |
"009" | 9 | 9 | 9 |
"-1" | -1 | -1 | -1 |
"7.2" | 7.2 | 7.2 | 7 |
" \r\n7.2 \r\n\t" | 7.2 | 7.2 | 7 |
"1e6" | 1000000 | 1000000 | 1 |
"-1e6" | -1000000 | -1000000 | -1 |
"0b10" | 2 | 0 | 0 |
"0o10" | 8 | 0 | 0 |
"0xFF" | 255 | 0 | 255 |
"-0x10" | NaN | 0 | -16 |
"+0x10" | NaN | 0 | 16 |
Infinity | Infinity | Infinity | NaN |
-Infinity | -Infinity | -Infinity | NaN |
NaN | NaN | NaN | NaN |
"Infinity" | Infinity | Infinity | NaN |
"-Infinity" | -Infinity | -Infinity | NaN |
"NaN" | NaN | NaN | NaN |
"7.2andGarbage" | NaN | 7.2 | 7 |
null | 0 | NaN | NaN |
true | 1 | NaN | NaN |
false | 0 | NaN | NaN |
undefined | NaN | NaN | NaN |
{} | NaN | NaN | NaN |
function(){} | NaN | NaN | NaN |
new Date() | 1548936984668 | NaN | NaN |
[] | 0 | NaN | NaN |
[1, 2, 3] | NaN | 1 | 1 |
[1] | 1 | 1 | 1 |
[[["17"]]] | 17 | 17 | 17 |
[" -17.3 "] | -17.3 | -17.3 | -17 |
new Number(77) | 77 | 77 | 77 |
new String("123") | 123 | 123 | 123 |
The major frameworks have their own custom implementations to check whether an input is numeric (i.e. can be parsed to a finite number, so that it can be used in arithmetic operations) or not.
value | +value | angular | jQuery 1.9.1 | jQuery 2.2.4 | jQuery 3.3.1 | rxjs | plotly | schlinkert | crossley | johannes | johannes_perm |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | true | true | true | true | true | true | true | true | true | true |
-0 | 0 | true | true | true | true | true | true | true | true | true | true |
100 | 100 | true | true | true | true | true | true | true | true | true | true |
-100 | -100 | true | true | true | true | true | true | true | true | true | true |
1e6 | 1000000 | true | true | true | true | true | true | true | true | true | true |
-1e6 | -1000000 | true | true | true | true | true | true | true | true | true | true |
0b10 | 2 | true | true | true | true | true | true | true | true | true | true |
0o10 | 8 | true | true | true | true | true | true | true | true | true | true |
010 | 8 | true | true | true | true | true | true | true | true | true | true |
0x10 | 16 | true | true | true | true | true | true | true | true | true | true |
"1" | 1 | true | true | true | true | true | true | true | true | true | true |
"009" | 9 | true | true | true | true | true | true | true | true | true | true |
"-1" | -1 | true | true | true | true | true | true | true | true | true | true |
"7.2" | 7.2 | true | true | true | true | true | true | true | true | true | true |
" \r\n7.2 \r\n\t" | 7.2 | true | true | true | true | true | true | true | true | true | true |
"1e6" | 1000000 | true | true | true | true | true | true | true | true | true | true |
"-1e6" | -1000000 | true | true | true | true | true | true | true | true | true | true |
"0b10" | 2 | true | true | true | true | true | true | true | true | true | true |
"0o10" | 8 | true | true | true | true | true | true | true | true | true | true |
"0xFF" | 255 | true | true | true | true | true | true | true | true | true | true |
"-0x10" | NaN | false | false | false | false | false | false | false | false | false | false |
"+0x10" | NaN | false | false | false | false | false | false | false | false | false | false |
Infinity | Infinity | false | false | false | false | false | false | false | false | false | false |
-Infinity | -Infinity | false | false | false | false | false | false | false | false | false | false |
NaN | NaN | false | false | false | false | false | false | false | false | false | false |
"Infinity" | Infinity | false | false | false | false | false | false | false | false | false | false |
"-Infinity" | -Infinity | false | false | false | false | false | false | false | false | false | false |
"NaN" | NaN | false | false | false | false | false | false | false | false | false | false |
"7.2andGarbage" | NaN | false | false | false | false | false | false | false | false | false | false |
null | 0 | false | false | false | false | false | false | false | false | false | false |
undefined | NaN | false | false | false | false | false | false | false | false | false | false |
{} | NaN | false | false | false | false | false | false | false | false | false | false |
function(){} | NaN | false | false | false | false | false | false | false | false | false | false |
new Date() | 1548939525579 | false | false | false | false | false | false | false | false | false | false |
[] | 0 | false | false | false | false | false | false | false | false | false | true |
[1, 2, 3] | NaN | false | false | false | false | false | false | false | false | false | false |
[1] | 1 | true | true | false | false | false | false | false | false | false | true |
[[["17"]]] | 17 | true | true | false | false | false | false | false | false | false | true |
[" -17.3 "] | -17.3 | true | true | false | false | false | false | false | false | false | true |
new Number(77) | 77 | true | true | true | false | true | false | false | true | false | true |
new String("123") | 123 | true | true | true | false | true | false | false | true | false | true |
This is a proposal for a strict and a loose parsing algorithm.
Strict means that a number is checked for being of type number
and being finite, nothing more. So no string literals or boxed types are allowed and no coercion takes place.
Loose mode allows for type coercions, but utilizes the strictest flaor of the surveyed checks and therefore disallows numbers in arrays (e.g. [[1]]
and boxed types like new Number(13)
or new String('13')
).
If a number can not be parsed, NaN
is returned. Maybe throwing an exception would be even better, as checking for NaN
in Javascript is a non-trivial thing to do (use Number.isNaN()
instead of isNaN()
!).
value | strict | loose |
---|---|---|
0 | 0 | 0 |
-0 | 0 | 0 |
100 | 100 | 100 |
-100 | -100 | -100 |
1e6 | 1000000 | 1000000 |
-1e6 | -1000000 | -1000000 |
0x10 | 16 | 16 |
0b10 | 2 | 2 |
0o10 | 8 | 8 |
010 | 8 | 8 |
"1" | NaN | 1 |
"009" | NaN | 9 |
"-1" | NaN | -1 |
"7.2" | NaN | 7.2 |
" \r\n7.2 \r\n\t" | NaN | 7.2 |
"1e6" | NaN | 1000000 |
"-1e6" | NaN | -1000000 |
"0xFF" | NaN | 255 |
"0b10" | NaN | 2 |
"0o10" | NaN | 8 |
"-0x10" | NaN | NaN |
"+0x10" | NaN | NaN |
Infinity | NaN | NaN |
-Infinity | NaN | NaN |
NaN | NaN | NaN |
"Infinity" | NaN | NaN |
"-Infinity" | NaN | NaN |
"NaN" | NaN | NaN |
"7.2andGarbage" | NaN | NaN |
null | NaN | NaN |
undefined | NaN | NaN |
{} | NaN | NaN |
function(){} | NaN | NaN |
new Date() | NaN | NaN |
[] | NaN | NaN |
[1, 2, 3] | NaN | NaN |
[1] | NaN | NaN |
[[["17"]]] | NaN | NaN |
[" -17.3 "] | NaN | NaN |
new Number(77) | NaN | NaN |
new String("123") | NaN | NaN |
parseFloat()
undparseInt()
wandeln ihre Eingabe vorher instring
um![1].toString()
="1"
, daher gehen Arrays.[1, 2].toString()
gibt"1,2"
zurück und wird daher vonparseFloat()
undparseInt()
akezptiert.