Достаточно ли преобразовать double в BigDecimal непосредственно перед сложением, чтобы сохранить исходную точность? - PullRequest
0 голосов
/ 02 января 2019

Мы решаем ошибку, связанную с числовой точностью. Наша система собирает некоторые числа и выплевывает их сумму. Проблема в том, что система не сохраняет числовую точность, например . 300,7 + 400,9 = 701,599 ... в то время как ожидаемый результат будет 701,6. Предполагается, что точность адаптируется к входным значениям, поэтому мы не можем просто округлить результаты до фиксированной точности.

Проблема очевидна, мы используем double для значений, а сложение накапливает ошибку из двоичного представления десятичного значения.

Путь данных следующий:

  1. XML-файл, тип xsd: десятичный
  2. Разобрать в java примитивный дубль. 15 десятичных знаков должно быть достаточно, мы ожидаем значения не более 10 цифр, 5 цифр дроби.
  3. Сохранение в БД MySql 5.5, тип double
  4. Загрузка через Hibernate в сущность JPA, т.е. все еще примитивный double
  5. Сумма связок этих значений
  6. Вывести сумму в другой XML-файл

Теперь я предполагаю, что оптимальным решением будет преобразование всего в десятичный формат. Неудивительно, что существует давление, чтобы пойти с самым дешевым решением. Оказывается, что преобразование удваивается в BigDecimal непосредственно перед добавлением пары чисел, работает в случае B в следующем примере:

import java.math.BigDecimal;

public class Arithmetic {
    public static void main(String[] args) {
        double a = 0.3;
        double b = -0.2;
        // A
        System.out.println(a + b);//0.09999999999999998
        // B
        System.out.println(BigDecimal.valueOf(a).add(BigDecimal.valueOf(b)));//0.1
        // C
        System.out.println(new BigDecimal(a).add(new BigDecimal(b)));//0.099999999999999977795539507496869191527366638183593750
    }
}

Подробнее об этом: Почему нам нужно преобразовать double в строку, прежде чем мы сможем преобразовать его в BigDecimal? непредсказуемость конструктора BigDecimal (double)

Я боюсь, что такой обходной путь будет бомбой-тикающим. Во-первых, я не уверен, что эта арифметика является пуленепробиваемой для всех случаев. Во-вторых, все еще существует некоторый риск того, что кто-то в будущем может реализовать некоторые изменения и изменить B на C, потому что эта ошибка далеко не очевидна, и даже модульный тест может не выявить ошибку.

Я хотел бы согласиться со вторым пунктом, но вопрос в следующем: даст ли этот обходной путь правильные результаты? Может ли быть случай, когда как-то

Double.valueOf("12345.12345").toString().equals("12345.12345")

неверно? Учитывая, что Double.toString, в соответствии с javadoc, печатает только те цифры, которые необходимы для уникального представления лежащего в основе двойного значения, так что при повторном анализе это дает то же самое двойное значение? Разве этого не достаточно для этого случая использования, когда мне нужно только добавить числа и вывести сумму с помощью этого волшебного Double.toString (Double d) метода? Чтобы было ясно, я предпочитаю то, что считаю чистым решением 1046 *, везде использующим BigDecimal, но мне не хватает аргументов для его продажи, в идеале я имею в виду пример, в котором преобразование в BigDecimal до добавления завершается неудачно чтобы выполнить работу, описанную выше.

Ответы [ 2 ]

0 голосов
/ 02 января 2019

Если вы не можете избежать синтаксического анализа в примитиве double или хранить его как double, вам следует преобразовать в BigDecimal как можно раньше.

double не может точно представлять десятичные дроби.Значение в double x = 7.3; никогда не будет ровно 7,3, но что-то очень очень близкое к нему, с разницей, видимой от 16-й цифры или около того справа (давая 50 десятичных знаков или около того).Не вводите в заблуждение тот факт, что печать может дать ровно «7,3», поскольку печать уже выполняет какое-то округление и не показывает точное число.

Если вы выполняете много вычислений с doubleчисла, крошечные различия в конечном итоге будут суммироваться, пока они не превысят вашу терпимость.Поэтому использование двойных чисел в вычислениях, где требуются десятичные дроби, действительно является бомбой замедленного действия.

[...] мы ожидаем, что значения не превышают всего 10 цифр, 5 цифр дроби.

Я прочитал это утверждение, чтобы означать, что все числа, с которыми вы имеете дело, должны быть точными кратными 0.00001, без каких-либо дополнительных цифр.Вы можете преобразовать двойные числа в такие BigDecimals с помощью

new BigDecimal.valueOf(Math.round(doubleVal * 100000), 5)

. Это даст вам точное представление числа с 5 десятичными дробными цифрами, 5-значными дробями, ближайшим к входу doubleVal.Таким образом, вы исправляете крошечные различия между doubleVal и десятичным числом, которое вы изначально имели в виду.

Если вы просто используете BigDecimal.valueOf(double val), вы пройдете через строковое представление двойного васвы используете, который не может гарантировать, что это то, что вы хотите.Это зависит от процесса округления внутри класса Double, который пытается представить двойное приближение 7,3 (возможно, 7,30000000000000123456789123456789125) с наиболее вероятным числом десятичных цифр.В результате получается «7.3» (и, слава разработчикам, довольно часто совпадает с «ожидаемой» строкой), а не «7.300000000000001» или «7.3000000000000012», которые кажутся мне одинаково правдоподобными.

Вот почемуЯ рекомендую не полагаться на это округление, а выполнить округление самостоятельно, сдвинув десятичное число на 5 позиций, затем округлив до ближайшего длинного и построив BigDecimal, уменьшенное на 5 десятичных знаков.Это гарантирует, что вы получите точное значение с (не более) 5 дробными десятичными разрядами.

Затем выполните вычисления с BigDecimals (используя соответствующий MathContext для округления, если необходимо).

Когданаконец, вам нужно сохранить число как двойное, используйте BigDecimal.doubleValue().Результирующее двойное число будет достаточно близко к десятичному знаку, так что вышеупомянутое преобразование, несомненно, даст вам тот же BigDecimal, который у вас был раньше (если только у вас нет действительно огромных чисел, таких как 10 цифр перед десятичной точкой - вы потерялись с double в любом случае).

PS Обязательно используйте BigDecimal, только если дроби десятичные относятся к вам - были времена, когда валюта британского шиллинга состояла из двенадцати пенсов.Представление дробных фунтов в виде BigDecimal даст катастрофу намного хуже, чем использование двойных.

0 голосов
/ 02 января 2019

Это зависит от базы данных, которую вы используете. Если вы используете SQL Server, вы можете использовать тип данных как числовой (12, 8), где 12 представляют числовое значение, а 8 представляет точность. аналогично, для моего SQL DECIMAL (5,2) вы можете использовать.

Вы не потеряете значение точности, если будете использовать вышеупомянутый тип данных.

Java Hibernate Класс:

Вы можете определить частная двойная широта;

База данных:

Database Design

...