Числа Java, человеческое округление

Бьюсь об заклад, здесь на SO был аналогичный вопрос, я просто не мог его найти. Вопрос заключается в исправлении неточности математики Java, когда я получаю, например, число, подобное 235.00000002 или 9875.999999997. Конечно, черт возьми, для человека эти два на самом деле означают 235 и 9876, когда дается определенный порог точности, давайте назовем его «5 нулей или девяток».

Но дело не только в цифрах справа от запятой, это же правило должно применяться и к таким числам, как 23500000001.0 и 5699999999.0.

Есть идеи/библиотеки?

Обновление: Похоже, я ошибался, говоря, что это известная тема, и через несколько минут появится кто-нибудь с известной библиотекой. Итак, вот логика, которую я ожидаю: для данного числа, когда в его строковом представлении встречаются N последовательных нулей или девяток, эти нули и девятки округляются вместе с остальной частью числа справа от самого правого «0» или « 9". Например, когда N=5, число 239999995.546 становится 240000000.0, число 34.0000007 становится 34, а 340000.007 становится 340000.

Обновление 2. Вот что бывает, когда спешишь или не уделяешь должного внимания вопросу. Извиняюсь. Я говорю о "человеческом" округлении. Хорошим примером может служить сравнение вывода df и df -h на Linux-боксе. Что касается «неточности», о которой я говорил, запустите следующий код:

double d = 1000.0;
double mult = 0.12351665;
System.out.println(d * mult / mult);

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

Я уже закончил с кодом, но все еще есть шанс, что кто-то сделал это лучше.


person andbi    schedule 26.04.2014    source источник
comment
Здесь, похоже, нет актуального вопроса.   -  person Oliver Charlesworth    schedule 26.04.2014
comment
@OliCharlesworth, а что мешает задаться вопросом?   -  person andbi    schedule 26.04.2014
comment
Вам нужно более точное определение вашего вопроса. Попробуйте определить общее правило округления (в соответствии с вашими требованиями). Затем, если у вас возникнут проблемы с его реализацией, опубликуйте его здесь и попросите о помощи. Ваш вопрос в его нынешнем виде крайне расплывчат.   -  person barak manos    schedule 26.04.2014
comment
Хорошо, понял, я просто подумал, что это очевидная и хорошо известная проблема с известным решением. Я перепишу вопрос.   -  person andbi    schedule 26.04.2014
comment
какое это имеет отношение к неточности, что бы вы под этим ни подразумевали?   -  person Denis Tulskiy    schedule 26.04.2014
comment
Почему 239999995.546 хуже, чем 239989995.546? Но, возможно, вам поможет что-то вроде этого вопроса: stackoverflow.com/questions/7906996/   -  person Sebastian Höffner    schedule 26.04.2014
comment
И я просто понимаю, что ваши правила округления несовместимы: почему 240000000.0 сохраняет .0, а 340000 и 34 нет? Также вам может помочь научная нотация, предоставленная Formatter %e, она дает 2.400000e+08 для 239999995.546   -  person Sebastian Höffner    schedule 26.04.2014
comment
@SebastianHöffner, я снова обновил вопрос)) Что касается несоответствия - забудьте о .0, его можно опустить, это просто способ показать, что он все еще двойной.   -  person andbi    schedule 26.04.2014
comment
После вашего второго редактирования я должен признать, что на самом деле ответ, предлагающий BigDecimal, кажется именно тем, что вам нужно. Проблема, с которой вы сталкиваетесь, связана с точностью с плавающей запятой, а не с форматированием результатов или чем-то еще. Попробуйте 1000.0 * 0.123 / 0.123 и у вас не будет проблем с получением в результате 999.99999. Тем не менее, вы говорите, что у вас есть готовый код, но, возможно, кто-то мог бы сделать его лучше — как насчет того, чтобы вы предоставили нам код, и мы могли бы посмотреть, сможем ли мы его улучшить?   -  person Sebastian Höffner    schedule 26.04.2014
comment
Мое решение на самом деле также отлично работает с вашим примером: System.out.println(simplify(d*mult/mult)); печатает 1000.   -  person Sebastian Höffner    schedule 26.04.2014
comment
Я чувствую, что мне все еще нужно немного прояснить мою точку зрения. Мое решение просто печатает, как вы просили. Но чтобы решить проблему лучше, нужно следовать ответу, данному @Aleksander, потому что он решает корень вашей проблемы. Никто никогда не говорил, что вы должны использовать BigDecimal только для валют - и ваши примеры процентов, больших чисел, мелких дробей и т. д. идеально подходят для приложений для BigDecimals.   -  person Sebastian Höffner    schedule 26.04.2014
comment
@SebastianHöffner, конечно, я бы использовал BigDecimal, но я никогда не говорил, что это я занимаюсь математикой, я получаю эти числа через веб-сервис, который находится вне моего контроля((   -  person andbi    schedule 26.04.2014
comment
Хорошо, я думаю, что ваши расчеты вводят в заблуждение, они каким-то образом подразумевают, что вы выполняете расчеты и должны иметь дело с проблемными результатами.   -  person Sebastian Höffner    schedule 26.04.2014
comment
возможный дубликат Как округлить число до n десятичные разряды в Java   -  person mmmmmm    schedule 27.04.2014


Ответы (3)


Я немного поиграл с научной нотацией, как уже предлагал в комментариях. Вот что я придумал:

public static double simplify(Number n) {
    String numberString = String.format("%e", n);
    int indexE = numberString.indexOf('e');
    String baseValue = numberString.substring(0, indexE);
    String exponent = numberString.substring(indexE + 1);
    double base = Double.parseDouble(baseValue);
    int    exp = Integer.parseInt(exponent);

    return base * Math.pow(10, exp);
}

Я использовал все числа, которые нашел в вашем вопросе, и добавил отрицательное значение, чтобы проверить его.

public static void main(String[] args) {
    Number[] ns = new Number[]{
            239999995.546,
            239989995.546,
               340000.007,
                   34.0000007,
           5699999999.0,
                  235.00000002,
                 9875.999999997,
                -4334.345345,
          23500000001.0,
                    0.30000007,
                   -0.053999949
    };
    DecimalFormat df = new DecimalFormat("0.#####");
    for(Number n : ns) {
        String s = df.format(simplify(n));
        System.out.println("    " + n + " is " + s);
    }
}

Результаты:

2.39999995546E8 is 240000000
2.39989995546E8 is 239990000
340000.007 is 340000
34.0000007 is 34
5.699999999E9 is 5700000000
235.00000002 is 235
9875.999999997 is 9876
-4334.345345 is -4334.345
2.3500000001E10 is 23500000000
0.30000007 is 0.3
-0.053999949 is -0.054

Редактировать Я изменил код для использования двойного числа, исправил ошибку с показателями степени ‹ 0 и добавил еще один пример. Кроме того, я подключил DecimalFormat. Обратите внимание, что добавление большего количества # может изменить некоторые результаты, т. е. -0.053999949 теперь будет отображаться как -0.054, с большим количеством цифр это приведет к -0.05399995.

person Sebastian Höffner    schedule 26.04.2014
comment
Выглядит интересно. Но что случилось с -4334.345345, почему он изменился? - person andbi; 26.04.2014
comment
Научное обозначение - -4.334345e+03, захват кода делит его на -4.334345 и 3 и вычисляет 4.334345 * 10^3 = 4334.345, которое затем преобразуется в long, поэтому десятичные цифры отбрасываются. Измените тип возвращаемого значения на double, и вы получите лучшие результаты, чем вам придется бороться с большим количеством 0 после точки. редактировать: и для этого уже есть ответы: stackoverflow.com /вопросы/11284938/ - person Sebastian Höffner; 26.04.2014
comment
Да я вижу. Только что проверил, он не работает, например, с 0.30000007, но я думаю, что понял вашу идею, могу исправить это самостоятельно. Спасибо за ваше терпение)) - person andbi; 26.04.2014
comment
Я проверил, попробуйте +1 вместо +2 для показателя степени, я не думал об отрицательных показателях степени. Я обновлю свой ответ, также чтобы отразить двойники. - person Sebastian Höffner; 27.04.2014

Вы, вероятно, пытаетесь спросить, как хранить валюту и обращаться с ней. Короткие ответы: используйте тип BigDecimal или создайте собственную реализацию валюты с отдельными целочисленными полями для евро и центов (или долларов, если хотите :)).

Уже объяснено: Использование BigDecimal для работы с валютами

Почему бы не использовать Double или Float для представления валюты?

person Aleksandar Stojadinovic    schedule 26.04.2014
comment
На самом деле дело не в валюте, а в человеческом округлении чисел. - person andbi; 26.04.2014
comment
Ок, я сначала не понял вопроса. Делаю после обновления. - person Aleksandar Stojadinovic; 26.04.2014

Раз уж вы просите идей, то как насчет этого: сделать N не количеством последовательных нулей или девяток, а порогом, при котором округление числа все еще близко к исходному числу. Затем переходите от наименее значащей десятичной позиции, округляя число по одной позиции за раз и разделяя полученное число на то, что у вас было раньше. Остановитесь, если соотношение превышает ваш порог. Поэкспериментируйте с соотношением и граничными условиями, при которых округление выглядит хорошо. Используйте BigDecimal, чтобы избежать проблем с арифметикой с плавающей запятой.

person Denis Tulskiy    schedule 26.04.2014