BigDecimal e metodi di arrotondamento

Categories:

Visto il grande successo dell’articolo introduttivo sulla classe BigDecimal ho deciso di proseguire la serie con qualche esempio su una delle operazioni più complesse: la divisione.

Il problema…

…è il solito: le dimensioni :)

BigDecimal, abbiamo detto, può avere un numero arbitrario di cifre decimali: arbitrario si, ma non infinito, infatti abbiamo a disposizione circa quattro miliardi e rotti di decimali. Non male, direte…beh allora fate questa prova: quanti computer ci vogliono per contenere tutti i decimali della divisione “1 / 3” ? Risposta: non basterebbero tutti i computer dell’universo!

Il risultato, ahimè, è che se proviamo a fare questa operazione con i BigDecimal senza prendere le dovute precauzioni ottieniamo (a ragione) solo un grosso errore, più precisamente una “ArithmeticException”.

La soluzione

Per aggirare questi limiti ci sono diverse metodologie, quella che affronteremo in questa sede è la solita: approssimazione. Per gestire più facilmente gli arrotondamenti è stata introdotta nel JDK 1.5 la classe java.math.MathContext, utilizzata per memorizzare e riutilizzare le impostazioni di precisione e arrotondamento.

Ecco un esempio del suo utilizzo nel caso dell’operazione 1 / 3:

package it.megadix.math;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

public class BigDecimalDivision {
    public static void main(String[] args) {
        BigDecimal n = new BigDecimal("1");
        BigDecimal BIGDEC_3 = new BigDecimal("3");
        try {
            // questa istruzione genera una java.lang.ArithmeticException
            System.out.println(n.divide(BIGDEC_3));
        } catch (Exception ex) {
            System.err.println(ex.getClass().getName() + ": " + ex.getMessage());
        }

        // creo un MathContext con un limitato numero di decimali e la modalità
        // di arrotondamento "HALF_UP" (quella che viene insegnata alle elementari)
        MathContext mc = new MathContext(10, RoundingMode.HALF_UP);
        System.out.println(n.divide(BIGDEC_3, mc));
    }
}

Da notare:

  • la semplice divisione “1 / 3” senza specificare il numero massimo di decimali e la modalità di arrotondamento genera una ArithmeticException;

  • se invece utilizziamo un MathContext non vengono lanciate eccezioni ma il risultato è approssimato.

Output del programma:

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
0.3333333333

Modalità di arrotondamento

Le modalità di arrotondamento disponibili sono molte e ognuna ha il proprio motivo di esistere; le riassumiamo qui ma rimandiamo agli abbondanti Javadoc per i dettagli:

  • RoundingMode.CEILING: arrotonda verso l’alto;

  • RoundingMode.FLOOR: arrotonda verso il basso;

  • RoundingMode.UP: arrotonda allontanandosi dallo zero, considerando il valore assoluto del numero;

  • RoundingMode.DOWN: arrotonda avvicinandosi verso lo zero, considerando il valore assoluto del numero;

  • RoundingMode.HALF_UP: è la modalità che ci insegnano alle elementari; se la cifra scartata è >= 5 allora arrotonda verso l’alto (come RoundingMode.UP), altrimenti verso il basso (RoundingMode.DOWN);

  • RoundingMode.HALF_DOWN: simile a RoundingMode.HALF_UP ma verso il basso;

  • RoundingMode.HALF_EVEN: è una modalità “mista” fra RoundingMode.HALF_UP e RoundingMode.HALF_DOWN che mira a diminuire l’errore che si accumula quando viene ripetutamente applicata in una serie di operazioni;

  • RoundingMode.UNNECESSARY: questa modalità asserisce che il risultato dell’arrotondamento deve essere esatto, se ciò non si verifica allora è un errore da segnalare con una ArithmeticException.

Vediamo ora un esempio di utilizzo di tutte queste modalità:

package it.megadix.math;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

public class BigDecimalRounding {
    public static void main(String[] args) {
        BigDecimal n11 = new BigDecimal("0.11");
        BigDecimal n15 = new BigDecimal("0.15");
        BigDecimal n19 = new BigDecimal("0.19");
        BigDecimal neg_n11 = new BigDecimal("-0.11");
        BigDecimal neg_n15 = new BigDecimal("-0.15");
        BigDecimal neg_n19 = new BigDecimal("-0.19");

        // diversi tipi di MathContext
        MathContext mc_CEILING = new MathContext(1, RoundingMode.CEILING);
        MathContext mc_FLOOR = new MathContext(1, RoundingMode.FLOOR);
        MathContext mc_UP = new MathContext(1, RoundingMode.UP);
        MathContext mc_DOWN = new MathContext(1, RoundingMode.DOWN);
        MathContext mc_HALF_UP = new MathContext(1, RoundingMode.HALF_UP);
        MathContext mc_HALF_EVEN = new MathContext(1, RoundingMode.HALF_EVEN);
        MathContext mc_HALF_DOWN = new MathContext(1, RoundingMode.HALF_DOWN);
        MathContext mc_UNNECESSARY = new MathContext(1, RoundingMode.UNNECESSARY);

        print(n11, mc_CEILING);
        print(n15, mc_CEILING);
        print(n19, mc_CEILING);
        print(neg_n11, mc_CEILING);
        print(neg_n15, mc_CEILING);
        print(neg_n19, mc_CEILING);
        System.out.println("");

        print(n11, mc_FLOOR);
        print(n15, mc_FLOOR);
        print(n19, mc_FLOOR);
        print(neg_n11, mc_FLOOR);
        print(neg_n15, mc_FLOOR);
        print(neg_n19, mc_FLOOR);
        System.out.println("");

        print(n11, mc_UP);
        print(n15, mc_UP);
        print(n19, mc_UP);
        print(neg_n11, mc_UP);
        print(neg_n15, mc_UP);
        print(neg_n19, mc_UP);
        System.out.println("");

        print(n11, mc_DOWN);
        print(n15, mc_DOWN);
        print(n19, mc_DOWN);
        print(neg_n11, mc_DOWN);
        print(neg_n15, mc_DOWN);
        print(neg_n19, mc_DOWN);
        System.out.println("");

        print(n11, mc_HALF_UP);
        print(n15, mc_HALF_UP);
        print(n19, mc_HALF_UP);
        print(neg_n11, mc_HALF_UP);
        print(neg_n15, mc_HALF_UP);
        print(neg_n19, mc_HALF_UP);
        System.out.println("");

        print(n11, mc_HALF_EVEN);
        print(n15, mc_HALF_EVEN);
        print(n19, mc_HALF_EVEN);
        print(neg_n11, mc_HALF_EVEN);
        print(neg_n15, mc_HALF_EVEN);
        print(neg_n19, mc_HALF_EVEN);
        System.out.println("");

        print(n11, mc_HALF_DOWN);
        print(n15, mc_HALF_DOWN);
        print(n19, mc_HALF_DOWN);
        print(neg_n11, mc_HALF_DOWN);
        print(neg_n15, mc_HALF_DOWN);
        print(neg_n19, mc_HALF_DOWN);
        System.out.println("");

        // questa istruzioni generano ArithmeticException
        try {
            print(n11, mc_UNNECESSARY);
        } catch (Exception ex) {
            System.err.println(ex.getClass().getName() + ": " + ex.getMessage());
        }
        try {
            print(n19, mc_UNNECESSARY);
        } catch (Exception ex) {
            System.err.println(ex.getClass().getName() + ": " + ex.getMessage());
        }
    }

    private static void print(BigDecimal n, MathContext mc) {
        System.out.println(n + " -> (RoundingMode." + mc.getRoundingMode().toString() + ") -> " + n.round(mc));
    }
}

Ed ecco l’output prodotto:

0.11 -> (RoundingMode.CEILING) -> 0.2
0.15 -> (RoundingMode.CEILING) -> 0.2
0.19 -> (RoundingMode.CEILING) -> 0.2
-0.11 -> (RoundingMode.CEILING) -> -0.1
-0.15 -> (RoundingMode.CEILING) -> -0.1
-0.19 -> (RoundingMode.CEILING) -> -0.1

0.11 -> (RoundingMode.FLOOR) -> 0.1
0.15 -> (RoundingMode.FLOOR) -> 0.1
0.19 -> (RoundingMode.FLOOR) -> 0.1
-0.11 -> (RoundingMode.FLOOR) -> -0.2
-0.15 -> (RoundingMode.FLOOR) -> -0.2
-0.19 -> (RoundingMode.FLOOR) -> -0.2

0.11 -> (RoundingMode.UP) -> 0.2
0.15 -> (RoundingMode.UP) -> 0.2
0.19 -> (RoundingMode.UP) -> 0.2
-0.11 -> (RoundingMode.UP) -> -0.2
-0.15 -> (RoundingMode.UP) -> -0.2
-0.19 -> (RoundingMode.UP) -> -0.2

0.11 -> (RoundingMode.DOWN) -> 0.1
0.15 -> (RoundingMode.DOWN) -> 0.1
0.19 -> (RoundingMode.DOWN) -> 0.1
-0.11 -> (RoundingMode.DOWN) -> -0.1
-0.15 -> (RoundingMode.DOWN) -> -0.1
-0.19 -> (RoundingMode.DOWN) -> -0.1

0.11 -> (RoundingMode.HALF_UP) -> 0.1
0.15 -> (RoundingMode.HALF_UP) -> 0.2
0.19 -> (RoundingMode.HALF_UP) -> 0.2
-0.11 -> (RoundingMode.HALF_UP) -> -0.1
-0.15 -> (RoundingMode.HALF_UP) -> -0.2
-0.19 -> (RoundingMode.HALF_UP) -> -0.2

0.11 -> (RoundingMode.HALF_EVEN) -> 0.1
0.15 -> (RoundingMode.HALF_EVEN) -> 0.2
0.19 -> (RoundingMode.HALF_EVEN) -> 0.2
-0.11 -> (RoundingMode.HALF_EVEN) -> -0.1
-0.15 -> (RoundingMode.HALF_EVEN) -> -0.2
-0.19 -> (RoundingMode.HALF_EVEN) -> -0.2

0.11 -> (RoundingMode.HALF_DOWN) -> 0.1
0.15 -> (RoundingMode.HALF_DOWN) -> 0.1
0.19 -> (RoundingMode.HALF_DOWN) -> 0.2
-0.11 -> (RoundingMode.HALF_DOWN) -> -0.1
-0.15 -> (RoundingMode.HALF_DOWN) -> -0.1
-0.19 -> (RoundingMode.HALF_DOWN) -> -0.2

java.lang.ArithmeticException: Rounding necessary
java.lang.ArithmeticException: Rounding necessary
See also