PHP7.1 json_encode () Problem zmiennoprzecinkowy

92

To nie jest pytanie, ponieważ należy bardziej uważać. Zaktualizowałem aplikację, która używa json_encode()PHP7.1.1 i zauważyłem problem ze zmienianiem liczb zmiennoprzecinkowych, aby czasami wydłużały 17 cyfr. Zgodnie z dokumentacją, PHP 7.1.x zaczęło używać serialize_precisionzamiast precyzji przy kodowaniu podwójnych wartości. Domyślam się, że spowodowało to przykładową wartość

472,185

zostać

472.18500000000006

po przejściu tej wartości json_encode(). Od czasu mojego odkrycia wróciłem do PHP 7.0.16 i nie mam już problemu z json_encode(). Próbowałem też zaktualizować do PHP 7.1.2 przed powrotem do PHP 7.0.16.

Powód tego pytania pochodzi z PHP - Floating Number Precision , jednak ostateczny powód tego wszystkiego wynika ze zmiany precyzji na użycie serialize_precision w json_encode().

Jeśli ktoś zna rozwiązanie tego problemu, byłbym więcej niż szczęśliwy, mogąc wysłuchać uzasadnienia / poprawki.

Wyciąg z tablicy wielowymiarowej (przed):

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

                    )

a po przejściu json_encode()...

"staticYaxisInfo":
            {
                "17":
                {
                    "variable_id": "17",
                    "static": "1",
                    "min": 0,
                    "max": 472.18500000000006,
                    "locked_static": "1"
                }
            },
Gwi7d31
źródło
6
ini_set('serialize_precision', 14); ini_set('precision', 14);prawdopodobnie spowodowałoby to serializację, tak jak kiedyś, jednak jeśli naprawdę polegasz na określonej precyzji na swoich pływakach, robisz coś źle.
apokryfos
1
„Jeśli ktoś zna rozwiązanie tego problemu” - jaki problem? Nie widzę tutaj żadnego problemu. Jeśli zdekodujesz JSON za pomocą PHP, odzyskasz zakodowaną wartość. A jeśli zdekodujesz go przy użyciu innego języka, najprawdopodobniej otrzymasz tę samą wartość. Tak czy inaczej, jeśli wydrukujesz wartość z 12 cyframi, odzyskasz oryginalną („poprawną”) wartość. Czy potrzebujesz dokładności większej niż 12 cyfr dziesiętnych dla liczb zmiennoprzecinkowych używanych w aplikacji?
Axiac
12
@axiac 472.185! = 472.18500000000006. Istnieje wyraźna różnica przed i po.Jest to część żądania AJAX do przeglądarki i wartość musi pozostać w pierwotnym stanie.
Gwi7d31
4
Próbuję uniknąć używania konwersji ciągów, ponieważ produkt końcowy to Highcharts i nie akceptuje ciągów. Myślę, że uznałbym to za bardzo nieefektywne i niechlujne, jeśli weźmiesz wartość zmiennoprzecinkową, wyrzucisz ją jako ciąg, odeślesz, a następnie javascript zinterpretuje ciąg z powrotem na zmiennoprzecinkowy za pomocą parseFloat (). Prawda?
Gwi7d31
1
@axiac Zauważam, że jesteś PHP json_decode () przywraca oryginalną wartość float. Jednak gdy javascript zamienia ciąg JSON z powrotem na obiekt, nie konwertuje wartości z powrotem na 472.185, tak jak potencjalnie insynuowałeś ... stąd problem. Będę trzymał się tego, co zamierzam.
Gwi7d31

Odpowiedzi:

97

To doprowadziło mnie do szału, aż w końcu znalazłem ten błąd, który wskazuje na ten RFC, który mówi

Obecnie json_encode()używa EG (precyzja), która jest ustawiona na 14. Oznacza to, że do wyświetlania (drukowania) liczby używa się najwyżej 14 cyfr. IEEE 754 double obsługuje wyższą precyzję i serialize()/ var_export()używa PG (serialize_precision), który domyślnie jest ustawiony na 17, aby być bardziej precyzyjnym. Ponieważ json_encode()używa EG (precyzja), json_encode()usuwa mniejsze cyfry części ułamkowych i niszczy oryginalną wartość, nawet jeśli zmiennoprzecinkowa PHP może przechowywać dokładniejszą wartość zmiennoprzecinkową.

I (podkreślenie moje)

Ten dokument RFC proponuje wprowadzenie nowego ustawienia EG (precyzja) = - 1 i PG (serialize_precision) = - 1, które używa trybu 0 zend_dtoa (), który wykorzystuje lepszy algorytm do zaokrąglania liczb zmiennoprzecinkowych (-1 jest używane do wskazania trybu 0) .

Krótko mówiąc, istnieje nowy sposób wykorzystania w PHP 7.1 json_encodenowego i ulepszonego silnika precyzyjnego. W php.ini musisz zmienić serialize_precisionna

serialize_precision = -1

Możesz sprawdzić, czy działa z tym wierszem poleceń

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

Powinieneś wziąć

{"price":45.99}
Machavity
źródło
G(precision)=-1i PG(serialize_precision)=-1 może być również używany w PHP 5.4
kittygirl
1
Uważaj z serialize_precision = -1. Z -1, ten kod jest echo json_encode([528.56 * 100]);drukowany[52855.99999999999]
vl.lapikov
3
@ vl.lapikov To brzmi bardziej jak ogólny błąd zmiennoprzecinkowy . Oto demo , na którym widać wyraźnie, że to nie tylko json_encodeproblem
Machavity
38

Jako programista wtyczek nie mam ogólnego dostępu do ustawień php.ini serwera. Tak więc, w oparciu o odpowiedź Machavity, napisałem ten mały fragment kodu, którego możesz użyć w swoim skrypcie PHP. Po prostu umieść go na górze skryptu, a json_encode będzie działał jak zwykle.

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

W niektórych przypadkach konieczne jest ustawienie jeszcze jednej zmiennej. Dodam to jako drugie rozwiązanie, ponieważ nie jestem pewien, czy drugie rozwiązanie działa dobrze we wszystkich przypadkach, w których pierwsze rozwiązanie sprawdziło się.

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
alev
źródło
3
Uważaj z tym, ponieważ Twoja wtyczka może zmienić nieoczekiwane ustawienia dla reszty aplikacji deweloperskiej. Ale IMO, nie jestem pewien, jak destrukcyjna może być ta opcja ... lol
igorsantos 07
Pamiętaj, że zmiana wartości precyzji (drugi przykład) może mieć większy wpływ na inne operacje matematyczne, które tam masz. php.net/manual/en/ini.core.php#ini.precision
Ricardo Martins
@RicardoMartins: Zgodnie z dokumentacją, domyślna precyzja to 14. Powyższa poprawka zwiększa ją do 17. Więc powinno być jeszcze bardziej precyzyjne. Czy sie zgadzasz?
alev
@alev powiedziałem, że wystarczy zmienić tylko parametr serialize_precision i nie naruszyć innych zachowań PHP, których może doświadczyć Twoja aplikacja
Ricardo Martins
4

Kodowałem wartości pieniężne i miałem takie rzeczy, jak 330.46kodowanie 330.4600000000000363797880709171295166015625. Jeśli nie chcesz lub nie możesz zmienić ustawień PHP i znasz strukturę danych z góry, istnieje bardzo proste rozwiązanie, które działało dla mnie. Po prostu rzuć to na ciąg (oba poniższe robią to samo):

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

W moim przypadku było to szybkie i skuteczne rozwiązanie. Zwróć uwagę, że oznacza to, że kiedy dekodujesz go z powrotem z JSON, będzie to ciąg znaków, ponieważ będzie zawinięty w podwójne cudzysłowy.

texelate
źródło
4

Rozwiązałem to, ustawiając zarówno precyzję, jak i serialize_precision na tę samą wartość (10):

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

Możesz również ustawić to w swoim php.ini

cokolwiek_sa
źródło
3

Miałem ten sam problem, ale tylko serialize_precision = -1 nie rozwiązał problemu. Musiałem zrobić jeszcze jeden krok, aby zaktualizować wartość precyzji z 14 do 17 (tak jak została ustawiona w moim pliku ini PHP7.0). Najwyraźniej zmiana wartości tej liczby powoduje zmianę wartości obliczonej liczby zmiennoprzecinkowej.

Alin Pop
źródło
3

Inne rozwiązania u mnie nie działały. Oto, co musiałem dodać na początku wykonywania kodu:

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
Mike P. Sinn
źródło
Czy to nie jest w zasadzie to samo, co odpowiedź Alina Popa?
igorsantos 07
1

Jak dla mnie problem był gdy przekazano JSON_NUMERIC_CHECK jako drugi argument json_encode (), który rzutował wszystkie liczby typu na int (nie tylko integer)

Acuna
źródło
1

Zapisz go jako ciąg z dokładną precyzją, której potrzebujesz, używając number_format, a następnie json_encodeużyj JSON_NUMERIC_CHECKopcji:

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

Dostajesz:

{"max": 472.185}

Zauważ, że spowoduje to, że WSZYSTKIE ciągi liczbowe w obiekcie źródłowym zostaną zakodowane jako liczby w wynikowym formacie JSON.

pasqal
źródło
1
Przetestowałem to w PHP 7.3 i nie działa (wynik nadal ma zbyt dużą precyzję). Najwyraźniej flaga JSON_NUMERIC_CHECK jest zepsuta od PHP 7.1 - php.net/manual/de/json.constants.php#123167
Filip
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
źródło
0

Wygląda na to, że problem występuje, gdy serializei serialize_precisionsą ustawione na różne wartości. W moim przypadku odpowiednio 14 i 17. Ustawienie ich obu na 14 rozwiązało problem, podobnie jak ustawienie serialize_precisionna -1.

Wartość domyślna serialize_precision została zmieniona na -1 w PHP 7.1.0, co oznacza, że ​​"zostanie użyty rozszerzony algorytm zaokrąglania takich liczb". Ale jeśli nadal występuje ten problem, może to być spowodowane tym, że masz plik konfiguracyjny PHP z poprzedniej wersji. (Może zachowałeś plik konfiguracyjny podczas aktualizacji?)

Inną rzeczą do rozważenia jest to, czy w twoim przypadku ma sens używanie wartości zmiennoprzecinkowych. Używanie wartości ciągów zawierających liczby w celu zapewnienia, że ​​w formacie JSON zawsze zostanie zachowana odpowiednia liczba miejsc dziesiętnych, może mieć sens lub nie.

Code Commander
źródło
-1

Możesz zmienić [max] => 472.185 z float na ciąg ([max] => '472.185') przed json_encode (). Ponieważ json i tak jest łańcuchem, konwersja wartości zmiennoprzecinkowych na ciągi przed json_encode () zachowa żądaną wartość.

Everett Staley
źródło
Jest to do pewnego stopnia prawdą technicznie, ale bardzo nieefektywne. Jeśli wartość Int / Float w ciągu JSON nie jest umieszczona w cudzysłowie, JavaScript może ją zobaczyć jako rzeczywistą wartość Int / Float. Wykonanie wersji zmusza cię do rzutowania każdej pojedynczej wartości z powrotem na Int / Float raz po stronie przeglądarki. Często miałem do czynienia z wartościami 10000+ podczas pracy nad tym projektem na żądanie. Skończyło się na tym, że doszłoby do wielu przeróbek.
Gwi7d31
Jeśli używasz JSON do wysyłania danych gdzieś i oczekiwana jest liczba, ale wysyłasz ciąg, nie ma gwarancji, że zadziała. W sytuacjach, gdy twórca aplikacji wysyłającej nie ma kontroli nad aplikacją odbierającą, nie jest to rozwiązanie.
osullic