Skip to content

Instantly share code, notes, and snippets.

@suspectpart
Last active February 18, 2019 16:38
Show Gist options
  • Save suspectpart/1b2703ea1ba77eb30086b3975e0517e5 to your computer and use it in GitHub Desktop.
Save suspectpart/1b2703ea1ba77eb30086b3975e0517e5 to your computer and use it in GitHub Desktop.
Parse numbers in JS
const assert = require('assert');
describe('Parse', () => {
it('strict', function () {
// these are fine
assert.strictEqual(parseStrict(0), 0);
assert.strictEqual(parseStrict(-0), -0);
assert.strictEqual(parseStrict(1), 1);
assert.strictEqual(parseStrict(3.14159), 3.14159);
assert.strictEqual(parseStrict(-3.14159), -3.14159);
assert.strictEqual(parseStrict(.123), 0.123);
assert.strictEqual(parseStrict(1e6), 1000000);
assert.strictEqual(parseStrict(0b10), 2);
assert.strictEqual(parseStrict(0o10), 8);
assert.strictEqual(parseStrict(0x10), 16);
assert.strictEqual(parseStrict(Number(1000)), 1000);
assert.strictEqual(parseStrict(Number.MAX_VALUE), Number.MAX_VALUE);
assert.strictEqual(parseStrict(Number.MIN_VALUE), Number.MIN_VALUE);
// forbidden in strict mode
assert.strictEqual(parseStrict("1"), NaN);
assert.strictEqual(parseStrict("3.14159"), NaN);
assert.strictEqual(parseStrict("-3.14159"), NaN);
assert.strictEqual(parseStrict(".123"), NaN);
assert.strictEqual(parseStrict(" 17\r\n\r "), NaN);
assert.strictEqual(parseStrict("-1"), NaN);
assert.strictEqual(parseStrict("1e6"), NaN);
assert.strictEqual(parseStrict("-1e6"), NaN);
assert.strictEqual(parseStrict("0b10"), NaN);
assert.strictEqual(parseStrict("0o10"), NaN);
assert.strictEqual(parseStrict("0x10"), NaN);
// never ok
assert.strictEqual(parseStrict(Infinity), NaN);
assert.strictEqual(parseStrict("Infinity"), NaN);
assert.strictEqual(parseStrict(-Infinity), NaN);
assert.strictEqual(parseStrict("-Infinity"), NaN);
assert.strictEqual(parseStrict(NaN), NaN);
assert.strictEqual(parseStrict("NaN"), NaN);
assert.strictEqual(parseStrict(""), NaN);
assert.strictEqual(parseStrict(" "), NaN);
assert.strictEqual(parseStrict("a"), NaN);
assert.strictEqual(parseStrict("7.2andGarbage"), NaN);
assert.strictEqual(parseStrict(null), NaN);
assert.strictEqual(parseStrict(undefined), NaN);
assert.strictEqual(parseStrict(function() {}), NaN);
assert.strictEqual(parseStrict(false), NaN);
assert.strictEqual(parseStrict("false"), NaN);
assert.strictEqual(parseStrict(true), NaN);
assert.strictEqual(parseStrict("true"), NaN);
assert.strictEqual(parseStrict(new Array(10)), NaN);
assert.strictEqual(parseStrict([]), NaN);
assert.strictEqual(parseStrict([1]), NaN);
assert.strictEqual(parseStrict(["17"]), NaN);
assert.strictEqual(parseStrict([1, 2, 3]), NaN);
assert.strictEqual(parseStrict({}), NaN);
assert.strictEqual(parseStrict({a: 1}), NaN);
assert.strictEqual(parseStrict(new Date()), NaN);
});
it('loose', function () {
// these are fine
assert.strictEqual(parseLoose(0), 0);
assert.strictEqual(parseLoose(-0), -0);
assert.strictEqual(parseLoose(1), 1);
assert.strictEqual(parseLoose(3.14159), 3.14159);
assert.strictEqual(parseLoose(-3.14159), -3.14159);
assert.strictEqual(parseLoose(.123), 0.123);
assert.strictEqual(parseLoose(1e6), 1000000);
assert.strictEqual(parseLoose(0b10), 2);
assert.strictEqual(parseLoose(0o10), 8);
assert.strictEqual(parseLoose(0x10), 16);
assert.strictEqual(parseLoose(Number(1000)), 1000);
assert.strictEqual(parseLoose(Number.MAX_VALUE), Number.MAX_VALUE);
assert.strictEqual(parseLoose(Number.MIN_VALUE), Number.MIN_VALUE);
// ok in loose mode
assert.strictEqual(parseLoose("1"), 1);
assert.strictEqual(parseLoose("3.14159"), 3.14159);
assert.strictEqual(parseLoose("-3.14159"), -3.14159);
assert.strictEqual(parseLoose(".123"), .123);
assert.strictEqual(parseLoose(" 17\r\n\r "), 17);
assert.strictEqual(parseLoose("-1"), -1);
assert.strictEqual(parseLoose("1e6"), 1000000);
assert.strictEqual(parseLoose("-1e6"), -1000000);
assert.strictEqual(parseLoose("0b10"), 2);
assert.strictEqual(parseLoose("0o10"), 8);
assert.strictEqual(parseLoose("0x10"), 16);
// never ok
assert.strictEqual(parseLoose(Infinity), NaN);
assert.strictEqual(parseLoose("Infinity"), NaN);
assert.strictEqual(parseLoose(-Infinity), NaN);
assert.strictEqual(parseLoose("-Infinity"), NaN);
assert.strictEqual(parseLoose(NaN), NaN);
assert.strictEqual(parseLoose("NaN"), NaN);
assert.strictEqual(parseLoose(""), NaN);
assert.strictEqual(parseLoose(" "), NaN);
assert.strictEqual(parseLoose("a"), NaN);
assert.strictEqual(parseLoose("7.2andGarbage"), NaN);
assert.strictEqual(parseLoose(null), NaN);
assert.strictEqual(parseLoose(undefined), NaN);
assert.strictEqual(parseLoose(function() {}), NaN);
assert.strictEqual(parseLoose(false), NaN);
assert.strictEqual(parseLoose(true), NaN);
assert.strictEqual(parseLoose(new Array(10)), NaN);
assert.strictEqual(parseLoose([]), NaN);
assert.strictEqual(parseLoose([1]), NaN);
assert.strictEqual(parseLoose(["17"]), NaN);
assert.strictEqual(parseLoose([1, 2, 3]), NaN);
assert.strictEqual(parseLoose({}), NaN);
assert.strictEqual(parseLoose({a: 1}), NaN);
assert.strictEqual(parseLoose(new Date()), NaN);
});
});
function parseStrict(value) {
return typeof value === 'number' && Number.isFinite(value) ? value : NaN;
}
function parseLoose(value) {
const isNumeric = (typeof value === "number" || typeof value === "string") && !isNaN(value - parseFloat(value));
return isNumeric ? +value : NaN;
}

Tables

Javascript Number parsing

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 zu parseFloat und parseInt nicht konvertiert
  • Eine leere Zeichenkette wird zu 0 (daher auch [] === 0)
  • null ẁird zu 0
  • true wird zu 1, false zu 0
  • 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.

Framework-Implementierungen von isNumeric()

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()!

Fazit

???

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

Survey of isNumeric() implementations

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

Proposal

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

Tabelle 1 - Konvertierungsfunktionen

v Number(v) / +v parseFloat parseInt
"1" 1 1 1
"-1" -1 -1 -1
"7.2" 7.2 7.2 7
" 7.2 \t" 7.2 7.2 7
"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
"7.2 and more" NaN 7.2 7
null 0 NaN NaN
undefined NaN NaN NaN
{} NaN NaN NaN
new Date() 1550507673871 NaN NaN
[] 0 NaN NaN
[1, 2, 3] NaN 1 1
[1] 1 1 1
{valueOf: () => 3, toString: () => 7} 3 7 7
new Number(77) 77 77 77
new String("123") 123 123 123

Tabelle 2 - Framework-Übersicht

value +value angular jQuery 1.9.1 jQuery 2.2.4 jQuery 3.3.1 rxjs fast-isnumeric is-number crossley
"1" 1 true true true true true true true true
"-1" -1 true true true true true true true true
"7.2" 7.2 true true true true true true true true
" 7.2 \t" 7.2 true true true true true true true true
"1e6" 1000000 true true true true true true true true
"0b10" 2 true true true true true true true true
"0o10" 8 true true true true true true true true
"0xFF" 255 true true true true true true true true
"-0x10" NaN false false false false false false false false
"+0x10" NaN false false false false false false false false
"Infinity" Infinity false false false false false false false false
"-Infinity" -Infinity false false false false false false false false
"7.2 and more" NaN false false false false false false false false
null 0 false false false false false false false false
undefined NaN false false false false false false false false
{} NaN false false false false false false false false
new Date() 1550507673871 false false false false false false false false
[] 0 false false false false false false false false
[1, 2, 3] NaN false false false false false false false false
[1] 1 true true false false false false false false
{valueOf: () => 3, toString: () => 7} 3 true true true false false false false true
new Number(77) 77 true true true false true false false true
new String("123") 123 true true true false true false false true
@suspectpart
Copy link
Author

parseFloat() und parseInt() wandeln ihre Eingabe vorher in string um! [1].toString() = "1", daher gehen Arrays. [1, 2].toString() gibt "1,2" zurück und wird daher von parseFloat() und parseInt() akezptiert.

@suspectpart
Copy link
Author

class NotANumber {
    toString() {
        return "1000";
    }
}

parseFloat(new NotANumber()); // 1000

@suspectpart
Copy link
Author

Number scheint einen Satz Regeln zu haben.

  • Date => +Date
  • null: 0
  • Array => Array.toString()
    • [].toString() === "" => Number([]) == Number("") == 0
    • [{toString: () => "100"}].toString() == "100" => Number("100") == Number([{toString: () => "100"}])
  • Object: Number(obj.toString())

@suspectpart
Copy link
Author

Shortest parse implementation:

function parseLoose(value) {
    return (typeof value === "number" || typeof value === "string") ? +value : NaN;
}

@suspectpart
Copy link
Author

true / false fehlen.

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