在 .Net 中使用哪种货币舍入算法?

Which currency rounding algorithm to use in .Net?

对于考虑后端系统的十进制货币舍入操作,在 .Net 中使用哪种舍入算法有最佳实践吗?赞赏真实世界的经验。


.Net 默认使用 Banker’s Rounding。 (MidpointRounding.ToEven) 这与我将要使用的 SQL 服务器后端不一致,因为 SQL 服务器使用算术舍入 (MidpointRounding.AwayFromZero) 并且没有内置函数来模仿银行家的舍入。

注意:我在 SQL 服务器中使用 decimal(18, 4),在 .Net

中使用 decimal

这是 .Net 默认银行家四舍五入到两位小数与 SQL 服务器四舍五入到两位小数的示例:

| Value | .Net  | SQL Server  |
|-------|-------|-------------|
| 2.445 | 2.44  | 2.45        |
| 2.455 | 2.46  | 2.46        |
| 2.465 | 2.46  | 2.47        |
| 3.445 | 3.44  | 3.45        |
| 3.455 | 3.46  | 3.46        |
| 3.465 | 3.46  | 3.47        |

// t-sql
declare @decimalPlaces int
set @decimalPlaces = 2

select round(convert(decimal(18, 4), 2.445), @decimalPlaces) -- 2.45
select round(convert(decimal(18, 4), 2.455), @decimalPlaces) -- 2.46
select round(convert(decimal(18, 4), 2.465), @decimalPlaces) -- 2.47
select round(convert(decimal(18, 4), 3.445), @decimalPlaces) -- 3.45
select round(convert(decimal(18, 4), 3.455), @decimalPlaces) -- 3.46
select round(convert(decimal(18, 4), 3.465), @decimalPlaces) -- 3.47

// .Net
var algorithm = MidpointRounding.ToEven;
var decimalPlaces = 2;
Console.WriteLine(decimal.Round(2.445M, decimalPlaces, algorithm).ToString()); // 2.44
Console.WriteLine(decimal.Round(2.455M, decimalPlaces, algorithm).ToString()); // 2.46
Console.WriteLine(decimal.Round(2.465M, decimalPlaces, algorithm).ToString()); // 2.46
Console.WriteLine(decimal.Round(3.445M, decimalPlaces, algorithm).ToString()); // 3.44
Console.WriteLine(decimal.Round(3.455M, decimalPlaces, algorithm).ToString()); // 3.46
Console.WriteLine(decimal.Round(3.465M, decimalPlaces, algorithm).ToString()); // 3.46

如果我从 SQL 服务器检索一个值并让它处理舍入,与 .Net 告诉我的相比,我会在这里和那里少花钱,因为它是默认的 Banker's四舍五入。

看来我应该继续前进并在我的 .Net 代码库中使用算术舍入,但我看到一个开源项目 (nopCommerce) 使用默认的 Banker's Rounding 所以这让我想知道最好的方法是什么是。


也许更好的问题是:是否有任何理由 对以下货币使用算术舍入 (MidpointRounding.AwayFromZero) .Net?

当你真的需要四舍五入时,一种环境。如果不这样做,您将依赖默认设置和配置,这最终会伤害您。所有其他地方都保持你所传递的精度以避免任何奇怪的问题。

您使用哪种算法取决于您的应用程序的用例。但无论如何,选择一个并确保您将其集中化并根据需要进行更改。

还有。净违约是货币价值最常见的四舍五入,所以除非你有合理的商业理由不使用它,否则我会坚持这样做。否则,随着时间的推移,分配不均会导致实际的货币后果。

使用 decimal 而不是 float / double 因为浮点数和双精度数需要以 2 为基数存储,而小数以 10 为基数存储。如果将 SQL 服务器类型映射到decimal 在 .NET 中,您的舍入问题将消失。

RE: 算法,处理货币价值,你必须

  • 确保在每个 value/calculation 步骤中存储足够的小数位,以确保任何舍入误差都非常小,可以忽略不计
  • 使用银行四舍五入来尝试减少舍入误差

想象一下:

        Real Value  Standard    Bankers
        0.25        0.3         0.2
        0.75        0.8         0.8
-----------------------------------------
Sum     1           1.1         1

基本上,只要您有

,就使用庄家四舍五入
x.5  (e.g. 6.5)

使用标准舍入,您将始终向上舍入,这意味着对于每个中点舍入,您将总值增加 0.5,但是使用银行家舍入,当 x 是偶数时,您向下舍入(-0.5 ) 并且当 x 为奇数时,您向上取整 (+0.5),因此使用随机值时 +0.5-0.5 相互抵消的可能性更高。

当然,如果你能以足够大的精度存储每个值,这些舍入误差非常小,可以舍弃。

个人计算我总是使用 Bankers,但为了显示 UI 我使用 AwayFromZero(因为它更自然)

据我所见,我们的做法是将 .NET 和 SQL 服务器的所有内容都保持为默认值 - 这使得事情更易于维护。然而,这意味着您需要确保在整个系统中始终如一地进行舍入,正如蒂姆指出的那样。

我们所做的是,我们确保所有数字都由应用程序本身四舍五入,无论是单一价格(有时可能是 4dp)还是总价(通常是 2dp),调用 Math.Round小数位数的适当值。 SQL 服务器然后只存储它收到的内容(通常使用十进制 (18,4))。

然后您可以在您的用户界面上添加额外的一致性层,这可能取决于您使用的是什么控件,但是您可以例如将数字字段限制为适当的位数,这样用户就不会t 输入例如1.32682 如果您的应用程序只打算保存 1.33。

来自 the documentation:

Rounding away from zero is the most widely known form of rounding, while rounding to nearest even is the standard in financial and statistical operations. It conforms to IEEE Standard 754, section 4. When used in multiple rounding operations, rounding to nearest even reduces the rounding error that is caused by consistently rounding midpoint values in a single direction. In some cases, this rounding error can be significant.

银行家四舍五入很少在现实世界中有意义。我曾经在银行业工作,他们曾经派我去执行一项任务,以消除由于这个罪魁祸首造成的报告中的“舍入错误”。

除了银行业务,计算运费、所得税和销售税始终使用“远离零”舍入。

我能想到的银行家四舍五入在现实世界中的唯一用途可能是计算利息和拆分差额或支付佣金。

Microsoft 没有选择默认值 MidpointRounding.ToEven 是有充分理由的。他们这样做是为了保持与 Visual Basic(.NET 之前)的向后兼容性。之所以没有这样做,是因为它是一个合理的默认值,或者是任何类型的最佳默认值。如果他们今天必须再次做出决定,那很可能是MidpointRounding.AwayFromZero

请记住,当有人检查您的作业时,他们将使用我们在小学时都学到的“远离零”方法。在我看来,“因为 Microsoft 将其设置为默认值”并不足以成为该程序使用它的借口。如果有必要,应该有正当的商业理由使用银行四舍五入。如果应用需求中没有明确调用,应该使用MidpointRounding.AwayFromZero。最好 将应用程序框架中的默认设置 更改为 MidpointRounding.AwayFromZero