Какой тип данных я должен использовать для игровой валюты?

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

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

bobobobo
источник
3
Если вы используете, roundто нет никакой реальной причины, чтобы бросить int, не так ли?
Сэм Хочевар
19
+1. Числа с плавающей запятой запутывают сравнения равенства, их сложнее правильно отформатировать и со временем вводят забавные ошибки округления. Лучше все делать самому, чтобы потом не было жалоб на это. Это действительно не много работы.
Добес Вандермер
+1. Похоже, хороший способ пойти на мою игру. Я сам разберусь с закруглениями и держусь подальше от всех проблем с плавающей точкой.
Лукас Тулио
Просто отказ от ответственности: я изменил правильный ответ на этот, потому что в конечном итоге это то, что я использовал. Это намного проще, и в контексте такой простой игры неучтенные десятичные дроби не имеют большого значения.
Лукас Тулио
3
Однако используйте базовые баллы, а не центы (где 100 базовых баллов = 1 цент) с коэффициентом 10 000 вместо 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, с возможной потерей information.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с.
Панда Пижама
8
@ В последний раз, когда я проверял свой банковский счет, мой баланс не заканчивался на .0000000000000001. Реальные валютные системы должны работать с неделимыми единицами, и это правильно представлено целыми числами. Если вы должны правильно разделить валюту (что на самом деле не так часто встречается в реальном финансовом мире), вы должны делать это, чтобы не потерять деньги. Например 10/3 = 3+3+4или 10/3 = 3+3+3 (+1). Для всех других операций целые числа работают отлично, в отличие от числа с плавающей запятой, которое может вызвать проблемы округления в любой операции.
Панда Пижама
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
@ Сэм, цифры были указаны для примера кола. Тем не менее, я лично видел странные результаты с такими простыми числами, но это не частый сценарий. Когда в игру вступают с плавающей точкой, это, скорее всего, произойдет, но вряд ли это заметит
Ивайло Славов
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 при отображении.

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

От Блоха, Дж. Эффективная Ява, 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 для денежных расчетов.

Также посмотрите на

Md Mahbubur Rahman
источник
Я только не согласен с той частью, которая долго подходит для небольших расчетов. В моем реальном жизненном опыте это оказалось достаточно, чтобы справиться с хорошей точностью и большими суммами денег. Да, его использование может быть немного громоздким, но оно превосходит BigDecimal в двух важных моментах: 1) оно быстрее (BigDecimal использует программное округление, которое намного медленнее, чем встроенное округление процессора, используемое в float / double) и 2) BigDecimal сложнее сохранить в базе данных без потери точности - не все базы данных поддерживают такой «пользовательский» тип. Лонг хорошо известен и широко поддерживается во всех основных базах данных.
Ивайло Славов
@ Ивайло Славов, извините за ошибку. Я обновил свой ответ.
Г-н Махбубур Рахман
Не нужно извинений, чтобы дать другой подход к той же проблеме :)
Ивайло Славов
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

Это просто сломано.

Теперь давайте используем long и будем отслеживать десятые доли центов (поэтому 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. Но эта конструкция делает поплавки довольно сложными в использовании.

Таким образом, ответ на простой вопрос таков: если вы хотите взять управление на себя, используйте целые числа, если нет - используйте числа с плавающей точкой.

Но обсуждаемый вопрос - это еще одна форма вопроса: хотите ли вы взять под контроль?

AAAAAAAAAAAA
источник
5

Даже если бы это была «только игра», я бы использовал Money Pattern от Мартина Фаулера, подкрепленную долго.

Почему?

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

А также

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

Это означает, что вы можете хранить 9000-кратную текущую денежную массу США в M2 (~ 10000 миллиардов долларов). Предоставление вам достаточно места, чтобы использовать любую другую мировую валюту, возможно, даже те, кто имел / имеет гиперинфляцию (если любопытно, посмотрите инфляцию в Германии после Первой мировой войны , где 1 фунт хлеба составлял 3 000 000 000 марок)

Лонг легко сохранить, он очень быстрый и должен дать вам достаточно места для выполнения всех расчетов процентов с использованием только целочисленной арифметики, а ответ на электронный бизнес дает объяснение, как это сделать.

grprado
источник
2

Сколько работы вы хотите вложить в это? Насколько важна точность? Вас интересует отслеживание дробных ошибок, возникающих при округлении, и неточности представления десятичных чисел в двоичной системе?

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

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

Ричард Арнольд Мид
источник
1

Конечно, не плавать. Только с 7 цифрами у вас есть только точность, как:

12 345,67 123 456,7x

Итак, вы видите, уже с 10 ^ 5 долларов, вы теряете счет пенни. double может использоваться в игровых целях, а не в реальной жизни из-за проблем с точностью.

Но long (и под этим я имею в виду 64-битный или long long в 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 доллара США к 4 британским фунтам, это автоматически даст вам 6,6 фунтов (на момент написания).

Azaral
источник
0

Как упоминалось в одном из комментариев к исходному вопросу, эта проблема была рассмотрена в StackExchange: https://stackoverflow.com/questions/8148684/what-is-the-best-data-type-to-use-for- деньги-в-Java-приложение

По сути, типы чисел с плавающей точкой никогда не должны использоваться для представления валют, и, как и в большинстве языков, Java имеет более подходящий тип. Собственная документация Oracle по примитивам предлагает использовать BigDecimal .

FlintZA
источник
-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
    }
}

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

Давид Дрозд
источник