Woher wissen Sie, ob ein BigDecimal genau in Float oder Double konvertieren kann?

10

Class BigDecimalverfügt über einige nützliche Methoden, um eine verlustfreie Konvertierung zu gewährleisten:

  • byteValueExact()
  • shortValueExact()
  • intValueExact()
  • longValueExact()

Methoden floatValueExact()und doubleValueExact()existieren jedoch nicht.

Ich habe den OpenJDK-Quellcode für Methoden floatValue()und gelesen doubleValue(). Beide scheinen zu Rückfall Float.parseFloat()und Double.parseDouble()verbunden, die positive oder negative Unendlichkeit kann zurückzukehren. Wenn Sie beispielsweise eine Zeichenfolge von 10.000 9s analysieren, wird eine positive Unendlichkeit zurückgegeben. Soweit ich BigDecimalweiß , gibt es kein internes Konzept der Unendlichkeit. Das Parsen einer Zeichenfolge von 100 9s doubleergibt 1.0E100, was nicht unendlich ist, sondern an Präzision verliert.

Was ist eine vernünftige Implementierung floatValueExact()und doubleValueExact()?

Ich dachte an einer doubleLösung , die durch die Kombination von BigDecimal.doubleValue(), BigDecial.toString(), Double.parseDouble(String)und Double.toString(double), aber es sieht chaotisch. Ich möchte hier fragen, weil es eine einfachere Lösung geben kann (muss!).

Um klar zu sein, brauche ich keine Hochleistungslösung.

Kevinarpe
quelle
7
Ich denke, Sie könnten das Double in Double umwandeln und dann das Double zurück in BigDecimal transformieren und prüfen, ob Sie den gleichen Wert erhalten. Ich bin mir jedoch nicht sicher, was der Anwendungsfall ist. Wenn Sie Doubles verwenden, akzeptieren Sie bereits nicht exakte Werte. Wenn Sie genaue Werte wünschen, bleiben Sie bei BigDecimal.
JB Nizet

Antworten:

6

Vom Lesen der Dokumente , die alle mit dem tun numTypeValueExactVarianten sind die Existenz eines Bruchteils zu prüfen , oder wenn der Wert zu groß für den numerischen Typen ist und Ausnahmen werfen.

Für floatValue()und doubleValue()wird eine ähnliche Überlaufprüfung durchgeführt, aber anstatt eine Ausnahme auszulösen, wird stattdessen Double.POSITIVE_INFINITYoder Double.NEGATIVE_INFINITYfür Double und / Float.POSITIVE_INFINITYoder Float.NEGATIVE_INFINITYFloats zurückgegeben.

Daher sollte die vernünftigste (und einfachste) Implementierung der exactMethoden für float und double einfach prüfen, ob die Konvertierung POSITIVE_INFINITYoder zurückgibt NEGATIVE_INFINITY.


Außerdem , denken Sie daran , dass BigDecimalder Mangel an Präzision zu handhaben war entworfen , die aus der Verwendung kommt floatoder doublefür große irrationalen daher als @JB Nizet kommentiert , eine weitere Überprüfung Sie die hinzufügen können oben wäre das zu konvertieren doubleoder floatwieder zu BigDecimalsehen , wenn Sie noch erhalten der gleiche Wert. Dies sollte beweisen, dass die Konvertierung korrekt war.

So würde eine solche Methode aussehen floatValueExact():

public static float floatValueExact(BigDecimal decimal) {
    float result = decimal.floatValue();
    if (!Float.isInfinite(result)) {
        if (new BigDecimal(String.valueOf(result)).compareTo(decimal) == 0) {
            return result;
        }
    }
    throw new ArithmeticException(String.format("%s: Cannot be represented as float", decimal));
}

Die Verwendung von compareToanstelle von equalsoben ist beabsichtigt, um bei den Kontrollen nicht zu streng zu werden. equalswird nur dann als wahr ausgewertet, wenn die beiden BigDecimalObjekte den gleichen Wert und die gleiche Skalierung haben (Größe des Bruchteils der Dezimalstelle), während compareTodieser Unterschied übersehen wird, wenn er keine Rolle spielt. Zum Beispiel 2.0vs 2.00.

smac89
quelle
Sehr schlau. Genau das, was ich brauchte! Hinweis: Möglicherweise möchten Sie auch Float.isFinite()oder verwenden Float.isInfinite(), dies ist jedoch optional. :)
Kevinarpe
3
Sie sollten auch compareTo gegenüber equals bevorzugen, da equals () in BigDecimal 2.0 und 2.00 als unterschiedliche Werte betrachtet.
JB Nizet
Werte, die nicht genau als float dargestellt werden können, funktionieren nicht. Beispiel : 123.456f. Ich denke, dies liegt an der unterschiedlichen Größe des Signifikanten (Mantisse) zwischen 32-Bit-Float und 64-Bit-Double. In Ihrem obigen Code erhalte ich bessere Ergebnisse mit : if (new BigDecimal(result, MathContext.DECIMAL32).equals(decimal)) {. Ist dies eine vernünftige Änderung ... oder fehlt mir ein weiterer Eckfall von Gleitkommawerten?
Kevinarpe
1
@kevinarpe - entspricht 123.456fkonzeptionell Ihrem 1.0E100-Beispiel. Es fällt mir auf, dass, wenn Sie überprüfen müssen , ob die Konvertierung von BigDecimal -> binärem Gleitkomma genau ist , diese Notwendigkeit das Problem ist; dh die Umwandlung sollte nicht in Betracht gezogen werden.
Stephen C