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 - Floating Number Precisionอย่างไรก็ตามสาเหตุทั้งหมดนี้เป็นเพราะการเปลี่ยนแปลงจากความแม่นยำเป็นการใช้งาน 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"
                }
            },

6
ini_set('serialize_precision', 14); ini_set('precision', 14);อาจจะทำให้มันเป็นอนุกรมเหมือนที่เคยทำมา แต่ถ้าคุณอาศัยความแม่นยำเฉพาะในการลอยตัวคุณกำลังทำอะไรผิดพลาด
apokryfos

1
"ถ้าใครไม่รู้วิธีแก้ปัญหานี้" - ปัญหาอะไร? ฉันไม่เห็นปัญหาใด ๆ ที่นี่ หากคุณถอดรหัส JSON โดยใช้ PHP คุณจะได้รับค่าที่เข้ารหัสกลับคืนมา และถ้าคุณถอดรหัสโดยใช้ภาษาอื่นส่วนใหญ่คุณจะได้รับค่าเดียวกัน ไม่ว่าจะด้วยวิธีใดก็ตามหากคุณพิมพ์ค่าด้วยตัวเลข 12 หลักคุณจะได้ค่าดั้งเดิม ("ถูกต้อง") กลับคืนมา คุณต้องการความแม่นยำของทศนิยมมากกว่า 12 หลักสำหรับการลอยตัวที่แอปพลิเคชันของคุณใช้หรือไม่?
axiac

12
@axiac 472.185! = 472.18500000000006. มีความแตกต่างก่อนและหลังอย่างชัดเจนนี่เป็นส่วนหนึ่งของคำขอ AJAX ไปยังเบราว์เซอร์และค่าจะต้องอยู่ในสถานะดั้งเดิม
Gwi7d31

4
ฉันพยายามหลีกเลี่ยงการใช้การแปลงสตริงเนื่องจากผลิตภัณฑ์สุดท้ายคือ Highcharts และจะไม่ยอมรับสตริง ฉันคิดว่าฉันคิดว่ามันไม่มีประสิทธิภาพและเลอะเทอะมากถ้าคุณใช้ค่า float โยนเป็นสตริงส่งออกไปจากนั้นให้ javascript ตีความสตริงกลับไปเป็น float ด้วย parseFloat () คุณไม่?
Gwi7d31

1
@axiac ฉันทราบว่าคุณเป็น PHP json_decode () จะนำค่า float เดิมกลับมา อย่างไรก็ตามเมื่อ 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 (ความแม่นยำ) = - 1 และPG (serialize_precision) = - 1 ที่ใช้โหมด zend_dtoa () 0 ซึ่งใช้ algorigthm ที่ดีกว่าสำหรับการปัดเศษตัวเลขลอย (-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
Ricardo Martins

@RicardoMartins: ตามเอกสารความแม่นยำเริ่มต้นคือ 14 การแก้ไขด้านบนจะเพิ่มค่านี้เป็น 17 ดังนั้นจึงควรแม่นยำยิ่งขึ้น คุณเห็นด้วยหรือไม่?
Alev

@alev สิ่งที่ฉันพูดคือการเปลี่ยนเพียง serialize_precision ก็เพียงพอแล้วและอย่าประนีประนอมพฤติกรรม PHP อื่น ๆ ที่แอปพลิเคชันของคุณอาจประสบ
Ricardo Martins

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 ของคุณ


3

ฉันมีปัญหาเดียวกัน แต่มีเพียง serialize_precision = -1 เท่านั้นที่ไม่สามารถแก้ปัญหาได้ ฉันต้องทำอีกขั้นตอนหนึ่งเพื่ออัปเดตค่าความแม่นยำจาก 14 เป็น 17 (เนื่องจากตั้งค่าไว้ในไฟล์ PHP7.0 ini ของฉัน) เห็นได้ชัดว่าการเปลี่ยนค่าของตัวเลขนั้นจะเปลี่ยนค่าของทศนิยมที่คำนวณได้


3

วิธีแก้ปัญหาอื่น ๆ ไม่ได้ผลสำหรับฉัน นี่คือสิ่งที่ฉันต้องเพิ่มเมื่อเริ่มต้นการเรียกใช้โค้ด:

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

โดยพื้นฐานแล้วนี่ไม่เหมือนกับคำตอบของ Alin Pop ใช่หรือไม่?
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
Philipp


0

ดูเหมือนว่าปัญหาจะเกิดขึ้นเมื่อใดserializeและserialize_precisionถูกตั้งค่าเป็นค่าอื่น ในกรณีของฉัน 14 และ 17 ตามลำดับ การตั้งค่าทั้งสองเป็น 14 ช่วยแก้ปัญหาได้เช่นเดียวกับการตั้งค่าserialize_precisionเป็น -1

ค่าเริ่มต้นของserialize_precision เปลี่ยนเป็น -1 เมื่อ PHP 7.1.0ซึ่งหมายความว่า "จะใช้อัลกอริทึมขั้นสูงสำหรับการปัดเศษตัวเลขดังกล่าว" แต่ถ้าคุณยังคงประสบปัญหานี้อาจเป็นเพราะคุณมีไฟล์ config PHP จากเวอร์ชันก่อนหน้า (บางทีคุณอาจเก็บไฟล์กำหนดค่าไว้เมื่อคุณอัปเกรด)

สิ่งที่ควรพิจารณาอีกประการหนึ่งคือการใช้ค่าลอยตัวในกรณีของคุณเหมาะสมหรือไม่ อาจหรือไม่สมเหตุสมผลที่จะใช้ค่าสตริงที่มีตัวเลขของคุณเพื่อให้แน่ใจว่าจำนวนตำแหน่งทศนิยมที่เหมาะสมจะยังคงอยู่ใน JSON ของคุณเสมอ


-1

คุณสามารถเปลี่ยน [max] => 472.185 จาก float เป็นสตริง ([max] => '472.185') ก่อน json_encode () เนื่องจาก json เป็นสตริงการแปลงค่า float ของคุณเป็นสตริงก่อน json_encode () จะรักษาค่าที่คุณต้องการ


นี่เป็นความจริงทางเทคนิคในระดับหนึ่ง แต่ไม่มีประสิทธิภาพมาก ถ้า Int / Float ในสตริง JSON ไม่ได้ถูกยกมา Javascript จะเห็นว่าเป็น Int / Float จริง การแสดงผลของคุณบังคับให้คุณส่งทุกค่ากลับไปเป็น Int / Float หนึ่งครั้งทางด้านเบราว์เซอร์ ฉันมักจะจัดการกับค่ามากกว่า 10,000 รายการเมื่อทำงานในโครงการนี้ต่อคำขอ กระบวนการขยายตัวจำนวนมากจะเกิดขึ้น
Gwi7d31

หากคุณใช้ JSON เพื่อส่งข้อมูลที่ใดที่หนึ่งและคาดว่าจะมีตัวเลข แต่คุณส่งสตริงจะไม่รับประกันว่าจะใช้งานได้ ในสถานการณ์ที่ผู้พัฒนาแอปพลิเคชันการส่งไม่สามารถควบคุมแอปพลิเคชันรับได้นี่ไม่ใช่วิธีแก้ปัญหา
osullic
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.