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.
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.
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
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
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.
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?