Перемещение десятичных знаков в двойном

97

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

Чтобы сделать это, я умножаю 0,1 на 1234 два раза, примерно так

double x = 1234;
for(int i=1;i<=2;i++)
{
  x = x*.1;
}
System.out.println(x);

Результат будет напечатан: «12.340000000000002».

Есть ли способ, без простого форматирования до двух десятичных знаков, правильно иметь двойное хранилище 12,34?

BlackCow
источник
43
Есть ли причина, по которой вы этого не сделали x /= 100;?
Марк Ингрэм

Ответы:

189

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

Проблема в том, что 0,1 не является точным представлением, и, выполняя расчет дважды, вы усугубляете эту ошибку.

Однако 100 можно представить точно, поэтому попробуйте:

double x = 1234;
x /= 100;
System.out.println(x);

который печатает:

12.34

Это работает, потому что Double.toString(d)выполняет небольшое округление от вашего имени, но это не так много. Если вам интересно, как это может выглядеть без округления:

System.out.println(new BigDecimal(0.1));
System.out.println(new BigDecimal(x));

печатает:

0.100000000000000005551115123125782702118158340454101562
12.339999999999999857891452847979962825775146484375

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


Примечание: x / 100и x * 0.01не совсем то же самое, когда дело доходит до ошибки округления. Это связано с тем, что ошибка округления для первого выражения зависит от значений x, тогда как 0.01во втором выражении фиксированная ошибка округления.

for(int i=0;i<200;i++) {
    double d1 = (double) i / 100;
    double d2 = i * 0.01;
    if (d1 != d2)
        System.out.println(d1 + " != "+d2);
}

отпечатки

0.35 != 0.35000000000000003
0.41 != 0.41000000000000003
0.47 != 0.47000000000000003
0.57 != 0.5700000000000001
0.69 != 0.6900000000000001
0.7 != 0.7000000000000001
0.82 != 0.8200000000000001
0.83 != 0.8300000000000001
0.94 != 0.9400000000000001
0.95 != 0.9500000000000001
1.13 != 1.1300000000000001
1.14 != 1.1400000000000001
1.15 != 1.1500000000000001
1.38 != 1.3800000000000001
1.39 != 1.3900000000000001
1.4 != 1.4000000000000001
1.63 != 1.6300000000000001
1.64 != 1.6400000000000001
1.65 != 1.6500000000000001
1.66 != 1.6600000000000001
1.88 != 1.8800000000000001
1.89 != 1.8900000000000001
1.9 != 1.9000000000000001
1.91 != 1.9100000000000001
Питер Лоури
источник
26
Не могу поверить, что я вообще не думал об этом! Спасибо :-P
BlackCow
6
Хотя 100 может быть представлено точно в двоичном формате, деление на 100 не может быть представлено точно. Таким образом, написание 1234/100, как вы это сделали, на самом деле ничего не делает с основной проблемой - оно должно быть в точности равно письму 1234 * 0.01.
Brooks Moses
1
@ Питер Лоури: Не могли бы вы объяснить, почему четное или нечетное число влияет на округление? Я бы подумал, что / = 100 и * =. 01 будут одинаковыми, потому что, хотя 100 - это int, оно все равно будет преобразовано в 100.0 в результате приведения типов.
eremzeit
1
/100и *0.01эквивалентны друг другу, но не OP *0.1*0.1.
Amadan
1
Все, что я говорю, это то, что умножение на 0,1 дважды в среднем вносит большую ошибку, чем умножение на 0,01 один раз; но я с радостью признаю, что точка @JasperBekkers о 100 отличается, будучи в точности двоично-представимой.
Amadan
52

Нет - если вы хотите точно хранить десятичные значения, используйте BigDecimal. doubleпросто не может точно представить такое число, как 0,1, точно так же, как вы не можете точно записать значение трети с конечным числом десятичных цифр.

Джон Скит
источник
46

если это просто форматирование, попробуйте printf

double x = 1234;
for(int i=1;i<=2;i++)
{
  x = x*.1;
}
System.out.printf("%.2f",x);

вывод

12.34
Аугусто
источник
8
Ответы с более высоким рейтингом технически более содержательны, но это правильный ответ на проблему OP. Обычно нас не заботит небольшая неточность double, поэтому BigDecimal - это излишне, но при отображении мы часто хотим, чтобы наш вывод соответствовал нашей интуиции, поэтому System.out.printf()это правильный путь.
dimo414
28

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

int i=1234;
printf("%d.%02d\r\n",i/100,i%100);

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

Для base=p1^n1*p2^n2... вы можете представить любое N, где N = n * p1 ^ m1 * p2 ^ m2.

Пусть base=14=2^1*7^1... вы можете представить 1/7 1/14 1/28 1/49, но не 1/3

Я знаю о финансовом программном обеспечении - я преобразовал финансовые отчеты Ticketmaster из VAX asm в PASCAL. У них был свой formatln () с кодами для грошей. Причиной преобразования было то, что 32-битных целых чисел уже было недостаточно. +/- 2 миллиарда пенни - это 20 миллионов долларов, и я забыл, что это переполнено на чемпионат мира или Олимпийские игры.

Я поклялся хранить тайну. Ну что ж. В академии, если это хорошо, вы публикуете; в промышленности вы держите это в секрете.

Терренс Эндрю Дэвис
источник
12

вы можете попробовать целочисленное представление

int i =1234;
int q = i /100;
int r = i % 100;

System.out.printf("%d.%02d",q, r);
Ангел Ко
источник
5
@ Дэн: Почему? Это правильный подход для финансовых приложений (или любых других приложений, в которых недопустима даже крошечная ошибка округления), при сохранении скорости аппаратного уровня. (Конечно, это было бы обернуто в класс, обычно не выписывалось каждый раз)
Амадан
7
С этим решением есть небольшая проблема - если остаток rменьше 10, нулевое заполнение не происходит, и 1204 даст результат 12,4. Правильная строка форматирования больше похожа на "% d.% 02d"
jakebman
10

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

CanSpice
источник
Ага, я просто писал объяснение со ссылкой на то же самое место. +1.
Pops
@ Господи, хаха, прости. Я все равно получил Скитед. :-)
CanSpice
Я подумал, вот почему, но мне интересно, есть ли какой-нибудь творческий способ переместить десятичный разряд? Поскольку можно чисто сохранить 12,34 в двойном, ему просто не нравится умножение на .1
BlackCow
1
Если бы можно было аккуратно хранить 12,34 в двойнике, не думаете ли вы, что Java сделала бы это? Это не. Вам нужно будет использовать другой тип данных (например, BigDecimal). Кроме того, почему бы вам просто не разделить на 100 вместо того, чтобы делать это в цикле?
CanSpice
Да ... да, разделив это на 100, мы
получим
9

Забавно, что в многочисленных сообщениях упоминается использование BigDecimal, но никто не беспокоится о том, чтобы дать правильный ответ на основе BigDecimal? Потому что даже с BigDecimal вы все равно можете пойти не так, как показано в этом коде.

String numstr = "1234";
System.out.println(new BigDecimal(numstr).movePointLeft(2));
System.out.println(new BigDecimal(numstr).multiply(new BigDecimal(0.01)));
System.out.println(new BigDecimal(numstr).multiply(new BigDecimal("0.01")));

Дает этот вывод

12.34
12.34000000000000025687785232264559454051777720451354980468750
12.34

Конструктор BigDecimal особо отмечает, что лучше использовать конструктор String, чем числовой конструктор. На максимальную точность также влияет необязательный MathContext.

Согласно BigDecimal Javadoc, можно создать BigDecimal, который точно равен 0,1, при условии, что вы используете конструктор String.

Джастин Роу
источник
5

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

Любая приличная книга по обработке чисел описывает это. Например: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html.

И чтобы ответить на ваш вопрос:

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

double x = 1234;
for(int i=1;i<=2;i++)
{
  x =  x / 10.0;
}
System.out.println(x);
Ян Котек
источник
3

Нет, поскольку типы с плавающей запятой Java (на самом деле все типы с плавающей запятой) - это компромисс между размером и точностью. Хотя они очень полезны для множества задач, если вам нужна произвольная точность, вы должны использовать BigDecimal.

бизиклоп
источник