Cómo representar moneda o dinero en C

TL; DR 1 ¿Qué es un enfoque preciso y sostenible para representar moneda o dinero en C?


Antecedentes de la pregunta:
Esto ha sido respondido para varios otros idiomas, pero no pude encontrar una respuesta sólida para el lenguaje C.

  • C # ¿Qué tipo de datos debo usar para representar dinero en C #?

  • Java ¿Por qué no usar Double o Float para representar la moneda?

  • Objective-C ¿Cómo representar el dinero en Objective-C / iOS?

Nota: Hay muchas más preguntas similares para otros idiomas, solo saqué algunas para fines de representación.

Todas estas preguntas se pueden destilar para “usar un tipo de datos decimal “, donde el tipo específico puede variar según el idioma.

Hay una pregunta relacionada que termina sugiriendo usar un enfoque de “punto fijo”, pero ninguna de las respuestas aborda con un tipo de datos específico en C.

Del mismo modo, he examinado bibliotecas de precisión arbitrarias como GMP , pero no tengo claro si este es el mejor enfoque para usar o no.


Simplificando suposiciones:

  • Supongamos una architecture basada en x86 o x64, pero por favor llame a cualquier suposición que impacte una architecture basada en RISC, como un chip de potencia o un chip Arm.

  • La precisión en los cálculos es el requisito principal. La facilidad de mantenimiento sería el siguiente requisito. La velocidad de los cálculos es importante, pero es terciaria para los demás requisitos.

  • Los cálculos deben ser capaces de soportar de forma segura operaciones precisas para la fábrica , así como valores de soporte que van hasta los billones (10 ^ 9)


Diferencias de otras preguntas:

Como se señaló anteriormente, este tipo de pregunta se ha formulado anteriormente para muchos otros idiomas. Esta pregunta es diferente de las otras preguntas por un par de razones.

Usando la respuesta aceptada de: ¿Por qué no usar Double o Float para representar la moneda? , resaltemos las diferencias.

( Solución 1 ) Una solución que funciona en casi cualquier idioma es usar enteros en su lugar, y contar centavos. Por ejemplo, 1025 sería de $ 10.25. Varios idiomas también tienen tipos incorporados para manejar el dinero. ( Solución 2 ) Entre otros, Java tiene la clase BigDecimal, y C # tiene el tipo decimal.

Énfasis añadido para resaltar las dos soluciones sugeridas

La primera solución es esencialmente una variante del enfoque de “punto fijo”. Hay un problema con esta solución en el sentido de que el rango sugerido (centavos de seguimiento) es insuficiente para los cálculos basados ​​en el molino y se perderá información significativa al redondear.

La otra solución es usar una clase decimal nativa que no está disponible dentro de C.

Del mismo modo, la respuesta no considera otras opciones como crear una estructura para manejar estos cálculos o usar una biblioteca de precisión arbitraria. Esas son diferencias comprensibles ya que Java no tiene estructuras y por qué considerar una biblioteca de terceros cuando hay soporte nativo dentro del lenguaje.

Esta pregunta es diferente de esa pregunta y otras preguntas relacionadas porque C no tiene el mismo nivel de compatibilidad de tipo nativo y tiene características de idioma que otros idiomas no tienen. Y no he visto ninguna de las otras preguntas abordar las múltiples formas en que esto podría abordarse en C.


La pregunta:
De mi investigación, parece que el float no es un tipo de datos apropiado para usar para representar la moneda dentro de un progtwig C debido a un error de coma flotante.

¿Qué debería usar para representar el dinero en C y por qué ese enfoque es mejor que otros enfoques?

1 Esta pregunta comenzó en una forma más corta, pero los comentarios recibidos indicaron la necesidad de aclarar la pregunta.

Utilice un tipo de datos enteros (long long, long, int) o una biblioteca aritmética BCD (decimal codificado en binario). Debe almacenar décimas o centésimas de la cantidad más pequeña que muestre. Es decir, si usa dólares estadounidenses y presenta centavos (centésimas de dólar), sus valores numéricos deben ser enteros que representan molinos o millrays (décimas o centésimas de un centavo). Las cifras extra significativas asegurarán su interés y ronda de cálculos similares de manera consistente.

Si usa un tipo entero, asegúrese de que su rango sea lo suficientemente bueno como para manejar las cantidades de preocupación.

La mejor representación de dinero / moneda es usar un tipo de punto flotante de precisión lo suficientemente alto como el double que tiene FLT_RADIX == 10 . Estas plataformas / cumplidores son raros ya que la gran mayoría de los sistemas tienen FLT_RADIX == 2 .

Cuatro alternativas: enteros, punto flotante no decimal, punto flotante decimal especial, estructura definida por el usuario.

Números enteros : una solución común usa el número entero de la denominación más pequeña en la moneda elegida. Ejemplo que cuenta centavos estadounidenses en lugar de dólares. El rango de enteros debe ser razonablemente amplio. Algo como long long lugar de int como int solo puede manejar aproximadamente +/- $ 320.00. Esto funciona bien para tareas simples de contabilidad que involucran agregar / restar / múltiple pero comienza a romperse con divisiones y funciones complejas como se usan en los cálculos de interés. Fórmula de pago mensual . Las matemáticas enteras firmadas no tienen protección de desbordamiento. Se debe aplicar cuidado al redondear los resultados de la división. q = (a + b/2)/b no es lo suficientemente bueno.

Punto flotante binario : 2 errores comunes: 1) usando el float que a menudo es de precisión insuficiente y 2) redondeo incorrecto. El uso del double pozo soluciona el problema n. ° 1 para muchos límites contables. Sin embargo, el código todavía necesita a menudo usar una ronda en la unidad monetaria mínima deseada para obtener resultados satisfactorios.

 // Sample - does not properly meet nuanced corner cases. double RoundToNearestCents(double dollar) { return round(dollar * 100.0)/100.0; } 

Una variación en el double es usar una cantidad double de la unidad más pequeña (0.01 o 0.001). Una ventaja importante es la capacidad de redondear simplemente mediante el uso de la función de round() que, por sí sola, atiende casos de esquina.

Punto flotante decimal especial Algunos sistemas proporcionan un tipo “decimal” distinto del double que cumple con decimal64 o algo similar. Aunque esto maneja la mayoría de los problemas anteriores, se sacrifica la portabilidad.

La estructura definida por el usuario (como el punto fijo ), por supuesto, puede resolverlo todo, excepto que es un código propenso a errores y funciona (en cambio ). El resultado puede funcionar perfectamente pero carecer de rendimiento.

Conclusión Este es un tema profundo y cada enfoque merece una discusión más amplia. La respuesta general es: no hay una solución general ya que todos los enfoques tienen debilidades significativas. Entonces depende de los detalles de la aplicación.

[Editar]
Dadas las ediciones adicionales de OP, recomendamos usar el número double de la unidad de moneda más pequeña (ejemplo: $ 0.01 -> double money = 1.0; ). En varios puntos del código siempre que se requiera un valor exacto , use round() .

 double interest_in_cents = round( Monthly_payment(0.07/12 /* percent */, N_payments, principal_in_cents)); 

Mi bola de cristal dice que para el 2022 EE. UU. Reducirá los $ 0.01 y la unidad más pequeña será de $ 0.05. Utilizaría el enfoque que mejor pueda manejar ese cambio.

Si la velocidad es su principal preocupación, utilice un tipo integral escalado a la unidad más pequeña que necesita representar (como una fábrica , que es 0.001 dólares o 0.1 centavos). Por lo tanto, 123456 representa $123.456 .

El problema con este enfoque es que puede quedarse sin dígitos; un int sin signo de 32 bits puede representar algo así como 10 dígitos decimales, por lo que el valor más grande que podría representar bajo este esquema sería de $9,999,999.999 . No es bueno si necesita lidiar con valores en los miles de millones.

Otro enfoque es usar un tipo de estructura con un miembro integral para representar el monto total en dólares, y otro miembro integral para representar la cantidad fraccional en dólares (nuevamente, escalado a la unidad más pequeña que necesita representar, ya sea centavos, molinos o algo más pequeño), similar a la estructura timeval que guarda segundos enteros en un campo y nanosegundos en el otro:

 struct money { long whole_dollars; // long long if you have it and you need it int frac_dollar; }; 

Un int es más que suficiente para manejar la escala que cualquier persona en su sano juicio usaría. whole_dollars firmado en caso de que la porción whole_dollars sea ​​0.

Si está más preocupado por almacenar valores arbitrariamente grandes, siempre hay BCD , que puede representar mucho más dígitos que cualquier tipo de integral nativo o de coma flotante.

Sin embargo, la representación es solo la mitad de la batalla; también debe ser capaz de realizar aritmética en estos tipos, y las operaciones en la moneda pueden tener reglas de redondeo muy específicas. Por lo tanto, querrás tener eso en cuenta cuando decidas sobre tu representación.

int (32 o 64 según lo necesite) y piense en centavos o centavos parciales según sea necesario. Con 32 bits y pensando en centavos, puede representar hasta 40 millones de dólares en un solo valor. Con 64 bits, está mucho más allá de todo el departamento de EE. UU. Jamás combinado .

Hay algunos problemas al hacer los cálculos que debes tener en cuenta para no dividir la mitad de los números significativos.

Es un juego de conocer los rangos y cuando el redondeo después de la división es bueno.

Por ejemplo, hacer una ronda adecuada (de la variante de .5 arriba) después de la división puede hacerse sumndo primero la mitad del numerador al valor y luego haciendo la división. Sin embargo, si usted está haciendo finanzas, necesitará un sistema de ronda un poco más avanzado, aunque aprobado por sus contadores.

 long long res = (amount * interest + 500)/1000; 

Solo convierta al dólar (o lo que sea) al comunicarse con el usuario.