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"
                }
            },

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}

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 );
}

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.


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


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.


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 );
}

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)


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.


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
}

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.


-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ść.


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.