Який тип даних я повинен використовувати для ігрової валюти?


96

Чи слід у простій грі з бізнес-імітацією (вбудованою в Java + Slick2D) зберігати поточну суму гравця як floatабо int, або щось інше?

У моєму випадку використання більшості транзакцій використовуватиме центи (0,50 долара США, 1,20 долара тощо), і будуть задіяні прості розрахунки процентних ставок.

Я бачив людей, які говорять, що ви ніколи не використовуєте floatдля валюти, а також людей, які говорять, що ви ніколи не повинні використовувати intвалюту. Я відчуваю, що я повинен використовувати intі округляти будь-які необхідні відсоткові розрахунки. Що я повинен використовувати?


2
Це питання обговорювалося на StackOverflow. Здається, BigDecimal - це, мабуть, шлях. stackoverflow.com/questions/285680/…
Аде Міллер

2
Чи у Java немає такого Currencyтипу, як Delphi, який використовує масштабовану математику з фіксованою точкою, щоб дати вам десяткову математику без проблем з точністю, притаманними плаваючою комою?
Мейсон Уілер

1
Це гра. Бухгалтерський облік не збирається лишити вас за закруглену копійку, і тому нормальні причини проти використання плаваючої точки для валюти не мають значення.
Лорен Печтел

@MasonWheeler: у Java є BigDecimalтакі проблеми.
Мартін Шредер

Відповіді:


92

Ви можете користуватися intі враховувати все в центах. 1,20 долара - це лише 120 центів. На дисплеї ви ставите десяткову точку, де вона належить.

Розрахунки відсотків були б просто усіченими або округлими. Тому

newAmt = round( 120 cents * 1.04 ) = round( 124.8 ) = 125 cents

Таким чином, у вас немає безладних десятків, які завжди стикаються. Ви можете розбагатіти, додавши незрахувані гроші (через округлення) на свій банківський рахунок


3
Якщо ви використовуєте round, немає справжніх причин для того, щоб відмовитися int, чи є?
sam hocevar

19
+1. Числа з плаваючою комою наводять порівняння рівності, їх складніше правильно відформатувати та впроваджувати смішні помилки округлення з часом. Краще все це робити самостійно, щоб згодом не отримувати скарги на це. Насправді це не велика робота.
Dobes Vandermeer

+1. Звучить як хороший шлях для моєї гри. Я сам розберуся з округленням ints і тримаюсь подалі від усіх проблем з поплавком.
Лукас Туліо

Просто відмова від відповідальності: я змінив правильну відповідь на цю, бо в кінцевому підсумку я став тим, чим я користувався. Це набагато простіше, і в контексті такої простої гри незлічені десяткові знаки насправді не мають значення.
Лукас Туліо

3
Однак використовуйте Базові бали, а не центи (де 100 базових балів = 1 цент) з коефіцієнтом 10000 замість 100. Це потрібно GAAP для всіх програм фінансового, банківського та бухгалтерського обліку.
Пітер Геркенс

66

Гаразд, я вскочу.

Моя порада: це гра. Зробіть це просто та користуйтеся double.

Ось моє обгрунтування:

  • floatУ вас є проблема точності, яка з’являється при додаванні одиниць до мільйонів, тому, хоч це може бути доброякісним, я б уникну цього типу. doubleтільки починають виникати проблеми навколо квінтилонів (мільярд мільярдів).
  • Оскільки ви будете мати процентні ставки, то в будь-якому випадку вам знадобиться безмежна теоретична точність: при 4% процентних ставках 100 доларів стануть $ 104, потім 108,16 дол., Потім 112,4864 дол. І т. Д. Це робить intі longмарним, оскільки ви не знаєте, де зупинитись десяткових знаків.
  • BigDecimalдасть вам довільну точність, але стане болісно повільною, якщо ви не зафіксуєте точність на деякий час. Які правила округлення? Як ви обираєте, де зупинитись? Чи варто мати більше бітів точності, ніж double? Я не вірю.

Причина арифметики з фіксованою точкою використовується у фінансових програмах, тому що вони детерміновані. Правила округлення ідеально визначені, іноді законом, і повинні суворо застосовуватися, але округлення все ж відбувається в якийсь момент. Будь-який аргумент на користь даного типу на основі точності, ймовірно, хибний. Усі типи мають проблеми з точністю до типу обчислень, які ви будете робити.

Практичні приклади

Я бачу досить багато коментарів, які стверджують, що стосується округлення або точності, з якою я не згоден. Ось декілька додаткових прикладів для ілюстрації того, що я маю на увазі.

Зберігання : якщо базовою одиницею є цент, ви, ймовірно, захочете округлити до найближчого цента, зберігаючи значення:

void SetValue(double v) { m_value = round(v * 100.0) / 100.0; }

Ви не отримаєте абсолютно жодних проблем із округленням при використанні цього методу, який у вас також не виникне з цілим типом.

Отримання : всі обчислення можна проводити безпосередньо за подвійними значеннями, без перетворень:

double value = data.GetValue();
value = value / 3.0 * 12.0;
[...]
data.SetValue(value);

Зауважте, що наведений вище код не працює, якщо ви заміните doubleна int64_t: відбудеться неявна конверсія double, а потім усічення до int64_tможливої ​​втрати інформації.data.GetValue ()

Порівняння : порівняння - це одне, що має бути правильним для типів з плаваючою комою. Я пропоную використовувати такий метод порівняння, як цей:

/* Are values equal to a tenth of a cent? */
bool AreCurrencyValuesEqual(double a, double b) { return abs(a - b) < 0.001; }

Справедливість округлення

Припустимо, у вас на рахунку 9,99 дол. США з відсотками 4%. Скільки повинен заробляти гравець? Цілим кругообігом ви отримуєте 0,03 долара; з округленням з плаваючою комою ви отримуєте 0,04 дол. Я вважаю, що остання справедливіша.


4
+1. Єдине уточнення, яке я хотів би додати, - це лише те, як фінансові програми відповідають певним заздалегідь визначеним вимогам, як і алгоритм округлення. Якщо гра казино, вона відповідає аналогічним стандартам, а потім подвійне - це не варіант.
Івайло Славов

2
Вам потрібно подумати над правилами округлення для форматування інтерфейсу користувача. Оскільки завжди є частина гравців, які намагатимуться розглядати гру як електронну таблицю, якщо ви не хочете мати справу з людьми, що стрибають вгору і вниз, кричачи, коли вони знаходять ваш бекенд, не використовують ту саму точність, як ваш інтерфейс, що призводить до Показ неправильних результатів найкращим варіантом мовчання є використання однакового внутрішнього та зовнішнього подання, що означає формат з фіксованою точкою. Якщо продуктивність не стане проблемою, BigDecimal - найкращий вибір для цього.
Ден Нілі

10
Я не згоден з цією відповіддю. Виникають проблеми з округленням, і якщо вони знижуються, і якщо ви не використовуєте додаткового округлення, 1,00 долара може раптом стати 0,99 дол. Але найголовніше, що повільні децималі точності, що є повільними, є (майже) абсолютно неістотними. Ми майже в 2013 році, і якщо ви не будете робити великі факторизації, триггі або логарифми на кілька мільйонів цифр з тисячами цифр, ви ніколи не помітите ні зубців у виконанні. У будь-якому випадку, найкраще завжди найкраще, тому моя рекомендація - зберігати всі цифри як центи. Таким чином, всі вони int_64t.
Піжама Panda

8
@У останній раз, коли я перевірив свій банківський рахунок, мій баланс не закінчився .0000000000000001. Системи реальної валюти повинні працювати з нероздільними одиницями, і це правильно представлено цілими числами. Якщо ви повинні робити валютні підрозділи правильно (які насправді зовсім не такі поширені в реальному фінансовому світі), ви повинні їх робити, щоб гроші не були втрачені. Наприклад 10/3 = 3+3+4або 10/3 = 3+3+3 (+1). Для всіх інших операцій цілі числа працюють ідеально, на відміну від плаваючої точки, яка може отримати проблеми округлення в будь-якій операції.
Піжама Panda

2
@SamHocevar 999 * 4/100. Якщо це не передбачено регламентом, припускаємо, що все округлення буде здійснено на користь банків.
Дан Нілі

21

Типи з плаваючою комою в Java ( float, double) не є хорошим представленням для валют через одну головну причину - в машині є помилка округлення. Навіть якщо простий розрахунок повертає ціле число - як 12.0/2(6,0), з плаваючою точкою може неправильно навколо нього (з - Тхо конкретного уявлення цих типів в пам'яті) , як 6.0000000000001і 5.999999999999998чи подібне. Це результат специфічного округлення машини, яке відбувається в процесорі, і воно унікальне для комп'ютера, який його розраховував. Зазвичай, рідко виникає проблема оперувати цими значеннями, оскільки помилка є досить недбалою, але це болісно відображати її користувачеві.

Можливим рішенням цього було б використання спеціальних реалізацій типу даних з плаваючою комою, наприклад BigDecimal. Він підтримує кращі механізми обчислення, які принаймні виокремлюють помилки округлення, щоб вони не були специфічними для машини, але були повільнішими щодо продуктивності.

Якщо вам потрібна висока продуктивність, вам краще дотримуватися простих типів. Якщо ви працюєте з важливими фінансовими даними , і кожен цент є важливим (наприклад, програма Forex або якась гра в казино), то я рекомендую вам використовувати Longабо long. Longдозволить вам обробляти велику кількість і хорошу точність. Просто припустимо, що вам потрібно, скажімо, 4 цифри після десяткової крапки. Все, що вам потрібно, - це помножити суму на 10000. Маючи досвід розробки онлайн-ігор в казино, я бачив, Longяк часто використовували для представлення грошей в центах . У програмах Forex важливіша точність, тому вам знадобиться більший множник - все-таки цілі числа не мають проблем із машинним округленням (звичайно, ручне округлення, як у 3/2, ви повинні вирішити самостійно).

Прийнятним варіантом було б використання стандартних типів з плаваючою комою - Floatі Double, якщо продуктивність важливіша за точність до сотих відсотків. Потім, за вашою логікою відображення, все, що вам потрібно, - це використовувати заздалегідь визначене форматування , щоб потворність потенційного машинного округлення не потрапляла до користувача.


4
+1 Подробиця: "Просто припустимо, що потрібно, скажімо, 4 цифри після десяткової крапки. Все, що вам потрібно, - це помножити суму на 1000." -> Це називається фіксованою точкою.
Лоран Кувіду

1
@Sam, номери були надані для прикладу ставки. Тим не менш, я особисто бачив дивні результати з такими простими цифрами, але це нечастий сценарій. Коли плаваючі точки приходять грати, це, швидше за все, трапиться, але більш навряд чи це помітить
Івайло Славов

2
@ Івайло Славов, я з вами згоден. У своїй відповіді я просто зробив свою думку. Я люблю обговорювати і маю бажання прийняти правильне. Також дякую за вашу думку. :)
Md Mahbubur Rahman

1
@SamHocevar: Це міркування працює не у всіх випадках. Дивіться: ideone.com/nI2ZOK
Самаруса

1
@SamHocevar: Це також не гарантується. Числа в межах діапазону, які все ще є цілими числами, починають накопичувати помилки в першому поділі: ideone.com/AKbR7i
Самаруса

17

Для гри в малих масштабах і де швидкість процесу, важлива проблема пам’яті (завдяки точності або роботі з математичним співпроцесором може зробити болісно повільним), подвійного достатньо.

Але для великомасштабних ігор (наприклад, соціальних ігор) і де швидкість процесу пам'ять не обмежена, там BigDecimal краще. Тому що тут,

  • int або long для грошових розрахунків.
  • поплавці та парні не можуть точно представляти більшість базових 10 реальних чисел.

Ресурси:

З https://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency

Оскільки плавці та парні не можуть точно представляти більшість базових 10 реальних чисел.

Ось так працює число з плаваючою комою IEEE-754: воно виділяє трохи для знака, кілька біт для зберігання експонента, а решта для фактичної дроби. Це призводить до того, що числа представлені у формі, подібній 1,45 * 10 ^ 4; за винятком того, що замість основи 10, це два.

Усі реальні десяткові числа насправді можна розглядати як точні частки потужності десять. Наприклад, 10,45 дійсно становить 1045/10 ^ 2. І так як деякі фракції неможливо представити точно як частку потужності десять (1/3 приходить в голову), деякі з них не можуть бути представлені точно як часткою потужності двох. Як простий приклад, ви просто не можете зберігати 0,1 всередині змінної з плаваючою комою. Ви отримаєте найближче представлене значення, яке є на зразок 0.0999999999999999996, і програмне забезпечення округлятиме його до 0,1 при відображенні.

Однак, коли ви будете виконувати більше додавання, віднімання, множення та ділення на неточні числа, ви втратите все більшу і більш точну, оскільки складні крихітні помилки. Це робить поплавці та дублювання неадекватними для поводження з грошима, де потрібна ідеальна точність.

З Bloch, J., Ефективна Java, 2-е видання, пункт 48:

The float and double types are particularly ill-suited for 

грошові розрахунки, тому що неможливо представити 0,1 (або будь-яку іншу негативну потужність десяти) як поплавок або подвійний точно.

For example, suppose you have $1.03 and you spend 42c. How much money do you have left?

System.out.println(1.03 - .42);

prints out 0.6100000000000001.

The right way to solve this problem is to use BigDecimal, 

int або long для грошових розрахунків.

Також подивіться


Я не згоден лише з тією частиною, яка довго підходить для невеликих розрахунків. У моєму реальному життєвому досвіді це досить добре зарекомендувало себе, щоб впоратися з хорошою точністю та великими сумами грошей. Так, це може бути трохи громіздким у використанні, але він перемагає BigDecimal у двох вирішальних моментах - 1) це швидше (BigDecimal використовує програмне округлення, що набагато повільніше, ніж вбудоване CPU округлення, яке використовується у float / double) та 2) BigDecimal важче зберігати базу даних, не втрачаючи точності, - не всі бази даних підтримують такий тип "на замовлення". Long відомий і широко підтримується у всіх основних базах даних.
Івайло Славов

@ Івайло Славов, вибачте за мою помилку. Я оновив свою відповідь.
Md Mahbubur Rahman

Не потрібно вибачення, щоб дати інший підхід для тієї ж проблеми :)
Івайло Славов

2
@ Івайло Славов, я з вами згоден. У своїй відповіді я просто зробив свою думку. Я люблю обговорювати і маю бажання прийняти правильне. Також дякую за вашу думку. :)
Md Mahbubur Rahman

метою цього веб-сайту є роз'яснення питань та допомога ОП у прийнятті правильного рішення залежно від конкретного сценарію. Все, що ми можемо зробити, це допомогти прискорити процес :)
Івайло Славов

13

Ви хочете зберігати свою валюту longі обчислювати свою валюту в double, принаймні, як резервну копію. Ви хочете, щоб усі транзакції проходили як long.

Причина, в якій ви хочете зберігати свою валюту, longполягає в тому, що ви не хочете втрачати жодну валюту.

Припустимо, ви використовуєте double, а у вас немає грошей. Хтось дає вам три димери, а потім повертає їх назад.

You:       0.1+0.1+0.1-0.1-0.1-0.1 = 2.7755575615628914E-17

Ну, це не так круто. Можливо, хтось із 10 доларів хоче віддати своє статок, спочатку давши вам три десятки, а потім давши 9,70 долара комусь іншому.

Them: 10.0-0.1-0.1-0.1-9.7 = 1.7763568394002505E-15

А потім ти повертаєш їм копійки:

Them: ...+0.1+0.1+0.1 = 0.3000000000000018

Це просто зламано.

Тепер давайте скористаємося довгим, і ми будемо відслідковувати десяті частки копійок (так 1 = 0,001 долара). Дамо кожному на планеті один мільярд, сто дванадцять мільйонів, сімдесят п’ять тисяч, сто сорок три долари:

Us: 7000000000L*1112075143000L = 1 894 569 218 048

Гм, зачекайте, ми можемо дати кожному понад мільярд доларів, а витратити лише трохи більше двох? Переповнення тут - катастрофа.

Таким чином, всякий раз , коли ви розраховуєте суму грошей для передачі, використання doubleі Math.roundйого отримати long. Потім виправте залишки (додайте та віднімайте обидва рахунки), використовуючи long.

Ваша економіка не просочиться, і вона складе до чотирильйона доларів.

Є більш складні проблеми - наприклад, що ви робите, якщо зробите двадцять платежів? * - але це повинно розпочати роботу.

* Ви обчислюєте, що таке один платіж, круглий long; потім помножте на 20.0і перевірте, чи знаходиться в межах; якщо так, ви помножуєте платіж 20Lна отримання суми, що віднімається з балансу. Взагалі, всі транзакції повинні оброблятися як long, тому вам справді потрібно підсумувати всі окремі транзакції; ви можете помножити на ярлик, але вам потрібно переконатися, що ви не додаєте помилок округлення та не переповнюєте, що означає, що вам потрібно перевірити, doubleперш ніж робити реальний розрахунок long.


8

Я б сказав, що будь-яке значення, яке може відображатися користувачеві, майже завжди повинно бути цілим числом. Гроші - лише найвизначніший приклад цього. Нанесення 225 збитків монстру в 900 HP і виявлення, що у нього ще залишилось 1 HP, буде віднімати досвід так само, як і виявлення того, що ви - невидима частка копійки, ніж чогось собі дозволити.

Щодо більш технічної сторони, я думаю, що варто відзначити, що не потрібно повертатися до плавців, щоб робити передові речі, такі як інтерес. До тих пір, поки у вас буде достатньо проголосу у вибраному цілому типі, множення і ділення буде стояти на множення на десяткове число, наприклад, щоб додати 4%, закругленим вниз:

number=(number*104)/100

Щоб додати 4%, округлені стандартними умовами:

number=(number*104+50)/100

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

Редагувати, справжнє питання:

Бачачи, як пройшли дебати, я починаю вважати, що окреслення того, про що йдеться, може бути кориснішим, ніж простий int/ floatвідповідь. В основі питання не про типи даних, а про контроль над деталями програми.

Використання цілого числа для представлення не цілого значення примушує програміста обробляти деталі реалізації. "Яку точність використовувати?" і "Яким способом обійти?" - це питання, на які слід відповісти чітко.

Поплавок з іншого боку не змушує програміста хвилюватися, він вже робить дуже багато того, що можна було б очікувати. Але оскільки поплавок не є нескінченною точністю, відбудеться деяке округлення, і це округлення є досить непередбачуваним.

Що трапляється за один плавець, і хоче взяти під контроль округлення? Це виявляється майже неможливо. Єдиний спосіб зробити float справді передбачуваним - використовувати лише ті значення, які можуть бути представлені цілими 2^ns. Але ця конструкція робить поплавці досить важкими у використанні.

Таким чином, відповідь на просте запитання: Якщо ви хочете взяти під контроль, використовуйте цілі числа, якщо ні, використовуйте поплавці.

Але питання, яке обговорюється, - це лише інша форма запитання: Ви хочете взяти під контроль?


5

Навіть якщо це "лише гра", я б використовував шаблон грошей від Мартіна Фаулера, підкріплений довго.

Чому?

Локалізація (L10n): За допомогою цього шаблону ви можете легко локалізувати свою ігрову валюту. Подумайте про старі ігри магнатів на кшталт «Транспортний магнат». Вони легко дозволяють гравцеві змінювати валюту гри (тобто з британського фунта на долар США), щоб зустріти реальну світову валюту.

І

Тип довгих даних - це 64-бітове ціле число доповнення двох. Він має мінімальне значення -9,223,372,036,854,775,808 та максимальне значення 9,223,372,036,854,775,807 (включно) ( навчальні програми Java )

Це означає, що ви можете зберігати 9000 разів поточну M2 американську грошову масу (~ 10 000 мільярдів доларів). Дайте вам достатньо місця для використання будь-якої іншої світової валюти, напевно, навіть тих, хто переніс / має гіперінфляцію (Якщо цікаво, дивіться інфляцію Німеччини після Першої світової війни , де 1 фунт хліба становив 3 000 000 000 марок)

Long довго зберігається, дуже швидко і повинен дати вам достатньо місця для всіх розрахунків відсотків, використовуючи лише цілу арифметику, відповідь електронного бізнесу дає пояснення, як це зробити.


2

Скільки роботи ви хочете докласти до цього? Наскільки важлива точність? Чи вам цікаво відстежувати дробові помилки, що трапляються при округленні, і неточність подання десяткових чисел у двійковій системі?

Зрештою, я схиляюсь витрачати трохи більше часу на кодування та реалізацію одиничних тестів для "кутових випадків" та відомих проблемних випадків - тому я б міркував над зміненим BigInteger, який охоплює довільно великі суми та підтримує дробові біти з BigDecimal або частина BigRational (два BigIntegers, один для знаменника та інший для чисельника) - і включають код для збереження дробової частини фактичного дробу шляхом (можливо, лише періодично) додавання будь-якої нефракційної частини до основного BigInteger. Тоді я б внутрішньо відстежував усе в центнах, щоб уникнути дробових частин з розрахунків GUI.

Напевно, спосіб складної для (простої) гри - але хороший для бібліотеки, виданої як відкритий код! Просто потрібно було б розібратися, як зберегти продуктивність при роботі з дробовими бітами ...


1

Звичайно не плавають. Маючи лише 7 цифр, ви маєте лише точність:

12,345,67 123,456.7x

Отже, бачите, вже з 10 ^ 5 доларів ви втрачаєте сліди грошей. подвійний можна використовувати для ігрових цілей, а не в реальному житті через точність.

Але довго (і я маю на увазі 64-бітний або довгий довгий C ++) недостатньо, якщо ви збираєтеся відстежувати транзакції та підсумовувати їх. Достатньо підтримувати чисті холдинги будь-якої компанії, але всі операції за рік можуть переповнитися. Це залежить від того, наскільки великий ваш фінансовий "світ" у грі.


0

Ще одне рішення, яке я додам тут, - це зробити грошовий клас. Клас був би чимось на кшталт (може навіть просто бути структура).

class Money
{
     string currencyType; //the type of currency example USD could be an enum as well
     bool isNegativeValue;
     unsigned long int wholeUnits;
     unsigned short int partialUnits;
}

Це дозволить представити центи в цьому випадку як ціле число, а цілий долар - як ціле число. Оскільки у вас є окремий прапор як негативний, ви можете використовувати непідписані цілі числа для своїх значень, що вдвічі перевищує можливу суму (ви також можете написати клас чисел, який застосовує цю ідею до чисел, щоб отримати РЕАЛЬНІ великі числа). Все, що вам потрібно зробити, це перевантажити математичні оператори, і ви можете використовувати це, як і будь-який старий тип даних. Це займає більше пам’яті, але воно дійсно розширює межі значень.

Це також може бути розширено, щоб включити такі речі, як кількість часткових одиниць на цілі одиниці, щоб у вас були валюти, які поділяються на щось, крім 100 підрозділів, центів у цьому випадку за долар. Можна, наприклад, 125 флоппій скласти один крило.

Редагувати:

Я розширюю думку про те, що ви також можете мати таблицю сортування, щоб отримати доступ до грошового класу, який би забезпечив обмінний курс на його валюту порівняно з усіма іншими валютами. Ви можете вбудувати це в функції перевантаження оператора. Тому, якщо ви автоматично спробуєте додати 4 USD до 4 британських фунтів, це автоматично дасть вам 6,6 фунта (на момент написання).


0

Як згадувалося в одному з коментарів до вашого первинного запитання, ця проблема висвітлювалася на StackExchange: https://stackoverflow.com/questions/8148684/what-is-the-best-data-type-to-use-for- гроші-в-Java-додаток

В основному типи з плаваючою комою ніколи не повинні використовуватися для представлення валют, і як і більшість мов, Java має більш відповідний тип. Власна документація Oracle про примітиви пропонує використання BigDecimal .


-1

Використовувати клас - це найкращий спосіб. Ви можете приховати реалізацію своїх грошей. Ви можете будь-коли перемикати подвійний / довгий, що завгодно.

Приклад

class Money
{
    private long count;//Store here what ever you want in what type you want i suggest long or int
    //methods constructors etc.
    public String print()
    {
        return count/100.F; //convert this to string
    }
}

У використанні вашого коду просто дістаньтеся - це краще, я думаю, і ви можете зберігати більше корисних методів.

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