Que tipo de dados devo usar para a moeda do jogo?

96

Em um jogo simples de simulação de negócios (construído em Java + Slick2D), a quantia atual de dinheiro de um jogador deve ser armazenada como um floatou um int, ou algo mais?

No meu caso de uso, a maioria das transações usará centavos (US $ 0,50, US $ 1,20, etc.), e cálculos simples de taxa de juros serão envolvidos.

Vi pessoas dizendo que você nunca deveria usar floatpara moeda, bem como pessoas dizendo que você nunca deveria usar intpara moeda. Sinto que devo usar inte arredondar todos os cálculos de porcentagem necessários. O que devo usar?

Lucas Tulio
fonte
2
Esta pergunta foi discutida no StackOverflow. Parece que BigDecimal é provavelmente o caminho a percorrer. stackoverflow.com/questions/285680/…
Ade Miller
2
O Java não possui um Currencytipo como o Delphi, que usa matemática em ponto fixo escalado para fornecer matemática decimal sem os problemas de precisão inerentes ao ponto flutuante?
Mason Wheeler
1
É jogo. A contabilidade não vai linchar você por um centavo arredondado e, portanto, as razões normais contra o uso de ponto flutuante para moeda não importam.
Loren Pechtel
@ MasonWheeler: Java tem BigDecimalpara esse tipo de problemas.
Martin Schröder

Respostas:

92

Você pode usar inte considerar tudo em centavos. $ 1,20 são apenas 120 centavos. Na exibição, você coloca o decimal no lugar a que pertence.

Os cálculos de juros seriam truncados ou arredondados. assim

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

Dessa forma, você não tem decimais confusos sempre aparecendo. Você pode ficar rico adicionando o dinheiro não contabilizado (devido a arredondamentos) em sua própria conta bancária

bobobobo
fonte
3
Se você usar, roundnão há motivo real para rejeitar int, existe?
21712 Sam 18:40
19
+1. Os números de ponto flutuante alteram as comparações de igualdade, são mais difíceis de formatar adequadamente e introduzem erros de arredondamento engraçados ao longo do tempo. É melhor fazer tudo isso sozinho para não receber reclamações sobre isso mais tarde. Realmente não é muito trabalho.
Dobes Vandermeer
+1. Parece um bom caminho a percorrer para o meu jogo. Vou lidar com o arredondamento das tintas e ficar longe de todos os problemas de flutuação.
Lucas Tulio
Apenas um aviso: eu mudei a resposta correta para esta, porque acabou sendo o que eu usei. É muito mais simples e, no contexto de um jogo tão simples, os decimais não contabilizados realmente não importam.
Lucas Tulio
3
No entanto, use pontos de base e não centavos (onde 100 pontos de base = 1 centavo) com um fator de 10.000 em vez de 100. Isso é exigido pelo GAAP para todos os aplicativos financeiros, bancários ou contábeis.
Pieter Geerkens
66

Ok, eu vou pular.

Meu conselho: é um jogo. Acalme-se e use-o double.

Aqui está o meu raciocínio:

  • floattem um problema de precisão que aparece ao adicionar unidades a milhões, portanto, embora possa ser benigno, eu evitaria esse tipo. doublesó começa a ter problemas em torno dos quintilhões (um bilhão de bilhões).
  • Desde que você está indo para ter taxas de juros, você terá precisão infinita teórica de qualquer maneira: com taxas de juros de 4%, US $ 100 se tornará $ 104, depois US $ 108,16, então $ 112,4864, etc. Isso faz inte longinútil, porque você não sabe onde parar com os decimais.
  • BigDecimalfornecerá precisão arbitrária, mas ficará dolorosamente lento, a menos que você a prenda em algum momento. Quais são as regras de arredondamento? Como você escolhe onde parar? Vale a pena ter mais bits de precisão do que double? Eu acredito que nao.

A razão pela qual a aritmética de ponto fixo é usada em aplicações financeiras é porque elas são determinísticas. As regras de arredondamento são perfeitamente definidas, às vezes por lei, e devem ser rigorosamente aplicadas, mas o arredondamento ainda ocorre em algum momento. Qualquer argumento a favor de um determinado tipo com base na precisão é provavelmente falso. Todos os tipos têm problemas de precisão com o tipo de cálculo que você fará.

Exemplos práticos

Vejo alguns comentários afirmando coisas sobre arredondamento ou precisão com as quais discordo. Aqui estão alguns exemplos adicionais para ilustrar o que quero dizer.

Armazenamento : se sua unidade base for o centavo, você provavelmente desejará arredondar para o centavo mais próximo ao armazenar valores:

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

Você não terá absolutamente nenhum problema de arredondamento ao adotar esse método que também não teria com um tipo inteiro.

Recuperando : todos os cálculos podem ser feitos diretamente nos valores duplos, sem conversões:

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

Observe que o código acima não funcionará se você substituir doublepor int64_t: haverá uma conversão implícita em e double, em seguida, truncamento em int64_t, com uma possível perda de informações.data.GetValue ()

Comparando : comparações são a única coisa a se acertar com tipos de ponto flutuante. Sugiro usar um método de comparação como este:

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

Justiça de arredondamento

Suponha que você tenha US $ 9,99 na conta com juros de 4%. Quanto o jogador deve ganhar? Com ​​arredondamentos inteiros, você recebe $ 0,03; com o arredondamento de ponto flutuante, você recebe US $ 0,04. Eu acredito que o último é mais justo.

sam hocevar
fonte
4
+1. O único esclarecimento que gostaria de acrescentar é que as aplicações financeiras estão em conformidade com requisitos predefinidos específicos, o mesmo ocorre com o algoritmo de arredondamento. Se o jogo for de cassino, está de acordo com padrões semelhantes e, em seguida, o dobro não é uma opção.
Ivaylo Slavov
2
Você ainda precisa pensar em regras de arredondamento para formatar a interface do usuário. Como sempre há uma fração de jogadores que tentarão tratar um jogo como uma planilha se você não quiser lidar com pessoas pulando e berrando quando descobrirem que seu back-end não está usando a mesma precisão que a interface do usuário, resultando no resultados "errados" sendo exibidos, sua melhor opção para mantê-los quietos é usar a mesma representação interna e externa, o que significa usar um formato de ponto fixo. A menos que o desempenho se torne um problema, o BigDecimal é a melhor opção para fazer isso.
Dan Neely
10
Eu discordo desta resposta. Os problemas de arredondamento são exibidos e, se eles caírem, e se você não estiver usando nenhum arredondamento adicional, US $ 1,00 poderá repentinamente se tornar US $ 0,99. Mas o mais importante é que decimais de precisão arbitrários sendo lentos são (quase) completamente irrelevantes. Estamos quase em 2013 e, a menos que você esteja fazendo grandes fatorações, coisas trigonométricas ou logaritmos em vários milhões de números com milhares de dígitos, você nunca notará uma queda no desempenho. De qualquer forma, simples é sempre o melhor, então minha recomendação é armazenar todos os números como centavos. Dessa forma, eles são todos int_64ts.
Panda Pajama
8
@ Sam Na última vez que verifiquei minha conta bancária, meu saldo não terminou em .0000000000000001. Os sistemas de moeda real devem funcionar com unidades não divisíveis, e isso é representado corretamente com números inteiros. Se você precisa fazer divisões de moeda corretamente (que na verdade não são tão comuns no mundo financeiro real), é necessário fazê-las para que o dinheiro não seja perdido. Por exemplo 10/3 = 3+3+4ou 10/3 = 3+3+3 (+1). Para todas as outras operações, números inteiros funcionam perfeitamente, diferentemente do ponto flutuante, que pode obter problemas de arredondamento em qualquer operação.
Panda Pajama
2
@SamHocevar 999 * 4/100. Onde não for estipulado pela regulamentação, assuma que todo arredondamento será feito a favor dos bancos.
Dan Neely
21

Os tipos de ponto flutuante em Java ( float, double) não são uma boa representação para moedas devido a um motivo principal - há um erro de máquina no arredondamento. Mesmo que um cálculo simples retorne um número inteiro, como 12.0/2(6.0), o ponto flutuante pode arredondá-lo incorretamente (devido à representação específica desses tipos na memória) como 6.0000000000001ou 5.999999999999998ou semelhante. Isso é resultado do arredondamento específico da máquina que ocorre no processador e é exclusivo do computador que o calculou. Geralmente, raramente é um problema operar com esses valores, já que o erro é bastante negligente, mas é difícil mostrar isso para o usuário.

Uma solução possível para isso seria usar implementações personalizadas do tipo de dados de ponto flutuante, como BigDecimal. Ele suporta melhores mecanismos de cálculo que, pelo menos, isolam os erros de arredondamento para não serem específicos da máquina, mas são mais lentos em termos de desempenho.

Se você precisar de alta produtividade, é melhor seguir os tipos simples. Caso você opere com dados financeiros importantes e cada centavo seja importante (como um aplicativo de Forex ou algum jogo de cassino), recomendo que você use Longou long. Longpermitiria lidar com grandes quantidades e boa precisão. Suponha que você precise, digamos, 4 dígitos após o ponto decimal, tudo o que você precisa é multiplicar a quantia por 10000. Tendo experiência no desenvolvimento de jogos de cassino on-line, já vi Longser frequentemente usado para representar o dinheiro em centavos. . Em aplicações Forex, a precisão é mais importante, portanto, você precisará de um multiplicador maior - ainda assim, números inteiros estão livres de problemas de arredondamento de máquinas (é claro que o arredondamento manual, como em 3/2, você deve cuidar de si mesmo).

Uma opção aceitável seria usar os tipos de ponto flutuante padrão - Floate Double, se o desempenho for mais importante que a precisão, até centésimos de centavo. Então, na sua lógica de exibição, tudo o que você precisa é usar uma formatação predefinida , para que a feiura de um possível arredondamento da máquina não chegue ao usuário.

Ivaylo Slavov
fonte
4
+1 Um detalhe: "Suponha que você precise, digamos, 4 dígitos após o ponto decimal, tudo que você precisa é multiplicar o valor por 1000". -> Isso é chamado ponto fixo.
Laurent Couvidou
1
@ Sam, os números foram fornecidos para a estaca de exemplo. Ainda assim, eu pessoalmente vi resultados estranhos com números tão simples, mas não é um cenário frequente. Quando os pontos vêm para jogar flutuante, esta é provavelmente mais provável de ocorrer, mas o mais improvável de detectar
Ivaylo Slavov
2
@Ivaylo Slavov, concordo com você. Na minha resposta, acabei de fazer minha opinião. Adoro discutir e ter vontade de tomar a coisa certa. Também obrigado pela sua opinião. :)
Md Mahbubur Rahman
1
@ SamHocevar: Esse raciocínio não funciona em todos os casos. Veja: ideone.com/nI2ZOK
Samaursa
1
@ SamHocevar: Isso também não é garantido. Os números dentro do intervalo que ainda são números inteiros começam a acumular erros na primeira divisão: ideone.com/AKbR7i
Samaursa
17

Para jogos de pequena escala e onde a velocidade do processo, a memória é um problema importante (devido à precisão ou ao trabalho com o co-processador matemático pode tornar-se dolorosamente lento), há o dobro .

Mas para jogos de grande escala (por exemplo, jogos sociais) e onde a velocidade do processo, a memória não é limitada, o BigDecimal é melhor. Porque aqui

  • int ou long para cálculos monetários.
  • flutuadores e duplos não podem representar com precisão a maioria dos números reais da base 10.

Recursos:

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

Como floats e dobros não podem representar com precisão a maioria dos números reais da base 10.

É assim que um número de ponto flutuante IEEE-754 funciona: ele dedica um pouco ao sinal, alguns bits para armazenar um expoente e o restante para a fração real. Isso faz com que os números sejam representados em um formato semelhante a 1,45 * 10 ^ 4; exceto que, em vez de a base ser 10, são duas.

Todos os números decimais reais podem ser vistos, de fato, como frações exatas de uma potência de dez. Por exemplo, 10.45 é realmente 1045/10 ^ 2. E, assim como algumas frações não podem ser representadas exatamente como uma fração da potência de dez (1/3 vem à mente), algumas delas também não podem ser representadas exatamente como uma fração da potência de dois. Como um exemplo simples, você simplesmente não pode armazenar 0,1 dentro de uma variável de ponto flutuante. Você obterá o valor representável mais próximo, que é algo como 0,0999999999999999996, e o software o arredondará para 0,1 ao exibi-lo.

No entanto, à medida que você executa mais adições, subtrações, multiplicações e divisões em números inexatos, você perde mais e mais precisão à medida que os pequenos erros aumentam. Isso torna os carros alegóricos e duplos inadequados para lidar com dinheiro, onde é necessária uma precisão perfeita.

De Bloch, J., Effective Java, 2ª ed. Item 48:

The float and double types are particularly ill-suited for 

cálculos monetários porque é impossível representar 0,1 (ou qualquer outro poder negativo de dez) como flutuador ou dobrar exatamente.

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 ou long para cálculos monetários.

Também dê uma olhada

Md Mahbubur Rahman
fonte
Não concordo apenas com a parte que é adequada para pequenos cálculos. Na minha experiência na vida real, provou o suficiente para lidar com boa precisão e grandes quantias de dinheiro. Sim, pode ser um pouco complicado de usar, mas supera o BigDecimal em dois pontos cruciais - 1) é mais rápido (o BigDecimal usa arredondamento de software, que é bem mais lento que o arredondamento da CPU interno usado em float / double) e 2) O BigDecimal é mais difícil de persistir em um banco de dados sem perder a precisão - nem todos os bancos de dados suportam esse tipo 'personalizado'. Long é bem conhecido e amplamente suportado em todos os principais bancos de dados.
Ivaylo Slavov
@Ivaylo Slavov, desculpe pelo meu erro. Eu atualizei minha resposta.
Md Mahbubur Rahman
Sem desculpas necessário para dar uma abordagem diferente para o mesmo problema :)
Ivaylo Slavov
2
@Ivaylo Slavov, concordo com você. Na minha resposta, acabei de fazer minha opinião. Adoro discutir e ter vontade de tomar a coisa certa. Também obrigado pela sua opinião. :)
Md Mahbubur Rahman
O objetivo deste site é esclarecer os problemas e ajudar o OP a tomar a decisão certa, dependendo do cenário específico. Tudo o que podemos fazer é ajudar a acelerar o processo :)
Ivaylo Slavov
13

Você quer armazenar sua moeda em longe calcular a sua moeda em double, pelo menos, como um backup. Você deseja que todas as transações ocorram como long.

O motivo pelo qual você deseja armazenar sua moeda longé que não deseja perder nenhuma moeda.

Vamos supor que você use a doublee não tenha dinheiro. Alguém te dá três centavos e depois os leva de volta.

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

Bem, isso não é tão legal. Talvez alguém com US $ 10 queira doar sua fortuna dando-lhe três centavos e depois US $ 9,70 a outra pessoa.

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

E então você devolve os centavos:

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

Isso está quebrado.

Agora, vamos usar um longo e acompanharemos décimos de centavos (então 1 = $ 0,001). Vamos dar a todos no planeta um bilhão, cento e doze milhões, setenta e cinco mil, cento e quarenta e três dólares:

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

Espere, podemos dar a todos mais de um bilhão de dólares e gastar apenas um pouco mais de dois? O estouro é um desastre aqui.

Assim, sempre que você está calculando uma quantia de dinheiro para transferir, utilização doublee Math.roundpara obter um long. Em seguida, corrija os saldos (adicione e subtraia ambas as contas) usando long.

Sua economia não vai vazar e chegará a um bilhão de dólares.

Existem problemas mais complicados - por exemplo, o que você faz se fizer vinte pagamentos? * - mas isso deve ajudá-lo a começar.

* Você calcula o que é um pagamento, arredondado para long; depois multiplique 20.0e verifique se está dentro do alcance; Nesse caso, você multiplica o pagamento 20Lpara obter o valor deduzido do seu saldo. Em geral, todas as transações devem ser tratadas como long, portanto, você realmente precisa resumir todas as transações individuais; você pode multiplicar como um atalho, mas não se esqueça de adicionar erros de arredondamento e não estourar, o que significa que você precisa verificar doubleantes de fazer o cálculo real long.

Rex Kerr
fonte
8

Eu diria que qualquer valor que possa ser exibido ao usuário quase sempre deve ser um número inteiro. O dinheiro é apenas o exemplo mais importante disso. Causar 225 de dano quatro vezes a um monstro com 900 HP e descobrir que ele ainda tem 1 HP restante subtrairá da experiência tanto quanto descobrir que você é uma fração invisível de um centavo, sem fornecer algo.

No lado mais técnico, acho que vale a pena notar que não é preciso voltar aos carros alegóricos para fazer coisas avançadas, como o interesse. Desde que você tenha espaço suficiente no tipo inteiro escolhido, uma multiplicação e uma divisão substituirão uma multiplicação por um número decimal, por exemplo, para adicionar 4%, arredondado para baixo:

number=(number*104)/100

Para adicionar 4%, arredondado por convenções padrão:

number=(number*104+50)/100

Aqui não há imprecisão do ponto de flutuação, o arredondamento sempre se divide exatamente na .5marca.

Edite, a verdadeira questão:

Vendo como o debate tem ido eu começo a pensar que descrevendo o que a pergunta é tudo pode ser mais útil do que um simples int/ floatresposta. No cerne da questão, não se trata de tipos de dados, mas de controlar os detalhes de um programa.

Usar um número inteiro para representar um valor não inteiro força o programador a lidar com os detalhes da implementação. "Que precisão usar?" e "Que maneira de arredondar?" são perguntas que devem ser respondidas explicitamente.

Um float, por outro lado, não força o programador a se preocupar, ele já faz praticamente o que seria de esperar. Mas como um flutuador não é uma precisão infinita, ocorrerão alguns arredondamentos, e esse arredondamento é bastante imprevisível.

O que acontece em um uso flutua e deseja assumir o controle do arredondamento? Acontece que é quase impossível. A única maneira de tornar um float verdadeiramente previsível é usar apenas valores que possam ser representados em 2^ns inteiros . Mas essa construção torna os carros alegóricos bastante difíceis de usar.

Portanto, a resposta para a pergunta simples é: Se você deseja assumir o controle, use números inteiros; caso contrário, use flutuadores.

Mas a pergunta que está sendo debatida é apenas outra forma de pergunta: você quer assumir o controle?

aaaaaaaaaaaa
fonte
5

Mesmo que seja "apenas um jogo", eu usaria o Money Pattern de Martin Fowler, apoiado por um longo tempo.

Por quê?

Localização (L10n): Usando esse padrão, você pode localizar facilmente a moeda do seu jogo. Pense nos jogos antigos de magnatas como "Transport Tycoon". Eles permitem facilmente que o jogador mude a moeda do jogo (ou seja, da libra esterlina para o dólar americano) para atender à moeda do mundo real.

E

O tipo de dados longo é um inteiro de complemento assinado por dois de 64 bits. Possui um valor mínimo de -9.223.372.036.854.775.808 e um valor máximo de 9.223.372.036.854.775.807 (inclusive) ( Java Tutorials )

Isso significa que você pode armazenar 9.000 vezes o suprimento de dinheiro M2 dos EUA atual (~ 10.000 bilhões de dólares). Dando-lhe espaço suficiente para usar qualquer outra moeda mundial, provavelmente, mesmo aqueles que tiveram / têm hiperinflação (se curioso, veja a inflação alemã pós-Segunda Guerra Mundial , onde 1 libra de pão era 3.000.000.000 de marcos)

Longo é fácil de persistir, muito rápido e deve fornecer espaço suficiente para todos os cálculos de juros usando apenas aritmética inteira, a resposta do eBusiness fornece uma explicação sobre como fazer isso.

grprado
fonte
2

Quanto trabalho você deseja dedicar a isso? Quão importante é a precisão? Você se preocupa em rastrear os erros fracionários que ocorrem com o arredondamento e a imprecisão de representar números decimais em um sistema binário?

Por fim, tenho a tendência de gastar um pouco mais de tempo codificando e implementando testes de unidade para "casos de canto" e casos problemáticos conhecidos - então, eu estaria pensando em termos de um BigInteger modificado que engloba quantidades arbitrariamente grandes e mantém os bits fracionários com um BigDecimal ou uma parte do BigRational (dois BigIntegers, um para o denominador e outro para o numerador) - e inclua o código para manter a parte fracionária em uma fração real, adicionando (talvez apenas periodicamente) qualquer parte não fracionária ao BigInteger principal. Então, eu controlava internamente tudo em centavos apenas para manter as partes fracionárias fora dos cálculos da GUI.

Provavelmente maneira complexa para um jogo (simples) - mas bom para uma biblioteca publicada como código aberto! Só precisaria descobrir maneiras de manter o bom desempenho ao lidar com os bits fracionários ...

Richard Arnold Mead
fonte
1

Certamente não flutuar. Com apenas 7 dígitos, você só tem precisão como:

12.345,67 123.456,7x

Veja bem, já com 10 ^ 5 dólares, você está perdendo a noção dos centavos. double é utilizável para fins de jogos, não na vida real devido a preocupações de precisão.

Mas longo (e com isso quero dizer 64 bits ou muito tempo em C ++) não é suficiente se você estiver rastreando transações e resumindo-as. É o suficiente para manter as participações líquidas de qualquer empresa, mas todas as transações de um ano podem transbordar. Isso depende do tamanho do seu "mundo" financeiro no jogo.

Dov
fonte
0

Outra solução que adicionarei aqui é fazer uma classe de dinheiro. A classe seria algo como (poderia simplesmente ser uma estrutura).

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;
}

Isso permitiria representar os centavos nesse caso como um número inteiro e os dólares inteiros como um número inteiro. Como você tem um sinalizador separado por ser negativo, você pode usar números inteiros não assinados para seus valores, o que duplica a quantidade possível (você também pode escrever uma classe numérica que aplique essa idéia aos números para obter números REALMENTE grandes). Tudo o que você precisa fazer é sobrecarregar os operadores matemáticos e você pode usá-lo como qualquer tipo de dados antigo. Consome mais memória, mas realmente expande os limites de valor.

Isso poderia ser expandido ainda mais para incluir itens como número de unidades parciais por unidades inteiras, para que você possa ter moedas que se subdividem em algo que não seja 100 subunidades, neste caso, centavos por dólar. Você poderia ter 125 floopies para formar um flooper, por exemplo.

Editar:

Expandirei a ideia de que você também pode ter uma espécie de tabela de consulta, que a classe monetária pode acessar que forneceria uma taxa de câmbio para sua moeda versus todas as outras moedas. Você pode incorporar isso nas funções de sobrecarga do operador. Portanto, se você tentar adicionar automaticamente 4 USD a 4 libras esterlinas, isso fornecerá automaticamente 6,6 libras (no momento da escrita).

Azaral
fonte
-1

Use classe , é o melhor caminho. Você pode ocultar a implementação do seu dinheiro, pode alternar o dobro / o que quiser a qualquer momento.

Exemplo

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
    }
}

No seu código, use simplesmente getter, é a melhor maneira que eu penso e você pode armazenar métodos mais úteis.

Dawid Drozd
fonte