Pregunta

Debido a que el tipo de datos flotantes en PHP es inexacto, y un FLOAT en MySQL ocupa más espacio que un INT (y es inexacto), siempre almaceno los precios como INT, multiplicándolos por 100 antes de almacenarlos para garantizar que tenemos exactamente 2 decimales Lugares de precisión. Sin embargo creo que PHP se está portando mal. Código de ejemplo:

echo "<pre>";

$price = "1.15";
echo "Price = ";
var_dump($price);

$price_corrected = $price*100;
echo "Corrected price = ";
var_dump($price_corrected);


$price_int = intval(floor($price_corrected));
echo "Integer price = ";
var_dump($price_int);

echo "</pre>";

Salida producida:

Price = string(4) "1.15"
Corrected price = float(115)
Integer price = int(114)

Me sorprendió. Cuando el resultado final fue inferior al esperado por 1, esperaba que la salida de mi prueba se pareciera más a:

Price = string(4) "1.15"
Corrected price = float(114.999999999)
Integer price = int(114)

lo que demostraría la inexactitud del tipo flotante. Pero ¿por qué el piso (115) regresa 114 ??

¿Fue útil?

Solución

Prueba esto como una solución rápida:

$price_int = intval(floor($price_corrected + 0.5));

El problema que está experimentando no es culpa de PHP, todos los lenguajes de programación que usan números reales con aritmética de punto flotante tienen problemas similares.

La regla general de oro para los cálculos monetarios es nunca utilizar flotantes (ni en la base de datos ni en su script). Puede evitar todo tipo de problemas almacenando siempre los centavos en lugar de los dólares. Los centavos son enteros, y puedes sumarlos libremente y multiplicarlos por otros enteros. Siempre que muestre el número, asegúrese de insertar un punto delante de los dos últimos dígitos.

La razón por la que obtienes 114 en lugar de 115 es que floor redondea hacia abajo, hacia el entero más cercano, por lo que floor (114.999999999) se convierte en 114. La pregunta más interesante es por qué 1.15 * 100 es 114.999999999 en lugar de 115. La razón de esto es que 1.15 no es exactamente 115/100, pero es un poco menos, así que si multiplicas por 100, obtienes un número un poco más pequeño que 115.

Aquí hay una explicación más detallada de lo que hace echo 1.15 * 100; :

  • Analiza 1.15 a un número de punto flotante binario. Esto implica redondear, pasa a redondear un poco hacia abajo para obtener el número binario de punto flotante más cercano a 1.15. La razón por la que no puede obtener un número exacto (sin error de redondeo) es que 1.15 tiene un número infinito de números en la base 2.
  • Analiza 100 a un número de punto flotante binario. Esto implica redondear, pero como 100 es un entero pequeño, el error de redondeo es cero.
  • Calcula el producto de los dos números anteriores. Esto también implica un pequeño redondeo, para encontrar el número de punto flotante binario más cercano. El error de redondeo es cero en esta operación.
  • Convierte el número de punto flotante binario en un número decimal de base 10 con un punto e imprime esta representación. Esto también implica un poco de redondeo.

La razón por la que PHP imprime el sorprendente Corrected price = float (115) (en lugar de 114.999 ...) es que var_dump no imprime el número exacto ( !), pero imprime el número redondeado a dígitos de n - 2 (o n - 1 ), donde n dígitos es la precisión del cálculo. Puedes verificar esto fácilmente:

echo 1.15 * 100;  # this prints 115
printf("%.30f", 1.15 * 100);  # you 114.999....
echo 1.15 * 100 == 115.0 ? "same" : "different";  # this prints `different'
echo 1.15 * 100 < 115.0 ? "less" : "not-less";    # this prints `less'

Si está imprimiendo flotantes, recuerde: no siempre ve todos los dígitos cuando imprime el flotador .

Vea también la gran advertencia cerca del comienzo de los PHP float docs.

Otros consejos

Las otras respuestas han cubierto la causa y una buena solución al problema, creo.

Para tratar de solucionar el problema desde un ángulo diferente:

Para almacenar valores de precios en MySQL, probablemente debería consultar DECIMAL type , que le permite almacenar valores exactos con posiciones decimales.

PHP está haciendo redondeo basado en dígitos significativos. Está ocultando la inexactitud (en la línea 2). Por supuesto, cuando aparece el piso, no sabe nada mejor y lo recorre completamente.

Quizás sea otra solución posible para este " problema " ;:

intval(number_format($problematic_float, 0, '', ''));

Como se indicó, este no es un problema con PHP per se, es más un problema de manejo de fracciones que no se pueden expresar como valores finitos de punto flotante, lo que conlleva una pérdida de carácter al redondear.

La solución es asegurarse de que cuando esté trabajando con valores de punto flotante y necesite mantener la precisión, use las funciones gmp o las funciones matemáticas de BC - bcpow, bcmul et al. y el problema se resolverá fácilmente.

E.g en lugar de $ price_corrected = $ price * 100;

use $ price_corrected = bcmul ($ price, 100);

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top