У мяне была цана 4.35. Яе трэба было перавесці ў капейкі:
$ cat Price.java
public class Price {
public static void main(String[] args) {
System.out.println(4.35 * 100);
System.out.println((long) (4.35 * 100));
}
}
$ javac Price.java && java Price
434.99999999999994
434
Прывядзенне да цэлага адкідае хвост. Капейкі няма. Параду не захоўваць грошы ў double я прыняў, ні разу не змераўшы, чаго яна каштуе, таму змераў.
new BigDecimal(double) друкуе дакладнае дзесятковае значэнне бітаў, нічога не акругляючы на выхадзе. Іншага інструмента для разбору тут і не трэба:
4.35 -> 4.3499999999999996447286321199499070644378662109375
0.1 -> 0.1000000000000000055511151231257827021181583404541015625
0.2 -> 0.200000000000000011102230246251565404236316680908203125
0.3 -> 0.299999999999999988897769753748434595763683319091796875
0.5 -> 0.5
У double 4.35 ляжыць крыху ніжэй за 4.35. Множанне на сто пакідае яго крыху ніжэй за 435, а прывядзенне да long адсякае ўсё пасля кропкі. Math.round дасць тут 435. Але ён не ратуе, калі недакладная сама стаўка.
Чатыры дакладныя цаны са ста
Я прагнаў праз тое ж параўнанне кожнае значэнне з двума знакамі ад 0.01 да 100.00: параўноўваючы double з дзесятковым лікам, які ён абазначае.
for (long c = 1; c <= 10000; c++) {
double v = c / 100.0;
if (new BigDecimal(v).compareTo(BigDecimal.valueOf(c, 2)) == 0) exactCount++;
}
exact: 400 of 10000
first hundred: 0.25 0.50 0.75 1.00
Чатыры працэнты. Лік double складаецца са ступеняў двойкі, таму сотая частка ўкладваецца ў яго толькі тады, калі скарачаецца да чвэрцяў. Усякая іншая цана робіцца набліжэннем ужо пры разборы радка.
Астатнія дзевяноста шэсць ужо набліжэнні, да ўсялякай арыфметыкі.
Складаем 0.10 дзесяць разоў
Спачатку хрэстаматыйны прыклад:
double acc = 0.0;
for (int i = 0; i < 10; i++) acc += 0.10;
sum of ten 0.10 = 0.9999999999999999
acc == 1.0 ? false
exact = 0.99999999999999988897769753748434595763683319091796875
Я чакаў, што дорага абыдзецца менавіта назапашванне, таму запусціў мільён складанняў па 0.01 і побач палічыў тое самае на BigDecimal:
after 1000000 additions, double = 10000.000000171856
exact = 10000.00
difference in rubles = 1.71856299857608973979949951171875E-7
Потым я акругляў суму ў double да капеек на кожным з мільёна крокаў і звяраў з дакладнай сумай. Разыходжанняў не было. Ніводнага за мільён крокаў. Дрэйф сапраўды ёсць, на сёмым знаку. Акругленне да другога з’ядае яго цалкам.
Капейка знікае на дакладнай палове
Бяром расійскую стаўку ПДВ у 18 працэнтаў і лічым падатак двума спосабамі для кожнай сумы ад 0.01 да 10000.00:
long viaDouble = Math.round(cents / 100.0 * 0.18 * 100.0);
long exact = BigDecimal.valueOf(cents, 2)
.multiply(new BigDecimal("0.18"))
.setScale(2, RoundingMode.HALF_UP)
.movePointRight(2).longValueExact();
18%: mismatches = 3656 of 1000000, first: 1.25(exact 23 vs 22) 5.75(exact 104 vs 103)
6.75(exact 122 vs 121) 10.75(exact 194 vs 193) 11.75(exact 212 vs 211)
На самым маленькім выпадку відаць усю механіку:
1.25 as a double 1.25
0.18 as a double 0.179999999999999993338661852249060757458209991455078125
product 0.22499999999999997779553950749686919152736663818359375
times 100 22.499999999999996447286321199499070644378662109375
Math.round 22
1.25 уваходзіць у тыя самыя чатырыста дакладных цэн, а 0.18 захоўваецца ніжэй за сваё дзесятковае значэнне. Дакладны здабытак роўны 0.225. Ён ляжыць роўна паміж дзвюма капейкамі, таму HALF_UP падымае яго да 0.23. Double аказваецца на 3.55e-15 ніжэй за сярэдзіну і акругляецца ўніз, да 0.22.
Double аказваецца ніжэй. Math.round адпраўляе яго да 22. Разрыў намаляваны куды шырэй, чым 3.55e-15.
Я паўтарыў прагон на шасці стаўках, лічачы разыходжанні асобна ў залежнасці ад таго, ці трапіў дакладны здабытак на палову:
| стаўка | палавінных выпадкаў | памылак на палове | памылак па-за палавінамі |
|---|---|---|---|
| 3% | 10000 | 2030 | 0 |
| 5% | 50000 | 818 | 0 |
| 7% | 10000 | 0 | 0 |
| 18% | 20000 | 3656 | 0 |
| 20% | 0 | 0 | 0 |
| 25% | 250000 | 16405 | 0 |
Нечаканай аказалася апошняя калонка. Шэсць ставак, па мільёне сум на кожную. Гэты лічыльнік так і не зрушыўся з нуля.
У стаўкі 20 працэнтаў палавін няма зусім. Падатак у капейках гэта цана ў капейках, памножаная на два і падзеленая на дзесяць. Цотны лічнік ніколі не дае астачы пяць. Гэта чыстая арыфметыка. Яна трымаецца па-за прагонам. У сямі працэнтаў палавіны ёсць. Іх дзесяць тысяч на мільён. Ніводнай памылкі. Я пашырыў той прагон да дзесяці мільёнаў сум, каб упэўніцца. Сто тысяч палавін, усё гэтак жа нуль.
Мне хацелася вывесці правіла з таго, у які бок захоўваецца кожная стаўка. 0.05, 0.07 і 0.20 ляжаць вышэй за сваё дзесятковае значэнне, 0.03 і 0.18 ніжэй, 0.25 дакладная. Калі адсартаваць па долі згубленых палавін, амаль сыходзіцца. Больш за ўсіх губляюць тыя, што захоўваюцца ніжэй: 20.3 паловы са ста на трох працэнтах і 18.3 на васямнаццаці. 0.25 захоўваецца дакладна і губляе 6.6. Далей 0.05 ляжыць вышэй і ўсё роўна губляе 1.6, хоць па адным кірунку мусіў бы быць нуль. Цана паспявае прайсці праз cents / 100.0 да таго, як стаўка яе кранае, так што стаўка гэта толькі палова таго, што адбываецца. Правіла ў мяне няма.
У BigDecimal свае вострыя куты
Змена тыпу сама па сабе праблему не здымае:
new BigDecimal(0.1) = 0.1000000000000000055511151231257827021181583404541015625
BigDecimal.valueOf(0.1) = 0.1
new BigDecimal("0.1") = 0.1
Канструктар, які прымае double, пераносіць памылку ўнутр як ёсць. valueOf ідзе праз Double.toString і атрымлівае кароткі запіс. Радковы канструктар double не бачыць увогуле.
1.0 equals 1.00 = false
1.0 compareTo 1.00 = 0
hash 1.0 = 311
hash 1.00 = 3102
Маштаб, лік знакаў пасля коскі, удзельнічае ў equals, таму HashSet трымае 1.0 і 1.00 як дзве розныя цаны. Гэтыя два хэшы тое, што робіць рэалізацыя, а не тое, што абяцае javadoc. А дзяленне ўвогуле адмаўляецца адгадваць:
1/3 -> ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
Мне падабаецца, што яно кідае выключэнне. double вярнуў бы 0.3333333333333333 і дазволіў цягнуць гэта далей.
Цэлыя капейкі ў long справіліся не горш
Той самы падатак, той самы мільён сум, але ў цэлых капейках і з акругленнем, напісаным рукамі:
long ints = (cents * 18 + 50) / 100;
(cents*18 + 50)/100 vs BigDecimal HALF_UP: mismatches=0
Нуль разыходжанняў супраць setScale(2, HALF_UP) на кожнай з іх. У long змяшчаецца 9223372036854775807 капеек. Нават double лічыць цэлыя капейкі дакладна да 9007199254740992 штук. Гэта дзевяноста трыльёнаў рублёў, так што памер ліку ніколі і не быў праблемай. Плата ў іншым: правіла акруглення цяпер жыве ў маім кодзе і трымаць яго мушу я.
Чаго я не правяраў
HALF_EVEN памяншае эфект прыкладна ўдвая, але не здымае. Праз Math.rint той жа прагон на 18 працэнтах дае 1830 памылак на мільён. Палова ад 3656 гэта 1828, прыкладна столькі я і чакаю, калі палова серадзінных выпадкаў і так акруглялася ўніз. Дзве астатнія я не разбіраў.
Адкрытым засталося вось што: валюты з трыма знакамі пасля коскі. Падзел адной сумы на некалькі радкоў рахунку так, каб часткі складаліся назад у цэлае. І JDBC-драйвер з калонкай тыпу NUMERIC, куды я пакуль не зазіраў.