BigDecimal 和价格缩放

BigDecimal and scaling for prices

我想弄清楚什么是对价格进行两位小数缩放的最佳方法。

场景是这样的。假设我有 100.00 的全价,折扣后你支付 90。所以折扣百分比是 10%。为了实现这一点,我写了类似的东西,效果很好

BigDecimal grossPrice = new BigDecimal(100);
BigDecimal discountedPrice = new BigDecimal(90);
BigDecimal.ONE.subtract(discountedPrice.divide(grossPrice,2, RoundingMode.HALF_EVEN))
            .multiply(BigDecimal.valueOf(100))
            .setScale(2, RoundingMode.HALF_EVEN)
            .doubleValue();

但是一旦我将 discountedPrice 更改为 89.5,我希望折扣百分比为 10.5,但我仍然得到 10,原因很清楚,因为 89.5/100 给出 0.895 并且由于它的一半甚至四舍五入到 0.9 所以仍然是 10%

如果我这样做 HALF_UP,它和 half_even 一样好。如果我这样做,HALF_DOWN,该值将是 0.89,我的折扣百分比将是 11。所以我对在这种情况下实际给我 10.5% 折扣的东西有点困惑。

将比例设置为 3 怎么样?记得改成两个处:

  • 在你分开的地方,
  • 在你四舍五入的地方。
BigDecimal grossPrice = new BigDecimal(100);
BigDecimal discountedPrice = new BigDecimal("89.5");
double doubleValue = BigDecimal.ONE
        .subtract(discountedPrice.divide(grossPrice,3, RoundingMode.HALF_EVEN))  // here
        .multiply(BigDecimal.valueOf(100))
        .setScale(3, RoundingMode.HALF_EVEN)                                     // here
        .doubleValue();

System.out.println(doubleValue);                                                 // 10.5

您可能希望定义 MathContext 以支持您的计算并避免拼写错误:

MathContext halfEvenTo3 = new MathContext(3, RoundingMode.HALF_EVEN);
BigDecimal grossPrice = new BigDecimal(100);
BigDecimal discountedPrice = new BigDecimal("89.5");
double doubleValue = BigDecimal.ONE
        .subtract(discountedPrice.divide(grossPrice, halfEvenTo3))              // here
        .multiply(BigDecimal.valueOf(100))
        .round(halfEvenTo3)                                                     // here
        .doubleValue();

System.out.println(doubleValue);                                                // 10.5

鉴于您的舍入模式,很明显您希望系统永远不会以美分的分数结束。一个好的计划;大多数金融系统无法处理小数美分(您无法转账半美分,或在收银机上支付半美分,甚至无法在 POS 工具中输入半美分)。

这意味着您可以完全放弃 BigDecimal,而使用普通的 jane long,它将代表 'atomic currency units' 的数字。日元代表日元,欧元和美元代表美分,英镑代表便士,比特币代表聪,等等。

你在这里试图表示的数字完全不同,最好先回到事物的含义,然后再编写代码。

在您的代码中,grossPricediscountedPrice 都是货币金额。

另一方面,您要查找的值根本不是货币数量:它是完全不同的东西 - 比率。这也显示了您的愿望:对于货币金额,您希望始终四舍五入到小数点后两位,但对于您不希望的比率,这是明智的 - 它们是 2 个完全不同的概念。

比率很棘手。例如,1 和 3 之间的比率在基数 10 (0.333333) 或基数 2 中都不能完美地表示 table,例如double 并共同使用。因此,不可能具有完美的比率作为一般规则。因此,您必须与那个吻别,取而代之的是选择一些任意的精度。

一个简单的方法就是说:好吧,嘿,如果 table 不再追求完美,我会确保在编写代码和文档时牢记这一点,而且我不再需要它了。在这种情况下,您不妨使用 doubledouble 是一个 可怕的 想法来表示货币金额,但是比率 - 这很好..并且在你的代码中你无论如何都会加倍。

换句话说,在这里停止使用 BigDecimal:您将它用于两件事,并且在这两种情况下,都没有实际点:

  • 你用它来表示货币数量,但在某种程度上,小数原子单位是不可能的。您可以这样做,但它设计过度且不必要地复杂,只需使用 long 来存储那些原子单元即可。
  • 您使用它来计算比率,但将其转换为双精度数,因此您想要应用的任何细粒度精度控制都会被您的转换取消。
long grossPrice = 10000; // 0
long discountedPrice = 8950; // .50
double ratio = 100.0 * discountedPrice / grossPrice;

打印这些数字时,小数点在这里:

System.out.printf("Ratio: %.3f\n", ratio); // print to 3 decimal places.

如果你确实需要细粒度的控制,或者你想要分数原子金融单位,那么请记住:

  • 在很多方面'fractional atomic finance'是无解的。例如,如果您想将 4 美分的附加费分摊给 3 个人,您就是做不到,再多的 BigDecimal 也无济于事:BigDecimal is incapable of perfectly representing '1 1 /3 美分',即使可以,您实际上也无法收取费用。最好的解决方案是要么四舍五入(向每个成员收取 2 美分),要么掷硬币,向 2 个人收取 1 美分,并随机向一个人收取 2 美分。没有办法自动执行此操作,您必须对其进行编码。 总的来说,一旦涉及除法或比率,就无法完美table
  • 有货币库,例如 joda-currency。如果您的目标是 'convenience'(BigDecimal 不是很方便,因此为什么“我将使用 BD 而不是 long;更方便”是一个奇怪的结论),您可能想使用它们来代替。
  • 多头 CAN overflow/underflow,但这并不相关,除非您谈论的是几个世纪以来的世界经济产出。你需要很多的钱才能溢出'store cents in a long'的模型。
  • int 然而,这并不好:你很容易溢出这些(你只需要 4000 万美元!),所以一定要使用多头。