Как узнать, может ли BigDecimal точно конвертировать в float или double?

10

В классе BigDecimalесть несколько полезных методов, гарантирующих конвертацию без потерь:

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

Однако методов floatValueExact()и doubleValueExact()не существует.

Я прочитал исходный код OpenJDK для методов floatValue()и doubleValue(). Оба, кажется, отступают Float.parseFloat()и Double.parseDouble(), соответственно, могут возвращать положительную или отрицательную бесконечность. Например, разбор строки 10000 9s вернет положительную бесконечность. Как я понимаю, BigDecimalнет внутренней концепции бесконечности. Кроме того, анализ строки 100 9 с, как doubleдает 1.0E100, который не является бесконечностью, но теряет точность.

Что такое разумная реализация floatValueExact()и doubleValueExact()?

Я думал о doubleрешении путем объединения BigDecimal.doubleValue(), BigDecial.toString(), Double.parseDouble(String)и Double.toString(double), но это выглядит неаккуратно. Я хочу спросить здесь, потому что может (должно быть!) Быть более простое решение.

Чтобы было ясно, мне не нужно высокопроизводительное решение.

kevinarpe
источник
7
Я думаю, вы могли бы преобразовать в double, затем преобразовать обратно в double в BigDecimal, и посмотреть, получите ли вы то же значение. Не уверен, что в случае использования, хотя. Если вы используете double, вы уже принимаете не точные значения. Если вы хотите точные значения, то оставайтесь с BigDecimal.
JB

Ответы:

6

От чтения в документации , все это делает с numTypeValueExactвариантами, чтобы проверить существование дробной части или если значение слишком велико для числового типа и бросать исключения.

Что касается floatValue()и doubleValue(), аналогичная проверка переполнения выполняется, но вместо того, чтобы генерировать исключение, вместо этого он возвращает Double.POSITIVE_INFINITYили Double.NEGATIVE_INFINITYдля удвоений и Float.POSITIVE_INFINITYили Float.NEGATIVE_INFINITYдля чисел с плавающей запятой.

Поэтому наиболее разумная (и самая простая) реализация exactметодов для float и double должна просто проверять, возвращает ли преобразование POSITIVE_INFINITYили NEGATIVE_INFINITY.


Кроме того , помните, что это BigDecimalбыло разработано, чтобы справиться с недостатком точности, возникающим из-за использования floatили doubleдля больших иррациональных чисел , поэтому, как прокомментировал @JB Nizet , другой проверкой, которую вы можете добавить к вышеприведенному, будет преобразование doubleили floatобратно, BigDecimalчтобы увидеть, если вы все еще получаете такое же значение. Это должно доказать, что преобразование было правильным.

Вот как будет выглядеть такой метод 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));
}

Использование compareToвместо equalsвышеупомянутого является намеренным, чтобы не стать слишком строгим с проверками. equalsбудет BigDecimalиметь значение true, только если два объекта имеют одинаковое значение и масштаб (размер дробной части десятичной дроби), тогда как compareToпропустит эту разницу, когда это не имеет значения. Так , например 2.0против 2.00.

smac89
источник
Очень хитрый. Именно то, что мне было нужно! Примечание: вы также можете использовать Float.isFinite()или Float.isInfinite(), но это не обязательно. :)
Кевинарпе
3
Вы также должны предпочесть CompareTo над равными, потому что equals () в BigDecimal будет рассматривать 2.0 и 2.00 как разные значения.
JB
Значения, которые нельзя точно представить как float, работать не будут. Пример: 123.456f. Я предполагаю, что это происходит из-за разного размера значений и (мантиссы) между 32-разрядным и 64-разрядным двойным. В вашем коде выше, я получаю лучшие результаты с: if (new BigDecimal(result, MathContext.DECIMAL32).equals(decimal)) {. Это разумное изменение ... или я пропускаю еще один угловой случай значений с плавающей запятой?
Кевинарпе
1
@kevinarpe - 123.456fконцептуально такой же, как ваш пример 1.0E100. Она поражает меня , что , если нужно , чтобы проверить , что преобразование из BigDecimal -> двоичная с плавающей точкой является точным , то этой потребностью является проблемой; т.е. преобразование не должно быть предусмотрено.
Стивен С