Как сравнить bigdecimal java

Как использовать BigDecimal в Java

Когда мы начали создавать сервисы General Ledger для бухгалтерского учета, то обнаружили, что во многих местах были ошибки в 0.01 или более процентов. Это делает учёт денег практически невозможным. Кто хотел бы выставить счёт клиенту на $4.01, когда в его заказе $4.00?

Причины этих ошибок стали понятны достаточно быстро: расчёты, которые содержат суммы, количества, коррективы, и многие другие вещи обычно выполняются с малым или вообще без внимания к особенностям проблем точности и округления, которые возникают при работе с финансами.

Все эти вычисления использовали тип Java double , который не предлагает способа управления округлением числа или ограничения точности в расчётах. Мы пришли к решению, включающему использование java.math.BigDecimal , что дает нам управлять всем этим.

Эта статья — пример проблем математики финансов и учебник по использованию BigDecimal в целом.

Пример проблем при учёте финансов

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

Например, предположим, что у нас есть продукт, который стоит 10.00 в заданной валюте и местный налог с продаж 0.0825, или 8.25%. Если посчитать налог на бумаге, сумма будет:

10.00 * 0.0825 = 0.825

Поскольку точность расчёта для данной валюты две цифры после запятой, требуется округлить число 0.825. Кроме того, поскольку это налог, обычной практикой является постоянное округление до цента в большую сторону. Таким образом, после расчёта баланса по счетам в конце дня мы никогда не получим недоплату налогов.

И таким образом клиенту выставляется общий счёт на сумму 10.83 в местной валюте, а сборщику налогов выплачивается 0.83. Обратите внимание, что если продать 1000 таких продуктов, то переплата сборщику налогов была бы:

1000 * (0.83 — 0.825) = 5.00

Другой важный вопрос: где делать округление в данном расчёте. Предположим, жидкий азот продаётся по цене 0.528361 за литр. Клиент приходит и покупает 100.00 литров, поэтому посчитаем полную цену:

100.0 * 0.528361 = 52.8361

Так как это не налог, можно округлить эту цифру вверх или вниз на своё усмотрение. Предположим, округление выполняется в соответствии со стандартными правилами округления: если следующая значащая цифра меньше 5, округляем в меньшую сторону. В противном случае округляем вверх. Это даёт для окончательной цены значение 52.84.

Теперь предположим, что мы хотим дать рекламную скидку в размере 5% от всей покупки. Делать скидку с цифры 52.8361 или 52.84? Какова разница?

Расчёт 1: 52.8361 * 0.95 = 50.194295 = 50.19

Расчёт 2: 52.84 * 0.95 = 50.198 = 50.20

Обратите внимание, что окончательная цифра округлена по стандартному правилу округления.

Видите разницу в один цент между двумя цифрами? Старый код не беспокоился о принятии во внимание округления, поэтому он всегда делал вычисления как в Расчёте 1. Но в новом коде перед расчётом скидок, налогов и всего другого сначала выполняется округление, как и в Расчёте 2. Это одна из главных причин для ошибки в один цент.

Знакомство с BigDecimal

Из примеров в предыдущем разделе должно стать ясным, что необходимы две вещи:

1. Возможность задать масштаб, который представляет собой количество цифр после десятичной точки

2. Возможность задать метод округления

Класс java.math.BigDecimal принимает во внимание оба этих соображения. Смотрите документацию по BigDecimal .

Создать BigDecimal из (скалярного) числа типа double просто:

bd = new BigDecimal(1.0);

Чтобы создать BigDecimal из Double , сначала воспользуйтесь методом doubleValue() .

Однако лучшей идеей является создание из строки:

Если этого не сделать, то результат будет следующим:

bd = new BigDecimal(1.5);

Округление и масштабирование

Чтобы задать количество цифр после запятой, используйте метод .setScale(scale). Тем не менее, хорошей практикой является одновременное указание вместе с масштабом режима округления с помощью .setScale(scale, roundingMode) . Режим округления задаёт правило округления числа.

Почему желательно указать режим округления? Давайте в качестве примера используем приведённый выше BigDecimal из 1.5:

bd = new BigDecimal(1.5); // на самом деле 1.4999.

bd.setScale(1); // получаем исключение ArithmeticException

Будет сгенерировано исключение, потому что не известно, как округлить 1.49999. Так что всегда использовать .setScale(scale, roundingMode) — это хорошая идея.

Есть восемь вариантов режима округления:

ROUND_CEILING: В большую сторону

ROUND_DOWN: Отбрасывание разряда

ROUND_FLOOR: В меньшую сторону

ROUND_HALF_UP: Округление вверх, если число после запятой >= .5

ROUND_HALF_DOWN: Округление вверх, если число после запятой > .5

Округление половины по чётности округляет как обычно. Однако, когда округляемая цифра 5, округление будет идти вниз, если цифра слева от 5 чётная и вверх, если нечётная. Это лучше всего иллюстрируется примером:

a = new BigDecimal("2.5"); // цифра слева от 5 чётная, поэтому округление вниз

b = new BigDecimal("1.5"); // цифра слева от 5 нечётная, поэтому округление вверх

a.setScale(0, BigDecimal.ROUND_HALF_EVEN).toString() // => 2

b.setScale(0, BigDecimal.ROUND_HALF_EVEN).toString() // => 2

Документация Java говорит о ROUND_HALF_EVEN так: обратите внимание, что это такой режим округления, который сводит к минимуму совокупную ошибку когда при выполнении последовательности вычислений постоянно выполняется округление.

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

При делении BigDecimals будьте осторожны и указывайте способ округления в методе .divide(. ) . В противном случае можно получить ArithmeticException , если нет точного округлённого результирующего значения, например, 1/3. Таким образом, всегда следует делать так:

a = b.divide(c, decimals, rounding);

Неизменяемость и арифметика

Числа BigDecimal являются неизменными. Это означает, что если создаётся новый объект BigDecimal со значением "2.00", такой объект останется "2.00" и никогда не может быть изменён.

Так как же тогда выполняются математические расчёты? Методы .add() , .multiply() и другие возвращают новый объект BigDecimal , содержащий результат. Например, чтобы получить результат при расчёте суммы:

amount = amount.add( thisAmount );

Убедитесь, что вы не делаете это так:

ЭТО САМАЯ РАСПРОСТРАНЁННАЯ ОШИБКА ПРИ РАБОТЕ С BIGDECIMAL!

Сравнение

Важно никогда не использовать для сравнения BigDecimal метод .equals() . Этого нельзя делать потому, что функция equals будет сравнивать масштабы. Если масштабы различаются, .equals() вернёт ложь, даже если они математически равны:

BigDecimal a = new BigDecimal("2.00");

BigDecimal b = new BigDecimal("2.0");

Вместо этого следует использовать методы .compareTo() и .signum() .

a.compareTo(b); // возвращает (-1 если a < b), (0 если a == b), (1 если a > b)

a.signum(); // возвращает (-1 если a < 0), (0 если a == 0), (1 если a > 0)

Где делать округление: размышления о точности

Теперь, когда есть возможность управлять округлением расчёта, до какого знака следует округлять? Ответ зависит от того, как планируется использовать полученное число.

Вам известна требуемая точность конечного результата из потребностей пользователей. Для чисел, которые будут складываться и вычитаться для получения конечного результата, необходимо добавить ещё один десятичный разряд, так что сумма 0.0144 + 0.0143 будет округлена до 0.03, в то время как если округление выполняется до 0.01, результатом будет 0.02.

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

Сравнение BigDecimal

Даны две точки и середина между ними. Почему результат сравнения неверный? Изначально я использовал тип double, оказалось, что он не очень подходит для подобных задач. И я подумал, что ситуацию исправит класс BigDecimal.

Если Вы предлагаете использовать вместо этого сравнение разности с эпсилон, то как быть с немного иной задачей:

Дан отрезок и его середина. Имеется два метода. На вход функции подается число из отрезка, результатом функции будет ближайшая точка к входному параметру (начало или конец отрезка). Если на вход функции подается середина отрезка, то для первого метода результатом функции должно быть начало отрезка, для второго — его конец.

Код функции (на самом деле, тут несколько отрезков):

Проблема в том, что здесь нужно сравнить по модулю эти числа, и они не обязательно будут равны. А сравнение разности чисел с эпсилон удобно, если нужна проверка на равенство.

Как использовать BigDecimal в Java

методы для осуществления арифметических операций: add() , subtract() , multiply() , divide() . Используются для операций сложения, вычитания, умножения и деления соответственно.

doubleValue() , intValue() , floatValue() , longValue() и т.д. — используются для преобразования большого числа к примитивному типу Java. Будь осторожен при их использовании и не забывай про разницу во вместимости!

Вывод в консоль:

min() и max() — позволяют найти минимальное и максимальное значение из двух переданных больших чисел.
Обрати внимание: методы не являются статическими!

Вывод в консоль:

Управление округлением BigDecimal

ROUND_CEILING — округление в большую сторону

ROUND_DOWN — отбрасывание разряда

ROUND_FLOOR — округление в меньшую сторону

ROUND_HALF_UP — округление в большую сторону, если число после запятой >= .5

ROUND_HALF_DOWN — округление в большую сторону, если число после запятой > .5

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

Цифра слева от запятой — 2 — четная. Округление происходит вниз. Поскольку нам требуется 0 знаков после запятой, результатом будет 2.

Цифра слева от запятой — 3 — нечетная. Округление происходит вверх. Поскольку нам требуется 0 знаков после запятой, результатом будет 4.

ROUND_UNNECCESSARY — используется в тех случаях, когда в какой-то метод нужно передать режим округления, но число в округлении не нуждается. Если попробовать произвести округление числа при выставленном режиме ROUND_UNNECCESSARY — выброшено исключение ArithmeticException.

Сравнение BigDecimal и int в Java

какой из них является лучшим способом сравнения BigDecimal и int в Java: покрытие BigDecimal в int или преобразование int в BigDecimal ?

4 ответов

если вы ожидаете BigDecimal значение должно быть действительно большим (т. е. вне диапазона int значения, которое составляет -2 31 в 2 31 -1) и / или содержать десятичные цифры, или просто хотите играть безопасно, вы должны преобразовать int to BigDecimal чтобы избежать ошибок переполнения / усечения.

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

вы хотите преобразовать int до BigDecimal .

это потому, что вы не всегда сможете преобразовать BigDecimal в int; вы потеряете любую информацию после десятичной точки, и значение BigDecimal может находиться вне диапазона int.

As BigDecimal расширяет диапазон int вам придется преобразовать int на BigDecimal во всяком случае, их можно сравнить.

одной из причин не конвертировать int в BigDecimal является тот факт, что вы будете создавать новый объект, поэтому вы действительно бросаете мусор в свою кучу для выполнения равенства. Кроме того, примитивное равенство было бы быстрее, чем на фактическом объекте BigInteger.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *