PHP7.1 json_encode () Masalah Float

93

Ini bukan pertanyaan karena lebih merupakan kewaspadaan. Saya memperbarui aplikasi yang menggunakan json_encode()PHP7.1.1 dan saya melihat masalah dengan float yang diubah terkadang melebihi 17 digit. Menurut dokumentasi, PHP 7.1.x mulai digunakan serialize_precisionsebagai pengganti presisi saat mengenkode nilai ganda. Saya menduga ini menyebabkan nilai contoh

472.185

untuk menjadi

472.18500000000006

setelah nilai itu masuk json_encode(). Sejak penemuan saya, saya telah kembali ke PHP 7.0.16 dan saya tidak lagi memiliki masalah dengan json_encode(). Saya juga mencoba memperbarui ke PHP 7.1.2 sebelum kembali ke PHP 7.0.16.

Alasan di balik pertanyaan ini memang berasal dari PHP - Floating Number Precision , namun pada akhirnya semua alasan untuk ini adalah karena perubahan dari penggunaan presisi ke serialize_precision di json_encode().

Jika ada yang tahu solusi untuk masalah ini, saya akan dengan senang hati mendengarkan alasan / perbaikannya.

Kutipan dari array multidimensi (sebelum):

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

                    )

dan setelah melalui json_encode()...

"staticYaxisInfo":
            {
                "17":
                {
                    "variable_id": "17",
                    "static": "1",
                    "min": 0,
                    "max": 472.18500000000006,
                    "locked_static": "1"
                }
            },
Gwi7d31
sumber
6
ini_set('serialize_precision', 14); ini_set('precision', 14);mungkin akan membuatnya berseri seperti dulu, namun jika Anda benar-benar mengandalkan presisi tertentu pada float Anda, Anda melakukan sesuatu yang salah.
apokryfos
1
"Jika ada yang tahu solusi untuk masalah ini" - masalah apa? Saya tidak melihat ada masalah di sini. Jika Anda mendekode JSON menggunakan PHP, Anda mendapatkan kembali nilai yang Anda encode. Dan jika Anda mendekodekannya menggunakan bahasa yang berbeda kemungkinan besar Anda mendapatkan nilai yang sama. Bagaimanapun juga, jika Anda mencetak nilai dengan 12 digit Anda mendapatkan kembali nilai asli ("benar"). Apakah Anda memerlukan presisi lebih dari 12 digit desimal untuk float yang digunakan oleh aplikasi Anda?
aksiak
12
@axiac 472.185! = 472.18500000000006. Ada perbedaan sebelum dan sesudah yang jelas. Ini adalah bagian dari permintaan AJAX ke browser dan nilainya harus tetap dalam keadaan aslinya.
Gwi7d31
4
Saya mencoba untuk menghindari penggunaan konversi string karena produk akhirnya adalah Highchart dan tidak akan menerima string. Saya pikir saya akan menganggapnya sangat tidak efisien dan ceroboh jika Anda mengambil nilai float, melemparkannya sebagai string, mengirimkannya, dan kemudian meminta javascript menafsirkan string kembali ke float dengan parseFloat (). Bukan?
Gwi7d31
1
@axiac Saya perhatikan bahwa Anda PHP json_decode () tidak mengembalikan nilai float asli. Namun, ketika javascript mengubah string JSON kembali ke objek, itu tidak mengubah nilai kembali ke 472.185 seperti Anda berpotensi menyisipkan ... maka masalahnya. Saya akan tetap berpegang pada apa yang saya lakukan.
Gwi7d31

Jawaban:

97

Ini membuat saya sedikit gila sampai akhirnya saya menemukan bug ini yang mengarahkan Anda ke RFC yang mengatakan

Saat ini json_encode()menggunakan EG (precision) yang diset ke 14. Artinya paling banyak 14 digit yang digunakan untuk menampilkan (mencetak) nomor tersebut. IEEE 754 double mendukung presisi yang lebih tinggi dan serialize()/ var_export()menggunakan PG (serialize_precision) yang disetel ke 17 agar lebih tepat. Sejak json_encode()menggunakan EG (presisi), json_encode()menghapus digit yang lebih rendah dari bagian pecahan dan menghancurkan nilai asli bahkan jika float PHP dapat menampung nilai float yang lebih tepat.

Dan (penekanan saya)

RFC ini mengusulkan untuk memperkenalkan pengaturan baru EG (presisi) = - 1 dan PG (serialize_precision) = - 1 yang menggunakan mode 0 zend_dtoa () yang menggunakan algoritme yang lebih baik untuk membulatkan bilangan mengambang (-1 digunakan untuk menunjukkan mode 0) .

Singkatnya, ada cara baru untuk membuat PHP 7.1 json_encodemenggunakan mesin presisi yang baru dan lebih baik. Di php.ini Anda perlu mengubah serialize_precisionke

serialize_precision = -1

Anda dapat memverifikasi bahwa itu berfungsi dengan baris perintah ini

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

Kamu harus mendapatkan

{"price":45.99}
Machavity
sumber
G(precision)=-1dan PG(serialize_precision)=-1 juga dapat digunakan di PHP 5,4
kittygirl
1
Berhati-hatilah serialize_precision = -1. Dengan -1, kode ini echo json_encode([528.56 * 100]);mencetak[52855.99999999999]
vl.lapikov
3
@ vl.lapikov Kedengarannya lebih seperti kesalahan floating point umum . Berikut adalah demo , di mana Anda dapat melihatnya dengan jelas bukan hanya json_encodemasalah
Machavity
39

Sebagai pengembang plugin, saya tidak memiliki akses umum ke pengaturan php.ini server. Jadi, berdasarkan jawaban Machavity saya menulis potongan kecil kode ini yang dapat Anda gunakan dalam skrip PHP Anda. Letakkan saja di atas skrip dan json_encode akan tetap berfungsi seperti biasa.

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

Dalam beberapa kasus, perlu menetapkan satu variabel lagi. Saya menambahkan ini sebagai solusi kedua karena saya tidak yakin apakah solusi kedua berfungsi dengan baik dalam semua kasus di mana solusi pertama terbukti berhasil.

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
alev
sumber
3
Berhati-hatilah dengan itu, karena plugin Anda mungkin mengubah pengaturan yang tidak terduga untuk aplikasi pengembang lainnya. Tapi, IMO, saya tidak yakin seberapa destruktif opsi ini bisa ... lol
igorsantos07
Sadarilah bahwa nilai presisi perubahan (contoh kedua) dapat berdampak lebih besar dalam operasi matematika lain yang Anda miliki di sana. php.net/manual/en/ini.core.php#ini.precision
Ricardo Martins
@RicardoMartins: Sesuai dokumentasi, presisi defaultnya adalah 14. Perbaikan di atas meningkatkan ini menjadi 17. Jadi, seharusnya lebih tepat. Apa kamu setuju?
alev
@alev apa yang saya katakan adalah bahwa dengan mengubah hanya serialize_precision sudah cukup dan tidak membahayakan perilaku PHP lain yang mungkin dialami aplikasi Anda
Ricardo Martins
4

Saya menyandikan nilai moneter dan memiliki hal-hal seperti 330.46penyandian 330.4600000000000363797880709171295166015625. Jika Anda tidak ingin, atau tidak bisa, mengubah pengaturan PHP dan Anda mengetahui struktur datanya sebelumnya, ada solusi yang sangat sederhana yang berhasil untuk saya. Cukup lemparkan ke string (kedua hal berikut melakukan hal yang sama):

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

Untuk kasus penggunaan saya, ini adalah solusi yang cepat dan efektif. Perhatikan bahwa ini berarti ketika Anda mendekodekannya kembali dari JSON, itu akan menjadi string karena akan dibungkus dengan tanda kutip ganda.

texelate
sumber
4

Saya menyelesaikan ini dengan mengatur presisi dan serialize_precision ke nilai yang sama (10):

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

Anda juga dapat mengatur ini di php.ini Anda

terserah_sa
sumber
3

Saya memiliki masalah yang sama tetapi hanya serialize_precision = -1 yang tidak menyelesaikan masalah. Saya harus melakukan satu langkah lagi, untuk memperbarui nilai presisi dari 14 menjadi 17 (seperti yang ditetapkan pada file ini PHP7.0 saya). Rupanya, mengubah nilai angka itu mengubah nilai float yang dihitung.

Alin Pop
sumber
3

Solusi lain tidak berhasil untuk saya. Inilah yang harus saya tambahkan di awal eksekusi kode saya:

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
Mike P. Sinn
sumber
Bukankah ini pada dasarnya sama dengan jawaban Alin Pop?
igorsantos07
1

Bagi saya masalahnya adalah ketika JSON_NUMERIC_CHECK sebagai argumen kedua json_encode () berlalu, yang mentransmisikan semua jenis angka ke int (tidak hanya integer)

Acuna
sumber
1

Simpan sebagai string dengan presisi yang Anda butuhkan dengan menggunakan number_format, lalu json_encodegunakan JSON_NUMERIC_CHECKopsi:

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

Anda mendapatkan:

{"max": 472.185}

Perhatikan bahwa ini akan membuat SEMUA string numerik di objek sumber Anda dienkode sebagai angka dalam JSON yang dihasilkan.

pasqal
sumber
1
Saya telah menguji ini di PHP 7.3 dan tidak berhasil (keluaran masih memiliki presisi yang terlalu tinggi). Rupanya bendera JSON_NUMERIC_CHECK rusak sejak 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
sumber
0

Sepertinya masalah terjadi ketika serializedan serialize_precisiondiatur ke nilai yang berbeda. Dalam kasus saya masing-masing 14 dan 17. Mengatur keduanya ke 14 menyelesaikan masalah, seperti halnya pengaturanserialize_precision ke -1.

Nilai default serialize_precision diubah menjadi -1 pada PHP 7.1.0 yang berarti "algoritma yang disempurnakan untuk pembulatan angka-angka seperti itu akan digunakan". Tetapi jika Anda masih mengalami masalah ini, itu mungkin karena Anda memiliki file konfigurasi PHP dari versi sebelumnya. (Mungkin Anda menyimpan file konfigurasi Anda saat meningkatkan?)

Hal lain yang perlu dipertimbangkan adalah jika masuk akal untuk menggunakan nilai float sama sekali dalam kasus Anda. Mungkin masuk akal atau tidak untuk menggunakan nilai string yang berisi angka Anda untuk memastikan jumlah tempat desimal yang tepat selalu dipertahankan di JSON Anda.

Code Commander
sumber
-1

Anda bisa mengubah [max] => 472.185 dari float menjadi string ([max] => '472.185') sebelum json_encode (). Karena json adalah string, mengonversi nilai float Anda ke string sebelum json_encode () akan mempertahankan nilai yang Anda inginkan.

Everett Staley
sumber
Ini secara teknis benar sampai batas tertentu, tetapi sangat tidak efisien. Jika Int / Float dalam string JSON tidak dikutip, maka Javascript dapat melihatnya sebagai Int / Float yang sebenarnya. Melakukan rendisi memaksa Anda untuk mengembalikan setiap nilai ke Int / Float satu kali di sisi browser. Saya sering berurusan dengan 10.000+ nilai saat mengerjakan proyek ini per permintaan. Banyak pemrosesan mengasapi akhirnya akan terjadi.
Gwi7d31
Jika Anda menggunakan JSON untuk mengirim data ke suatu tempat, dan sebuah angka diharapkan tetapi Anda mengirim string, itu tidak dijamin akan berhasil. Dalam situasi di mana pengembang aplikasi pengirim tidak memiliki kendali atas aplikasi penerima, ini bukanlah solusi.
osullic