Kwoty, wartości pieniężne, taxy, BigDecimal vs float

0

Poleci ktoś jakiś dobry artykuł o BigDecimal i różnych RoundingMode i ich zastosowania? Generalnie jest problem w aplikacji
bo w jednym miejscu w raportach pojawia się kwota 27.99 a w innym 28. Jest niby dedykowana klasa Money, która w środku
ma currency i BigDecimala i zaokrągla używając CEILING z drugiej strony w innym miejscu ten BigDecimal jest wyciągany
i jest zaokrąglane używając HALF_EVEN. Dodatkowo w dto / modelach transportowych pojawia się float, to ja powiedziałem,
że float to jest trochę lipa, że lepiej żeby to w Stringu było i parsowane do BigDecimala z tym, że to już jest taka specyfikacja api
poza tym kolega z zespołu mówi, że ten float jest ok bo i tak zaokrąglamy do 2 miejsc po przecinku. Kto ma racje i jak najlepiej to ogarnąć?

4
lookacode1 napisał(a):

poza tym kolega z zespołu mówi, że ten float jest ok bo i tak zaokrąglamy do 2 miejsc po przecinku. Kto ma racje i jak najlepiej to ogarnąć?

Niech go ktoś solidnie [CIACH!] w łeb IEEE754 albo czymś cięższym. Błędy wynikające z przeprowadzania kalkulacji na float będą się kumulować. Klasycznie:

float a = 0.1f + 0.2f; // 0.29999...
float b = 100000000 * (0.1f + 0.2f); // 30000002

decimal a1 = 0.1M + 0.2M;
decimal b1 = 100000000 * (0.1M + 0.2M); // no ciekawe ile tutaj wyjdzie

Strzelam, że w Javie różnice będą tylko w nazewnictwie klas.

Wniosek: zrobienie gdzieś kiedyś jakiś hacków na roundingu jest błędogenne, bo nie wystarczy roundować wyników obliczeń.

3

Zespol ekspertow ;D Float nie ma dokladnej reprezentacji dla ulamkow typu 1/10. Czyli przyslowiowe 1/10+1/10 BigDecimal ogarnie, a float i double juz nie zawsze - juz na tak podstawowym poziomie jest roznica.

0

Wiem, że operacje na float nie są precyzyjne i właśnie u nas obliczenia wykonywane są na Money, które jest wrapperem na BigDecimal.
Float pojawia się z tego co widziałem w modelu transportowym i później jest robione coś w stylu BigDecimal.valueOf(float).
Oczywiście wiem, że najlepszy byłby jakiś string ale pytanie czy to jest problem skoro docelowo w tym floatcie będą wartości do 2 miejsc po przecinku czyli już po zaokrągleniu?
np. 10.1f + 0.1f = 10.200000762939453 ale jak zaokrąglimy to i tak jest ok czyli 10.20.

PS. To jest system legacy nie ja pisałem ;P

3
  1. Float jest zupełnie precyzyjny - w sensie obliczenia są zestandaryzowane (* ok - sprawa jest trochę bardziej skompliokowana - można poszukać po haśle strictfp).

po prostu inaczej precyzyjny niż BigDecimal

  1. Obliczenia wartość pienieżnych wykonuje się zwykle na wartościach dziesiętnych - stąd BigDecimal,
    ale to nie wystarcza !!
    Do tego w zależności od konkretnej dziedziny musisz wykonywać obliczenia z podaną dokładnością i zaokrągleniami - to jest ustalone normami prawnymi. Np. obliczenia częsciowe do setnej części grosza zaokrąglane w górę, a na koniec wynik podawany do grosza (też z jakims zaokrągleniem).

Jak nie będziesz przestrzegał to może dojść do zabawnej sytuacji, gdzie w wielomilionowym budżecie jakiejś firmy nie zgadza się grosz w bilansie. I jest to bardzo drogi grosz :-) (zwykle udaje się wyjaśnić, ale śledzenie wymaga czasem zatrudnienia specjalistów od każdego z systemów informatycznych firmy ).
Generalnie jeśli z tych wyników ma wyjść kwota podatków, rata kredytu czy cokolwiek takiego to nie można sie bawić w zgadywanie - inaczej kara może być sroga - trzeba zatrudnić analityka do sprawdzenia jakie są regulacje.

  1. Jakiekolwiek skrupulatne zakrąglanie na floatach itp (widziałem wielokrotnie nawet w systemach bankowych....) powoduje tylko, że wystąpienie błędu jest mniej prawdopodobne - za to bardziej spektakularne. Np. zamiast błędu na co drugiej fakturze dostaniesz błąd tylko w przypadku faktur mających więcej niż 1000 pozycji (i pewnie kiedyś taka przez system przejdzie).

EDIT (w końcu zrozumiałem o czym jest pytanie)
4: Ten Float w warstwie transportowej to jest lipa - sam masz przykład, że coś to zaokrąglanie nie wyszło :-)
(pewnie da się poprawić) - ale przerzucenie się na chociaż String (jak nie ma wsparcia do decimal) byłoby jednak lepsze.
W teorii - jak i klient i serwer są dokładnie ugadane co do zaokrąglania / konwersji Float-BigDecimal to niby może działać. W praktyce komuś się gdzieś omsknie ręka.

1

Float pojawia się z tego co widziałem w modelu transportowym i później jest robione coś w stylu BigDecimal.valueOf(float).

@lookacode1: dla własnego zdrowia psychicznego najlepiej nie parsować do floatów. Powinień być String -> BigDecimal

np. 10.1f + 0.1f = 10.200000762939453 ale jak zaokrąglimy to i tak jest ok czyli 10.20.

A co w przypadku ogromnych liczb? Pamiętaj, że float to pod spodem dwie wartości: mantysa i cecha. O ile pieniężne bliskie zera są całkiem dobrze reprezentowane przez liczby zmiennoprzecinkowe to w przypadku większych liczb jest coraz gorzej https://www.researchgate.net/figure/The-distribution-of-representable-floating-point-number-in-a-computer_fig9_320451960

1

Używaj zawsze tego samego typu - np. BigDecimal - z odpowiednim zaokrąglaniem (w Polsce zwykle ROUND_HALF_UP).
Jakie zaokrąglanie powinno być - zależy od kraju i domeny.
Nie mam zdania co do javax.money ale być może warto spróbować.
Niektórzy używają BigInteger - ale z tego co widzę wymaga to implementacji swojego zaokrąglania.
Stringa używaj najlepiej tylko do prezentacji ale nie do przechowywania, format tekstowy musi mieć zdefiniowany separator dziesiętny co jest upierdliwe.
Zaokrąglanie CEILING raczej się nie przyda, ja przynajmniej nigdy nie widziałem w swojej karierze (a pracowałem głównie w fintech).
float / double możesz używać jeśli nie lubisz danej firmy i/lub kolegów.

0

@vpiotr:

Stringa używaj najlepiej tylko do prezentacji ale nie do przechowywania

W jakimś dto nie ma wyjścia i imo najlepiej przechować w Stringu. Nie tylko do prezentacji np.
wysyłka tych danych do jakiegoś innego serwisu, który też będzie po swojemu przetwarzał kase.

0

Problem chyba rozwiązany. Zrobiłem sobie małą symulację:

public static void main(String[] args) {
   MathContext MATH_CONTEXT = new MathContext(2, RoundingMode.HALF_EVEN);
   float value = 27.99f;
   System.out.println(BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_EVEN)); // 27.99
   System.out.println(BigDecimal.valueOf(value).round(MATH_CONTEXT));                // 28
}

W jednym przypadku jakiś developer użył setScale a w drugim round z precyzją 2 🤦‍♂️

0

Jeżeli chodzi o zaokrąglenia to to powinno być wyspecyfikowane w dokumentacji bankowej przez analityka nikt Ci tutaj na forum nie powie jak ma być, tak samo ile miejsc po przecinku.

Co do tego ja pracowałem na zawsze na BigDecimal/JodaMoney/Tidelift.

0
lookacode1 napisał(a):

Wiem, że operacje na float nie są precyzyjne i właśnie u nas obliczenia wykonywane są na Money, które jest wrapperem na BigDecimal.

Strasznie się kiwasz w próbach. Money niczego sensownego dla ciebie nie wprowadza.

slsy napisał(a):

A co w przypadku ogromnych liczb? Pamiętaj, że float to pod spodem dwie wartości: mantysa i cecha. O ile pieniężne bliskie zera są całkiem dobrze reprezentowane przez liczby zmiennoprzecinkowe to w przypadku większych liczb jest coraz gorzej

Dziwne rozważanie. Wartość całego świata jest w pełni do wyrażenia w BigDecimal

@jarekr000000:

Zgadzam się z tym co wyjaśniasz.
Początkującym się wydaje, że wiedzą jaki jest problem z floatami, i że zaokrąglanie leczy ... i właśnie dlatego są początkującymi. Miałem spięcie z sam-sobie-sterem-okrętem borlandowym. Jak się dowiedział - bo nie wiedział wcześniej - że problem jest, w sumowaniu tysiącpozycyjnych ciągów po każdym dodaniu zaokrąglał. PRZECIEZ PROFESJONALNIE ZABEZPIECZYŁ.
Prawdziwi hackerzy w helpy nie zaglądają (skądinąd, borlandy od zawsze miały absolutnie najlepsze), i nie wiedział ze jest do tego specjalizowany typ.

Akurat dziedzina była, ze chowają przed fiskusem to i owo, mniemam że dlatego błąd sumowania wielostronicowych słupków się nie ujawnił

1 użytkowników online, w tym zalogowanych: 0, gości: 1