La odisea de cómo pasar de String a Decimal o Double

posterHoy me gustaría hacer un inciso para hablar de un problema recurrente, de esos como los de convertir o tratar fechas… vamos, un problema que a priori es una tontería pero que nos puede llevar unas cuántas horas de trabajo, y no es otro que… pasar de string a decimal o a double

Escenario

Tenemos una fuente de datos, llámese base de datos, xml, json,…que debemos recibir y parsear en nuestra aplicación .net que incluye un campo de tipo decimal o double y necesitamos parsearlo.

Cualquiera de nosotros lo tendría claro… hacer un parseo con cualquiera de los métodos que hay:

  • decimal.Parse(string)
  • Convert.ToDecimal(string)
  • double.Parse(string)
  • Convert.ToDouble(string)

En principio todos esperamos que esto funcione tal y como podéis comprobar ¿o no?

            string myDecimal = "0,123456789";
            Console.WriteLine("n** Cadena recibida: " + myDecimal + "n");
 
            if (!string.IsNullOrEmpty(myDecimal))
            {
                decimal attempt1 = decimal.Parse(myDecimal);
                Console.WriteLine("1) decimal.Parse(myDecimal) => " + attempt1);
 
                decimal attempt2 = Convert.ToDecimal(myDecimal);
                Console.WriteLine("2) Convert.ToDecimal(myDecimal) => " + attempt2);
 
                double attempt3 = double.Parse(myDecimal);
                Console.WriteLine("3) double.Parse(myDecimal) => " + attempt3);
 
                double attempt4 = Convert.ToDouble(myDecimal);
                Console.WriteLine("4) Convert.ToDouble(myDecimal) => " + attempt4);
            }
            else
            {
                Console.WriteLine("* Valor nulo => 0 ");
            }
            Console.ReadLine();

 

Si ejecutamos este pedazo de código, efectivamente funcionará y en cada una de las líneas mostrará el valor correcto.

image

Yujuuuuu… Pero hay algo que se nos escapa y no lo estamos viendo venir porque nos empeñamos una y otra vez en pensar que los datos siempre van a venir como en el ejemplo de turno que teníamos.

 

Problema

Hasta aquí todo ha ido muy bien, somos unos campeones de tomo y lomo y seguimos programando como si no hubiera mañana pero… se nos ha escapado que la fuente de datos no tiene por qué ser nuestra y debemos controlar que todos los datos estén bien recogidos y parseados. ¿Cómo,  no estaba hecho ya? Pues no. Hay algo muy importante que se debe tener en cuenta y es la “cultura” del origen de datos con lo que… en el ejemplo anterior, en vez de “0,123456789” puede que hayamos recibido la cadena “0.123456789” y aquí ya se joroba todo

image

 

Oh my god!! ¿Y esto por qué pasa?

La explicación es sencilla, como la cultura que tenemos definida en nuestra máquina de desarrollo define que los decimales se separan con una coma “,” y las unidades de millar con un punto “.”, al hacer el parseo, la aplicación entiende que ese punto es un separador de miles y… nos ha jorobado el día porque a veces no es tan fácil de determinar como en esta “exageración” de decimal que he puesto, pero imaginad que tenéis 137,4 por ejemplo. Si te pilla despistado te puede tener un día entero pensando por qué en el listado que le muestras al usuario salen números que no cuadran con la realidad.

 

Solución

La solución pasa por pensar que no somos las únicas personas existentes sobre el planeta tierra y que los datos no nos van a venir siempre como queremos, así que tenemos que decirle al sistema de qué cultura vienen nuestros datos. Para ello, basta con mirar la documentación que Microsoft nos aporta sobre Convert.ToDecimal(string, IFormatProvider) y de Convert.ToDouble(string, IFormatProvider) donde se nos indica que basta con establecer la cultura de origen de los datos mediante CultureInfo culture = new CultureInfo(IDENTIFICADOR_DE_CULTURA); y pasárselo al método que estemos usando del Convert. Así pues… el código nos podría quedar así:

NOTA: No os olvidéis de añadir la referencia a System.Globalization

            string myDecimal = "0.123456789";
            Console.WriteLine("n** Cadena recibida: " + myDecimal + "n");
 
            CultureInfo culture = new CultureInfo("en-US");
            if (!string.IsNullOrEmpty(myDecimal))
            {
                decimal attempt1 = decimal.Parse(myDecimal, culture);
                Console.WriteLine("1) decimal.Parse(myDecimal) => " + attempt1);
 
                decimal attempt2 = Convert.ToDecimal(myDecimal, culture);
                Console.WriteLine("2) Convert.ToDecimal(myDecimal) => " + attempt2);
 
                double attempt3 = double.Parse(myDecimal, culture);
                Console.WriteLine("3) double.Parse(myDecimal) => " + attempt3);
 
                double attempt4 = Convert.ToDouble(myDecimal, culture);
                Console.WriteLine("4) Convert.ToDouble(myDecimal) => " + attempt4);
            }
            else
            {
                Console.WriteLine("* Valor nulo => 0 ");
            }
            Console.ReadLine();

 

Comprobamos y… voilà funciona de nuevo

image

NOTA: Tengamos en cuenta que debemos conocer la cultura en la que recibimos los datos.

 

Solución +

Pero vamos a ir más allá… y, si seguimos con la idea en la mente de que si quien nos da los datos, nos los da con un formato, seguramente querrá verlos con el mismo formato. Esto vamos a hacerlo mediante el método ToString(IFormatProvider) donde podemos pasarle la cultura que tenemos definida.

            string myDecimal = "0.123456789";
            Console.WriteLine("n** Cadena recibida: " + myDecimal + "n");
 
            CultureInfo culture = new CultureInfo("en-US");
 
            if (!string.IsNullOrEmpty(myDecimal))
            {
                decimal attempt1 = decimal.Parse(myDecimal, culture);
                Console.WriteLine("1) decimal.Parse(myDecimal) => " + attempt1.ToString(culture));
 
                decimal attempt2 = Convert.ToDecimal(myDecimal, culture);
                Console.WriteLine("2) Convert.ToDecimal(myDecimal) => " + attempt2.ToString(culture));
 
                double attempt3 = double.Parse(myDecimal, culture);
                Console.WriteLine("3) double.Parse(myDecimal) => " + attempt3.ToString(culture));
 
                double attempt4 = Convert.ToDouble(myDecimal, culture);
                Console.WriteLine("4) Convert.ToDouble(myDecimal) => " + attempt4.ToString(culture));
            }
            else
            {
                Console.WriteLine("* Valor nulo => 0 ");
            }
            Console.ReadLine();

Y ahora el usuario podrá ver los datos tal y como los envió

image

 

Solución ++

Ahora que nos estamos recreando y podemos decir que somos unos auténticos cracks, que tenemos en cuenta la cultura de los datos y blablablabla… vamos a dar algo de mejora visual al usuario y nos vamos a crear nuestro propio formato numérico para devolver por ejemplo… esos decimales en formato Moneda y en formato Numérico (Tabla de formatos). Así pues, creemos nuestro propio NumberFormatInfo.

            string myDecimal = "12,3456.123456789";
            Console.WriteLine("n** Cadena recibida: " + myDecimal + "n");
 
            CultureInfo culture = new CultureInfo("en-US");
 
            NumberFormatInfo nfi = new CultureInfo("en-US", true).NumberFormat;
            nfi.NegativeSign = "(-)";
            nfi.CurrencyDecimalDigits = 2;
            nfi.NumberDecimalDigits = 3;
 
            if (!string.IsNullOrEmpty(myDecimal))
            {
                decimal attempt1 = decimal.Parse(myDecimal, culture);
                Console.WriteLine("1) decimal.Parse(myDecimal) => " + attempt1.ToString("c", nfi) + "  AND NEGATIVE  " + (-1*attempt1).ToString("c", nfi));
 
                decimal attempt2 = Convert.ToDecimal(myDecimal, culture);
                Console.WriteLine("2) Convert.ToDecimal(myDecimal) => " + attempt2.ToString("c", nfi) + "  AND NEGATIVE  " + (-1 * attempt2).ToString("c", nfi));
 
                double attempt3 = double.Parse(myDecimal, culture);
                Console.WriteLine("3) double.Parse(myDecimal) => " + attempt3.ToString("n", nfi) + "  AND NEGATIVE  " + (-1 * attempt3).ToString("n", nfi));
 
                double attempt4 = Convert.ToDouble(myDecimal, culture);
                Console.WriteLine("4) Convert.ToDouble(myDecimal) => " + attempt4.ToString("n", nfi) + "  AND NEGATIVE  " + (-1 * attempt4).ToString("n", nfi));
            }
            else
            {
                Console.WriteLine("* Valor nulo => 0 ");
            }
            Console.ReadLine();

 

Como podréis observar, he introducido los siguientes cambios:

  • Crear NumberFormatInfo
    • Definir el signo negativo del formato numérico
    • Definir el número de decimales para el formato numérico
    • Definir el número de decimales para el formato moneda
  • Las dos primeras escrituras en consola lo harán con el formato Moneda – “c”  usando el NumberFormatInfo que he creado
  • Las dos últimas escrituras en consola lo harán con el formato Numértico – “n” usando el NumberFormatInfo que he creado
  • Para todas las salidas muestro su negativo

 

El resultado final es este:

image

 

Conclusión

No podemos olvidarnos de los usuarios ya que, al fin y al cabo, son los que consumen nuestras aplicaciones y los que nos dan de comer, así que tengamos en cuenta desde donde obtenemos los datos y cómo los vamos a mostrar para que los usuarios siempre reciban la información de la mejor forma posible.

 

Espero que os haya gustado. Enjoy!

5 Comentarios

  1. jirigoyen

    Hola Santiago, estoy muy de acuerdo contigo, los valores con decimales son bastante engorrosos de tratar, en Asp.Mvc causan un monton de problemas, en el tratamiento de Web internacionalizadas, solo un apunte, yo soy mas partidario de utilizar decimal.TryParse, creo que muchas veces es mas adecuado para controlar cualquier error que pueda aparecer.

    Un saludo.

  2. santypr

    Efectivamente Juan, un método más correcto es usar TryParse que ya te obliga en cierta medida a tener en cuenta el formato y además te protege de algunos errores. No obstante la idea del artículo era la de partir de lo que suelen usar los desarrolladores cuando van con prisas o no han investigado mucho.

  3. jclemente

    y quien no ha hecho un parse(cadena.replace(“.”,”,”)) …?

  4. santypr

    Jejejeje creo que todos lo hemos hecho en alguna ocasión… o en muchas ocasiones… pero no hay nada peor que eso para tratar con decimales

  5. christianamados

    También se podría solucionar colocando la siguiente línea de código:
    decimal.Parse(convertedToString, CultureInfo.InvariantCulture);
    Esto permitirá que funcione para . o ,

    Saludos.

Leave a Reply

Tema creado por Anders Norén