Convertir un texte en nombre en JavaScript
Par Delicious Insights • Publié le 26 déc. 2012

Quoi de plus simple, a priori, que de convertir un texte en nombre en JavaScript ? Souvent, on n'a même pas l'impression d'en avoir besoin. Et sinon, on recourt à parseInt ou parseFloat, vite fait…

Et pourtant… Que de pièges, que de subtilités de fonctionnement nous attendent sur un sujet au premier abord aussi basique. Explorons tout ça ensemble.

A-t-on besoin de convertir ?

Comme bien souvent, la vraie réponse est : ça dépend.

Dans la mesure où je vous recommande par ailleurs de toujours comparer avec === sauf cas spécifique bien identifié, vous aurez en effet besoin, si vous appliquez cette consigne, de convertir explicitement vos nombres « textuels » en nombres effectifs (type Number) pour obtenir une comparaison valide.

Toutefois, certains opérateurs qui n'ont de sens que sur les nombres (ou principalement sur les nombres) entraîneront automatiquement une conversion numérique de leurs arguments. On parle alors de protocole de conversion implicite. C'est notamment le cas des opérateurs arithmétiques, à l'exception du cas « String plus quelque chose ». Toutefois, l'enchaînement de protocoles de conversion, chaque opérande étant traité indépendamment et à tour de rôle, peut donner à terme des String là où on aurait cru des Number, ce qui n'est pas sans surprise…

5 + 2           // => 7
'5' + 2         // => '52'
5 + '2'         // => '52'
'5' + '2'       // => '52'
[] + 3          // => '3'
3 + []          // => '3'
{} + 3          // => 3 ou '[object Object]3', selon version de JS
3 + {}          // => '3[object Object]'
false + 3       // => 3
3 + true        // => 4
3 + null        // => 3
var x; x + 3    // => NaN

Dans les exemples ci-dessus, on voit que l'opérateur + entraîne souvent des conversions amenant à String, mais propose aussi pas mal de cas où le résultat est, de façon inattendue parfois, un Number. Les opérateurs moins ambigüs, tels que - ou *, fonctionnent presque toujours en Number (par exemple, 3 - [] est toujours 3), quitte à renvoyer NaN (qui comme son nom ne l'indique pas est bien de type Number…).

Le problème, évidemment, c'est qu'on ne va pas commencer à se souvenir de tous les cas de figure, opérateur par opérateur, et types d'opérande par types d'opérande. D'où la nécessité de clarifier parfois les choses en forçant explicitement le type de nos opérandes à Number, de façon contrôlée.

Pour ce faire, la majorité des auteurs de code JavaScript ont recours, suivant le besoin (entier ou flottant), à parseInt et parseFloat. À mon sens, c'est plus problématique qu'autre chose.

Méfie-toi de parseInt et parseFloat

Les deux ne se comportent pas exactement pareil. Comme leurs noms l'indiquent, le premier renvoie un entier (un Number tronqué sur sa partie entière) et l'autre un flottant (toujours un Number, mais non tronqué). Mais ce n'est pas tout. Par exemple, avant ES5 (testez sur une console JS qui ne force pas ES5, ou dans une REPL node par exemple), on obtenait ceci :

// Pre-ES5 -- consoles Firefox, Safari, node, etc.  Mais pas Chrome !
parseFloat('08') // => 8
parseInt('08')   // => 0 -- OMGWTFBBQ?!

C'est un problème bien connu : parseInt accepte en fait deux arguments : le texte et une base optionnelle (de 2 à 36) dans lequel ce texte encode le nombre. Mais avant EcmaScript 5, au lieu de supposer une base 10 (ce qui, de l'avis général, serait nettement plus utile), il devine. Aïe. En l'occurrence, il se base sur les syntaxes C/Java/JS/etc. de litéraux entiers, donc démarrer par 0x ou 0X indique un hexadécimal, sinon démarrer par 0 indique un octal ; les chiffres octaux valides allant de 0 à 7, le 8 est invalide.

Incidemment, JSLint (comme JSHint, je crois) vous couineront à l'oreille s'ils vous voient faire un parseInt qui ne précise pas la base. Et ils auront raison.

Vous me direz : « mais alors, parseInt('08') devrait plutôt renvoyer NaN, non ?! ». Et je serais bien d'accord avec vous. Seulement voilà : parseInt et parseFloat se contentent d'ignorer la suite du texte dès qu'ils rencontrent un caractère invalide. Ce qui est un comportement pourri, mais toujours d'actualité.

Je ne sais pas vous, mais moi, si je demande à l'internaute de saisir un nombre et qu'il me pond 42foobar, je ne veux pas « voir » 42, je veux me prendre un NaN. Pas de chance :

parseInt('42foobar', 10) // => 42
parseFloat('42foobar')   // => 42

Alors que faire ?

Le protocole de conversion numérique de JavaScript

Il se trouve que JavaScript est depuis toujours équipé d'un mécanisme qui convertit une entité quelconque en Number : c'est son protocole de conversion numérique, que la spec appelle affectueusement « ToNumber ». Il est déclenché implicitement lors d'expressions nécessitant un traitement numérique d'un opérande.

La manière la plus simple de transformer une valeur (ou undefined) en Number consiste probablement à la passer à l'opérateur + unaire (c'est-à-dire ne prenant qu'un opérande) : +truc. Bon point pour lui : il refusera tout caractère invalide. Voyez plutôt :

+42          // => 42
+'42'        // => 42
+'08'        // => 8
+'0x10'      // => 16
+'42.17'     // => 42.17
+'42foobar'  // => NaN -- aaaaah !
+'   42   '  // => 42
+{}          // => NaN -- pas de conversion générique Object -> Number
+[]          // => 0 -- <=> +([].toString()) <=> +''
+[3]         // => 3 -- <=> +'3'
+[3, 4]      // => NaN  <=> +'3,4'
+false       // => 0
+true        // => 1
+null        // => 0
var x; +x    // => NaN

Comme vous pouvez le voir, le + unaire est un moyen rapide, concis et efficace de convertir ce qu'on veut en Number. Il ne change pas le signe éventuel et réagit à tout caractère invalide en retournant NaN. Il est donc en tous points supérieur à parseFloat.

Il faut souligner qu'il existe une manière plus explicite de déclencher la conversion : il suffit d'employer Number comme fonction, conformément à la section 15.7.1.1 de la spec (ES3) :

Number('0x10')      // => 16
Number('42.17')     // => 42.17
Number('42foobar')  // => NaN
Number(false)       // => 0
var x; Number(x)    // => NaN

C'est certes un peu plus verbeux que le + unaire, mais nettement plus lisible pour les débutants…

Le seul souci potentiel, c'est que ce protocole de conversion ne fournit pas d'équivalent à parseInt, au sens où il ne tronque jamais sur la partie entière. Comment obtenir ce comportement ? Selon que vous voulez préserver un résultat NaN sur les textes pourris ou non, la solution varie.

Et pour garantir des entiers ?

On a deux manières de procéder :

  • une qui est aussi performante que possible mais ignore les infinis et les NaN
  • une qui est plutôt lisible et préserve les infinis (sait-on jamais…), au prix d'une performance très légèrement inférieure (ce qui n'est généralement pas un souci).

Voyons-les tour à tour.

La méthode « hack », ultra-performante mais peu lisible et non sans limites

À condition que ça ne vous dérange pas d'être à zéro par défaut (textes moisis compris), vous pouvez tirer parti de la sémantique de l'opérateur ou bit-à-bit, à savoir le pipe simple (|) :

42.17 | 0       // => 42
-42.3 | 0       // => -42
'42'  | 0       // => 42
'42.13' | 0     // => 42
'-17.7' | 0     // => -17
'0x10' | 0      // => 16
'42foobar' | 0  // => 0 -- pas NaN
'xyz' | 0       // => 0 -- pas NaN

À ce stade, vous avez probablement trois questions :

  • Pourquoi est-ce que ça force les entiers ? C'est parce que selon la section 11.10 de la spec (pour ES3 en tout cas, mais la sémantique persiste en ES5), les opérandes sont passés par une conversion interne ToInt32, pour en faire des entiers signés 32-bit (oui, attention, pas 64-bit !).
  • Pourquoi est-ce que ça ne fait pas de NaN ? Parce que la spec de ToInt32 revient à passer par ToNumber puis, pour tous les cas « foireux » (NaN, zéros signés et infinis), à remplacer par 0 (section 9.5 de la spec ES3).
  • Pourquoi le OU et pas une autre opération bit-à-bit ? Il nous faut une opération neutre sur l'entier d'origine (le résultat du ToInt32 sur votre texte). Passer par un ET (&) serait donc assez pénible car il nous faudrait un masque intégral, alors que le OU (|) et le OU exclusif (XOR, ^) sont neutres à zéro. J'imagine que le pipe a gagné de peu sur le caret parce qu'il est plus souvent tapé…

Petit conseil d'ami : si vous employez une telle syntaxe, ajoutez un commentaire sur la ligne précédente indiquant que vous faites une conversion entière à zéro par défaut : ce n'est vraiment pas lisible tel quel…

OK, mais si je veux du NaN pour les textes moisis ?

C'est en effet un cas de figure probable. Pour le gérer correctement, vous devrez probablement combiner la conversion ToNumber classique et le déclenchement implicite de ToInt32 au moyen d'un ou bit-à-bit :

function cleanInt(x) {
  x = +x; // x = Number(x);
  return isNaN(x) ? x : (x | 0);
}

cleanInt('-42.17')   // => -42
cleanInt('42foobar') // => NaN
cleanInt('Infinity') // => 0 -- hmmm…

La méthode un poil moins performante, mais lisible et universelle

Quitte à faire une fonction dédiée, autant préserver les infinis aussi. Pour cela, plutôt que de passer par l'absconse syntaxe | 0, utilisons les fonctions d'arrondi de JavaScript. C'est certes un micro-poil moins rapide, mais ça ne devrait généralement pas nous poser de souci concret.

function cleanInt(x) {
  x = Number(x);
  return x >= 0 ? Math.floor(x) : Math.ceil(x);
}

cleanInt('-42.17')    // => -42
cleanInt('42foobar')  // => NaN
cleanInt('Infinity')  // => Infinity
cleanInt('-Infinity') // => -Infinity

Examinons les cas possibles :

  • x est un texte moisi ou x est NaN : il sera NaN au final, la condition sera fausse (toute comparaison de NaN renvoie false) et Math.ceil préservera NaN.
  • x décrit un nombre valide positif ou nul : on utilisera l'arrondi à l'entier immédiatement inférieur, Math.floor.
  • x décrit un nombre valide négatif : on utilisera l'arrondi à l'entier immédiatement supérieur, Math.ceil.
  • x décrit un infini positif ou négatif : Math.floor comme Math.ceil préserveront l'infini concerné.

En fait, le ternaire (l'opérateur … ? … : …) ci-dessus se prête particulièrement bien à une petite optimisation syntaxique, sans nuire pour autant—à mon sens—à la lisibilité, au moyen de l'opérateur [] qui permet de sélectionner dynamiquement une propriété (donc une méthode) au sein d'un objet :

function cleanInt(x) {
  x = Number(x);
  return Math[x >= 0 ? 'floor' : 'ceil'](x);
}

En résumé…

  • parseInt et parseFloat sont juste bons à sucer des cailloux
  • Le + unaire, ou sa version explicite Number(…), sont souvent supérieurs à parseFloat car ils renvoient NaN pour les textes moisis
  • Le … | 0 tronque bien sur la partie entière, mais renvoie 0 pour les textes moisis
  • On peut facilement combiner les deux pour obtenir une version plus clean de parseInt(…, 10)
  • On peut aussi choisir de sacrifier quelques microsecondes (ahem…) pour gagner en lisibilité en préservant au passage les infinis.

Envie d’en apprendre davantage ?

Notre formation Web Apps Modernes explore en profondeur tous les aspects techniques avancés du langage JavaScript lui-même, et toute la stack technique web front. Des protocoles de conversion à la programmation fonctionnelle en passant par les méthodes méconnues des types natifs et le fonctionnement des prototypes, cette formation JavaScript vous donne un gros coup de boost dans le développement de vos codes JS quels qu’ils soient.