Runden und Formatieren von
Gleitkommazahlen in Java

Aus der FAQ [3.13.2] von de.comp.lang.java.
Autor: Peter Luschny; Datum: 2004-02-04.

::: 1. Einführung

Die Dezimaldarstellung einer Zahl a notieren wir

         a = ± an an-1 ... a0 , a-1 a-2 ... a-k

wobei die ai Ziffern '0','1,','2',...,'9' sind und an ungleich 0. 
Die rechts vom Vorzeichen stehenden Ziffern heißen die tragenden
Stellen von a, die rechts vom Komma stehenden Ziffern 
Nachkommastellen von a.

Jede Zahl a ungleich 0 kann eindeutig in der Form dargestellt 
werden a = m * 10q, wobei 1 <= |m| < 10 und q eine ganze Zahl ist.
Diese Darstellung nennen wir die Gleitkommadarstellung von a.

Beispiel: Die Dezimalzahl 123,45678 hat die Gleitkommadarstellung
1,2345678 * 102. Will man auf 2 Nachkommastellen runden, so möchte
man die Dezimaldarstellung 123,46 erhalten, will man auf 2 tragende
Stellen runden, so möchte man die Gleitkommadarstellung 1,2 * 102 
erhalten.

Die Java-Dokumentation wählt noch eine andere Umsetzung. Hier wird
die Zahl aus dem Beispiel als [12345678, 5] dargestellt und 
allgemein die Notation [m,s] verwendet, wobei a = m*10-s.

Hier geht man also von der Anzahl der Nachkommastellen aus (deren
Endlichkeit vorausgesetzt wird), und es wird von rechts nach links
abgezählt (man beachte das Minuszeichen des Exponenten). Wir haben
also die 3 Darstellungen

        123,45678 = 1,2345678 * 102 = [12345678, 5]

In einem Computer sind natürlich nur endlich viele Gleitkommazahlen
darstellbar. In der Mathematik gibt es aber unendlich viele. Will 
man also das mathematische Rechnen auf einem Computer 'simulieren',
muss man ständig versuchen, trotz dieses Umstandes möglichst nahe 
an den 'wahren' Werten zu bleiben. Dieses Auswählen eines geeigneten
Stellvertreters unter den im Rechner darstellbaren Zahlen ist es, 
was man, allgemein gesprochen, Runden nennt, und diese Operation ist
im Grunde vor und nach jeder arithmetischen Operationen notwendig, 
wenn man sicherstellen will, dass die Gesetze der Mathematik nicht
verletzt werden. Die bekanntesten Arten zum Runden sind: den 'oberen'
Nachbarn, den 'unteren' Nachbarn oder die 'nächstliegende' Zahl im 
System der darstellbaren Gleitkommazahlen auszuwählen.

Neben dem Runden einer Gleitkommazahl gehört zu den Standardaufgaben
sie geeignet zu formatieren. Hier entscheidet man über die graphische
Darstellung einer Ziffernfolge beim Anzeigen oder Ausdrucken. Beide
Operationen sind begrifflich unabhängig voneinander, werden aber
manchmal verwechselt - dies ist einer der Gründen, warum wir sie hier
gemeinsam besprechen. Das Ergebnis einer Rundung ist ein 'double',
das Ergebnis einer Formatierung ein 'String'. Für beide Operationen
stellt die Java-Bibliothek Klassen zur Verfügung.

Für das Rechnen und Runden: java.math.BigDecimal.
Für das Formatieren: java.text.DecimalFormat.

Die folgenden Vorschläge setzen eine Version des JDK >= 1.5 voraus.
Wie komplex die Dinge sind, zeigt, dass die Dokumentation von 
BigDecimal in der Version 1.5 allein 41 Seiten, die von 'DecimalFormat'
22 Seiten lang ist. Es wird daher dringend empfohlen, diese 
Dokumentation zu lesen.

::: 2. ::: Das Runden einer Gleitkommazahl

Eine wichtige Art, Gleitkommazahlen in Java in den Griff zu
bekommen, ist es, die Klasse BigDecimal zu verwenden. Dies bietet
Vorteile gegenüber dem direkten Arbeiten mit 'doubles': In den
Feinheiten und Sonderfällen der Gleitkomma-Arithmetik sind viele
Fußangeln versteckt, ihnen durch den Gebrauch der Bibliotheks-
funktionen aus dem Weg zu gehen, ist ein guter Rat nicht nur für
Anfänger. Folgende einfache Funktion zum Runden einer Gleitkommazahl
zeigt, wie man dabei vorgehen kann.

public static double round
             (double d, int scale, RoundingMode mode, FormatType type)
{
   if (Double.isNaN(d) || Double.isInfinite(d)) return d;
   scale = Math.max(scale,0);  // Verhindert negative scale-Werte
   BigDecimal bd = BigDecimal.valueOf(d);
   if(type == FormatType.exp) {
      BigDecimal bc = new BigDecimal(bd.unscaledValue(),bd.precision()-1);
      return ((bc.setScale(scale, mode)).
               scaleByPowerOfTen(bc.scale()-bd.scale())).doubleValue();
   }
   return (bd.setScale(scale, mode)).doubleValue();
}

Kurzbeschreibung der Funktion:
/**
 * @param  d der zu rundende Gleitkommawert.
 * @param  scale die Anzahl der Nachkommastellen, falls type = fix,
 *         die Anzahl der tragenden Stellen - 1,  falls type = exp.
 *         scale sollte >= 0 sein (negative Werte werden auf 0 gesetzt).
 * @param  mode die Rundungsart: einer der Rundungsarten von BigDecimal,
 *         seit 1.5 in java.math.RoundingMode.
 * @param  type ein Element von "enum FormatType {fix, exp}" gibt an,
 *         auf welche Stellen sich die Rundung beziehen soll.
 *         FormatType.exp ('Exponential') steht für tragende Stellen,
 *         FormatType.fix ('Fixkomma') steht für Nachkommastellen.
 * @return der gerundete Gleitkommawert.
 * Anmerkung: Für die Werte double NaN und ±Infinity
 * liefert round den Eingabewert unverändert zurück.
 */

Beispiel:
double d = -Math.exp(702); 
for (int scale = 0; scale < 6; scale++)
System.out.println(round(d,scale,RoundingMode.HALF_EVEN,FormatType.exp));

    0 -> -7.0E304
    1 -> -7.5E304
    2 -> -7.49E304
    3 -> -7.494E304
    4 -> -7.4942E304
    5 -> -7.49422E304

Beispiel:
double d = Math.PI*10000.0; 
for (int scale = 0; scale < 6; scale++)
System.out.println(round(d,scale,RoundingMode.HALF_EVEN,FormatType.fix));

    0 -> 31416.0
    1 -> 31415.9
    2 -> 31415.93
    3 -> 31415.927
    4 -> 31415.9265
    5 -> 31415.92654

Die Enumeration "enum FormatType {fix, exp};" wird dabei vorausgesetzt.
Man beachte auch die Verwendung der Factory-Methode valueOf().
Diese Form ist in der Regel dem Konstruktor 'BigDecimal(double val)'
vorzuziehen. Man lese dazu die Erläuterungen in der Dokumentation
(Version >= 1.5) von BigDecimal. "This is generally the preferred way
to convert a float or double into a BigDecimal.."
Ein einfaches Beispiel veranschaulicht den Unterschied:

System.out.println(BigDecimal.valueOf(1.005));
System.out.println(new BigDecimal(1.005));

    >>   1.005
    >>   1.00499999999999989341858963598497211933135986328125

In Java-Versionen vor 1.5 läßt sich BigDecimal.valueOf(d) simulieren
durch new BigDecimal(Double.toString(d))). Der 'if'-Zweig der Funktion
'round' ist allerdings in älteren Versionen nicht (so einfach) zu
erhalten: Die Funktionen precision() und scaleByPowerOfTen() sind
erst ab 1.5 im API enthalten und müssten bei älteren Versionen
'nachgebaut' werden. Entweder eine nette Übungsaufgabe oder ein guter
Grund, auf eine Version >= 1.5 umzusteigen.

BigDecimal stellt 8 Möglichkeiten zur Rundung zur Verfügung. Will man
auf 2 Stellen im 'kaufmännischen Sinn' runden, so wähle man HALF_UP.
Der IEEE-Standard sieht diese Rundung allerdings nicht als den
Normalfall an, und bei numerischen Rechnungen wählt man besser HALF_EVEN.
Gebrauchsfertig in handliche Makros gepackt:

    public static double roundUpFix2 (double d) {
        return round (d, 2, RoundingMode.HALF_UP, FormatType.fix);
    }

    public static double roundEvenExp2 (double d) {
        return round (d, 2, RoundingMode.HALF_EVEN, FormatType.exp);
    }

::: 3. Das Formatieren einer Gleitkommazahl

Für das Formatieren ist 'java.text.DecimalFormat' das angebotene 
Werkzeug. Eine kleine Utility-Funktion 'format' mit einer ähnlichen
Aufrufstruktur wie die Funktion 'round', zeigt hier einen Ansatz:

public static String format (double d, int scale, FormatType type)
{
    if (Double.isNaN(d) || Double.isInfinite(d))
        return Double.toString(d);

    scale = Math.max(scale,0); // Verhindert negative scale-Werte
    DecimalFormat df = new DecimalFormat();
    df.setMaximumFractionDigits(scale);
    df.setMinimumFractionDigits(scale);

    if( type == FormatType.exp ) {
        StringBuilder sb = new StringBuilder("0E0");
        if(scale > 0) sb.append(".000000000000000000",0,scale+1);
        df.applyPattern(sb.toString());
    }
    else {
        df.setGroupingUsed(false);
        df.setMinimumIntegerDigits(1);
    }
    return df.format( d );
}

 Kurzbeschreibung der Funktion:
 /**
 * @param  d der zu formatierende Gleitkommawert.
 * @param  scale die Anzahl der Nachkommastellen, falls type = fix,
 *         die Anzahl der tragenden Stellen - 1,  falls type = exp.
 *         scale sollte >= 0 sein (negative Werte werden auf 0 gesetzt).
 * @param  type ein Element von "enum FormatType {fix, exp}".
 *         FormatType.exp fordert eine wissenschaftliche Exponential-
 *         darstellung an, FormatType.fix fordert ein Fixkommaformat an.
 * @return eine Zeichenkette, die den Gleitkommawert darstellt und
           entsprechend den gewünschten Parametern formatiert ist.
 * Anmerkung: Für die Werte double NaN und ±Infinity
 * liefert diese Methode Double.toString(d) zurück.
 */

Auch hier wird die Enumeration "enum FormatType {fix, exp};" vorausgesetzt.
Man beachte bei den folgenden Beispielen die Verwendung von ','
anstelle von '.' bei der Ausgabe. Wir bekommen also in der Tat
Gleitkommazahlen und nicht Gleitpunktzahlen geliefert, wie sich das auch
gehört, wenn die 'lokalen Einstellungen' auf Deutschland gesetzt sind.

Wer arabische oder indischen Ziffern bevorzugt, kann auch dies
erreichen - zur Verwendung von 'Locales' im Zusammenhang mit
DecimalFormat verweisen wir auf die Dokumentation. Die beiden
Beispiele von oben nehmen, mit den Voreinstellungen der deutschen
Locale, folgende Gestalt an:

Beispiel:
double d = -Math.exp(702); 
for (int scale = 0; scale < 6; scale++)
System.out.println(scale+" -> "+format(d, scale, FormatType.exp));

    0 -> -7E304
    1 -> -7,5E304
    2 -> -7,49E304
    3 -> -7,494E304
    4 -> -7,4942E304
    5 -> -7,49422E304

Beispiel:
double d = Math.PI*10000.0; 
for (int scale = 0; scale < 6; scale++)
System.out.println(scale+" -> "+format(d, scale, FormatType.fix));

    0 -> 31416
    1 -> 31415,9
    2 -> 31415,93
    3 -> 31415,927
    4 -> 31415,9265
    5 -> 31415,92654

Bequeme Makros machen die Funktion jetzt gebrauchsfertig:

    public static String formatFix2 (double d) {
        return format (d, 2, FormatType.fix);
    }

    public static String formatExp2 (double d) {
        return format (d, 2, FormatType.exp);
    }

In vielen Anwendungen sind die Anforderungen an die Formatierung
jedoch wesentlich komplexer, weil dabei auch der für die Anzeige zur
Verfügung stehende Platz berücksichtigt werden muss. 

::: 4. Runden plus Formatieren

Als letztes eine Warnung: DecimalFormat verwendet intern auch eine
Rundung, und zwar - fest verdrahtet (!) - RoundingMode.HALF_EVEN. Das
ist schwerlich etwas anderes als ein Design-Bug, denn damit wird die
Verwendung der 7 anderen Rundungsarten von BigDecimal in Verbindung 
mit dieser Formatierungsklasse problematisch.

Insbesondere sollte jeder, der DecimalFormat verwendet, sich klar
darüber sein, dass hier nicht kaufmännisch gerundet wird!

Dazu noch ein Beispiel:
Das einfache dform2 = new java.text.DecimalFormat("0.00") beschert
folgende 'Überraschung' beim Ausdruck einer Rechnung:

    dform2( 10.495 ) -> 10,50
    dform2( 10.505 ) -> 10,50
    dform2( 10.515 ) -> 10,52

Zum Glück beißt sich die interne Rundung von DecimalFormat nicht mit
einer vorgeschalteten Aufrundung, sofern es nur um das Formatieren geht,
so dass sich dieses Problem so lösen lässt:

    public static String formatFixUp2 (double d) {
        return formatFix2(roundUpFix2(d));
    }

    formatFixUp2( 10.495 ) -> 10,50
    formatFixUp2( 10.505 ) -> 10,51
    formatFixUp2( 11.515 ) -> 11,52

Aber dieses Beispiel ist in erster Linie zur Illustration des Gesagten
gedacht. Denn bei kaufmännischen Rechnungen gilt es die Maxime von Paul
Ebermann zu befolgen: "Beim Rechnen mit Geld verwende man NIE 'double'."
Zu diesem Thema lese man auch den Link [2].

::: 5. Zusammenfassung

  ----------------------------------------------------
       RUNDEN              |       FORMATIEREN
  ----------------------------------------------------
                     d = PI*10000.0;
   Nachkomma-Stellen       |  Fixpunkt-Format
   roundUpFix2(d)          |  formatFix2(d)
   Wert: double 31415.93   |  Wert: String "31415,93"
  ----------------------------------------------------
                     d = -exp(702);
   Tragende Stellen        |  Exponential-Format
   roundEvenExp2(d)        |  formatExp2(d)
   Wert: double -7.49E304  |  Wert: String "-7,49E304"
  ----------------------------------------------------

In dieser Gegenüberstellung wurde der Einfachheit willen ein Spezialfall
gewählt, für die allgemeine Darstellung gilt Analoges.

::: 6. ... und Format in Scala?

Die grundsätzlichen Aussagen bleiben alle auch in Scala gültig. Zur notwendigen syntaktischen Anpassung hier die obige Formatfunktion.

   1:  package AtYourOption
   2:  object DoubleFormat {
   3:   
   4:  def format (d: Double, scaleIn: Int, typus: String): String =
   5:  {
   6:      if (d.isNaN() || d.isInfinite()) return d.toString()
   7:   
   8:      val scale = scala.math.max(scaleIn, 0)
   9:      val df = new java.text.DecimalFormat()
  10:      df.setMaximumFractionDigits(scale)
  11:      df.setMinimumFractionDigits(scale)
  12:   
  13:      typus match {
  14:      case "exp" =>
  15:          val sb = new StringBuilder("0E0")
  16:          val ze = ".000000000000000000".toCharArray
  17:          if(scale > 0) sb.appendAll(ze, 0, scale+1)
  18:          df.applyPattern(sb.toString())
  19:      case "fix" =>
  20:          df.setGroupingUsed(false)
  21:          df.setMinimumIntegerDigits(1)
  22:      case _ => throw new IllegalArgumentException
  23:      }
  24:      return df.format(d)
  25:  }
  26:   
  27:  def fix2 (d: Double): String = { return format(d, 2, "fix") }
  28:  def exp2 (d: Double): String = { return format(d, 2, "exp") }
  29:   
  30:  def main(args: Array[String]): Unit = {
  31:   
  32:      val d: Double = -scala.math.exp(702)
  33:   
  34:      for (scale <- 0 until 6)
  35:          println(scale + " -> " + format(d, scale, "exp"));
  36:   
  37:      val t: Double = scala.math.Pi*10000.0
  38:   
  39:      for (scale <- 0 until 6)
  40:          println(scale + " -> " + format(t, scale, "fix"));
  41:    }
  42:  }

::: 7. Links und Literatur

Zur Verwendung der API:

[1] Sun Tutorial: Customizing Formats
[2] Java World: Format financial figures with BigDecimal

Grundsätzliches zur (dezimalen) Gleitkomma-Arithmetik:

[4] General Decimal Arithmetic
[5] Decimal Arithmetic FAQ

Nachtrag 2010:

Zwei Artikel von Elliotte Rusty Harold. 

Sometimes you're so familiar with a class you stop paying attention to it. If you could write the documentation for java.lang.Foo, and Eclipse will helpfully autocomplete the functions for you, why would you ever need to read its Javadoc? Such was my experience with java.lang.Math, a class I thought I knew really, really well. Imagine my surprise, then, when I recently happened to be reading its Javadoc for possibly the first time in half a decade and realized that the class had almost doubled in size with 20 new methods I'd never heard of. Obviously it was time to take another look.

Java's new math. Part 1: Real numbers. Part 2: Floating-point numbers.

::: ENDE