Problème de flotteur PHP7.1 json_encode ()

93

Ce n'est pas une question car il s'agit plus d'un être conscient. J'ai mis à jour une application qui utilise json_encode()PHP7.1.1 et je voyais un problème avec les flotteurs modifiés pour parfois étendre 17 chiffres. Selon la documentation, PHP 7.1.x a commencé à utiliser serialize_precisionau lieu de la précision lors de l'encodage de valeurs doubles. Je suppose que cela a causé un exemple de valeur de

472,185

devenir

472.18500000000006

après que cette valeur soit passée json_encode(). Depuis ma découverte, je suis revenu à PHP 7.0.16 et je n'ai plus le problème avec json_encode(). J'ai également essayé de mettre à jour PHP 7.1.2 avant de revenir à PHP 7.0.16.

Le raisonnement derrière cette question provient de PHP - Floating Number Precision , mais la fin de toute raison en est due au passage de la précision à l'utilisation de serialize_precision dans json_encode().

Si quelqu'un connaît une solution à ce problème, je serais plus qu'heureux d'écouter le raisonnement / la solution.

Extrait du tableau multidimensionnel (avant):

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

                    )

et après avoir traversé json_encode()...

"staticYaxisInfo":
            {
                "17":
                {
                    "variable_id": "17",
                    "static": "1",
                    "min": 0,
                    "max": 472.18500000000006,
                    "locked_static": "1"
                }
            },
Gwi7d31
la source
6
ini_set('serialize_precision', 14); ini_set('precision', 14);le ferait probablement sérialiser comme avant, mais si vous comptez vraiment sur une précision spécifique sur vos flotteurs, vous faites quelque chose de mal.
apokryfos
1
"Si quelqu'un connaît une solution à ce problème" - quel problème? Je ne vois aucun problème ici. Si vous décodez le JSON en utilisant PHP, vous récupérez la valeur que vous avez encodée. Et si vous le décodez en utilisant une langue différente, vous obtiendrez probablement la même valeur. Dans tous les cas, si vous imprimez la valeur avec 12 chiffres, vous récupérez la valeur d'origine («correcte»). Avez-vous besoin d'une précision de plus de 12 chiffres décimaux pour les flotteurs utilisés par votre application?
axiac
12
@axiac 472.185! = 472.18500000000006. Il y a une différence claire avant et après. Cela fait partie d'une requête AJAX à un navigateur et la valeur doit rester dans son état d'origine.
Gwi7d31
4
J'essaie d'éviter d'utiliser une conversion de chaîne car le produit final est Highcharts et il n'acceptera pas les chaînes. Je pense que je le considérerais comme très inefficace et négligé si vous prenez une valeur flottante, la castez comme une chaîne, la renvoyez, puis que javascript interprète la chaîne en flottant avec parseFloat (). Pas toi?
Gwi7d31
1
@axiac Je note que vous êtes PHP json_decode () ramène la valeur flottante d'origine. Cependant, lorsque javascript transforme la chaîne JSON en objet, il ne convertit pas la valeur en 472.185 comme vous l'aviez potentiellement insinué ... d'où le problème. Je vais m'en tenir à ce que je vais faire.
Gwi7d31

Réponses:

97

Cela m'a rendu fou pendant un moment jusqu'à ce que je trouve enfin ce bogue qui vous renvoie à cette RFC qui dit

json_encode()Utilise actuellement EG (précision) qui est réglé sur 14. Cela signifie que 14 chiffres au maximum sont utilisés pour afficher (imprimer) le nombre. IEEE 754 double prend en charge une précision plus élevée et serialize()/ var_export()utilise PG (serialize_precision) qui a la valeur 17 par défaut pour être plus précis. Depuis json_encode()utilise EG (précision), json_encode()supprime les chiffres inférieurs des parties de fraction et détruit la valeur d'origine même si le flottant de PHP pouvait contenir une valeur flottante plus précise.

Et (c'est moi qui souligne)

Cette RFC propose d'introduire un nouveau paramètre EG (precision) = - 1 et PG (serialize_precision) = - 1 qui utilise le mode 0 de zend_dtoa () qui utilise un meilleur algorigthme pour arrondir les nombres flottants (-1 est utilisé pour indiquer le mode 0) .

En bref, il existe une nouvelle façon de faire en sorte que PHP 7.1 json_encodeutilise le nouveau moteur de précision amélioré. Dans php.ini, vous devez changer serialize_precisionpour

serialize_precision = -1

Vous pouvez vérifier que cela fonctionne avec cette ligne de commande

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

Tu devrais obtenir

{"price":45.99}
Machavity
la source
G(precision)=-1et PG(serialize_precision)=-1 peut également être utilisé en PHP 5.4
kittygirl
1
Soyez prudent avec serialize_precision = -1. Avec -1, ce code echo json_encode([528.56 * 100]);imprime[52855.99999999999]
vl.lapikov
3
@ vl.lapikov Cela ressemble plus à une erreur générale en virgule flottante . Voici une démo , où vous pouvez voir clairement que ce n'est pas seulement un json_encodeproblème
Machavity
39

En tant que développeur de plugins, je n'ai pas d'accès général aux paramètres php.ini d'un serveur. Donc, sur la base de la réponse de Machavity, j'ai écrit ce petit morceau de code que vous pouvez utiliser dans votre script PHP. Mettez-le simplement au-dessus du script et json_encode continuera à fonctionner comme d'habitude.

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

Dans certains cas, il est nécessaire de définir une autre variable. J'ajoute cela comme deuxième solution parce que je ne suis pas sûr que la deuxième solution fonctionne correctement dans tous les cas où la première solution s'est avérée efficace.

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
alev
la source
3
Faites attention, car votre plugin peut modifier des paramètres inattendus pour le reste de l'application de développement. Mais, OMI, je ne sais pas à quel point cette option pourrait être destructrice ... lol
igorsantos07
Sachez que la valeur de précision de changement (deuxième exemple) peut avoir un impact plus important sur d'autres opérations mathématiques que vous avez là-bas. php.net/manual/en/ini.core.php#ini.precision
Ricardo Martins
@RicardoMartins: Selon la documentation, la précision par défaut est de 14. Le correctif ci-dessus augmente cela à 17. Cela devrait donc être encore plus précis. Êtes-vous d'accord?
alev
@alev ce que je disais, c'est qu'en changeant juste la serialize_precision est suffisant et ne compromettez pas les autres comportements PHP que votre application peut rencontrer
Ricardo Martins
4

J'encodais des valeurs monétaires et j'avais des choses comme l' 330.46encodage 330.4600000000000363797880709171295166015625. Si vous ne souhaitez pas ou ne pouvez pas modifier les paramètres PHP et que vous connaissez à l'avance la structure des données, il existe une solution très simple qui a fonctionné pour moi. Il suffit de le convertir en une chaîne (les deux opérations suivantes font la même chose):

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

Pour mon cas d'utilisation, c'était une solution rapide et efficace. Notez simplement que cela signifie que lorsque vous le décodez à partir de JSON, ce sera une chaîne car elle sera entourée de guillemets doubles.

texelate
la source
4

J'ai résolu ce problème en définissant à la fois precision et serialize_precision sur la même valeur (10):

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

Vous pouvez également définir cela dans votre php.ini

what_sa
la source
3

J'ai eu le même problème mais seul serialize_precision = -1 n'a pas résolu le problème. J'ai dû faire une étape de plus, mettre à jour la valeur de précision de 14 à 17 (comme elle était définie sur mon fichier ini PHP7.0). Apparemment, changer la valeur de ce nombre change la valeur du flottant calculé.

Alin Pop
la source
3

Les autres solutions n'ont pas fonctionné pour moi. Voici ce que j'avais à ajouter au début de mon exécution de code:

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
Mike P. Sinn
la source
N'est-ce pas fondamentalement la même chose que la réponse d'Alin Pop?
igorsantos07
1

Quant à moi, le problème était lorsque JSON_NUMERIC_CHECK en tant que deuxième argument de json_encode () est passé, ce qui convertissait tous les types de nombres en int (pas seulement en entier)

Acuna
la source
1

Stockez-le sous forme de chaîne avec la précision exacte dont vous avez besoin en utilisant number_format, puis json_encodeen utilisant l' JSON_NUMERIC_CHECKoption:

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

Vous obtenez:

{"max": 472.185}

Notez que cela obtiendra TOUTES les chaînes numériques de votre objet source à encoder sous forme de nombres dans le JSON résultant.

pasqal
la source
1
J'ai testé cela dans PHP 7.3 et cela ne fonctionne pas (la sortie a toujours une précision trop élevée). Apparemment, le drapeau JSON_NUMERIC_CHECK est cassé depuis 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
la source
0

Il semble que le problème se produit lorsque serializeet serialize_precisionsont définis sur des valeurs différentes. Dans mon cas, 14 et 17 respectivement. Leur définition sur 14 a résolu le problème, tout comme la définition serialize_precisionde -1.

La valeur par défaut de a serialize_precision été changée en -1 à partir de PHP 7.1.0, ce qui signifie "un algorithme amélioré pour arrondir ces nombres sera utilisé". Mais si vous rencontrez toujours ce problème, c'est peut-être parce que vous avez un fichier de configuration PHP en place à partir d'une version antérieure. (Peut-être avez-vous conservé votre fichier de configuration lors de la mise à niveau?)

Une autre chose à considérer est s'il est logique d'utiliser des valeurs flottantes dans votre cas. Il peut être judicieux ou non d'utiliser des valeurs de chaîne contenant vos nombres pour garantir que le nombre approprié de décimales est toujours conservé dans votre JSON.

Commandant du code
la source
-1

Vous pouvez changer le [max] => 472.185 d'un float en une chaîne ([max] => '472.185') avant le json_encode (). Comme json est de toute façon une chaîne, la conversion de vos valeurs flottantes en chaînes avant json_encode () maintiendra la valeur souhaitée.

Everett Staley
la source
C'est techniquement vrai dans une certaine mesure, mais très inefficace. Si un Int / Float dans une chaîne JSON n'est pas entre guillemets, Javascript peut le voir comme un Int / Float réel. L'exécution de votre rendu vous oblige à convertir chaque valeur en un Int / Float une fois du côté du navigateur. J'avais souvent affaire à plus de 10000 valeurs lorsque je travaillais sur ce projet par demande. Une grande partie du traitement des ballonnements aurait fini par se produire.
Gwi7d31
Si vous utilisez JSON pour envoyer des données quelque part, et qu'un nombre est attendu mais que vous envoyez une chaîne, cela n'est pas garanti. Dans les situations où le développeur de l'application d'envoi n'a pas de contrôle sur l'application de réception, ce n'est pas une solution.
osullic