首页 > 解决方案 > 为什么 Delphi 64 位中的 SimpleRoundTo 无法正确舍入货币数字?

问题描述


我想知道是否有任何官方和可靠的 Delphi 函数可以按预期舍入一种不会随平台而改变的货币(不处理变通方法)。

正如“预期的”,我的意思是我们从学校学到的距离零(向上取整)0.5 轮,任何数学家都会回答。你可以争论很多技术解释为什么它是“不可能的/语言/平台的限制”无论如何,但是当用户看到一份会计报告时,他们期望多年来的准确性和一致性,这就是我们所需要的一个开发工具。

这是一个很老的问题,已经有10多年了。我认为 Delphi 已经修复了 SimpleRoundTo 函数,而且他们确实做到了,据我在我所做的所有测试和我在这里找到的讨论中所见: https ://groups.google.com/forum/#!topic /borland.public.delphi.non-technical/a_HLanoaOs8

我坚信,经过这么多年,我终于可以安全地使用 SimpleRoundTo 函数了,但我错了。我用正数和负数进行了几次测试,这些测试以前被报告为使用 SimpleRoundTo 错误地四舍五入。所有这些都使用 Delphi 10.2.3(32 位)(在 Windows 10 64 位上)正确舍入。但我发现两个值(至少)79.615 和 4.225 在 64 位版本的函数中无法舍入,但在 32 位版本中可以。

据我了解,SimpleRoundTo 旨在进行算术舍入,这是我们从学校学到的(不是像 RoundTo 那样的银行舍入)。SimpleRoundTo 函数的旧修复与在内部变量 LFactor 中使用 Double(错误)而不是扩展(正确)有关。我不知道这是在哪个版本中修复的,但至少对于 Delphi 7/2007 ,根据博客,SimpleRoundTo 被搞砸了。

这是该函数在 Delphi 10.2.3 中的外观,我猜想是因为有很多版本:

function SimpleRoundTo(const AValue: Extended; const ADigit: TRoundToRange = -2): Extended;
var
  LFactor: Extended;
begin
  LFactor := IntPower(10.0, ADigit);
  if AValue < 0 then
    Result := Int((AValue / LFactor) - 0.5) * LFactor
  else
    Result := Int((AValue / LFactor) + 0.5) * LFactor;
end;

这是我一直在尝试的代码:

...
var
f1, fRes: Currency;
iR: TRoundingMode;
begin
    //Just to check, result is rmNearest
    iR:= GetRoundMode;

    f1:= 79.615
    fRes:= SimpleRoundTo(f1, -2);
    sValue:= CurrToStr(fRes);
    Memo1.Lines.Add(sValue);
end;

测试正面和负面版本,结果相同:
79.614 预期舍入值 ---> 79.61 32bit ok | 64bit ok
79.615 预期舍入值 ---> 79.62 32bit ok | 64bit错误 (79.61)
79.616 预期舍入值 ---> 79.62 32bit ok | 64bit ok

所有这些过去被报告为错误的值,现在被正确舍入:
1.234 预期的舍入值 ---> 1.23 32bit ok | 64bit ok
1.235 预期舍入值 ---> 1.24 32bit ok | 64bit ok
1.236 预期舍入值 ---> 1.24 32bit ok | 64bit ok

1.664 预期舍入值 ---> 1.66 32bit ok | 64bit ok
1.665 预期舍入值 ---> 1.67 32bit ok | 64位没问题
1.666 预期舍入值 ---> 1.67 32bit ok | 64bit ok

79.624 预期舍入值 ---> 79.62 32bit ok | 64bit ok
79.625 预期舍入值 ---> 79.63 32bit ok | 64bit ok
79.626 预期舍入值 ---> 79.63 32bit ok | 64bit ok

79.634 预期舍入值 ---> 79.63 32bit ok | 64bit ok
79.635 预期舍入值 ---> 79.64 32bit ok | 64bit ok
79.636 预期舍入值 ---> 79.64 32bit ok | 64bit ok

87.284 预期舍入值 ---> 87.28 32bit ok | 64bit ok
87.285 预期舍入值 ---> 87.29 32bit ok | 64bit ok
87.286 预期舍入值 ---> 87.29 32bit ok | 64位没问题

我在这里找到了对这种行为的一种解释:“在 64 位 Windows 上,使用扩展变量的浮点运算的精度降低到双精度。” http://docwiki.embarcadero.com/RADStudio/Rio/en/Delphi_Considerations_for_Multi-Device_Applications 但是如果一个函数的精度取决于平台,那么我们就迷失了,没有可靠的“原生”跨平台函数,没有精度一个简单的会计报告,它会随着新平台的到来而永远改变。也许毕竟我做错了?,这不是一个错误,它只是它的方式,我们必须忍受。我拒绝这样做,我期待确定性,我想相信这种语言。

标签: delphi-10.2-tokyorounding-error

解决方案


这在Delphi 10.4中仍然是一个问题,因为Double包含 9.645 的变量将为SimpleRoundTo9.64,但 9.645 的常量值为SimpleRoundTo9.65。

问题似乎是SimpleRoundTo不满足存储浮点值的不准确性。

就像你的Extended例子一样,Delphi 10.4单元中的Double重载版本是这样的:SimpleRoundToMath

function SimpleRoundTo(const AValue: Double; const ADigit: TRoundToRange = -2): Double;
var
  LFactor: Extended;
begin
  LFactor := IntPower(10.0, ADigit);
  if AValue < 0 then
    Result := Int((AValue / LFactor) - 0.5) * LFactor
  else
    Result := Int((AValue / LFactor) + 0.5) * LFactor;
end;

当我们的软件输出的数字不符合我们的预期时,这给我们带来了很多问题,因此我们编写了自己的软件。

这是对我们有用的修改后的函数(我们从不使用超过 8 位小数):

function FnRound(const AValue: Double; const ADigit: TRoundToRange = -2): Double;
var
  LFactor: Extended;
begin
  LFactor := IntPower(10.0, ADigit);
  if AValue < 0 then
    Result := Int((AValue / LFactor) - (0.500000001)) * LFactor
  else
    Result := Int((AValue / LFactor) + (0.500000001)) * LFactor;
end;

您需要包含该Math单元。


推荐阅读