Conversion de string en nombre en .Net (C# et VB.Net)

Introduction

Les occidentaux écrivent avec l'alphabet romain et de gauche à droite.
Selon le pays cet alphabet subit des modifications, il y a le ß en Allemagne, des accents de types différents en Scandinavie ou en Espagne (ů ñ...), pas d'accents en Angleterre etc...

L'arabe s'écrit avec son propre alphabet et de gauche à droite.

Le chinois s'écrit avec des idéogrammes et de bas en haut.

Les nombres aussi connaissent différentes représentations selon le pays où l'on se trouve.

C'est ce qu'en informatique, on appelle la culture.

Concernant les nombres, en France (au moins), le symbole décimal est la virgule et le séparateur des milliers est l'espace.
Au USA, le symbole décimal est le point et le séparateur des milliers est la virgule.
On voit de suite que si on est confronté à des nombres venant de ces 2 cultures, il peut y avoir confusion.

La solution adoptée au début était simple les symbole décimal était le point, et il n'y avait pas de séparateur des milliers. C’était comme ça pour tout le monde.
D'ailleurs, aujourd'hui, avec Visual Studio, dans le code ou en débugage, un nombre a toujours cette représentation.

Oui mais, Microsoft, dans sa volonté de conquérir le grand public du monde entier, a adapté Windows à la culture de la majorité des pays, dont la France.
Et donc, si en installant Windows, on choisit la France comme localisation, il faut écrire les nombres avec une virgule comme symbole décimal et l'espace comme séparateur des milliers, y compris pendant l'exécution d'un logiciel que l'on a écrit soi-même.
Essayer de convertir

"123.4"

en double avec

double.Parse()

ou

Convert.ToDouble()

plante.

Souvent, les utilisateurs « avertis » en informatique, remplacent le symbole décimal de la virgule par le point dans la configuration régionale. Mais souvent, ils laissent l'espace comme séparateur des milliers. On se retrouve alors avec une culture hybride.

Et avec tout ça, il faut souvent importer des données issues de fichiers texte ou de BDD dont on ne connait pas la culture, tout en anticipant comment les utilisateurs vont saisir les nombres dans les champs mis à leur disposition. Bref c’est le bazar.

La solution que je vais vous apporter, permet de s'affranchir dans 5 représentations (sauf 1 notoire), de savoir de quelle culture vient le texte représentant un nombre et quelle est la culture du PC.

Je vais vous montrer plusieurs méthodes d'extension de la classe string (en effet pour le code exemple c'est plus parlant). Chacune est une étape du cheminement.
En C#, ça s'écrit dans une classe static et le premier paramètre de la méthode est déclarée avec le modificateur

this

, ce modificateur indique qu'il s'agit d'une extension de la classe du type de ce paramètre.
En VB.Net, ça c'écrit dans un module avec le marqueur

<System.Runtime.CompilerServices.Extension>

avant la déclaration.
Je donnerai le contenu du fichier complet pour le premier exemple, pour les suivants, ce sera juste la nouvelle méthode.
Ces extensions retourneront Not A Number quand la conversion sera impossible.

D'abord utiliser TryParse

Comme son nom l'indique, cette méthode essaye de parser le texte, et donc au lieu de planter, on peut tester si on arrive à convertir ou non.

En C#

using System.Globalization;
using System.Text.RegularExpressions;

namespace ConversionStringDouble
{
    static class Extensions
    {
        /// <summary>
        /// Tente de convertir une string en double sans se poser de question
        /// </summary>
        /// <param name="Texte"></param>
        /// <returns>Le double si c'est possible, Not A Number dans le cas contraire</returns>
        public static double ToDoubleBasique(this string Texte)
        {
            double nombre;
            if (double.TryParse(Texte, out nombre))
                return nombre;
            else
                return double.NaN;
        }
    }
}

En VB.Net

Imports System.Globalization
Imports System.Text.RegularExpressions


Friend Module Extensions
    ''' <summary>
    ''' Tente de convertir une string en double sans se poser de question
    ''' </summary>
    ''' <param name="Texte"></param>
    ''' <returns>Le double si c'est possible, Not A Number dans le cas contraire</returns>
    <System.Runtime.CompilerServices.Extension>
    Public Function ToDoubleBasique(ByVal Texte As String) As Double
        Dim nombre As Double
        If Double.TryParse(Texte, nombre) Then
            Return nombre
        Else
            Return Double.NaN
        End If
    End Function
End Module

Le code test est en VB (pour C#, il suffit d'ajouter ; en fin de ligne et de changer le symbole de commentaire)

            'lesNombres est une liste de double
lesNombres.Add("123.4".ToDoubleBasique())   'ajoute 123.4 si le symbole décimal du PC est le point et NaN dans le cas contraire
lesNombres.Add("123,4".ToDoubleBasique())   'ajoute 123.4 si le symbole décimal du PC est la virgule et NaN dans le cas contraire
lesNombres.Add("1 234".ToDoubleBasique())   'ajoute 1234.0 si la culture est française et NaN dans le cas contraire
lesNombres.Add("1 234,1".ToDoubleBasique()) 'ajoute 1234.1 si la culture est française et NaN dans le cas contraire
lesNombres.Add("1,234.1".ToDoubleBasique()) 'ajoute 1234.1 si la culture est américaine et NaN dans le cas contraire

Dans ce cas, si le texte correspond à la culture du PC, le nombre est converti.

S'affranchir du symbole décimal

On part du principe que l'utilisateur n'utilisera pas de séparateur de milliers.
Si une virgule est présente, on la remplace par le point et on convertit en forçant la culture par défaut.

En C# (à ajouter dans la classe static)

        /// <summary>
        /// Tente de convertir un texte au format #######.#### ou #######,##### en remplaçant la virgule par le point et en forçant la culture par défaut (Invariant Culture)
        /// </summary>
        /// <param name="Texte"></param>
        /// <returns>Le double si c'est possible, Not A Number dans le cas contraire</returns>
        public static double ToDoubleGereSymboleDecimal(this string Texte)
        {
            double nombre;
            if (double.TryParse(Texte.Replace(',','.'),NumberStyles.Number, CultureInfo.InvariantCulture, out nombre))
                return nombre;
            else
                return double.NaN;
        }

En VB.Net (à ajouter dans le module)

    ''' <summary>
    ''' Tente de convertir un texte au format #######.#### ou #######,##### en remplaçant la virgule par le point et en forçant la culture par défaut (Invariant Culture)
    ''' </summary>
    ''' <param name="Texte"></param>
    ''' <returns>Le double si c'est possible, Not A Number dans le cas contraire</returns>
    <System.Runtime.CompilerServices.Extension>
    Public Function ToDoubleGereSymboleDecimal(ByVal Texte As String) As Double
        Dim nombre As Double
        If Double.TryParse(Texte.Replace(","c, "."c), NumberStyles.Number, CultureInfo.InvariantCulture, nombre) Then
            Return nombre
        Else
            Return Double.NaN
        End If
    End Function

Code test

lesNombres.Add("123.4".ToDoubleGereSymboleDecimal())   'ajoute 123.4 quelque soit le symbole décimal du PC
lesNombres.Add("123,4".ToDoubleGereSymboleDecimal())   'idem
lesNombres.Add("1 234".ToDoubleGereSymboleDecimal())   'ajoute NaN quelque soit le symbole décimal du PC
lesNombres.Add("1 234.1".ToDoubleGereSymboleDecimal()) 'idem
lesNombres.Add("1 234,1".ToDoubleGereSymboleDecimal()) 'idem
lesNombres.Add("1,234.1".ToDoubleGereSymboleDecimal()) 'idem

Essayer de trouver la culture du texte

Pour cela on va utiliser 2 Regex, l'une permet d'agir si le texte semble français (virgule pour la décimal avec ou sans espaces) et l'une si le texte semble américain (point pour la décimale avec ou sans virgules).

En C# (à ajouter dans la classe static)

        /// <summary>
        /// Tente de déterminer si le texte représente un nombre et de le convertir en double le cas échéant
        /// </summary>
        /// <param name="Texte"></param>
        /// <returns>Le double si c'est possible, Not A Number dans le cas contraire</returns>
        public static double ToDouble(this string Texte)
        {
            CultureInfo culture = CultureInfo.InvariantCulture;

            if (Regex.IsMatch(Texte, @"^\d{1,3}( ?\d{3})*([\.,](\d{3} ?)*\d{1,3})?$"))//teste avec une regex qui cherche un nombre représenté avec ou sans espace comme séparateur des milliers et soit le point soit la virgule comme séparateur décimal
                Texte = Texte.Replace(',', '.').Replace(" ", "");//on enlève les espaces éventuels et on force le . en symbole décimal
            else if (Regex.IsMatch(Texte, @"^\d{1,3}(,?\d{3})*(\.(\d{3},?)*\d{1,3})?$"))//teste avec une regex qui cherche un nombre représenté avec ou sans virgule comme séparateur des milliers et le point comme séparateur décimal
                Texte = Texte.Replace(",", "");//on enlève les espaces virgules
                                               //si on connait une autre culture il faut faire un autre esle if avec une regex adaptée

            double nombre;
            if (double.TryParse(Texte, NumberStyles.Number, CultureInfo.InvariantCulture, out nombre))
                return nombre;
            else
                return double.NaN;
        }

En VB.NET (à ajouter dans le module)

    ''' <summary>
    ''' Tente de déterminer si le texte représente un nombre et de le convertir en double le cas échéant
    ''' </summary>
    ''' <param name="Texte"></param>
    ''' <returns>Le double si c'est possible, Not A Number dans le cas contraire</returns>
    <System.Runtime.CompilerServices.Extension>
    Public Function ToDouble(ByVal Texte As String) As Double
        Dim culture As CultureInfo = CultureInfo.InvariantCulture

        If Regex.IsMatch(Texte, "^\d{1,3}( ?\d{3})*([\.,](\d{3} ?)*\d{1,3})?$") Then 'teste avec une regex qui cherche un nombre représenté avec ou sans espace comme séparateur des milliers et soit le point soit la virgule comme séparateur décimal
            Texte = Texte.Replace(","c, "."c).Replace(" ", "") 'on enlève les espaces éventuels et on force le. en symbole décimal
        ElseIf Regex.IsMatch(Texte, "^\d{1,3}(,?\d{3})*(\.(\d{3},?)*\d{1,3})?$") Then 'teste avec une regex qui cherche un nombre représenté avec ou sans virgule comme séparateur des milliers et le point comme séparateur décimal
            Texte = Texte.Replace(",", "") 'on enlève les espaces virgules
        End If
        'si on connait une autre culture il faut faire un autre else if avec une regex adaptée

        Dim nombre As Double
        If Double.TryParse(Texte, NumberStyles.Number, CultureInfo.InvariantCulture, nombre) Then
            Return nombre
        Else
            Return Double.NaN
        End If
    End Function

Code exemple

lesNombres.Add("123.4".ToDouble()) 'ajoute 123.4 quelque soit le symbole décimal du PC
lesNombres.Add("123,4".ToDouble()) 'idem
lesNombres.Add("1 234".ToDouble()) 'ajoute 1234.0 quelque soit la culture du PC
lesNombres.Add("1 234.1".ToDouble()) 'ajoute 1234.1 quelque soit la culture du PC
lesNombres.Add("1 234,1".ToDouble()) 'idem
lesNombres.Add("1 234.1".ToDouble()) 'idem
lesNombres.Add("1,234.1".ToDouble()) 'idem
lesNombres.Add("1,234,456".ToDouble()) 'ajoute 123456.0 quelque soit la culture du PC

Oui mais

Cette solution semble parfaite, malheureusement elle souffre de la limitation de votre serviteur. En présence d'un nombre avec une virgule, pas de point et pas d'espace, je suis incapable de déterminer la culture, ma méthode non plus. Moi je choisis aléatoirement ou en fonction d’autres nombres dans le même texte.
La méthode choisit français, car c'est la première Regex que j'ai codé (si on veut que l'américain soit choisi, il suffit d'inverser les Regex)

Code exemple

lesNombres.Add("1,234".ToDouble()) 'ajoute 1.234 quelque soit la culture du PC, car le texte est intercepté par la première Regex.

Et la monnaie dans tout ça?

La méthode TryParse nous permet de préciser que le texte dispose d’un symbole monétaire

double.TryParse(Texte.Replace(',','.'),NumberStyles.AllowCurrencySymbol, CultureInfo.InvariantCulture, out nombre)

Oui mais voilà, le symbole dépend de la culture…
Donc à vous de tenter de vous adapter en fonction des cas et des utilisateurs que vous rencontrez.
Il est possible de forcer la culture américaine si on rencontre un $ à la fin, mais cette culture est elle valable pour un nombre canadien?

A voir également
Ce document intitulé « Conversion de string en nombre en .Net (C# et VB.Net) » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous