Delphi Roundto 和 FormatFloat 不一致

Delphi Roundto and FormatFloat Inconsistency

我在 Delphi 2010 年遇到了一个四舍五入的问题,其中一些数字在 roundto 中向下舍入,但在 formatfloat 中向上舍入。

我完全知道十进制数字的二进制表示有时会产生误导性结果,但在那种情况下,我希望 formatfloat 和 roundto 给出相同的结果。

我还看到有人建议说 "Currency" 应该用于这种事情,但正如您在下面看到的,Currency 和 Double 给出相同的结果。

program testrounding;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,Math;

var d:Double;
    c:Currency;
begin
  d:=534.50;
  c:=534.50;
  writeln('Format: '  +formatfloat('0',d));
  writeln('Roundto: '+formatfloat('0',roundto(d,0)));
  writeln('C Format: '  +formatfloat('0',c));
  writeln('C Roundto: '+formatfloat('0',roundto(c,0)));
  readln;
end.

结果如下:

Format: 535
Roundto: 534
C Format: 535
C Roundto: 534

我查看了 Why is the result of RoundTo(87.285, -2) => 87.28,建议的补救措施似乎不适用。

在这种情况下,值 534.5 可以用双精度精确表示。

查看源代码,发现如果最后一个未决数字为 5 或更多,FormatFloat 函数向上舍入。

RoundTo 使用庄家四舍五入,并在本例中四舍五入到最接近的偶数 (534)。

首先,我们可以从问题中删除 Currency,因为您使用的两个函数没有 Currency 重载。该值被转换为 IEEE754 浮点值,然后遵循与您的 Double 代码相同的路径。

我们先来看RoundTo。使用调试器或额外的 WritelnRoundTo(d,0) = 534 可以快速检查。这是为什么?

嗯,RoundTodocumentation 说:

Rounds a floating-point value to a specified digit or power of ten using "Banker's rounding".

确实在 RoundTo 的实现中,我们看到舍入模式在恢复到原始值之前暂时切换为 TRoundingMode.rmNearest。舍入模式仅适用于值刚好是两个整数之间的一半的情况。这正是我们这里的情况。

所以银行家的四舍五入适用。这意味着当值刚好是两个整数的一半时,舍入算法会选择相邻的偶数。

所以 RoundTo(534.5,0) = 534 是有道理的,同样你可以检查 RoundTo(535.5,0) = 536.

理解FormatFloat是另一回事。坦率地说,它的行为有些不透明。它在因平台而异的代码中执行临时舍入。例如,它在 32 位 Windows 上是汇编程序,但在 64 位 Windows 上是 Pascal。总体方法似乎是采用浮点值的尾数,将其转换为整数,将其转换为文本数字,然后根据这些文本数字执行舍入。执行舍入时不考虑当前舍入模式,算法似乎实现了round half away from zero policy。然而,即便如此,也没有针对所有可能的浮点值稳健地实施。它适用于您的值,但对于尾数中包含更多数字的值,算法会崩溃。

事实上,众所周知,用于在浮点值和文本之间进行转换的 Delphi RTL 例程从根本上被设计破坏了。 Delphi RTL 中没有可以正确地将文本转换为浮点数或从浮点数转换为文本的例程。事实上,我最近实现了自己的转换例程,它基于其他语言运行时使用的现有开源代码正确地执行了此操作。总有一天我会着手发布这段代码供其他人使用。

我不确定您的确切需求是什么,但是如果您希望对舍入施加一些控制,那么您可以在负责舍入的情况下这样做。虽然 RoundTo 始终使用 Banker's rounding,但您可以改用 Round,它使用当前的舍入模式。这将允许您使用您选择的舍入算法(通过调用 SetRoundMode)执行舍入,然后您可以将舍入值转换为文本。这就是关键。将值保持为算术类型,执行舍入,并仅在应用正确舍入后的最后一刻才转换为文本。