¿Qué tipo de datos debo usar para la moneda del juego?

96

En un simple juego de simulación de negocios (construido en Java + Slick2D), ¿la cantidad actual de dinero de un jugador debe almacenarse como una floato una intu otra cosa?

En mi caso de uso, la mayoría de las transacciones usarán centavos ($ 0.50, $ 1.20, etc.), y se realizarán cálculos simples de tasas de interés.

He visto personas que dicen que nunca debes usar floatpara la moneda, así como personas que dicen que nunca debes usar intpara la moneda. Siento que debería usar inty redondear cualquier cálculo de porcentaje necesario. ¿Qué debo usar?

Lucas Tulio
fuente
2
Esta pregunta fue discutida en StackOverflow. Parece que BigDecimal es probablemente el camino a seguir. stackoverflow.com/questions/285680/…
Ade Miller
2
¿Java no tiene un Currencytipo como el de Delphi, que utiliza matemática de punto fijo escalado para darle matemática decimal sin los problemas de precisión inherentes a la coma flotante?
Mason Wheeler
1
Es un juego La contabilidad no te va a linchar por un centavo redondeado y, por lo tanto, las razones normales contra el uso de punto flotante para la moneda no importan.
Loren Pechtel
@MasonWheeler: Java tiene BigDecimalpara este tipo de problemas.
Martin Schröder

Respuestas:

92

Puede usar inty considerar todo en centavos. $ 1.20 es solo 120 centavos. En la pantalla, coloca el decimal en el lugar al que pertenece.

Los cálculos de intereses se truncarían o redondearían. Entonces

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

De esta manera no tienes decimales desordenados siempre pegados. Puede hacerse rico agregando el dinero no contabilizado (debido a redondeos) en su propia cuenta bancaria

bobobobo
fuente
3
Si usa, roundno hay una razón real para rechazar int, ¿ verdad ?
sam hocevar
19
+1. Los números de coma flotante arruinan las comparaciones de igualdad, son más difíciles de formatear correctamente e introducen errores de redondeo divertidos con el tiempo. Es mejor hacer todo eso usted mismo para no tener quejas más tarde. Realmente no es mucho trabajo.
Dobes Vandermeer
+1. Suena como un buen camino para mi juego. Me ocuparé de redondear las entradas yo mismo y me mantendré alejado de todos los problemas de flotación.
Lucas Tulio
Solo un descargo de responsabilidad: cambié la respuesta correcta a esta porque terminó siendo lo que usé. Es mucho más simple y en el contexto de un juego tan simple, los decimales no contados realmente no importan.
Lucas Tulio
3
Sin embargo, use puntos básicos, no centavos (donde 100 puntos básicos = 1 centavo) con un factor de 10,000 en lugar de 100. GAAP lo requiere para todas las aplicaciones financieras, bancarias o contables.
Pieter Geerkens
66

Ok, voy a saltar

Mi consejo: es un juego. Tómatelo con calma y úsalo double.

Aquí está mi justificación:

  • floattiene un problema de precisión que aparece al agregar unidades a millones, por lo que, aunque podría ser benigno, evitaría ese tipo. doublesolo comienza a tener problemas alrededor de los quintillones (mil millones de billones).
  • Puesto que usted va a tener tasas de interés, se necesita una precisión infinita teórica de todos modos: con las tasas de interés de 4%, $ 100 se convertirá en $ 104, entonces $ 108.16, luego $ 112,4864, etc. Esto hace que inte longinútil porque no se sabe dónde parar con los decimales
  • BigDecimalle dará precisión arbitraria pero se volverá dolorosamente lenta, a menos que sujete la precisión en algún momento. ¿Cuáles son las reglas de redondeo? ¿Cómo eliges dónde parar? ¿Vale la pena tener más brocas de precisión que double? Yo creo que no.

La razón por la que se usa la aritmética de punto fijo en las aplicaciones financieras es porque son deterministas. Las reglas de redondeo están perfectamente definidas, a veces por la ley, y deben aplicarse estrictamente, pero el redondeo todavía ocurre en algún momento. Cualquier argumento a favor de un tipo dado basado en la precisión es probablemente falso. Todos los tipos tienen problemas de precisión con el tipo de cálculos que va a hacer.

Ejemplos prácticos

Veo algunos comentarios alegando cosas sobre redondeo o precisión con las que no estoy de acuerdo. Aquí hay algunos ejemplos adicionales para ilustrar lo que quiero decir.

Almacenamiento : si su unidad base es el centavo, probablemente desee redondear al centavo más cercano al almacenar valores:

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

Obtendrá absolutamente ningún problema de redondeo al adoptar este método que tampoco tendría con un tipo entero.

Recuperación : todos los cálculos se pueden hacer directamente en los valores dobles, sin conversiones:

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

Tenga en cuenta que el código anterior no funciona si lo reemplaza doublecon int64_t: habrá una conversión implícita a double, luego truncamiento a int64_t, con una posible pérdida de información.data.GetValue ()

Comparación : las comparaciones son lo que hay que acertar con los tipos de punto flotante. Sugiero usar un método de comparación como este:

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

Equidad de redondeo

Suponga que tiene $ 9.99 en la cuenta con un 4% de interés. ¿Cuánto debería ganar el jugador? Con el redondeo de enteros obtienes $ 0.03; con redondeo de punto flotante obtienes $ 0.04. Creo que esto último es más justo.

sam hocevar
fuente
44
+1. La única aclaración que me gustaría agregar es que las aplicaciones financieras se ajustan a requisitos específicos predefinidos, también lo hace el algoritmo de redondeo. Si el juego es de casino, se ajusta a estándares similares y luego el doble no es una opción.
Ivaylo Slavov
2
Todavía tiene que pensar en redondear las reglas para formatear la IU. Porque siempre hay una fracción de jugadores que tratarán de tratar un juego como una hoja de cálculo si no quieres lidiar con personas que saltan de un lado a otro gritando cuando encuentran que tu backend no está usando la misma precisión que tu interfaz de usuario, lo que resulta en Si se muestran resultados "incorrectos", su mejor opción para mantenerlos en silencio es utilizar la misma representación interna y externa, lo que significa utilizar un formato de punto fijo. A menos que el rendimiento se convierta en un problema, BigDecimal es la mejor opción para hacerlo.
Dan Neely
10
No estoy de acuerdo con esta respuesta. Aparecen problemas de redondeo, y si se reducen, y si no está utilizando ningún redondeo adicional, $ 1.00 puede convertirse repentinamente en $ 0.99. Pero lo más importante, los decimales de precisión arbitrarios que son lentos es (casi) completamente irrelevante. Ya casi estamos en 2013, y a menos que esté haciendo grandes factorizaciones, factores trigonométricos o logaritmos en varios millones de números con miles de dígitos, nunca notará una abolladura en el rendimiento. De todos modos, simple es siempre lo mejor, por lo que mi recomendación es almacenar todos los números como centavos. De esa manera son todos int_64ts.
Panda Pyjama
8
@Sam la última vez que revisé mi cuenta bancaria, mi saldo no terminó en .0000000000000001. Los sistemas de moneda real deben funcionar con unidades no divisibles, y eso se representa correctamente con números enteros. Si debe hacer correctamente las divisiones de divisas (que en realidad no son tan comunes en el mundo financiero real), debe hacerlo para que no se pierda dinero. Por ejemplo 10/3 = 3+3+4o 10/3 = 3+3+3 (+1). Para todas las demás operaciones, los enteros funcionan perfectamente, a diferencia del punto flotante, que puede generar problemas de redondeo en cualquier operación.
Panda Pyjama
2
@SamHocevar 999 * 4/100. Donde no esté estipulado por la regulación, asuma que todo el redondeo se hará a favor de los bancos.
Dan Neely
21

Los tipos de punto flotante en Java ( float, double) no son una buena representación para las monedas debido a una razón principal: hay un error de máquina en el redondeo. Incluso si un simple cálculo devuelve un número entero - igual que 12.0/2(6.0), el punto flotante podría erróneamente alrededor de ella (aunque debido a la representación específica de este tipo en la memoria) como 6.0000000000001o 5.999999999999998o similar. Este es el resultado del redondeo específico de la máquina que ocurre en el procesador y es exclusivo de la computadora que lo calculó. Por lo general, rara vez es un problema operar con estos valores, ya que el error es bastante negligente, pero es difícil mostrarlo al usuario.

Una posible solución a esto sería utilizar implementaciones personalizadas de tipo de datos de coma flotante, como BigDecimal. Admite mejores mecanismos de cálculo que al menos aíslan los errores de redondeo para no ser específicos de la máquina, pero son más lentos en términos de rendimiento.

Si necesita una alta productividad, será mejor que se adhiera a los tipos simples. En caso de que opere con datos financieros importantes , y cada centavo sea ​​importante (como una aplicación de Forex o algún juego de casino), le recomendaría que use Longo long. Longle permitiría manejar grandes cantidades y buena precisión. Supongamos que necesita, digamos, 4 dígitos después del punto decimal, todo lo que necesita es multiplicar la cantidad por 10000. Teniendo experiencia en el desarrollo de juegos de casino en línea, he visto que Longa menudo se usa para representar el dinero en centavos. . En las aplicaciones de Forex, la precisión es más importante, por lo que necesitará un multiplicador mayor; aún así, los números enteros están libres de problemas de redondeo de la máquina (por supuesto, el redondeo manual como en 3/2 debe manejarse usted mismo).

Una opción aceptable sería utilizar los tipos de punto flotante estándar, Floaty Double, si el rendimiento es más importante que la precisión de centésimas de centavo. Luego, en su lógica de visualización, todo lo que necesita es usar un formato predefinido , de modo que la fealdad del redondeo potencial de la máquina no llegue al usuario.

Ivaylo Slavov
fuente
44
+1 Un detalle: "Supongamos que necesita, digamos, 4 dígitos después del punto decimal, todo lo que necesita es multiplicar la cantidad por 1000". -> Esto se llama punto fijo.
Laurent Couvidou
1
@ Sam, se proporcionaron los números para la apuesta de ejemplo. Aún así, personalmente he visto resultados extraños con números tan simples, pero no es un escenario frecuente. Cuando los puntos flotantes entran en juego, es más probable que esto ocurra, pero es más improbable que se
detecte
2
@Ivaylo Slavov, estoy de acuerdo contigo. En mi respuesta, acabo de hacer mi opinión. Me encanta discutir y tener la voluntad de tomar lo correcto. También gracias por tu opinión. :)
Md Mahbubur Rahman
1
@SamHocevar: Ese razonamiento no funciona en todos los casos. Ver: ideone.com/nI2ZOK
Samaursa
1
@SamHocevar: Eso tampoco está garantizado. Los números dentro del rango que todavía son números enteros comienzan a acumular errores en la primera división: ideone.com/AKbR7i
Samaursa
17

Para juegos de pequeña escala y donde la velocidad del proceso, la memoria es un problema importante (debido a la precisión o el trabajo con el coprocesador matemático puede hacer que sea muy lento), el doble es suficiente.

Pero para juegos a gran escala (por ejemplo, juegos sociales) y donde la velocidad del proceso, la memoria no está limitada, BigDecimal es mejor. Porque aquí

  • int o long para cálculos monetarios.
  • flotantes y dobles no pueden representar con precisión la mayoría de los números reales de base 10.

Recursos:

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

Porque los flotantes y los dobles no pueden representar con precisión la mayoría de los números reales de base 10.

Así es como funciona un número de coma flotante IEEE-754: dedica un bit para el signo, algunos bits para almacenar un exponente y el resto para la fracción real. Esto lleva a que los números se representen en una forma similar a 1.45 * 10 ^ 4; excepto que en lugar de que la base sea 10, son dos.

Todos los números decimales reales pueden verse, de hecho, como fracciones exactas de una potencia de diez. Por ejemplo, 10.45 realmente es 1045/10 ^ 2. Y así como algunas fracciones no pueden representarse exactamente como una fracción de una potencia de diez (1/3 viene a mi mente), algunas de ellas tampoco pueden representarse exactamente como una fracción de una potencia de dos. Como ejemplo simple, simplemente no puede almacenar 0.1 dentro de una variable de punto flotante. Obtendrá el valor representable más cercano, que es algo así como 0.0999999999999999996, y el software lo redondeará a 0.1 cuando lo muestre.

Sin embargo, a medida que realice más sumas, restas, multiplicaciones y divisiones en números inexactos, perderá cada vez más precisión a medida que se acumulen los pequeños errores. Esto hace que los flotadores y los dobles sean inadecuados para tratar con dinero, donde se requiere una precisión perfecta.

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

The float and double types are particularly ill-suited for 

cálculos monetarios porque es imposible representar 0.1 (o cualquier otra potencia negativa de diez) como flotante o doble exactamente.

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 o long para cálculos monetarios.

También eche un vistazo a

Md Mahbubur Rahman
fuente
Solo estoy en desacuerdo con la parte que es adecuada para cálculos pequeños. En mi experiencia de la vida real, ha demostrado ser suficiente para manejar una buena precisión y grandes cantidades de dinero. Sí, puede ser un poco engorroso de usar, pero supera a BigDecimal en dos puntos cruciales: 1) es más rápido (BigDecimal usa redondeo de software, que es mucho más lento que el redondeo de CPU incorporado utilizado en flotante / doble) y 2) BigDecimal es más difícil de mantener en una base de datos sin perder precisión; no todas las bases de datos admiten ese tipo 'personalizado'. Long es bien conocido y ampliamente compatible con todas las principales bases de datos.
Ivaylo Slavov
@Ivaylo Slavov, perdón por mi error. He actualizado mi respuesta.
Md Mahbubur Rahman
No se necesitan disculpas para dar un enfoque diferente para el mismo problema :)
Ivaylo Slavov
2
@Ivaylo Slavov, estoy de acuerdo contigo. En mi respuesta, acabo de hacer mi opinión. Me encanta discutir y tener la voluntad de tomar lo correcto. También gracias por tu opinión. :)
Md Mahbubur Rahman
El propósito de este sitio es aclarar los problemas y ayudar al OP a tomar la decisión correcta, dependiendo del escenario específico. Todo lo que podemos hacer es ayudar a acelerar el proceso :)
Ivaylo Slavov
13

Desea almacenar su moneda longy calcular su moneda double, al menos como respaldo. Desea que todas las transacciones tengan lugar como long.

La razón por la que desea almacenar su moneda longes que no quiere perder ninguna moneda.

Supongamos que usa un doubley no tiene dinero. Alguien te da tres monedas de diez centavos y luego las recupera.

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

Bueno, eso no es tan genial. Tal vez alguien con $ 10 quiera regalar su fortuna dándole primero tres monedas de diez centavos y luego dándole $ 9.70 a otra persona.

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

Y luego les devuelves las monedas de diez centavos:

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

Esto solo está roto.

Ahora, usemos un largo, y realizaremos un seguimiento de las décimas de centavos (entonces 1 = $ 0.001). Demos a todos en el planeta mil millones, ciento doce millones, setenta y cinco mil ciento cuarenta y tres dólares:

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

Espera, ¿podemos darles a todos más de mil millones de dólares y gastar solo un poco más de dos? El desbordamiento es un desastre aquí.

Así, cada vez que estés calcular una cantidad de dinero a transferir, el uso doubley Math.roundpara obtener una long. Luego arregle los saldos (sume y reste ambas cuentas) usando long.

Su economía no tendrá fugas y aumentará a un billón de dólares.

Hay problemas más difíciles, por ejemplo, ¿qué haces si haces veinte pagos? *, Pero esto debería ayudarte a comenzar.

* Calcula cuál es un pago, redondeado a long; luego multiplique por 20.0y verifique que esté dentro del rango; si es así, multiplique el pago por 20Lpara obtener el monto deducido de su saldo. En general, todas las transacciones deben manejarse como long, por lo que realmente necesita resumir todas las transacciones individuales; puede multiplicar como acceso directo, pero debe asegurarse de no agregar errores de redondeo y de que no se desborde, lo que significa que debe verificar doubleantes de realizar el cálculo real long.

Rex Kerr
fuente
8

Me atrevería a decir que cualquier valor que se pueda mostrar al usuario casi siempre debería ser un número entero. El dinero es solo el ejemplo más destacado de esto. Infligir 225 daños cuatro veces a un monstruo con 900 HP y descubrir que todavía le queda 1 HP restará de la experiencia tanto como descubrir que eres una fracción invisible de un centavo por debajo de lo que puedes permitirte.

En el aspecto más técnico, creo que vale la pena señalar que uno no tiene que volver a flotadores para hacer cosas avanzadas como el interés. Mientras tenga suficiente espacio libre en el número entero elegido, escriba una multiplicación y una división sustituirá a una multiplicación por un número decimal, por ejemplo, para agregar un 4%, redondeado hacia abajo:

number=(number*104)/100

Para agregar un 4%, redondeado por convenciones estándar:

number=(number*104+50)/100

Aquí no hay imprecisión de coma flotante, el redondeo siempre se divide exactamente en la .5marca.

Editar, la verdadera pregunta:

Al ver cómo ha ido el debate, empiezo a pensar que esbozar de qué se trata la pregunta puede ser más útil que una simple int/ floatrespuesta. En el centro de la pregunta no se trata de tipos de datos, se trata de tomar el control de los detalles de un programa.

El uso de un número entero para representar un valor no entero obliga al programador a ocuparse de los detalles de implementación. "¿Qué precisión usar?" y "¿Qué forma de redondear?" son preguntas que deben responderse explícitamente.

Un flotador, por otro lado, no obliga al programador a preocuparse, ya hace más o menos lo que uno esperaría. Pero dado que un flotador no es de precisión infinita, se producirá un redondeo, y ese redondeo es bastante impredecible.

¿Qué sucede en flotadores de un solo uso y quiere tomar el control del redondeo? Resulta ser casi imposible. La única forma de hacer que un flotante sea realmente predecible es usar solo valores que puedan representarse en 2^ns completos . Pero esa construcción hace que los flotadores sean bastante difíciles de usar.

Por lo tanto, la respuesta a la pregunta simple es: si desea tomar el control, use números enteros, si no, use flotantes.

Pero la pregunta que se debate es solo otra forma de la pregunta: ¿Quieres tomar el control?

aaaaaaaaaaaa
fuente
5

Incluso si es "solo un juego", usaría Money Pattern de Martin Fowler, respaldado por mucho tiempo.

¿Por qué?

Localización (L10n): Usando ese patrón puedes localizar fácilmente la moneda de tu juego. Piensa en los viejos juegos de magnate como "Transport Tycoon". Permiten fácilmente que el jugador cambie la moneda del juego (es decir, de la libra esterlina al dólar estadounidense) para cumplir con la moneda del mundo real.

Y

El tipo de datos largo es un entero de complemento de dos con signo de 64 bits. Tiene un valor mínimo de -9,223,372,036,854,775,808 y un valor máximo de 9,223,372,036,854,775,807 (inclusive) ( Tutoriales de Java )

Eso significa que puede almacenar 9,000 veces la oferta actual de dinero M2 de EE. UU. (~ 10,000 mil millones de dólares). Le da suficiente espacio para usar cualquier otra moneda mundial, probablemente, incluso aquellos que tenían / ​​tienen hiperinflación (si es curioso, vea la inflación alemana posterior a la Primera Guerra Mundial , donde 1 libra de pan era de 3,000,000,000 de marcos)

Long es fácil de persistir, muy rápido y debería darle suficiente espacio para hacer todos los cálculos de interés utilizando solo la aritmética de enteros, la respuesta de eBusiness brinda una explicación sobre cómo hacerlo.

grprado
fuente
2

¿Cuánto trabajo quieres poner en esto? ¿Qué tan importante es la precisión? ¿Le importa rastrear los errores fraccionarios que ocurren con el redondeo y la imprecisión de representar números decimales en un sistema binario?

En última instancia, tiendo a pasar un poco más de tiempo codificando e implementando pruebas unitarias para "casos de esquina" y casos problemáticos conocidos, por lo que estaría pensando en términos de un BigInteger modificado que abarca cantidades arbitrariamente grandes y mantiene los bits fraccionales con un BigDecimal o una parte de BigRational (dos BigIntegers, una para el denominador y otra para el numerador) e incluyen código para mantener la parte fraccional en una fracción real (quizás solo periódicamente) agregando cualquier parte no fraccional al BigInteger principal. Entonces, internamente, haría un seguimiento de todo en centavos solo para mantener las partes fraccionales fuera de los cálculos de la GUI.

Probablemente demasiado complejo para un juego (simple), ¡pero bueno para una biblioteca publicada como código abierto! Solo necesitaría encontrar formas de mantener un buen rendimiento cuando se trata de bits fraccionales ...

Richard Arnold Mead
fuente
1

Ciertamente no flota. Con solo 7 dígitos, solo tiene una precisión como:

12,345.67 123,456.7x

Como puede ver, ya con 10 ^ 5 dólares, está perdiendo la cuenta de los centavos. double es utilizable para fines de juego, no en la vida real debido a problemas de precisión.

Pero mucho (y con esto quiero decir 64 bits, o mucho tiempo en C ++) no es suficiente si va a rastrear transacciones y resumirlas. Es suficiente para mantener las tenencias netas de cualquier empresa, pero todas las transacciones durante un año podrían desbordarse. Eso depende de cuán grande sea tu "mundo" financiero en el juego.

Dov
fuente
0

Otra solución que agregaré aquí es hacer una clase de dinero. La clase sería algo así (incluso podría ser simplemente una estructura).

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

Esto le permitiría representar los centavos en este caso como un entero y los dólares enteros como un entero. Como tiene un indicador separado por ser negativo, puede usar enteros sin signo para sus valores que duplican la cantidad posible (también podría escribir una clase de números que aplique esta idea a los números para obtener REALMENTE números grandes). Todo lo que necesitaría hacer es sobrecargar los operadores matemáticos y podría usar esto como cualquier tipo de datos antiguo. Ocupa más memoria, pero realmente expande los límites de valor.

Esto podría expandirse aún más para incluir cosas como el número de unidades parciales por unidades enteras para que pueda tener monedas que se subdividan en algo diferente a 100 subunidades, centavos en este caso, por dólar. Podrías tener 125 floopies formando un flooper, por ejemplo.

Editar:

Ampliaré la idea de que también podría tener una especie de tabla de consulta, a la que puede acceder la clase de dinero que proporcionaría un tipo de cambio para su moneda frente a todas las demás monedas. Puede incorporar esto a las funciones de sobrecarga de su operador. Por lo tanto, si intenta agregar automáticamente 4 USD a 4 libras británicas, automáticamente le dará 6.6 libras (al momento de la escritura).

Azaral
fuente
-1

Usar clase es la mejor manera. Puede ocultar la implementación de su dinero, puede cambiar el doble / largo de lo que quiera en cualquier momento.

Ejemplo

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

En el uso de su código, simplemente getter, creo que es mejor y puede almacenar métodos más útiles.

Dawid Drozd
fuente