【问题标题】:How to represent currency or money in C如何在 C 中表示货币或金钱
【发布时间】:2015-11-19 15:24:54
【问题描述】:

TL;DR1用 C 表示货币或金钱的准确且可维护的方法是什么?


问题的背景:
许多其他语言已经回答了这个问题,但我找不到 C 语言的可靠答案。

注意:对于其他语言还有很多类似的问题,我只是出于代表性目的提取了一些。

所有这些问题都可以归结为“使用decimal 数据类型”,具体类型可能因语言而异。

有一个related question 最终建议使用“定点”方法,但没有一个答案涉及使用 C 中的特定数据类型。

同样,我查看了任意精度库,例如 GMP,但我不清楚这是否是最好的使用方法。


简化假设:

  • 假设是基于 x86 或 x64 的架构,但请指出任何会影响基于 RISC 的架构(例如 Power 芯片或 Arm 芯片)的假设。

  • 计算的准确性是首要要求。 Ease of maintenance 将是下一个要求。计算速度很重要,但比其他要求更重要。

  • 计算需要能够安全地支持精确到 mill 的操作以及支持高达数万亿 (10^9) 的值


与其他问题的区别:

如上所述,此类问题之前已针对多种其他语言提出过。由于几个原因,这个问题与其他问题不同。

使用来自Why not use Double or Float to represent currency? 的公认答案,让我们突出显示差异。

(解决方案 1) 几乎适用于任何语言的解决方案是使用整数,并计算美分。例如,1025 将是 10.25 美元。几种语言也有处理金钱的内置类型。 (方案二)其中Java有BigDecimal类,C#有decimal类型。

添加了强调以突出显示两个建议的解决方案

第一个解决方案本质上是“定点”方法的变体。此解决方案存在一个问题,即建议的范围(跟踪美分)不足以进行基于工厂的计算,并且在舍入时会丢失重要信息。

另一种解决方案是使用 C 中不可用的原生 decimal 类。

同样,答案不考虑其他选项,例如创建用于处理这些计算的结构或使用任意精度库。这些差异是可以理解的,因为 Java 没有结构,而且当该语言有本机支持时,为什么要考虑使用 3rd 方库。

这个问题与那个问题和其他相关问题不同,因为 C 没有相同级别的原生类型支持,并且具有其他语言没有的语言特性。而且我还没有看到任何其他问题涉及可以在 C 中解决的多种方式。


问题:
根据我的研究,由于浮点错误,float 似乎不是用于在 C 程序中表示货币的合适数据类型。

我应该用什么来表示 C 中的钱,为什么这种方法比其他方法更好?

1这个问题以较短的形式开始,但收到的反馈表明需要澄清这个问题。

【问题讨论】:

  • 您始终可以使用美分并使用intlonglong long
  • 我不知道是否是最好的方法,但我见过几个人使用由两个整数组成的结构,一个用于单位,另一个用于小数。
  • @Olaf - 谢谢你的链接。我已经对其进行了审查,并在我的问题中提到了它。这个问题集中在 Java 上,它具有与 C 不同的可用类型。
  • @Olaf - 我相信您指的是“几乎适用于任何语言的解决方案是使用整数,并计算美分。例如,1025 将是 10.25 美元。”如果您放弃十分之一或百分之一,该特定方法会丢失潜在的重要信息,如下面的棘轮怪胎所述。在 C 中不存在使用 decimal 类型的另一个建议。此外,这个问题正在超越这些建议,以确定可以通过 3 或 4 种不同方式来确定最佳行动方案。跨度>
  • @Olaf - 再次感谢您的帮助。在这一点上,我认为我们彼此之间的沟通并不清晰。请随时ping me in chat,以便我们可以在此处进一步讨论这些 cmets 之外的内容。

标签: c currency


【解决方案1】:

int(根据需要选择 32 或 64)并根据需要以美分或部分美分计算。使用 32 位并以美分计算,您可以在单个值中代表高达 4000 万美元。使用 64 位,它远远超出了美国所有部门的组合

在进行计算时必须注意一些问题,以免将有效数字的一半分开。

这是一个了解范围以及除法后四舍五入何时合适的游戏。

例如,在除法之后进行适当的舍入(0.5 向上变量)可以通过首先将分子的一半添加到值然后进行除法来完成。不过,如果您从事财务工作,您将需要更高级的轮次系统,尽管您的会计师已批准。

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

仅在与用户交流时转换为美元(或其他)。

【讨论】:

  • 可能是unsigned,除非有负数。
  • @FiddlingBits 我的 4000 万数字是基于未签名的
【解决方案2】:

使用整数数据类型(long long、long、int)或 BCD(二进制编码的十进制)算术库。您应该存储您将显示的最小数量的十分之一或百分之一。也就是说,如果您使用美元并表示美分(百分之一美元),那么您的数值应该是代表 Mills 或 Millrays(十分之一或百分之一美分)的整数。额外的有效数字将确保您的兴趣和类似的计算始终如一。

如果您使用整数类型,请确保其范围足够大以处理关注的数量。

【讨论】:

    【解决方案3】:

    如果速度是您最关心的问题,请使用按比例缩放到您需要表示的最小单位的整数类型(例如 mill,即 0.001 美元或 0.1 美分)。因此,123456 代表$123.456

    这种方法的问题是您可能会用完数字;一个 32 位无符号整数可以表示 10 个十进制数字,因此您可以在此方案下表示的最大值为 $9,999,999.999。如果您需要处理数十亿的价值,那就不好了。

    另一种方法是使用一个结构类型,其中一个整数成员表示整个美元金额,另一个整数成员表示小数美元金额(同样,缩放到您需要表示的最小单位,无论是美分、磨,或更小的东西),类似于timeval 结构,它在一个字段中保存整秒,在另一个字段中保存纳秒:

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

    int 的宽度更多足以处理任何理智的人都会使用的缩放。留下签名以防whole_dollars 部分为0。

    如果您更担心存储任意大的值,总是有BCD,它可以表示比任何原生整数或浮点类型更多的数字。

    不过,代理只是成功的一半;您还必须能够对这些类型执行算术运算,并且对货币的操作可能具有非常具体的rounding rules。因此,在决定您的代表时,您需要考虑到这一点。

    【讨论】:

    • unsignedsigned 更合适,不是吗?
    • @FiddlingBits:您需要能够表示负值,因此您需要在某处添加符号。要么为成员使用签名类型(frac_dollar 需要签名,以防 whole_dollars0),或者使它们都未签名并使用第三个成员进行签名。
    【解决方案4】:

    最好的货币/货币表示是使用精度足够高的浮点类型,例如double,它具有FLT_RADIX == 10。这些平台/编译器很少见1,因为绝大多数系统都有FLT_RADIX == 2

    四种选择:整数、非十进制浮点、特殊十进制浮点、用户定义的结构。

    整数:一种常见的解决方案是使用所选货币中最小面额的整数计数。例如计算美分而不是美元。整数的范围需要合理的宽。像long long 而不是int 之类的东西int 只能处理大约+/- 320.00 美元。这适用于涉及加/减/乘的简单会计任务,但在利息计算中使用的除法和复杂函数开始破解。 Monthly payment formula。有符号整数数学没有溢出保护。舍入除法结果时需要小心。 q = (a + b/2)/b 不够好。

    二进制浮点:2 个常见的陷阱:1) 使用 float,这通常精度不足,2) 舍入不正确。使用double 很好地解决了许多记帐限制的问题#1。然而,代码仍然经常需要对所需的最小货币单位进行四舍五入才能获得满意的结果。

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

    double 的变体是使用最小单位(0.01 或 0.001)的 double 数量。一个重要的优势是能够简单地使用 round() 函数进行舍入,该函数本身就可以满足极端情况。

    特殊十进制浮点数 一些系统提供除double 之外的符合decimal64 或类似内容的“十进制”类型。虽然这可以解决上述大多数问题,但牺牲了可移植性。1

    用户定义的结构(如fixed-point)当然可以解决所有问题,除非它很容易出错,而且它是工作Instead)。结果可能完美运行,但性能不佳。

    结论这是一个深奥的主题,每种方法都值得进行更广泛的讨论。一般的答案是:没有通用的解决方案,因为所有方法都有明显的弱点。所以这取决于应用程序的具体情况。

    [编辑]
    鉴于 OP 的额外编辑,建议使用 double 最小货币单位的数字(例如:$0.01 --> double money = 1.0;)。每当需要 exact 值时,在代码中的各个点使用round()

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

    [Edit 2021]
    1C23 旨在提供基于十进制的浮点。等到那个时候?


    我的水晶球说,到 2022 年,美国将下降 0.01 美元,最小单位将是 0.05 美元。我会使用最能应对这种转变的方法。

    【讨论】:

    • 也许你的水晶球也预测到加拿大会掉便士,这发生在 2013 年。在处理现金时(并且仅在处理现金时),它们四舍五入到最接近的 5c。不过,您仍然需要正确计算数学,因此计算方面确实没有什么重大变化。
    【解决方案5】:

    切勿使用浮点数来存储货币。浮点数不能表示十分之一或百分之一,只能表示二元有理数,即 p/q 形式的数字,其中 p 和 q 是整数,q 是 2 的幂。因此,任何试图表示其他美分的尝试超过 0、25、50 或 75 美分需要一个近似值,而这些近似值会转化为可被利用使您赔钱的漏洞。

    相反,以美分存储整数值(或任何最小的货币单位)。读取使用小数点格式化的值时,只需将整个货币单位和美分读取为单独的字段,然后乘以 100(或 10 的适当幂)并相加。

    【讨论】:

    • 应该详细说明“永远不要使用浮点存储货币”。
    • 常见的二进制浮点数并不精确地表示十分之一或百分之一。十进制浮点数。整数数学存在溢出和截断与舍入的问题。对于仅使用 +,-,* 的小程序来说,目前使用整数数学是很好的。一旦代码开始计算利率和抵押贷款,简单地使用整数是不够的。这个答案的弱点是它忽略了许多问题。
    • @chux:在符合附件 F 的实现上,C 浮点类型是二进制浮点。十进制浮点类型可以解决这些问题,但在我看来,它们不是标准语言功能,并且超出了 OP 问题的范围。兴趣计算之类的事情可能会或可能不会在 OP 感兴趣的范围内,但我正在回答这个问题,而不是写一本关于这个主题的书。在任何情况下,您仍然希望/需要使用整数,但您只需要额外的技术来应用利率等。
    • “永远不要使用浮点存储货币”夸大了您的简化答案。同意我们不需要写一本书,但像这样的全面指令并不承认存在程序员学习者需要注意的更大问题。
    • @chux:不存在(二进制)浮点类型适合存储货币金额的情况。指数只是浪费了存储空间,因为您需要一个精确的整数值。没有“永远不要使用浮点存储货币”的条件或警告;一些与货币相关的计算的难度并不会使这条规则失效。所以我不清楚你想表达什么观点。
    猜你喜欢
    • 1970-01-01
    • 2010-10-24
    • 1970-01-01
    • 2011-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多