округлить BigDecimal до ближайших 5 центов

Я пытаюсь понять, как округлить денежную сумму вверх до ближайших 5 центов. Ниже показаны мои ожидаемые результаты

1.03     => 1.05
1.051    => 1.10
1.05     => 1.05
1.900001 => 1.10

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

Обновлять

Следуя приведенным ниже советам, лучшее, что я мог сделать, это

    BigDecimal amount = new BigDecimal(990.49)

    // To round to the nearest .05, multiply by 20, round to the nearest integer, then divide by 20
   def result =  new BigDecimal(Math.ceil(amount.doubleValue() * 20) / 20)
   result.setScale(2, RoundingMode.HALF_UP)

Я не уверен, что это на 100% кошерно - я обеспокоен тем, что точность может быть потеряна при преобразовании в двойные числа и обратно. Тем не менее, это лучшее из того, что я придумал, и похоже работает.


person Dónal    schedule 21.01.2010    source источник
comment
По определению, вы все равно теряете точность, так как округляете. Я не думаю, что вам есть о чем беспокоиться в отношении потери точности.   -  person James Cronen    schedule 21.01.2010
comment
Кроме того, если вы беспокоитесь о точности, вам следует создавать свои BigDecimals с помощью конструктора String, а не двойного конструктора.   -  person Paul Wagland    schedule 24.01.2010
comment
См. Ответ @marcolopes о том, как это сделать с помощью BigDecimal без использования doubleValue().   -  person robinst    schedule 16.05.2013
comment
1:051 округляется до 1,05, а не 1,10.   -  person user207421    schedule 07.09.2017


Ответы (11)


Вы можете использовать простой двойной, чтобы сделать это.

double amount = 990.49;
double rounded = ((double) (long) (amount * 20 + 0.5)) / 20;

РЕДАКТИРОВАТЬ: для отрицательных чисел вам нужно вычесть 0,5

person Peter Lawrey    schedule 23.01.2010
comment
docs.oracle.com/javase/tutorial/java/nutsandbolts/ Как упоминалось выше, этот тип данных [double] никогда не следует использовать для точных значений, таких как валюта. - person daemonl; 22.07.2013
comment
@daemonl Хорошая цитата, за исключением того, что большинство торговых систем используют double или long, особенно те, которые используют C++. - person Peter Lawrey; 22.07.2013
comment
Источник @PeterLawrey для этого утверждения? Я никогда не видел такой системы, но я работал с системами PL/I, C и C++ в банках и других учреждениях... - person D. Kovács; 24.04.2017
comment
@ D.Kovács, какой тип данных они использовали? Я консультировал более половины крупных банков в Лондоне и Нью-Йорке по этим системам. - person Peter Lawrey; 24.04.2017
comment
@PeterLawrey Системы, с которыми я сталкивался, всегда использовали самодельные структуры данных для хранения денежных данных. Я не говорю это как абсолют, просто утверждаю, что использование простого double — очень, очень, ОЧЕНЬ плохая идея. - person D. Kovács; 27.04.2017
comment
@ D.Kovács Я могу понять, что разработчикам неудобно использовать double, и у него есть свои особенности, но в инвестиционных банках они редко используют что-то еще. - person Peter Lawrey; 27.04.2017
comment
@PeterLawrey интересно, сейчас я работаю в страховом секторе с Java, и все в BigDecimal... - person D. Kovács; 27.04.2017
comment
@D.Kovács Я работал над системами BigDecimal. Мой первый год консультирования в основном был оплачен заменой BigDecimal на double для разных клиентов, так что я многим ему обязан ;) - person Peter Lawrey; 27.04.2017
comment
@PeterLawrey Искренне интересно узнать, как вы это сделали без ошибок округления. Почему Oracle ошибается, говоря, что double никогда не следует использовать для точных значений, таких как валюта? - person David Easley; 28.04.2017
comment
@DavidEasley, существует миф о том, что вам не нужно беспокоиться об округлении. Редко когда ваши операции всегда настолько просты, что вам не нужно беспокоиться об округлении. Даже если вы используете BigDecimal, вам нужно правильно округлить. Хорошая вещь о double заключается в том, что мы часто явно ошибаемся, например, 0,9999999999999998, но BigDecimal может быть 0,99. Это правильно или неправильно? - person Peter Lawrey; 29.04.2017
comment
@DavidEasley в приведенном выше случае, когда у вас есть два значения, которые представлены без ошибок, и вы выполняете операцию, вы получаете ближайшее представимое значение к результату. Пока вы не работаете с пределами точности double, это не проблема. - person Peter Lawrey; 29.04.2017
comment
@PeterLawrey Спасибо за ответ, но вы все еще даете нам довольно расплывчатые ответы. В сети много упоминаний об опасности использования чисел с плавающей запятой для таких вещей, как валюта, т. е. там, где важна точность. Пожалуйста, не могли бы вы указать только одну ссылку (в идеале авторитетную), которая поддерживает ваше утверждение о том, что плавающая запятая (или, по крайней мере, двойная) подходит для финансовых расчетов. - person David Easley; 29.04.2017
comment
@DavidEasley Я бы указал на спецификацию IEEE-754, которая точно объясняет, как она работает последовательно и предсказуемо. То, что вы считаете заслуживающим доверия, является исключительно вопросом вашего мнения. Я могу сказать вам, что реальность такова, что большинство инвестиционных банков используют double в Java, и вы можете не верить мне, если хотите. Проекты, использующие BigDecimal, за эти годы принесли мне много денег на консультации, поэтому, пожалуйста, продолжайте использовать его, если он вам больше нравится. - person Peter Lawrey; 29.04.2017
comment
@PeterLawrey, вот почему у инвестиционного банкинга больше проблем, чем должно быть. - person D. Kovács; 03.05.2017
comment
@ D.Kovács, когда банк теряет 84 миллиарда долларов за квартал, это не была ошибка округления: P Я был в банке, который сделал это. - person Peter Lawrey; 03.05.2017
comment
@PeterLawrey Я был - и мы знаем, почему в прошедшем времени;) Кроме шуток: нет, это был очень сложный хак. Если мы говорим об одном и том же событии. Но в случае, который я имею в виду, это был не один банк, а несколько. Так что, может быть, это другое :) - person D. Kovács; 15.05.2017

Использование BigDecimal без двойников (улучшено в ответе от marcolopes):

public static BigDecimal round(BigDecimal value, BigDecimal increment,
                               RoundingMode roundingMode) {
    if (increment.signum() == 0) {
        // 0 increment does not make much sense, but prevent division by 0
        return value;
    } else {
        BigDecimal divided = value.divide(increment, 0, roundingMode);
        BigDecimal result = divided.multiply(increment);
        return result;
    }
}

Режим округления, например. RoundingMode.HALF_UP. Для ваших примеров вам действительно нужен RoundingMode.UP (bd - это помощник, который просто возвращает new BigDecimal(input)):

assertEquals(bd("1.05"), round(bd("1.03"), bd("0.05"), RoundingMode.UP));
assertEquals(bd("1.10"), round(bd("1.051"), bd("0.05"), RoundingMode.UP));
assertEquals(bd("1.05"), round(bd("1.05"), bd("0.05"), RoundingMode.UP));
assertEquals(bd("1.95"), round(bd("1.900001"), bd("0.05"), RoundingMode.UP));

Также обратите внимание, что в вашем последнем примере есть ошибка (округление 1,900001 до 1,10).

person robinst    schedule 21.05.2013
comment
Явно лучший ответ. Пожалуйста, проголосуйте, чтобы он получил более высокий рейтинг, чем те хакерские решения, которые используют плавающую точку. - person David Easley; 24.06.2014
comment
@DavidEasley Я могу понять, что разработчики не чувствуют себя комфортно, используя языковые примитивы и такие операции, как double, но это не делает их использование хакерским. - person Peter Lawrey; 27.04.2017
comment
Они не буквально хакерские, но они работают только с BigDecimals, достаточно маленькими, чтобы поместиться в double. Поэтому они не являются надежными решениями. Этот работает на любом BigDecimal. - person Tuupertunut; 18.08.2017
comment
Использование BigDecimal намного лучше выражает тот факт, что вы манипулируете суммами в валюте. - person AbuNassar; 21.10.2017
comment
это идеальный ответ и решает мою проблему. - person Vishal Patel; 20.09.2020

Я бы попробовал умножить на 20, округлить до ближайшего целого, а затем разделить на 20. Это хак, но должен дать вам правильный ответ.

person James Cronen    schedule 21.01.2010
comment
Совсем не халтура, вполне здравая техника. - person user207421; 07.09.2017

Я написал это на Java несколько лет назад: https://github.com/marcolopes/dma/blob/master/org.dma.java/src/org/dma/java/math/BusinessRules.java

/**
 * Rounds the number to the nearest<br>
 * Numbers can be with or without decimals<br>
 */
public static BigDecimal round(BigDecimal value, BigDecimal rounding, RoundingMode roundingMode){

    return rounding.signum()==0 ? value :
        (value.divide(rounding,0,roundingMode)).multiply(rounding);

}


/**
 * Rounds the number to the nearest<br>
 * Numbers can be with or without decimals<br>
 * Example: 5, 10 = 10
 *<p>
 * HALF_UP<br>
 * Rounding mode to round towards "nearest neighbor" unless
 * both neighbors are equidistant, in which case round up.
 * Behaves as for RoundingMode.UP if the discarded fraction is >= 0.5;
 * otherwise, behaves as for RoundingMode.DOWN.
 * Note that this is the rounding mode commonly taught at school.
 */
public static BigDecimal roundUp(BigDecimal value, BigDecimal rounding){

    return round(value, rounding, RoundingMode.HALF_UP);

}


/**
 * Rounds the number to the nearest<br>
 * Numbers can be with or without decimals<br>
 * Example: 5, 10 = 0
 *<p>
 * HALF_DOWN<br>
 * Rounding mode to round towards "nearest neighbor" unless
 * both neighbors are equidistant, in which case round down.
 * Behaves as for RoundingMode.UP if the discarded fraction is > 0.5;
 * otherwise, behaves as for RoundingMode.DOWN.
 */
public static BigDecimal roundDown(BigDecimal value, BigDecimal rounding){

    return round(value, rounding, RoundingMode.HALF_DOWN);

}
person marcolopes    schedule 21.01.2013
comment
Я думаю, что использование rounding.signum() == 0 было бы лучшим тестом для 0 вместо rounding.doubleValue() == 0. Кроме того, это решение хорошее. - person robinst; 16.05.2013
comment
Спасибо! Это правильно... этот код старый, и я был новичком в Java! :D signum() - это то, что нужно! - person marcolopes; 07.09.2017

Вот несколько очень простых методов на С#, которые я написал, чтобы всегда округлять вверх или вниз любое переданное значение.

public static Double RoundUpToNearest(Double passednumber, Double roundto)
    {

        // 105.5 up to nearest 1 = 106
        // 105.5 up to nearest 10 = 110
        // 105.5 up to nearest 7 = 112
        // 105.5 up to nearest 100 = 200
        // 105.5 up to nearest 0.2 = 105.6
        // 105.5 up to nearest 0.3 = 105.6

        //if no rounto then just pass original number back
        if (roundto == 0)
        {
            return passednumber;
        }
        else
        {
            return Math.Ceiling(passednumber / roundto) * roundto;
        }
    }
    public static Double RoundDownToNearest(Double passednumber, Double roundto)
    {

        // 105.5 down to nearest 1 = 105
        // 105.5 down to nearest 10 = 100
        // 105.5 down to nearest 7 = 105
        // 105.5 down to nearest 100 = 100
        // 105.5 down to nearest 0.2 = 105.4
        // 105.5 down to nearest 0.3 = 105.3

        //if no rounto then just pass original number back
        if (roundto == 0)
        {
            return passednumber;
        }
        else
        {
            return Math.Floor(passednumber / roundto) * roundto;
        }
    }
person NER1808    schedule 16.11.2010

В Scala я сделал следующее (Java ниже)

import scala.math.BigDecimal.RoundingMode

def toFive(
   v: BigDecimal,
   digits: Int,
   roundType: RoundingMode.Value= RoundingMode.HALF_UP
):BigDecimal = BigDecimal((2*v).setScale(digits-1, roundType).toString)/2

И на Яве

import java.math.BigDecimal;
import java.math.RoundingMode;

public static BigDecimal toFive(BigDecimal v){
    return new BigDecimal("2").multiply(v).setScale(1, RoundingMode.HALF_UP).divide(new BigDecimal("2"));
}
person John    schedule 14.09.2015

На основе вашего редактирования другим возможным решением будет:

BigDecimal twenty = new BigDecimal(20);
BigDecimal amount = new BigDecimal(990.49)

// To round to the nearest .05, multiply by 20, round to the nearest integer, then divide by 20
BigDecimal result =  new BigDecimal(amount.multiply(twenty)
                                          .add(new BigDecimal("0.5"))
                                          .toBigInteger()).divide(twenty);

Это имеет то преимущество, что гарантируется не потеря точности, хотя потенциально может быть медленнее, конечно...

И журнал испытаний scala:

scala> var twenty = new java.math.BigDecimal(20) 
twenty: java.math.BigDecimal = 20

scala> var amount = new java.math.BigDecimal("990.49");
amount: java.math.BigDecimal = 990.49

scala> new BigDecimal(amount.multiply(twenty).add(new BigDecimal("0.5")).toBigInteger()).divide(twenty)
res31: java.math.BigDecimal = 990.5
person Paul Wagland    schedule 23.01.2010

Чтобы этот тест прошел:

assertEquals(bd("1.00"), round(bd("1.00")));
assertEquals(bd("1.00"), round(bd("1.01")));
assertEquals(bd("1.00"), round(bd("1.02")));
assertEquals(bd("1.00"), round(bd("1.024")));
assertEquals(bd("1.05"), round(bd("1.025")));
assertEquals(bd("1.05"), round(bd("1.026")));
assertEquals(bd("1.05"), round(bd("1.049")));

assertEquals(bd("-1.00"), round(bd("-1.00")));
assertEquals(bd("-1.00"), round(bd("-1.01")));
assertEquals(bd("-1.00"), round(bd("-1.02")));
assertEquals(bd("-1.00"), round(bd("-1.024")));
assertEquals(bd("-1.00"), round(bd("-1.0245")));
assertEquals(bd("-1.05"), round(bd("-1.025")));
assertEquals(bd("-1.05"), round(bd("-1.026")));
assertEquals(bd("-1.05"), round(bd("-1.049")));

Изменить ROUND_UP в ROUND_HALF_UP :

private static final BigDecimal INCREMENT_INVERTED = new BigDecimal("20");
public BigDecimal round(BigDecimal toRound) {
    BigDecimal divided = toRound.multiply(INCREMENT_INVERTED)
                                .setScale(0, BigDecimal.ROUND_HALF_UP);
    BigDecimal result = divided.divide(INCREMENT_INVERTED)
                               .setScale(2, BigDecimal.ROUND_HALF_UP);
    return result;
}
person Mickaël Gauvin    schedule 21.08.2015

  public static BigDecimal roundTo5Cents(BigDecimal amount)
  {
    amount = amount.multiply(new BigDecimal("2"));
    amount = amount.setScale(1, RoundingMode.HALF_UP);
    // preferred scale after rounding to 5 cents: 2 decimal places
    amount = amount.divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP);
    return amount;
  }

Обратите внимание, что это в основном тот же ответ, что и у Джона.

person Reto Höhener    schedule 19.07.2017

У Тома правильная идея, но вам нужно использовать методы BigDecimal, поскольку вы якобы используете BigDecimal, потому что ваши значения не поддаются примитивному типу данных. Что-то типа:

BigDecimal num = new BigDecimal(0.23);
BigDecimal twenty = new BigDecimal(20);
//Might want to use RoundingMode.UP instead,
//depending on desired behavior for negative values of num.
BigDecimal numTimesTwenty = num.multiply(twenty, new MathContext(0, RoundingMode.CEILING)); 
BigDecimal numRoundedUpToNearestFiveCents
  = numTimesTwenty.divide(twenty, new MathContext(2, RoundingMode.UNNECESSARY));
person Matt J    schedule 21.01.2010
comment
Это вызывает исключение Исключение: необходимо округление java.lang.ArithmeticException: необходимо округление - person Dónal; 21.01.2010

person    schedule
comment
Этот псевдокод берет входные данные из линейной команды, где вы можете ввести значение для округления вместе со значением округления, т. е. введите валюту: 1,051 доллара США. Введите коэффициент округления: 0,05 доллара США. Окончательная цена после округления до 0,05: 1,10 доллара США. - person Gulshan; 16.11.2020