Floats auf "schöne" Dezimalzahl runden

Hi,
ich stehe gerade etwas auf dem Schlauch:

    String formatAmount() {
        BigDecimal s = BigDecimal.valueOf(amount);
        BigDecimal p = BigDecimal.valueOf(precision);
        String ps = s.divide(p, RoundingMode.DOWN).multiply(p).toPlainString();
        System.out.println("ps = " + ps);
        return ps;
    }

amount und precision sind float-Wert, die ich mit Float.parseFloat( ) eingelesen habe.

Es kommt so etwas heraus:
ps = 10.1375598907470698556860271860696

Wenn die Präzision aber zum Beispiel 0.01, dann sollte so etwas herauskommen: 10.13

Weiß jemand, woran das liegt?

Hallo,

das was du machen möchtest, lässt sich nativ mit BigDecimal#setScale(int, RoundingMode) realisieren. Allerdings gibst du dabei nicht 0.01, sondern 2 als scale an.

Gibt es einen Grund, weshalb du selbst rumrechnen möchtest?

Viele Grüße
Christian

Danke. Die Präzision, hier 0.01, ist vorgegeben, deshalb muss ich selber rechnen.

An welcher Stelle muss ich setScale( ) aufrufen? :slight_smile:

Jetzt schein es zu funktionieren…

String formatAmount() {
    BigDecimal s = new BigDecimal(String.valueOf(amount));
    BigDecimal p = new BigDecimal(String.valueOf(precision));
    String ps = s.divide(p, 0, RoundingMode.DOWN).multiply(p).toPlainString();
    System.out.println("ps = " + ps);
    return ps;
}

Aber es muss doch auch anders funktionieren. Logarithmus?

Grüße

Es gibt da in unserem hauseigenen Wiki sogar einen Artikel dazu: https://wiki.byte-welt.net/wiki/Fließkommazahlen_mit_Java_runden . (Die Dinger heißen aber Gleitkommazahlen, nicht Fließkommazahlen - hm).

Wenn es nur darum geht, die gegebene Zahl in einen „schönen“ String zu verwandeln, dann ist BigDecimal der falsche Weg. Das ist eine höchst-komplexe Klasse für mathematische Dinge, die man sehr selten braucht - und wenn sowas triviales wie eine Zahl auszugeben so ein komplexes Ding erfordern würde, wäre was falsch gelaufen.

Ganz trivial ist es aber tatsächlich nicht.

Die Anforderung, dass die „precision“ in der angedeuteten Form angegeben wird, ist … ja, blöd. Das kann man wohl so sagen. Aus „irgendeiner Zahl“ irgendeine sinnvolle Formatierungsregel abzuleiten führt fast zwangsläufig zu irgendwelchen Krämpfen: Was passiert bei precision=100, oder precision=-123, oder precision=-0.01, oder precision=0.09983, oder precision=Float.NaN? Das ergibt alles keinen Sinn. (Wer auch immer für diese Vorgabe verantwortlich ist: Sag’ ihm, dass das keinen Sinn ergibt!).

Besser wäre es, wenn direkt die Anzahl der gewünschten Nachkommastellen angegeben werden könnte.

Aber wenn es denn sein muss:

Ja, das ist das Zauberwort. Mit dem log10 kann man ausrechnen, wie viele Nachkommastellen die precision „0.01“ denn haben soll. (Das muss man dann noch runden - eigentlich immer _auf_runden, aber … wegen floating-point-Ungenauigkeiten ist’s hier ein round…).

Als Beispiel:

import java.util.Locale;

public class PrintFloat
{
    public static void main(String[] args)
    {
        float amount = 10.137559f;
        test(amount, 10.0f);
        test(amount, 1.0f);
        test(amount, 0.1f);
        test(amount, 0.01f);
        test(amount, 0.001f);
        test(amount, 0.0001f);
        test(amount, 0.00001f);
        test(amount, 0.000001f);
        test(amount, 0.0000001f);
    }
    
    private static void test(float amount, float precision)
    {
        System.out.printf("%-20s formatted with precision %-10s is %s\n", 
            amount, precision, formatAmount(amount, precision));
    }
    
    private static String formatAmount(float amount, float precision) 
    {
        int digits = Math.max(0, (int)Math.round(-Math.log10(precision)));
        return String.format(Locale.ENGLISH, "%."+digits+"f", amount);
    }

    // Better:
    private static String formatAmount(float amount, int digits) 
    {
        return String.format(Locale.ENGLISH, "%."+digits+"f", amount);
    }    
    
}

Ausgabe:

10.137559            formatted with precision 10.0       is 10
10.137559            formatted with precision 1.0        is 10
10.137559            formatted with precision 0.1        is 10.1
10.137559            formatted with precision 0.01       is 10.14
10.137559            formatted with precision 0.001      is 10.138
10.137559            formatted with precision 1.0E-4     is 10.1376
10.137559            formatted with precision 1.0E-5     is 10.13756
10.137559            formatted with precision 1.0E-6     is 10.137559
10.137559            formatted with precision 1.0E-7     is 10.1375589

Ok, Danke für deine Hilfe.

Ich hab das jetzt so:

String formatAmount() {
    String ps = String.valueOf(Math.floor(amount / precision) * precision);
    System.out.println("ps = " + ps);
    return ps;
}

Es muss, „aus Gründen“, immer abgerundet werden.

amount und precision habe ich geändert in double. Math.floor( ) will leider double haben und gibt auch nur double zurück. D. h., wenn man mit float rechnet, dann entstehen diese Rundungsfehler.

Es geht sicherlich auch besser, aber precision wird nun mal so von der Exchange vorgegeben. float und double beißen sich leider etwas, bzw., die gesamten Math-Funktionen sind für double konzipiert worden; dass aber bei der Umwandlung von double nach float Ungenauigkeiten auftreten, wurde nicht bedacht.

Das ist extrem kompliziert, und das:

funktioniert doch nicht immer, auch wenn amount und precision double-Werte sind.


Durch das floor bzw. down gibt es hier eine zusätzliche Anforderung. Neuer Versuch:

public static String formatAmount(double sum, double price, double precision) {
    final int digits = Math.max(0, (int) Math.round(-Math.log10(precision)));
    String ps = BigDecimal.valueOf(sum / price + 1e-5).setScale(digits, RoundingMode.DOWN).toPlainString();
    System.out.println("ps = " + ps);
    return ps;
}

DAS funktioniert (aber auch nicht für alle denkbaren Werte). Das würde zum Beispiel funktionieren:

System.out.println(formatAmount(9.99, 2.001, 0.01));
// L-> 4.99, ok denn: 2.001*4.99 <= 9.99

Die zusätzlich Anforderung ist hier, dass amount mal price <= sum IMMER sein muss.

Deshalb frage ich mich jetzt, wie dieses + 1e-5 gewählt werden muss… Hast du dazu eine Idee?

Sowohl bei double als auch bei float entstehen unter bestimmten Bedingungen Fehler. Es sind so gesehen auch die gleichen Fehler, sie sind nur unterschiedlich groß.

(Nebenbei: Wenn es um dieses Thema geht, neigen einige Schlauköpfe dazu, auf What Every Computer Scientist Should Know About Floating-Point Arithmetic zu verweisen, aber … falls jemand behaupten will, das von Anfang bis Ende gelesen zu haben, möge er sich bei mir melden. Und falls jemand behaupten will, das alles nachvollzogen und verstanden zu haben: Ja, du Nerd, und, wie formatiert man jetzt so einen albernen String? Man kann da unterschiedlichSTe Prioritäten setzen…)


Du redest jetzt von „funktionieren“, erwähnst nebenbei sowas wie amount mal price <= sum (und keiner weiß, was „amount“ in dem Fall ist), und stellst zu Recht die Frage, was es mit dieser 1e+5 auf sich hat, aber die übergeordnete Frage ist: Was heißt „funktionieren“? Hast du eine Spezifikation? Sagt die, was bei precision=-0.123 passieren soll?

Was auch immer du da für clevere Formatierungsregeln austüftelst: Wenn dann jemand kommt, und das mit formatAmount(1234, 0.000000234, -100.666) aufruft, wird irgendwas „nicht funktionieren“.

(Es könnte sein, dass die Regeln so abstrus sind, dass man das ganze wirklich am „einfachsten und sichersten“ auf Basis von Strings formatiert. Aber bevor man auf solche verzweifelten Lösungen zurückgreift, sollte man sicher sein, dass es nichts „besseres“ gibt…)

Danke für den Hinweis. Meinen Fehler habe ich behoben.

funktioniert == erfüllt die Anforderung (für die meisten Fälle)
funktioniert nicht == erfüllt nicht die Anforderung (für die meisten Fälle)

ich dachte, das sei nicht so schwer zu verstehen…

amount ist der Rückgabewert der Methode (das, was sie zurückgibt). precision kann nur 1.0, 0.1, 0.01, usw. sein. Und es ist 1e-5, nicht 1e+5 … ein kleiner, aber feiner Unterschied …

Was ist denn die eigentliche Problemstellung?

Wenn man zB. Werte von Geld/Währungen/Sicherheiten verarbeiten will, braucht man IME eher spezialisierte Lösungen.

Die Problemstellung ist: Geld(-Arithmetik) :grinning_face_with_smiling_eyes:

Die Rest Api liefert Strings, die Dezimalzahlen beinhalten.

Hier mal aus der Docu:

Für amount==Rückgabe der Methode==quantity muss also gelten: (quantity-precision)%precision==0 (<-- da steht 0! NICHT irgendetwas nahe 0…)

Wenns gar nicht anders geht, nehm ich halt long… Die 32 oberen Bit die Vorkommastellen und die 32 unteren Bit die Nachkommastellen. Müsste doch klappen?