Liczby, zaokrąglanie i pieniądze
- Przydatne narzędzia
- poziom: średni
- aktualizacja: 16 sierpnia 2026
Pieniądze i typ double to zły związek: ułamki dziesiętne zapisywane są dwójkowo, więc 0.1 + 0.2 daje 0.30000000000000004, a 4.35 * 100 — 434.99999999999994. Przy jednym mnożeniu tego nie widać, ale po kilku pozycjach koszyka brakuje grosza, którego nie wytłumaczysz księgowej. Lekarstwem jest BigDecimal: liczba dziesiętna z jawną skalą, tworzona zawsze z tekstu. Ma jednak własne kolce — equals porównuje też skalę, a divide bez podanej skali potrafi rzucić wyjątkiem. Skrypt pokazuje jedno i drugie na żywo, a na koniec liczy ten sam koszyk z rabatem i VAT-em dwoma sposobami: źle i dobrze.
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.List;
import java.util.Locale;
/**
* Liczby, zaokrąglanie i pieniądze — skryptyJAVA.pl
* Zapisz jako: LiczbyIPieniadze.java
*/
public final class LiczbyIPieniadze {
private static final Locale POLSKI = Locale.forLanguageTag("pl-PL");
private static final BigDecimal STAWKA_VAT = new BigDecimal("0.23");
private static final BigDecimal RABAT = new BigDecimal("15");
/** Pozycja koszyka: cena jednostkowa netto i liczba sztuk. */
public record Pozycja(String nazwa, BigDecimal cenaNetto, int ilosc) {}
/** Zaokrągla do pełnych groszy „w górę od połowy” — tak liczy większość faktur. */
public static BigDecimal doGroszy(BigDecimal kwota) {
return kwota.setScale(2, RoundingMode.HALF_UP);
}
/** Wartość pozycji po rabacie, jeszcze bez zaokrąglania — pełna dokładność mnożenia. */
public static BigDecimal wartoscDokladna(Pozycja pozycja, BigDecimal rabatProcent) {
BigDecimal mnoznik = BigDecimal.ONE.subtract(rabatProcent.movePointLeft(2));
return pozycja.cenaNetto().multiply(BigDecimal.valueOf(pozycja.ilosc())).multiply(mnoznik);
}
/** Suma zaokrąglonych pozycji — zgadza się z kwotami wydrukowanymi w wierszach. */
public static BigDecimal sumaPozycjiZaokraglonych(List<Pozycja> koszyk, BigDecimal rabat) {
BigDecimal suma = BigDecimal.ZERO;
for (Pozycja pozycja : koszyk) {
suma = suma.add(doGroszy(wartoscDokladna(pozycja, rabat)));
}
return suma;
}
/** Suma wartości dokładnych, zaokrąglona dopiero na końcu — bywa o grosz inna. */
public static BigDecimal sumaZaokraglonaNaKoncu(List<Pozycja> koszyk, BigDecimal rabat) {
BigDecimal suma = BigDecimal.ZERO;
for (Pozycja pozycja : koszyk) {
suma = suma.add(wartoscDokladna(pozycja, rabat));
}
return doGroszy(suma);
}
/** Ta sama arytmetyka na double — celowo błędna, żeby było co porównać. */
public static double sumaNaDouble(List<Pozycja> koszyk, double rabatProcent) {
double suma = 0.0;
for (Pozycja pozycja : koszyk) {
double wartosc = pozycja.cenaNetto().doubleValue() * pozycja.ilosc()
* (1.0 - rabatProcent / 100.0);
suma += Math.round(wartosc * 100) / 100.0;
}
return suma;
}
/** Rabat 0–100% na groszach, z symetrycznym HALF_UP i kontrolą przepełnienia. */
public static long rabatWGroszach(long groszy, int rabatProcent) {
if (rabatProcent < 0 || rabatProcent > 100) {
throw new IllegalArgumentException("Rabat musi mieścić się w zakresie 0–100%.");
}
long mnoznik = 100L - rabatProcent;
long cale = Math.multiplyExact(groszy / 100, mnoznik);
long reszta = Math.multiplyExact(groszy % 100, mnoznik);
return Math.addExact(cale, (reszta + Long.signum(reszta) * 50) / 100);
}
/** Kwota w polskim formacie, np. 12,34 zł. */
public static String pln(BigDecimal kwota) {
return NumberFormat.getCurrencyInstance(POLSKI).format(kwota);
}
// ── PRZYKŁADY ───────────────────────────────────────────
public static void main(String[] args) {
System.out.println("— czego double nie potrafi —");
System.out.println("0.1 + 0.2 = " + (0.1 + 0.2));
System.out.println("4.35 * 100 = " + (4.35 * 100));
System.out.println("\n— BigDecimal buduj z tekstu, nigdy z double —");
System.out.println("new BigDecimal(0.1) = " + new BigDecimal(0.1));
System.out.println("new BigDecimal(\"0.1\") = " + new BigDecimal("0.1"));
BigDecimal dwa = new BigDecimal("2.0");
BigDecimal dwaZero = new BigDecimal("2.00");
System.out.println("2.0.equals(2.00) = " + dwa.equals(dwaZero) + " (skala się liczy)");
System.out.println("2.0.compareTo(2.00) = " + dwa.compareTo(dwaZero) + " (0 znaczy równe)");
System.out.println("\n— dzielenie —");
BigDecimal trzy = new BigDecimal("3");
try {
System.out.println(BigDecimal.ONE.divide(trzy));
} catch (ArithmeticException blad) {
System.out.println("1/3 bez skali rzuca wyjątek: " + blad.getMessage());
}
System.out.println("1/3 ze skalą 4: " + BigDecimal.ONE.divide(trzy, 4, RoundingMode.HALF_UP));
System.out.println("\n— HALF_UP, HALF_EVEN i to, co po cichu wybiera pln() —");
for (String tekst : List.of("2.345", "2.355", "-2.345")) {
BigDecimal kwota = new BigDecimal(tekst);
System.out.printf("%-7s HALF_UP=%-6s HALF_EVEN=%-6s pln=%s%n", tekst,
kwota.setScale(2, RoundingMode.HALF_UP),
kwota.setScale(2, RoundingMode.HALF_EVEN), pln(kwota));
}
System.out.println("\n— koszyk: rabat 15%, VAT 23% —");
List<Pozycja> koszyk = List.of(
new Pozycja("Kubek termiczny", new BigDecimal("2.10"), 3),
new Pozycja("Zeszyt A5", new BigDecimal("4.30"), 1),
new Pozycja("Długopis", new BigDecimal("1.50"), 7));
for (Pozycja pozycja : koszyk) {
BigDecimal dokladna = wartoscDokladna(pozycja, RABAT);
System.out.printf("%-16s %8s -> %s%n", pozycja.nazwa(), dokladna, pln(doGroszy(dokladna)));
}
BigDecimal netto = sumaPozycjiZaokraglonych(koszyk, RABAT);
System.out.println("Suma zaokrąglonych pozycji: " + pln(netto));
System.out.println("Zaokrąglona suma dokładna: " + pln(sumaZaokraglonaNaKoncu(koszyk, RABAT)));
System.out.println("To samo policzone na double: " + sumaNaDouble(koszyk, 15.0));
// VAT naliczamy od podstawy, czyli od sumy już zaokrąglonych pozycji.
BigDecimal podatek = doGroszy(netto.multiply(STAWKA_VAT));
System.out.println("VAT 23%: " + pln(podatek) + ", brutto: " + pln(netto.add(podatek)));
System.out.println("\n— alternatywa: kwoty w groszach jako long —");
long groszy = rabatWGroszach(1050L, 15);
System.out.println("1050 gr - 15% = " + groszy + " gr = " + pln(BigDecimal.valueOf(groszy, 2)));
System.out.println("-1050 gr - 15% = " + rabatWGroszach(-1050L, 15)
+ " gr (symetryczne HALF_UP)");
}
}
Jak użyć tego skryptu
- Plik musi nazywać się
LiczbyIPieniadze.java, bo tak samo nazywa się klasa publiczna w środku. Wklej kod, wywołajjava LiczbyIPieniadze.javai czytaj wydruk od góry: najpierw wpadkidouble, potem pułapkiBigDecimal, na końcu ten sam koszyk policzony trzema drogami. - Kwoty wprowadzaj do
BigDecimalz napisu:new BigDecimal("24.90")albo wprost z tekstu wpisanego przez użytkownika. Liczbę sztuk zamieniaj przezBigDecimal.valueOf(ilosc), a kwotę zapisaną w groszach jakolong— przezBigDecimal.valueOf(893, 2), co od razu daje 8.93. - Ustal jedną zasadę zaokrąglania i trzymaj się jej w całym dokumencie.
sumaPozycjiZaokraglonychzaokrągla każdą pozycję i dopiero sumuje (17,95 zł),sumaZaokraglonaNaKoncurobi odwrotnie (17,94 zł). Na fakturze zwykle wygrywa ta pierwsza, bo podsumowanie musi zgadzać się z kwotami widocznymi w wierszach. - Przy dzieleniu zawsze podawaj skalę i tryb:
kwota.divide(dzielnik, 2, RoundingMode.HALF_UP). Wariant jednoargumentowy zadziała tylko wtedy, gdy wynik ma skończone rozwinięcie dziesiętne — przy dzieleniu na trzy rzuciArithmeticException, co widać w przykładach.
Warto wiedzieć
new BigDecimal(0.1)nie jest błędem składni, tylko cichą katastrofą: konstruktor dostaje już zepsutą liczbędoublei wiernie przepisuje wszystkie jej cyfry, więc w wydruku zobaczysz0.1000000000000000055511151231257827021181583404541015625. Taka wartość liczy się dokładnie tak samo źle jakdouble, tylko wolniej. Jeśli musisz przyjąćdoublez cudzego API, użyjBigDecimal.valueOf(x)— idzie przezDouble.toStringi da0.1, ale nie odzyska tego, co zgubiło się we wcześniejszych rachunkach.equalsicompareTonie zgadzają się ze sobą, a to rozjeżdża kolekcje.HashSet,HashMapiList.containspytają oequals, więc2.0i2.00są dla nich dwiema różnymi kwotami;TreeSetiTreeMappytają ocompareToi uznają je za jedną. Zanim wrzucisz kwoty do kolekcji albo porównasz je w teście, ujednolić skalę przezsetScale(2, RoundingMode.HALF_UP).- Metoda
plntylko formatuje, ale przy okazji zaokrągla po swojemu i nikogo o tym nie uprzedza:NumberFormat.getCurrencyInstancestartuje z trybemHALF_EVEN, więc niezaokrąglone2.345wyświetli jako2,34 zł, choćsetScale(2, RoundingMode.HALF_UP)dałoby2,35— w przykładach obie kolumny stoją obok siebie. Formatuj więc kwoty już zaokrąglone tam, gdzie je liczysz, albo rzutuj wynik naDecimalFormati wywołajsetRoundingModejawnie; inaczej sumy w bazie i na wydruku rozjadą się o grosz. rabatWGroszachpokazuje tańszą drogę — kwoty jakolongw groszach. Rozdziela liczbę na pełne setki i resztę, dzięki czemu mnożenie nie przepełnia się nawet dlaLong.MIN_VALUEiLong.MAX_VALUE, a poprawka o 50 ma znak reszty, więc HALF_UP działa symetrycznie dla zwrotów. Metoda jawnie przyjmuje rabat tylko od 0 do 100%; inne reguły biznesowe waliduj osobno.
Podobne skrypty
Generator bezpiecznych haseł
Generator haseł oparty na klasie SecureRandom: wybór długości i zestawów znaków, gwarancja użycia każdego zestawu i opcja pomijania mylących znaków (l, I, 1, O, 0).
początkujący Zobacz kod → Przydatne narzędziaHashowanie i weryfikacja haseł przez PBKDF2
Hasło w bazie ma być nieczytelne nawet dla Ciebie. PBKDF2WithHmacSHA256 z losową solą, kosztem iteracyjnym, przenośnym formatem rekordu i porównaniem odpornym na analizę czasu — bez zewnętrznych bibliotek, na czystym JDK 17.
zaawansowany Zobacz kod → Przydatne narzędziaSzyfrowanie i odszyfrowanie przez AES-GCM
Uwierzytelnione szyfrowanie tekstu algorytmem AES-GCM: klucz 256-bitowy, losowy 96-bitowy IV, dane powiązane AAD i przenośny zapis Base64. Ktoś przestawi jeden bit szyfrogramu — odszyfrowanie się nie uda, zamiast po cichu zwrócić przekłamaną treść.
zaawansowany Zobacz kod →