Como saber se um BigDecimal pode exatamente converter para flutuar ou dobrar?

10

A classe BigDecimalpossui alguns métodos úteis para garantir a conversão sem perdas:

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

No entanto, métodos floatValueExact()e doubleValueExact()não existem.

Eu li o código fonte do OpenJDK para métodos floatValue()e doubleValue(). Ambos parecem ter fallback Float.parseFloat()e Double.parseDouble(), respectivamente, que podem retornar infinito positivo ou negativo. Por exemplo, analisar uma sequência de 10.000 9s retornará infinito positivo. Pelo que entendi, BigDecimalnão tem um conceito interno de infinito. Além disso, a análise de uma sequência de 100 9s como double1.0E100, que não é infinito, mas perde precisão.

O que é uma implementação razoável floatValueExact()e doubleValueExact()?

Eu pensei em uma doublesolução através da combinação BigDecimal.doubleValue(), BigDecial.toString(), Double.parseDouble(String)e Double.toString(double), mas parece confuso. Quero perguntar aqui, porque pode (deve!) Haver uma solução mais simples.

Para ser claro, não preciso de uma solução de alto desempenho.

kevinarpe
fonte
7
Eu acho que você pode transformar para dobrar, depois transformar novamente para BigDecimal e ver se você obtém o mesmo valor. Não sei ao certo qual é o caso de uso. Se você usa duplos, já aceita ter valores não exatos. Se você deseja valores exatos, fique com BigDecimal.
JB Nizet 01/12/19

Respostas:

6

Ao ler os documentos , tudo o que faz com as numTypeValueExactvariantes é verificar a existência de uma parte da fração ou se o valor é muito grande para o tipo numérico e lançar exceções.

Quanto a floatValue()e doubleValue(), uma verificação de estouro similar está sendo feita, mas, em vez de lançar uma exceção, ela retorna Double.POSITIVE_INFINITYou Double.NEGATIVE_INFINITYpara duplas e / Float.POSITIVE_INFINITYou Float.NEGATIVE_INFINITYflutuantes.

Portanto, a implementação mais razoável (e mais simples) dos exactmétodos float e double deve simplesmente verificar se a conversão retorna POSITIVE_INFINITYou NEGATIVE_INFINITY.


Além disso , lembre-se de que ele BigDecimalfoi projetado para lidar com a falta de precisão resultante do uso floatou doublede irracionais grandes; portanto, como @JB Nizet comentou , outra verificação que você pode adicionar ao acima foi converter doubleou floatvoltar BigDecimalpara ver se você ainda consegue o mesmo valor. Isso deve provar que a conversão estava correta.

Aqui está como esse método seria 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));
}

O uso de em compareTovez de equalsacima é intencional, para não se tornar muito rigoroso com as verificações. equalsserá avaliado apenas como verdadeiro quando os dois BigDecimalobjetos tiverem o mesmo valor e escala (tamanho da parte da fração do decimal), enquanto compareToignorará essa diferença quando isso não for importante. Por exemplo 2.0vs 2.00.

smac89
fonte
Muito astuto. Exatamente o que eu precisava! Nota: Você também pode usar Float.isFinite()ou Float.isInfinite(), mas isso é opcional. :)
Kevinarpe
3
Você também deve preferir compareTo a igual a, porque igual a () no BigDecimal considerará 2,0 e 2,00 como valores diferentes.
JB Nizet 01/12/19
Valores que não podem ser representados com precisão como float não funcionarão. Exemplo: 123.456f. Eu acho que isso é devido ao tamanho diferente de significand (mantissa) entre float de 32 bits e duplo de 64 bits. Em seu código acima, eu obter melhores resultados com: if (new BigDecimal(result, MathContext.DECIMAL32).equals(decimal)) {. É uma mudança razoável ... ou estou perdendo outro caso de canto de valores de ponto flutuante?
Kevinarpe
11
@kevinarpe - 123.456fé conceitualmente o mesmo que o seu exemplo 1.0E100. Parece-me que, se você precisar verificar se a conversão do BigDecimal -> ponto flutuante binário é exata, essa necessidade é o problema; isto é, a conversão não deve ser contemplada.
Stephen C