Неточные цифры из-за двоичного представления в BigDecimals.Как мне обойти это? - PullRequest
0 голосов
/ 18 апреля 2019

Я хотел написать парсер, который преобразует String в BigDecimal.Требуется, чтобы он был на 100% точным.(Ну, я сейчас программирую для удовольствия. Поэтому я скорее прошу об этом ... ;-P)

Итак, я придумал эту программу:

public static BigDecimal parse(String term) {
    char[] termArray = term.toCharArray();

    BigDecimal val = new BigDecimal(0D);
    int decimal = 0;
    for(char c:termArray) {
        if(Character.isDigit(c)) {
            if(decimal == 0) {
                val = val.multiply(new BigDecimal(10D));
                val = val.add(new BigDecimal(Character.getNumericValue(c)));
            } else {
                val = val.add(new BigDecimal(Character.getNumericValue(c) * Math.pow(10, -1D * decimal)));
                decimal++;
            }
        }
        if(c == '.') {
            if(decimal != 0) {
                throw new IllegalArgumentException("There mustn't be multiple points in this number: " + term);
            } else {
                decimal++;
            }
        }
    }

    return val;
}

Итак, я попытался:

parse("12.45").toString();

Я ожидал, что это будет 12.45.Вместо этого это был 12.45000000000000002498001805406602215953171253204345703125.Я знаю, что это может быть связано с ограничениями двоичного представления.Но как мне обойти это?

Примечание: я знаю, что вы можете просто использовать new BigInteger("12.45");.Но это не моя точка зрения - я хочу написать это самостоятельно, независимо от того, насколько это глупо.

Ответы [ 2 ]

2 голосов
/ 18 апреля 2019

Да, это связано с ограничениями двоичного представления.Любая отрицательная сила 10 не может быть представлена ​​в точности как double.

Чтобы обойти это, замените всю арифметику double на всю арифметику BigDecimal.

val = val.add(
    new BigDecimal(Character.getNumericValue(c)).divide(BigDecimal.TEN.pow(decimal)));

С этим я получаю 12.45.

0 голосов
/ 20 апреля 2019

Это можно немного улучшить.Делить только один раз.Просто игнорируйте десятичную точку внутри цикла.Просто посчитайте десятичные дроби.Таким образом, "12.45" становится 1245 с decimal == 2.Теперь, в конце, вам нужно только разделить это на, в данном случае, BigDecimal.TEN.pow(2) (или 100), чтобы получить 12.45.

public static BigDecimal parse(String term) 
{
    char[] termArray = term.toCharArray();

    // numDecimals:  -1: no decimal point at all, so no need to divide
    //                0: decimal point found, but no digits counted yet
    //              > 0: count of digits after decimal point    

    int numDecimals = -1;
    BigDecimal val = new BigDecimal.ZERO;

    for(char c: termArray) 
    {
        if (Character.isDigit(c)) 
        {
            val = val.multiply(BigDecimal.TEN).add(BigDecimal.valueOf(Character.getNumericValue(c)));
            if (numDecimals != -1)
                numDecimals++;
        }
        else if (c == '.') 
        {
            if (numDecimals != -1) 
                throw new IllegalArgumentException("There mustn't be multiple points in this number: " + term);
            else 
                numDecimals = 0;
        }

    }

    if (numDecimals > 0)
        return val.divide(BigDecimal.TEN.pow(numDecimals));
    else
        return val;
}

Обратите внимание, что эта функция не работает для отрицательных значений,и при этом это не признает научную запись.Для этого, используя исходную строку, индекс и charAt(index), вероятно, будут более идеальными, чем текущий цикл.Но это был не вопрос.

...