Какой тип данных следует использовать для внутриигровой валюты?
В простой бизнес-симуляции (встроенной в Java + Slick2D), если текущая сумма денег игрока будет храниться как float
или int
, или что-то еще
В моем случае использования большинство транзакций будут использовать центы ($ 0.50, $ 1.20 и т. д.), и будут задействованы простые расчеты процентной ставки.
Я видел, как люди говорили, что вы не должны использовать float
для валюты, а также людей, говорящих, что вы никогда не должны использовать int
для валюты. Я чувствую, что должен использовать int
и округлять любые необходимые процентные вычисления. Что я должен использовать?
12 ответов
Вы можете использовать int
и рассматривать все в центах. $ 1,20 составляет всего 120 центов. На дисплее вы помещаете десятичное число в том месте, где оно принадлежит.
Процентные расчеты будут либо усечены, либо округлены. Так
newAmt = round( 120 cents * 1.04 ) = round( 124.8 ) = 125 cents
Таким образом, у вас нет грязных десятичных знаков, всегда торчащих вокруг. Вы можете разбогатеть, добавив неучтенные деньги (из-за округления) на свой собственный банковский счет
Хорошо, я вскочу.
Мой совет: это игра. Успокойтесь и используйте 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. Я считаю, что последнее более справедливо.
Типы с плавающей точкой в 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
, если производительность важнее точности до сотых долей процента. Затем в вашей логике отображения все, что вам нужно, это использовать предопределенное форматирование , так что уродство потенциального округления машины не доходит до пользователя.
Для мелкомасштабной игры и где скорость процесса важна проблема памяти (из-за точности или работы с математическим сопроцессором может сделать больно медленным), там double достаточно.
Но для крупномасштабных игр (например, для социальных игр) и где скорость процесса не ограничена памятью, там лучше BigDecimal . Потому что здесь
- int или долго для денежных расчетов.
- Поплавки и двойники не могут точно представлять большинство базовых 10 реальных числа.
Ресурсы
Поскольку поплавки и удвоения не могут точно представлять большинство базовых 10 действительные числа.
Так работает число с плавающей запятой IEEE-754: оно посвящает бит для знака, несколько бит для хранения экспоненты, а остальные для фактическая доля. Это приводит к тому, что числа представлены в форме аналогично 1.45 * 10 ^ 4; за исключением того, что вместо базы 10, это два.
Все действительные десятичные числа можно видеть, фактически, как точные доли сила десяти. Например, 10.45 действительно 1045/10 ^ 2. И так же, как некоторые фракции не могут быть представлены точно как доля мощности из десяти (1/3 приходит в голову), некоторые из них не могут быть представлены точно так же, как и доля мощности двух. В качестве простого примера, вы просто не можете хранить 0,1 внутри переменной с плавающей запятой. Вы будете получить ближайшее представимое значение, что-то вроде 0.0999999999999999996, и при его отображении программное обеспечение будет округлено до 0,1.
Однако, поскольку вы выполняете больше дополнений, вычитаний, умножений и деления на неточные числа, вы потеряете все больше и больше точности как крошечные ошибки складываются. Это делает поплавки и удваивает неадекватность для работы с деньгами, где требуется совершенная точность.
От Блоха, Дж., Эффективная 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
и рассчитать свою валюту в 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
.
Я бы сказал, что любое значение, которое может отображаться пользователю, должно всегда быть целым числом. Деньги - только самый яркий пример этого. Нанеся 225 урона четыре раза монстру с 900 HP и обнаружив, что он все еще имеет 1 HP, вычитает из опыта столько же, сколько и обнаруживает, что вы являетесь невидимой частью копейки, которая не дает что-то.
На более технической стороне я думаю, что стоит отметить, что не нужно возвращаться к поплавкам, чтобы делать продвинутые вещи, такие как интерес. До тех пор, пока у вас есть достаточный запас высоты в выбранном вами целочисленном типе, умножение и деление будут стоять за умножение на десятичное число, например, чтобы добавить 4%, округлить вниз:
number=(number*104)/100
Чтобы добавить 4%, округленные стандартными соглашениями:
number=(number*104+50)/100
Здесь не указана погрешность точки с плавающей запятой, округление всегда равномерно разбивается на метку .5
.
Изменить, реальный вопрос:
Увидев, как прошло обсуждение, я начинаю думать, что изложение того, что касается всего вопроса, может быть более полезным, чем простой ответ int
/float
. В основе вопроса - не о типах данных, а о контроле над деталями программы.
Использование целого числа для представления нецелого значения заставляет программиста иметь дело с деталями реализации. «Какую точность использовать?» и «Каким образом раунд?». это вопросы, на которые нужно ответить явно.
Поплавок с другой стороны не заставляет программиста волноваться, он уже делает многое, чего можно было бы ожидать. Но поскольку float не является бесконечной точностью, произойдет некоторое округление, и округление довольно непредсказуемо.
Что происходит в одном использовании float и хотите взять под контроль округление? Это оказывается практически невозможным. Единственный способ сделать поплавок по-настоящему предсказуемым - использовать только значения, которые могут быть представлены в целом 2^n
s. Но эта конструкция делает поплавки довольно сложными в использовании.
Таким образом, ответ на простой вопрос: если вы хотите взять управление, используйте целые числа, если нет, используйте float.
Но вопрос, который обсуждается, - это еще одна форма вопроса: хотите ли вы взять под свой контроль?
Даже если это «только игра», я бы использовал Money Pattern от Мартина Фаулера , подкрепленный длинным.
Почему?
Локализация (L10n): Используя этот шаблон, вы можете легко локализовать валюту своей игры. Подумайте о старых играх-магнатах, таких как «Transport Tycoon». Они легко позволяют игроку менять валюту в игре (т. Е. От британского фунта стерлингов к доллару США) для удовлетворения реальной мировой валюты.
и
Длинный тип данных - это целое число дополнений, состоящее из 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 марок)
Длинный легко переносится, очень быстро и должен предоставить вам достаточно места для выполнения всех вычислений процентов, используя только целую арифметику, eBusiness дает объяснение, как это сделать.
Сколько работы вы хотите внести в это? Насколько важна точность? Вы заботитесь о отслеживании дробных ошибок, возникающих при округлении, и о неточности представления десятичных чисел в двоичной системе?
В конечном счете я стремлюсь потратить немного больше времени на кодирование и реализацию модульных тестов для «краевых дел» и известных проблемных случаев, поэтому я бы подумал о модифицированном BigInteger, который включает в себя сколь угодно большие суммы и поддерживает дробные биты с помощью BigDecimal или BigRational (два BigIntegers, один для знаменателя и другой для числителя), - и включают код, чтобы дробная часть содержала фактическую долю (возможно, только периодически), добавляя любую недробную часть к основному BigInteger. Затем я буду внутренне отслеживать все в центах только для того, чтобы исключить частичные части из графических графиков.
Вероятно, путь к сложной для (простой) игре - но полезный для библиотеки, опубликованной как с открытым исходным кодом! Просто нужно будет выяснить, как поддерживать производительность при работе с дробными битами ...
Конечно, не плавать. Имея всего 7 цифр, у вас есть только такая точность:
12,345.67 123,456.7x
Итак, вы видите, уже с 10 ^ 5 долларов, вы теряете след копейки. double применим для игровых целей, а не в реальной жизни из-за проблем с точностью.
Но длинный (и под этим я имею в виду 64-битный или длинный длинный на C ++) недостаточно, если вы собираетесь отслеживать транзакции и суммировать их. Достаточно поддерживать чистые активы любой компании, но все транзакции за год могут переполняться. Это зависит от того, насколько велика ваша финансовая «мир» в игре.
Еще одно решение, которое я добавлю, это сделать класс денег. Класс будет чем-то вроде (может даже быть просто структурой).
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 floopies, например, один flooper.
Edit:
Я разберу идею в том, что у вас также может быть таблица со просмотром, которую может получить доступ к денежному классу, который обеспечит обменный курс для своей валюты по отношению ко всем другим валютам. Вы можете создать это в своих функциях перегрузки оператора. Поэтому, если вы автоматически попытаетесь добавить 4 доллара США к 4 британским фунтам, это автоматически даст вам 6,6 фунтов (с момента написания).
Как упоминалось в одном из комментариев по вашему первоначальному вопросу, этот вопрос был рассмотрен на StackExchange: https://stackoverflow.com/questions/8148684/what-is-the-best-data-type-to-use-for-money-in -java-приложение
В принципе, типы с плавающей точкой никогда не должны использоваться для представления валют, и, как и большинство языков, язык Java имеет более подходящий тип. Собственная документация Oracle по примитивам предлагает использовать BigDecimal .
Использовать класс , это лучший способ. Вы можете скрыть реализацию своих денег, вы можете переключать двойные /длинные, что захотите, в любое время.
Пример
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, это лучше, я думаю, и вы можете хранить более полезные методы.