2009-05-01 6 views
11

Como el tipo de datos flotantes en PHP es impreciso, y un FLOAT en MySQL ocupa más espacio que un INT (y es impreciso), siempre almaceno los precios como INT, multiplicándolo por 100 antes de almacenarlo para asegurar que tenemos exactamente 2 decimales de precisión. Sin embargo, creo que PHP se está portando mal. Código de ejemplo:php intval() y floor() valor de retorno que es demasiado bajo?

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) 

I se sorprendió. Cuando el resultado final fue menor de lo esperado por 1, que estaba esperando la salida de mi prueba para ver más como:

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

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

+0

Esto realmente no tiene nada que ver con PHP y tiene más que ver con cómo todas las computadoras manejan datos de punto flotante. Es posible que desee leer sobre ese tema si no ha encontrado imprecisiones en coma flotante anteriormente. – jmucchiello

+0

@jmucchiello, no estoy de acuerdo. Entiendo cómo las computadoras manejan los datos en coma flotante y es por eso que estoy usando datos enteros en su lugar. Este es un problema de PHP ya que PHP muestra 115 cuando los datos subyacentes son claramente 114.99999999 ... – Josh

Respuesta

25

Prueba esto como una solución rápida:

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

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

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

La razón por la que está recibiendo 114 en lugar de 115 es que floor rondas hacia abajo, hacia el número entero más próximo, por lo tanto baja (114.999999999) se convierte en 114. La pregunta más interesante es la razón por 1.15 * 100 es 114.999999999 en lugar de 115. La razón para eso 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 echo 1.15 * 100; hace:

  • Analiza 1.15 a un número de punto flotante binario. Esto implica redondeo, se redondea un poco para obtener el número binario 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 el redondeo, 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 resulta ser cero en esta operación.
  • Convierte el número de punto flotante binario a un número decimal base 10 con un punto, e imprime esta representación. Esto también implica un pequeño redondeo.

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

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 flotadores, recuerde: no siempre se ve todos los dígitos cuando se imprime el flotador.

Consulte también la advertencia grande cerca del comienzo de los documentos PHP float.

+0

$ price_int = intval (floor ($ price_corrected + 0.5)) es lo mismo que $ price_int = intval (round ($ price_corrected)). – chaos

+0

@pts, el caos es correcto y en realidad mi código original era así. Ninguna diferencia. Mi pregunta es, ¿por qué PHP * dice * $ price_converted es 115 cuando es realmente 114.999999? (Entiendo por qué es 114.99999 ...) – Josh

+0

@pts, me equivoqué cuando dije que el caos era correcto, leí mal tu código. Eso puede funcionar – Josh

3

PHP está haciendo el redondeo basado en dígitos significativos. Está ocultando la inexactitud (en la línea 2). Por supuesto, cuando llega el piso, no conoce nada mejor y lo baja todo el camino hacia abajo.

+0

@altCognito, ¿sabes por qué oculta la inexactitud en la línea 2? Creo que esa es realmente mi pregunta. – Josh

+1

Creo que tiene algo que ver con la normalización, y la respuesta está aquí: http://en.wikipedia.org/wiki/Floating_point#Multiplication: | – cgp

4

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

para apuntar a solucionar el problema desde un ángulo diferente:

para almacenar valores de precios en MySQL, probablemente debería mirar el DECIMAL type, lo que le permite almacenar valores exactos con cifras decimales.

+0

+1, pero me gustaría añadir que no estoy seguro de qué tan "inexacto" sea un tipo FLOAT en mySQL. Es mucho menos inexacto que realizar cualquier mojo de multiplicación adicional, y deja tus datos en un estado mucho mejor para el análisis. De todos modos, lo que dijo. – cgp

+0

@Chad Birch, ¿INT no usa mucho menos espacio que DECIMAL? Siempre he encontrado que la lógica INT + para saber cuántos lugares decimales compensar para ser más eficiente que DECIMAL. Especialmente para cosas como precios. Efectivamente, la columna es price_in_cents – Josh

+1

No, a menos que su DECIMAL tenga muchos dígitos. Según el manual, INT utiliza 4 bytes (suponiendo que esté utilizando una INT real y no un SMALLINT o algo), el almacenamiento DECIMAL varía: "Cada múltiplo de nueve dígitos requiere cuatro bytes, y los dígitos 'sobrantes' requieren una fracción de cuatro bytes ". Información completa: http://dev.mysql.com/doc/refman/5.1/en/storage-requirements.html –

3

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

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

Usted, sí usted, señor, merece un voto positivo. Acabo de encontrar un error extraño y eso lo resolvió. Acabo de agregar cadenas vacías a esa llamada 'number_format()' para que el número resultante sea un int sin comas y puntos. –

+0

Fixed intval (16.74 * 100) devolviendo 1673 para mí y la respuesta aceptada no ayudó en absoluto. – Billy

0

Como se indica que esto no es un problema con PHP per se, es más bien una cuestión de manejo de fracciones que no puede ser expresado como los valores finitos de punto flotante, por lo tanto, conducen a la pérdida de carácter al redondear.

La solución es garantizar que cuando trabaje en valores de coma flotante y necesite mantener la precisión, use las funciones de gmp o las funciones de matemática 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);