numeros bestias y descuentos

Que bonita esa cancion de los Maiden… 666, el numero de la bestia. Sin embargo, programanticamente hablando, el 666 es un numero de lo mas normalito (si lo ponemos  como 0x29A, 0o1232 o 0b1010011010 no impone el mismo respeto, verdad?)

Pero en programacion si que hay numeros “especiales” que son a la vez enormemente interesantes para los curiosos y terribles para los descuidados. En este articulo vamos a ver un par de puzzlers que espero que nos hagan pensar un par de veces la proxima vez que tengamos que evaluar estos “casos extremos” y de paso nos ayuden a repasar un poquito mas de aritmetica programante.

El primero, hablando de casos extremos, es para examinar unos conceptos basicos sobre matematica. Si tenemos este trocito de codigo que declara distintos valores y operaciones:

float zero = 0.0f;
float one = 1.0f;
float negativeone = -1.0f;

float division1 = zero / one;
float division2 = zero / negativeone;

float infinity1 = one / zero;
float infinity2 = negativeone / zero;

float division0 = zero / zero;

Console.WriteLine(division1 == division2);
Console.WriteLine(infinity1 == infinity2);

Console.WriteLine(infinity1 == infinity1);
Console.WriteLine(infinity2 == infinity2);
Console.WriteLine(division0 == division0);

¿Sabríais decir cuál será la salida que obtendremos al ejecutar?

Y el segundo es un caso de “depuracion pensando” que tiene que ver con numeros especiales tambien. Supongamos que tenemos una serie de productos a los que queremos aplicar un descuento (porque para eso estamos de rebajas de Enero). Tenemos algunos cuyo precio se ha reducido a la mitad, y otros que rebajamos un 10%. Como no nos gusta andar con la calculadora todo el rato nos hemos hecho un programita que aplica los descuentos por nosotros. El trocito de codigo que hace lo que queremos es algo como:

int[] precios = {20, 30, 20};
float[] descuentos = {0.5f, 0.5f, 0.1f};

for(int i = 0; i < precios.Length && i < descuentos.Length; i++) {
  int precioRebajado = (int) (precios[ i ] * (1 descuentos[ i ]));
  int porcentaje = (int) (100 * descuentos[ i ]);
  Console.WriteLine(“antes: {0} euros… ahora solo {1}!!! (-{2}%)”,
  precios[ i ], precioRebajado, porcentaje);
}

Sin embargo, al ejecutar nos hemos dado cuenta que la salida no es justo la que esperabamos. ¿Sabríais decir cuál es el error o errores que hemos cometido? ¿Como podemos hacer que nuestro programa funcione correctamente?

NOTAS/Actualizacion: Un par de apuntes sobre el problema de parte de un par de lectores avanzados (ver comentarios) }:)

  • Tal y como menciona Augusto, estos problemas pueden arreglarse usando la clase System.Decimal (o java.math.BigDecimal o el equivalente en cada plataforma). El tipo Decimal es bastante interesante, asi que dejamos aqui la referencia por si alguien es curioso y quiere adivinar por que sirve para este tipo de problemas (y cuales son sus ventajas e inconvenientes)
  • Curiosea que te curiosea, Tio_Luiso nos indica que en el primer trocito de codigo, C# parece que no representa el resultado de 0/-1 como “menos cero” (de acuerdo al estandar IEEE 754). En cuanto pueda informarme sobre esto pongo una actualizacion! }:)

Muchisimas gracias a los dos por tomaros el tiempo de compartir esta informacion! }:D

19 comentarios en “numeros bestias y descuentos”

  1. buenas Carlos,

    Efectivamente, nunca hay que hacer cast con flotantes, siempre es mejor Convert, que hace lo que debe… (casi siempre) }:)

    pero el tema curioso de este caso es: por que hace falta usar Convert? No deberia ser el 10% de 20 una cantidad exacta (2)? por que funciona en el caso del 50% y no en el del 10%?

  2. Pues yo diría que es por lo mismo que por lo que cuando sumas 10 veces 0.1, el resultado no es 1… [:)]

    Es decir, no existe una representación en binario del número que intentamos representar, por lo que se introducen errores de redondeo…

  3. Augusto es un cra! }:)

    Efectivamente, es lo que se llama “no tener expansion binaria finita”. Hay muchos numeros que no tienen expansion decimal finita (como por ejemplo 1/3) pero si tienen una representacion exacta en binario. Del mismo modo, hay numeros con los que ocurre justo lo contrario (como 0.1) Ademas, en ocasiones, el comportamiento depende de la precision binaria que usemos.

    Os posteo en un momento otro puzzler sobre este tema por si os animais… mientras tanto, algun comentario sobre el primero? }:)

  4. En cuanto al primer problema…

    Console.WriteLine(division1 == division2); -> false. 0 != -0

    Console.WriteLine(infinity1 == infinity2); -> false. Inf != -Inf

    Console.WriteLine(infinity1 == infinity1); -> true

    Console.WriteLine(infinity2 == infinity2); -> true
    Console.WriteLine(division0 == division0); -> En este caso no estoy del todo seguro… Creo que division0 debería ser NaN, y la expresión debería ser true, pero no estoy seguro…

  5. Augusto, vas bien…

    De aqui se pueden sacar dos cosas interesantes para mirar sobre aritmetica flotante: el concepto de cero y menos-cero (y el tratamiento que reciben, que varia segun la implementacion) y el de los no-numeros (NaN) y sus peculiaridades

    No cuento mas de momento para no aguarles la fiesta a los demas! }:)

  6. hola,

    en el último caso, se supone que debería de dar “true”, no? estoy de acuerdo en que NaN no es un número, pero habrá una representación interna para indicar este “valor”. Si no me equivoco, el valor es $FF en el primer byte. Entonces, se compara primero el signo(son iguales) y luego, la representación en hexadecimal(la cual, se supone que es la misma porque es el mismo valor). Como se encuentra con dicho valor, sabe que la comparación es sobre dos datos NaN; entonces, ¿no debería de dar como resultado TRUE? :S;

    Vale, creo que ya lo entiendo. y si no me equivoco, el resultado no es igual porque depende de la representación más significante que dé el sistema; la cual al realizar operaciones sobre datos NaN, no tiene porqué ser el resultado del primer dato NaN (creo que me he liado yo solito… :S).

    agur!

  7. hola de nuevo,

    eso es :), el segundo, el cast trunca. XD
    Lo que provoca ‘Convert’ es: “No se producirá una excepción si la conversión de un tipo numérico produce una pérdida de precisión, es decir, la pérdida de algunos de los dígitos menos significativos.” [1] 😉

    De todos modos, un compañero tuyo (seguí buscando por curiosidad), demuestra la diferencia y en el post David Salgado comenta una cosa bastante interesante usando el Reflector. os dejo el enlace. [2].

    agur!

    [1]: http://msdn2.microsoft.com/es-es/library/system.convert(VS.80).aspx
    [2]: http://geeks.ms/blogs/maramos/archive/2006/12/03/es-lo-mismo-un-cast-que-un-convert.aspx

  8. hola de nuevo :),

    cómo queda la solución/explicación del post ‘contando hacia atrás’? es que no me quedó muy claro. gracias!

    agur!

  9. Una cosa…

    Estos errores no se producen cuando se usa la estructura System.Decimal. (todos los que estéis escribiendo apps en .NET que manejen pasta, si no queréis volver locos a la gente de contabilidad, usad este tipo de datos).

    La pregunta es: ¿Por qué? 😉

  10. tio luiso: en IEEE 754 estandar el valor “cero” se representa con los bits de mantisa y exponente a cero, lo cual deja que el bit de signo sea 1 o 0. A estas dos representaciones se les llama “cero” y “menos cero”. Puedes ver mas info aqui:
    http://en.wikipedia.org/wiki/Minus_zero

    Albertito: muchas gracias por anyadir los links! Efectivamente, ahi tienes un poco mas de explicacion sobre las diferencias, y un juego de Unai que se basa en lo mismo que el problema de los descuentos.

    con respecto a tu comentario de los NaN, te propongo un jueguecito aclaratorio. Que resultado dara el ejecutar el siguiente pedacito de codigo?

    double myNan = Double.NaN;
    Console.WriteLine(myNan == Double.NaN)

    y respecto a tu pregunta del “contando hacia atras”… has probado a mirar (con depurador o con WriteLine) cual es el valor del counter en cada iteracion?

    Augusto: quieres que anyada tu pregunta sobre el tipo decimal como una nota en el post para incitar a la gente a que le eche un ojo? }:)

  11. Interesante concepto. Sin embargo, si compilas el ejemplo que has puesto, verás que tanto zero / one como zero / negativeone dan zero patatero. Sin signos.

    Igual es que no implementan el estandar de forma estricta.

  12. hola Ricardo,

    sí, lo comprobé con el debugger, y lo he vuelto a comprobar ahora. Mi pregunta es: ¿por qué no decrementa con tipos ‘float’? He revisado la msdn y no he encontrado nada que me pueda ayudar a entenderlo. he comprobado el valor máximo del tipo float y es muy superior al valor dado en el ejemplo.
    lo único que creo es que con el dato float realiza redondeos de forma continuada porque no puede almacenar más de 32 bits, y como siempre le da cero al redondear, no consigue realizar el decremento. :S

    si no quieres poner la respuesta aquí, dime y te envío un mail.

    agur y gracias!

  13. tio Luiso: acabo de mirar lo del menos cero y tienes toda la razon! Si comprobamos en codigo en otros lenguajes (eg: Java o C) al comparar menos cero con division1 y division2 nos devuelve falso y cierto, respectivamente… pero en C# devuelven ambos cierto!

    Le echo un ojo a esto con mas tiempo y te comento! Muchas gracias por la observacion! }:D
    (nota: aun asi, comentar que el resto del ejercicio esta ok, al comparar cero y menos cero con == debe dar cierto, la diferencia es solo a nivel de bits, no de significado)

    Albertito: por ahi van los tiros! La idea es que en realidad depende de la implementacion de float, pero para la mayoria de ellas es valido el ejemplo que puse: double puede representar exactamente todos los valores de 1e8 a 0, asi que hace el bucle sin problemas. sin embargo, para el caso de float, se alcanza un valor N para el cual no tiene representacion exacta y para el que N-1 = N. Asi que en cada iteracion volvemos a redondear para ese valor, y el bucle se convierte en infinito… cosas de la precision! }:)
    (si tienes mas dudas sobre el tema, encantado de que lo discutamos por mail)

  14. hola Ricardo,

    te envié un mail a tu dirección de tu página(“blablabla”@phobeo.com, o algo así) porque no encontraba ninguna otra :(.

    la lees? :S

    agur!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *