PHP7.1 json_encode () Проблема с плавающей точкой

93

Это не вопрос, так как нужно знать. Я обновил приложение, использующее json_encode()PHP7.1.1, и обнаружил проблему с изменением числа с плавающей запятой, которое иногда увеличивалось до 17 цифр. Согласно документации, PHP 7.1.x начал использовать serialize_precisionвместо точности при кодировании двойных значений. Я предполагаю, что это вызвало примерное значение

472,185

стать

472.18500000000006

после того, как это значение прошло json_encode(). С момента своего открытия я вернулся к PHP 7.0.16, и у меня больше нет проблем json_encode(). Я также пробовал обновиться до PHP 7.1.2, прежде чем вернуться к PHP 7.0.16.

Обоснование этого вопроса действительно связано с PHP - точность плавающих чисел , однако в конечном итоге все причины этого переходом от точности к использованию serialize_precision в json_encode().

Если кто-нибудь знает решение этой проблемы, я буду более чем счастлив выслушать рассуждения / исправления.

Выдержка из многомерного массива (ранее):

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

                    )

и после прохождения json_encode()...

"staticYaxisInfo":
            {
                "17":
                {
                    "variable_id": "17",
                    "static": "1",
                    "min": 0,
                    "max": 472.18500000000006,
                    "locked_static": "1"
                }
            },
Gwi7d31
источник
6
ini_set('serialize_precision', 14); ini_set('precision', 14);вероятно, заставит его сериализоваться, как раньше, однако, если вы действительно полагаетесь на определенную точность своих поплавков, вы делаете что-то не так.
apokryfos
1
«Если кто знает решение этой проблемы» - какая проблема? Я не вижу здесь никаких проблем. Если вы декодируете JSON с помощью PHP, вы вернете закодированное значение. И если вы декодируете его на другом языке, скорее всего, вы получите такое же значение. В любом случае, если вы напечатаете значение из 12 цифр, вы получите исходное («правильное») значение. Вам нужна точность более 12 десятичных знаков для чисел с плавающей запятой, используемых вашим приложением?
аксиак
12
@axiac 472.185! = 472.18500000000006. Существует явная разница до и после. Это часть запроса AJAX к браузеру, и значение должно оставаться в исходном состоянии.
Gwi7d31
4
Я стараюсь избегать преобразования строк, поскольку конечным продуктом является Highcharts, и он не принимает строки. Я думаю, что я считаю очень неэффективным и небрежным, если вы возьмете значение с плавающей запятой, преобразуете его как строку, отправите ее, а затем заставите javascript интерпретировать строку обратно в число с плавающей точкой с помощью parseFloat (). Не так ли?
Gwi7d31
1
@axiac Замечу, что вы PHP json_decode () действительно возвращает исходное значение с плавающей запятой. Однако, когда javascript превращает строку JSON обратно в объект, он не преобразует значение обратно в 472,185, как вы могли предположить ... отсюда и проблема. Я буду придерживаться того, что у меня есть.
Gwi7d31

Ответы:

97

Это немного свело меня с ума, пока я наконец не нашел эту ошибку, которая указывает вам на этот RFC, в котором говорится

В настоящее время json_encode()используется EG (точность), равная 14. Это означает, что для отображения (печати) числа используются не более 14 цифр. IEEE 754 double поддерживает более высокую точность и serialize()/ var_export()использует PG (serialize_precision), для большей точности по умолчанию установлено 17. Поскольку json_encode()использует EG (точность), json_encode()удаляет младшие разряды дробных частей и уничтожает исходное значение, даже если в PHP может храниться более точное значение с плавающей запятой.

И (выделено мной)

В этом RFC предлагается ввести новый параметр EG (precision) = - 1 и PG (serialize_precision) = - 1, который использует режим 0 zend_dtoa (), который использует лучший алгоритм для округления чисел с плавающей запятой (-1 используется для обозначения режима 0) .

Короче говоря, есть новый способ заставить PHP 7.1 json_encodeиспользовать новый улучшенный механизм точности. В php.ini вам нужно изменить serialize_precisionна

serialize_precision = -1

Вы можете убедиться, что он работает с этой командной строкой

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

Ты должен получить

{"price":45.99}
Мачавити
источник
G(precision)=-1а PG(serialize_precision)=-1 также может использоваться в PHP 5.4
kittygirl
1
Будьте осторожны с serialize_precision = -1. С -1 этот код echo json_encode([528.56 * 100]);печатает[52855.99999999999]
vl.lapikov
3
@ vl.lapikov Это больше похоже на общую ошибку с плавающей запятой . Вот демонстрация , из которой видно, что это не просто json_encodeпроблема
Machavity
39

Как разработчик плагинов у меня нет общего доступа к настройкам php.ini сервера. Итак, основываясь на ответе Machavity, я написал этот небольшой фрагмент кода, который вы можете использовать в своем PHP-скрипте. Просто поместите его поверх скрипта, и json_encode продолжит работать как обычно.

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

В некоторых случаях необходимо установить еще одну переменную. Я добавляю это как второе решение, потому что я не уверен, что второе решение работает нормально во всех случаях, когда первое решение оказалось эффективным.

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
алев
источник
3
Позаботьтесь об этом, так как ваш плагин может неожиданно изменить настройки для остальной части приложения разработчика. Но, IMO, я не уверен, насколько разрушительным может стать этот вариант ... lol
igorsantos07
Имейте в виду, что изменение значения точности (второй пример) может иметь большее влияние на другие математические операции, которые у вас есть. php.net/manual/en/ini.core.php#ini.precision
Рикардо Мартинс
@RicardoMartins: Согласно документации точность по умолчанию равна 14. Вышеуказанное исправление увеличивает ее до 17. Так что она должна быть еще более точной. Ты согласен?
алев
@alev то, что я сказал, это то, что достаточно изменить только serialize_precision и не ставить под угрозу другое поведение PHP, с которым может столкнуться ваше приложение
Рикардо Мартинс
4

Я кодировал денежные значения и имел такие вещи, как 330.46кодирование 330.4600000000000363797880709171295166015625. Если вы не хотите или не можете изменять настройки PHP и заранее знаете структуру данных, у меня есть очень простое решение. Просто преобразуйте его в строку (оба следующих элемента делают то же самое):

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

Для моего случая использования это было быстрое и эффективное решение. Просто обратите внимание, что это означает, что когда вы декодируете его обратно из JSON, это будет строка, поскольку она будет заключена в двойные кавычки.

текселат
источник
4

Я решил это, установив для точности и serialize_precision одно и то же значение (10):

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

Вы также можете установить это в своем php.ini

what_sa
источник
3

У меня была такая же проблема, но только serialize_precision = -1 не решила проблему. Мне пришлось сделать еще один шаг, чтобы обновить значение точности с 14 до 17 (как это было установлено в моем ini-файле PHP7.0). Очевидно, изменение значения этого числа изменяет значение вычисляемого числа с плавающей запятой.

Алин Поп
источник
3

Другие решения не помогли мне. Вот что мне пришлось добавить в начале выполнения кода:

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
Майк П. Синн
источник
Разве это не то же самое, что ответ Алин Поп?
igorsantos07
1

Что касается меня, проблема заключалась в том, что JSON_NUMERIC_CHECK в качестве второго аргумента json_encode () передавался, который приводил все типы чисел к int (не только целочисленному)

Акуна
источник
1

Сохраните его как строку с точной точностью, которая вам нужна, используя number_format, а затем json_encodeиспользуйте JSON_NUMERIC_CHECKопцию:

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

Вы получаете:

{"max": 472.185}

Обратите внимание, что при этом ВСЕ числовые строки в исходном объекте будут закодированы как числа в результирующем JSON.

паскаль
источник
1
Я тестировал это в PHP 7.3, и он не работает (вывод все еще имеет слишком высокую точность). По-видимому, флаг JSON_NUMERIC_CHECK не работает с PHP 7.1 - php.net/manual/de/json.constants.php#123167
Филипп
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
}
Б. Асселин
источник
0

Похоже, проблема возникает, когда serializeи serialize_precisionустановлены разные значения. В моем случае 14 и 17 соответственно. Установка их обоих на 14 решила проблему, как и установкаserialize_precision на -1.

Значение по умолчанию serialize_precision было изменено на -1 в PHP 7.1.0, что означает «будет использоваться улучшенный алгоритм округления таких чисел». Но если вы все еще сталкиваетесь с этой проблемой, это может быть связано с тем, что у вас есть файл конфигурации PHP из предыдущей версии. (Может быть, вы сохранили свой файл конфигурации при обновлении?)

Еще одна вещь, которую следует учитывать, - имеет ли смысл вообще использовать значения с плавающей запятой в вашем случае. Может иметь смысл или не иметь смысла использовать строковые значения, содержащие ваши числа, чтобы гарантировать, что в вашем JSON всегда сохраняется правильное количество десятичных знаков.

Code Commander
источник
-1

Вы можете изменить [max] => 472.185 с числа с плавающей запятой на строку ([max] => '472.185') перед json_encode (). Поскольку json в любом случае является строкой, преобразование ваших значений с плавающей запятой в строки до того, как json_encode () сохранит желаемое значение.

Эверетт Стейли
источник
В определенной степени это технически верно, но очень неэффективно. Если Int / Float в строке JSON не заключен в кавычки, то Javascript может рассматривать его как фактический Int / Float. Выполнение вашего представления заставляет вас один раз привести каждое отдельное значение обратно к Int / Float на стороне браузера. При работе над этим проектом на запрос я часто имел дело с 10000+ значениями. В конечном итоге произошла бы большая обработка раздутой информации.
Gwi7d31
Если вы используете JSON для отправки куда-то данных, и ожидается число, но вы отправляете строку, это не гарантируется. В ситуациях, когда разработчик отправляющего приложения не имеет контроля над получающим приложением, это не решение.
osullic