PHP7.1 json_encode () Problema de flotación

93

Esta no es una pregunta, ya que es más un aviso. json_encode()Actualicé una aplicación que usa PHP7.1.1 y estaba viendo un problema con los flotadores que se cambiaban para extenderse a veces hasta 17 dígitos. Según la documentación, PHP 7.1.x comenzó a usarse en serialize_precisionlugar de precisión al codificar valores dobles. Supongo que esto provocó un valor de ejemplo de

472.185

convertirse

472.18500000000006

después de que ese valor pasó json_encode(). Desde mi descubrimiento, he vuelto a PHP 7.0.16 y ya no tengo el problema con json_encode(). También intenté actualizar a PHP 7.1.2 antes de volver a PHP 7.0.16.

El razonamiento detrás de esta pregunta proviene de PHP - Precisión de números flotantes , sin embargo, la razón final de esto se debe al cambio de precisión al uso de serialize_precision en json_encode().

Si alguien sabe de una solución a este problema, estaría más que feliz de escuchar el razonamiento / solución.

Extracto de una matriz multidimensional (antes):

[staticYaxisInfo] => Array
                    (
                        [17] => stdClass Object
                            (
                                [variable_id] => 17
                                [static] => 1
                                [min] => 0
                                [max] => 472.185
                                [locked_static] => 1
                            )

                    )

y después de pasar por json_encode()...

"staticYaxisInfo":
            {
                "17":
                {
                    "variable_id": "17",
                    "static": "1",
                    "min": 0,
                    "max": 472.18500000000006,
                    "locked_static": "1"
                }
            },
Gwi7d31
fuente
6
ini_set('serialize_precision', 14); ini_set('precision', 14);probablemente haría que se serializara como solía hacerlo, sin embargo, si realmente confías en una precisión específica en tus flotadores, estás haciendo algo mal.
apokryfos
1
"Si alguien conoce una solución a este problema" , ¿qué problema? No veo ningún problema aquí. Si decodifica el JSON usando PHP, recupera el valor que codificó. Y si lo decodifica usando un idioma diferente, probablemente obtenga el mismo valor. De cualquier manera, si imprime el valor con 12 dígitos, obtendrá el valor original ("correcto"). ¿Necesita una precisión de más de 12 dígitos decimales para los flotadores utilizados por su aplicación?
axiac
12
@axiac 472.185! = 472.18500000000006. Hay una clara diferencia antes y después. Esto es parte de una solicitud AJAX a un navegador y el valor debe permanecer en su estado original.
Gwi7d31
4
Estoy tratando de evitar el uso de una conversión de cadenas, ya que el producto final es Highcharts y no aceptará cadenas. Creo que lo consideraría muy ineficiente y descuidado si toma un valor flotante, lo lanza como una cadena, lo envía y luego hace que JavaScript vuelva a interpretar la cadena como un flotante con parseFloat (). Tu no?
Gwi7d31
1
@axiac Observo que su PHP json_decode () recupera el valor flotante original. Sin embargo, cuando javascript convierte la cadena JSON en un objeto, no convierte el valor a 472.185 como lo había insinuado potencialmente ... de ahí el problema. Me quedaré con lo que tengo.
Gwi7d31

Respuestas:

97

Esto me volvió loco por un momento hasta que finalmente encontré este error que le indica este RFC que dice

Actualmente json_encode()usa EG (precisión) que se establece en 14. Eso significa que se usan 14 dígitos como máximo para mostrar (imprimir) el número. IEEE 754 double admite una mayor precisión y serialize()/ var_export()usa PG (serialize_precision) que se establece en 17 como predeterminado para ser más preciso. Dado que json_encode()usa EG (precisión), json_encode()elimina los dígitos más bajos de las partes de fracciones y destruye el valor original incluso si el flotante de PHP pudiera contener un valor flotante más preciso.

Y (énfasis mío)

Este RFC propone introducir una nueva configuración EG (precisión) = - 1 y PG (serialize_precision) = - 1 que usa el modo 0 de zend_dtoa () que usa un mejor algoritmo para redondear números flotantes (-1 se usa para indicar el modo 0) .

En resumen, hay una nueva forma de hacer que PHP 7.1 json_encodeuse el motor de precisión nuevo y mejorado. En php.ini necesitas cambiar serialize_precisiona

serialize_precision = -1

Puede verificar que funciona con esta línea de comando

php -r '$price = ["price" => round("45.99", 2)]; echo json_encode($price);'

Deberías conseguir

{"price":45.99}
Machavity
fuente
G(precision)=-1y PG(serialize_precision)=-1 también se puede usar en PHP 5.4
kittygirl
1
Tenga cuidado con serialize_precision = -1. Con -1, este código se echo json_encode([528.56 * 100]);imprime[52855.99999999999]
vl.lapikov
3
@ vl.lapikov Sin embargo, eso suena más como un error de punto flotante general . Aquí hay una demostración , donde puede ver claramente que no es solo un json_encodeproblema
Machavity
39

Como desarrollador de complementos, no tengo acceso general a la configuración php.ini de un servidor. Entonces, en base a la respuesta de Machavity, escribí este pequeño código que puede usar en su script PHP. Simplemente colóquelo encima del script y json_encode seguirá funcionando como de costumbre.

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'serialize_precision', -1 );
}

En algunos casos es necesario establecer una variable más. Estoy agregando esto como una segunda solución porque no estoy seguro de si la segunda solución funciona bien en todos los casos en los que la primera solución ha demostrado funcionar.

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
alev
fuente
3
Tenga cuidado con eso, ya que su complemento puede cambiar configuraciones inesperadas para el resto de la aplicación de desarrollador. Pero, en mi opinión, no estoy seguro de cuán destructiva podría ser esta opción ... lol
igorsantos07
Tenga en cuenta que cambiar el valor de precisión (segundo ejemplo) podría tener un impacto mayor en otras operaciones matemáticas que tenga allí. php.net/manual/en/ini.core.php#ini.precision
Ricardo Martins
@RicardoMartins: Según la documentación, la precisión predeterminada es 14. La corrección anterior aumenta esto a 17. Por lo que debería ser aún más preciso. ¿Estás de acuerdo?
alev
@alev, lo que estaba diciendo es que cambiar solo serialize_precision es suficiente y no comprometer otros comportamientos PHP que su aplicación pueda experimentar
Ricardo Martins
4

Estaba codificando valores monetarios y tenía cosas como 330.46codificar 330.4600000000000363797880709171295166015625. Si no desea, o no puede, cambiar la configuración de PHP y conoce la estructura de los datos de antemano, hay una solución muy simple que funcionó para mí. Simplemente conviértalo en una cadena (ambos hacen lo mismo):

$data['discount'] = (string) $data['discount'];
$data['discount'] = '' . $data['discount'];

Para mi caso de uso, esta fue una solución rápida y efectiva. Solo tenga en cuenta que esto significa que cuando lo decodifique de nuevo desde JSON será una cadena, ya que estará envuelto entre comillas dobles.

texelate
fuente
4

Resolví esto estableciendo precisión y serialize_precision en el mismo valor (10):

ini_set('precision', 10);
ini_set('serialize_precision', 10);

También puede configurar esto en su php.ini

lo que sea
fuente
3

Tuve el mismo problema, pero solo serialize_precision = -1 no resolvió el problema. Tuve que hacer un paso más para actualizar el valor de precisión de 14 a 17 (como estaba configurado en mi archivo ini PHP7.0). Aparentemente, cambiar el valor de ese número cambia el valor del flotador calculado.

Alin Pop
fuente
3

Las otras soluciones no funcionaron para mí. Esto es lo que tuve que agregar al comienzo de la ejecución de mi código:

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
Mike P. Sinn
fuente
¿No es esto básicamente lo mismo que la respuesta de Alin Pop?
igorsantos07
1

En cuanto a mí, el problema fue cuando JSON_NUMERIC_CHECK como segundo argumento de json_encode () pasó, que convertía todos los números en int (no solo integer)

Acuña
fuente
1

Guárdelo como una cadena con la precisión exacta que necesita usando number_format, luego json_encodeuse la JSON_NUMERIC_CHECKopción:

$foo = array('max' => number_format(472.185, 3, '.', ''));
print_r(json_encode($foo, JSON_NUMERIC_CHECK));

Usted obtiene:

{"max": 472.185}

Tenga en cuenta que esto hará que TODAS las cadenas numéricas en su objeto de origen se codifiquen como números en el JSON resultante.

pasqal
fuente
1
He probado esto en PHP 7.3 y no funciona (la salida todavía tiene una precisión demasiado alta). Aparentemente, la bandera JSON_NUMERIC_CHECK está rota desde PHP 7.1 - php.net/manual/de/json.constants.php#123167
Philipp
0
$val1 = 5.5;
$val2 = (1.055 - 1) * 100;
$val3 = (float)(string) ((1.055 - 1) * 100);
var_dump(json_encode(['val1' => $val1, 'val2' => $val2, 'val3' => $val3]));
{
  "val1": 5.5,
  "val2": 5.499999999999994,
  "val3": 5.5
}
B. Asselin
fuente
0

Parece que el problema se produce cuando serializey serialize_precisionse establecen en valores diferentes. En mi caso 14 y 17 respectivamente. Establecer ambos en 14 resolvió el problema, al igual que establecerserialize_precision en -1.

El valor predeterminado de serialize_precision se cambió a -1 a partir de PHP 7.1.0, lo que significa que "se utilizará un algoritmo mejorado para redondear dichos números". Pero si sigue experimentando este problema, puede deberse a que tiene un archivo de configuración PHP de una versión anterior. (¿Quizás guardó su archivo de configuración cuando actualizó?)

Otra cosa a considerar es si tiene sentido usar valores flotantes en su caso. Puede que tenga sentido o no usar valores de cadena que contengan sus números para garantizar que siempre se conserve la cantidad adecuada de decimales en su JSON.

Comandante de código
fuente
-1

Puede cambiar el [max] => 472.185 de un flotante a una cadena ([max] => '472.185') antes del json_encode (). Como json es una cadena de todos modos, convertir sus valores flotantes en cadenas antes de json_encode () mantendrá el valor que desea.

Everett Staley
fuente
Esto es técnicamente cierto hasta cierto punto, pero muy ineficiente. Si no se cita un Int / Float en una cadena JSON, Javascript puede verlo como un Int / Float real. Realizar su interpretación le obliga a devolver cada valor a un Int / Float una vez en el lado del navegador. A menudo lidiaba con más de 10000 valores cuando trabajaba en este proyecto por solicitud. Una gran cantidad de procesamiento de hinchazón habría terminado.
Gwi7d31 de
Si está utilizando JSON para enviar datos a algún lugar y se espera un número pero envía una cadena, no se garantiza que funcione. En situaciones en las que el desarrollador de la aplicación de envío no tiene control sobre la aplicación de recepción, esta no es una solución.
osullic