PHP7.1 json_encode () Problema de flutuação

98

Esta não é uma pergunta, pois é mais uma questão de estar ciente. Atualizei um aplicativo que usa json_encode()para PHP7.1.1 e estava vendo um problema com flutuações sendo alteradas para, às vezes, estender para 17 dígitos. De acordo com a documentação, o PHP 7.1.x passou a usar ao serialize_precisioninvés da precisão ao codificar valores duplos. Estou supondo que isso causou um valor de exemplo de

472,185

tornar-se

472.18500000000006

depois que esse valor passou json_encode(). Desde minha descoberta, voltei para o PHP 7.0.16 e não tenho mais problemas com o json_encode(). Também tentei atualizar para o PHP 7.1.2 antes de voltar para o PHP 7.0.16.

O raciocínio por trás desta questão deriva de PHP - Floating Number Precision , no entanto, a razão final para isso é por causa da mudança do uso de precisão para serialize_precision em json_encode().

Se alguém souber de uma solução para este problema, ficaria mais do que feliz em ouvir o raciocínio / correção.

Trecho da matriz multidimensional (antes):

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

                    )

e depois de passar json_encode()...

"staticYaxisInfo":
            {
                "17":
                {
                    "variable_id": "17",
                    "static": "1",
                    "min": 0,
                    "max": 472.18500000000006,
                    "locked_static": "1"
                }
            },
Gwi7d31
fonte
6
ini_set('serialize_precision', 14); ini_set('precision', 14);provavelmente faria com que fosse serializado como costumava fazer, no entanto, se você realmente confia em uma precisão específica em seus flutuadores, está fazendo algo errado.
apokryfos
1
"Se alguém souber de uma solução para este problema" - que problema? Não vejo nenhum problema aqui. Se você decodificar o JSON usando PHP, você receberá de volta o valor codificado. E se você decodificá-lo usando um idioma diferente, provavelmente obterá o mesmo valor. De qualquer forma, se você imprimir o valor com 12 dígitos, você receberá de volta o valor original ("correto"). Você precisa de mais de 12 dígitos decimais de precisão para os flutuadores usados ​​por seu aplicativo?
axiac
12
@axiac 472,185! = 472,18500000000006. Há uma diferença clara entre o antes e o depois. Isso faz parte de uma solicitação AJAX para um navegador e o valor precisa permanecer em seu estado original.
Gwi7d31
4
Estou tentando evitar o uso de uma conversão de string porque o produto final é Highcharts e não aceita strings. Acho que consideraria muito ineficiente e descuidado se você pegar um valor float, convertê-lo como uma string, enviá-lo embora e, em seguida, fazer com que o javascript interprete a string de volta para um float com parseFloat (). Não é?
Gwi7d31
1
@axiac Eu noto que você é PHP json_decode () traz de volta o valor float original. No entanto, quando o javascript transforma a string JSON de volta em um objeto, ele não converte o valor de volta para 472,185 como você potencialmente insinuou ... daí o problema. Eu vou continuar com o que estou fazendo.
Gwi7d31

Respostas:

101

Isso me deixou louco por um tempo, até que finalmente encontrei este bug que aponta para este RFC que diz

Atualmente json_encode()usa EG (precisão), que é definida como 14. Isso significa que 14 dígitos no máximo são usados ​​para exibir (imprimir) o número. IEEE 754 double suporta maior precisão e serialize()/ var_export()usa PG (serialize_precision) que definido como 17 como padrão para ser mais preciso. Uma vez que json_encode()usa EG (precisão), json_encode()remove os dígitos mais baixos das partes fracionárias e destrói o valor original, mesmo se o float do PHP pudesse conter um valor float mais preciso.

E (ênfase minha)

Este RFC propõe a introdução de uma nova configuração EG (precisão) = - 1 e PG (serialize_precision) = - 1 que usa o modo 0 de zend_dtoa () que usa um algoritmo melhor para arredondar números flutuantes (-1 é usado para indicar o modo 0) .

Resumindo, há uma nova maneira de fazer o PHP 7.1 json_encodeusar o novo e aprimorado mecanismo de precisão. No php.ini você precisa mudar serialize_precisionpara

serialize_precision = -1

Você pode verificar se funciona com esta linha de comando

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

Voce deveria pegar

{"price":45.99}
Machavity
fonte
G(precision)=-1e PG(serialize_precision)=-1 também pode ser usado no PHP 5.4
kittygirl
1
Tenha cuidado com serialize_precision = -1. Com -1, este código é echo json_encode([528.56 * 100]);impresso[52855.99999999999]
vl.lapikov
3
@ vl.lapikov Isso soa mais como um erro geral de ponto flutuante . Aqui está uma demonstração , onde você pode ver claramente que não é apenas um json_encodeproblema
Machavity
41

Como desenvolvedor de plugin, não tenho acesso geral às configurações do php.ini de um servidor. Então, com base na resposta de Machavity, escrevi este pequeno código que você pode usar em seu script PHP. Basta colocá-lo no topo do script e json_encode continuará funcionando normalmente.

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

Em alguns casos, é necessário definir mais uma variável. Estou adicionando isso como uma segunda solução porque não tenho certeza se a segunda solução funciona bem em todos os casos em que a primeira solução provou funcionar.

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
alev
fonte
3
Tome cuidado com isso, pois seu plug-in pode alterar configurações inesperadas para o restante do aplicativo de desenvolvedor. Mas, IMO, não tenho certeza de como essa opção pode ser destrutiva ... lol
igorsantos07
Esteja ciente de que alterar o valor de precisão (segundo exemplo) pode ter um impacto maior em outras operações matemáticas que você tenha lá. php.net/manual/en/ini.core.php#ini.precision
Ricardo Martins
@RicardoMartins: De acordo com a documentação a precisão padrão é 14. A correção acima aumenta para 17. Portanto, deve ser ainda mais precisa. Você concorda?
alev
@alev o que eu estava dizendo é que mudar apenas serialize_precision é o suficiente e não compromete outros comportamentos PHP que sua aplicação pode experimentar
Ricardo Martins
6

Resolvi isso definindo precisão e serialize_precision com o mesmo valor (10):

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

Você também pode definir isso no seu php.ini

seja o que for
fonte
4

Eu estava codificando valores monetários e tinha coisas como 330.46codificação para 330.4600000000000363797880709171295166015625. Se você não deseja ou não pode alterar as configurações do PHP e conhece a estrutura dos dados com antecedência, existe uma solução muito simples que funcionou para mim. Basta lançá-lo em uma corda (ambos fazem a mesma coisa):

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

Para o meu caso de uso, essa foi uma solução rápida e eficaz. Observe que isso significa que, quando você decodificá-lo de volta do JSON, será uma string, pois estará entre aspas duplas.

texelate
fonte
3

Eu tive o mesmo problema, mas apenas serialize_precision = -1 não resolveu o problema. Tive que fazer mais um passo, para atualizar o valor de precisão de 14 para 17 (como estava definido no meu arquivo PHP7.0 ini). Aparentemente, alterar o valor desse número altera o valor do float calculado.

Alin Pop
fonte
3

As outras soluções não funcionaram para mim. Aqui está o que eu tive que adicionar no início da minha execução de código:

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
Mike P. Sinn
fonte
Não é basicamente o mesmo que a resposta de Alin Pop?
igorsantos07
1

Quanto a mim, o problema foi quando JSON_NUMERIC_CHECK como segundo argumento de json_encode () passou, o que converte todos os tipos de números em int (não apenas inteiro)

Acuna
fonte
1

Armazene-o como uma string com a precisão exata de que você precisa usando e number_format, em seguida json_encode, use a JSON_NUMERIC_CHECKopção:

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

Você obtém:

{"max": 472.185}

Observe que isso fará com que TODAS as strings numéricas em seu objeto de origem sejam codificadas como números no JSON resultante.

pasqal
fonte
1
Eu testei isso no PHP 7.3 e não funciona (a saída ainda tem uma precisão muito alta). Aparentemente, a sinalização JSON_NUMERIC_CHECK está quebrada desde o 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
fonte
0

Parece que o problema ocorre quando serializee serialize_precisionsão definidos com valores diferentes. No meu caso, 14 e 17, respectivamente. Definir ambos como 14 resolveu o problema, assim como definir serialize_precisioncomo -1.

O valor padrão de serialize_precision foi alterado para -1 a partir do PHP 7.1.0, o que significa "um algoritmo aprimorado para arredondar esses números será usado". Mas se você ainda estiver enfrentando esse problema, pode ser porque você tem um arquivo de configuração do PHP de uma versão anterior. (Talvez você tenha mantido seu arquivo de configuração quando atualizou?)

Outra coisa a considerar é se faz sentido usar valores flutuantes no seu caso. Pode ou não fazer sentido usar valores de string contendo seus números para garantir que o número adequado de casas decimais seja sempre mantido em seu JSON.

Code Commander
fonte
-1

Você poderia mudar o [max] => 472.185 de um float para uma string ([max] => '472.185') antes do json_encode (). Como json é uma string de qualquer maneira, converter seus valores float em strings antes de json_encode () manterá o valor que você deseja.

Everett Staley
fonte
Isso é tecnicamente verdadeiro até certo ponto, mas muito ineficiente. Se um Int / Float em uma string JSON não estiver entre aspas, o Javascript pode vê-lo como um Int / Float real. Executar sua renderização força você a lançar cada valor de volta para um Int / Float uma vez no lado do navegador. Freqüentemente, lidava com mais de 10000 valores ao trabalhar neste projeto por solicitação. Muito processamento de inchaço teria acontecido.
Gwi7d31
Se você estiver usando JSON para enviar dados para algum lugar, e um número for esperado, mas você enviar uma string, não é garantido que funcione. Em situações em que o desenvolvedor do aplicativo de envio não tem controle sobre o aplicativo de recebimento, isso não é uma solução.
osullic