Переміщення десяткових знаків у подвійному


97

Тож у мене подвійний набір дорівнює 1234, я хочу перемістити десяткове місце на 12,34

Тому для цього я помножую .1 на 1234 два рази, начебто так

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

Це надрукує результат, "12.340000000000002"

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



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.
Брукс Мойсей

1
@ Петер Лоурі: Чи можете ви пояснити більше, чому чи число непарне чи навіть вплине на округлення? Я думаю, що / = 100 і * =. 01 буде однаковим, тому що, хоча 100 є цілим, він все одно буде перетворений на 100,0 у результаті примусу типу.
eremzeit

1
/100і *0.01є рівнозначними один одному, але не ОП *0.1*0.1.
Амадан

1
Все, що я говорю, полягає в тому, що множення на 0,1 двічі в середньому введе більшу помилку, ніж множення на 0,01 раз; але я щасливо визнаю, що точка ЯсперБекерса про те, що 100 є різними, є точно бінарними.
Амадан

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
Відповіді з більш високим рейтингом є більш технічно зрозумілими, але це правильна відповідь на проблему ОП. Ми, як правило, не переймаємось незначною неточністю подвійності, тому 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
@Dan: Чому? Це правильний підхід для фінансових додатків (або будь-яких інших додатків, коли навіть крихітна помилка округлення неприпустима), зберігаючи швидкість апаратного рівня. (Звичайно, це було б перекладено в клас, як правило, не виписується кожен раз)
Амадан

7
Існує невелика проблема з цим рішенням - якщо залишок rменше 10, не виникає 0 прокладки і 1204 призведе до результату 12,4. Правильний рядок форматування більше схожий на "% d.% 02d"
jakebman

10

Це викликано тим, як комп'ютери зберігають номери з плаваючою комою. Вони так не роблять. Як програміст, ви повинні прочитати цей посібник з плаваючою комою, щоб ознайомитись з випробуваннями і негараздами обробки чисел з плаваючою комою.


Аргу, я просто писав пояснення, що посилаються на саме те місце. +1.
Попс

@ Lord Haha, вибач. Я як-небудь отримав Skeeted. :-)
CanSpice

Я зрозумів, чому, але мені цікаво, чи є якийсь креативний спосіб переміщення десяткового знака? Оскільки можна зберігати 12,34 чисто в подвійному, просто не подобається множення на .1
BlackCow

1
Якби можна було зберігати 12,34 чисто в подвійному, не вважаєш, що Java зробила б це? Це не. Вам доведеться використовувати інший тип даних (наприклад, BigDecimal). Крім того, чому б вам просто не розділити на 100, а не робити це в циклі?
CanSpice

Так ... так, поділивши це на 100 результатів на чисті 12,34 ... дякую :-P
BlackCow

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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.