在Delphi中,Math.Round()的MidpointRounding.AwayFromZero等效于什么?

15

我怎样在Delphi中使用类似于Math.Round并带有MidpointRounding.AwayFromZero的功能?

以下代码将等价于什么:

double d = 2.125;
Console.WriteLine(Math.Round(d, 2, MidpointRounding.AwayFromZero));

输出:2.13

在Delphi中?


1
我认为没有一个开箱即用的函数可以做到这一点。有向上取整、向下取整、朝零方向取整和银行家舍入,但是很遗憾没有 AwayFromZero。 - GolezTrol
2个回答

15
我相信Delphi RTL的SimpleRoundTo函数基本上是这样实现的,至少在FPU舍入模式“正确”的情况下是如此。请仔细阅读其文档和实现,然后决定它是否足够适合您的目的。
但要注意,像这样为单个舍入操作设置舍入模式是使用全局更改来解决局部问题。这可能会引起问题(多线程、库等)。
额外的闲聊:如果问题是关于“常规”舍入(到整数),我想我会尝试以下方法
function RoundMidpAway(const X: Real): Integer;
begin
  Result := Trunc(X);
  if Abs(Frac(X)) >= 0.5 then
    Inc(Result, Sign(X));
end;

当然,即使对于n个小数位的一般情况,编写类似的功能也是可能的。(但要小心处理边缘情况、溢出、浮点问题等。)

更新:我相信以下代码可以解决问题(而且速度很快):

function RoundMidpAway(const X: Real): Integer; overload;
begin
  Result := Trunc(X);
  if Abs(Frac(X)) >= 0.5 then
    Inc(Result, Sign(X));
end;

function RoundMidpAway(const X: Real; ADigit: integer): Real; overload;
const
  PowersOfTen: array[-10..10] of Real =
    (
      0.0000000001,
      0.000000001,
      0.00000001,
      0.0000001,
      0.000001,
      0.00001,
      0.0001,
      0.001,
      0.01,
      0.1,
      1,
      10,
      100,
      1000,
      10000,
      100000,
      1000000,
      10000000,
      100000000,
      1000000000,
      10000000000
    );
var
  MagnifiedValue: Real;
begin
  if not InRange(ADigit, Low(PowersOfTen), High(PowersOfTen)) then
    raise EInvalidArgument.Create('Invalid digit index.');
  MagnifiedValue := X * PowersOfTen[-ADigit];
  Result := RoundMidpAway(MagnifiedValue) * PowersOfTen[ADigit];
end;

当然,如果您将在生产代码中使用此功能,则还应添加至少50个单元测试用例来测试其正确性(每天运行)。
更新:我相信以下版本更稳定:
function RoundMidpAway(const X: Real; ADigit: integer): Real; overload;
const
  FuzzFactor = 1000;
  DoubleResolution = 1E-15 * FuzzFactor;
  PowersOfTen: array[-10..10] of Real =
    (
      0.0000000001,
      0.000000001,
      0.00000001,
      0.0000001,
      0.000001,
      0.00001,
      0.0001,
      0.001,
      0.01,
      0.1,
      1,
      10,
      100,
      1000,
      10000,
      100000,
      1000000,
      10000000,
      100000000,
      1000000000,
      10000000000
    );
var
  MagnifiedValue: Real;
  TruncatedValue: Real;
begin

  if not InRange(ADigit, Low(PowersOfTen), High(PowersOfTen)) then
    raise EInvalidArgument.Create('Invalid digit index.');
  MagnifiedValue := X * PowersOfTen[-ADigit];

  TruncatedValue := Int(MagnifiedValue);
  if CompareValue(Abs(Frac(MagnifiedValue)), 0.5, DoubleResolution * PowersOfTen[-ADigit]) >= EqualsValue  then
    TruncatedValue := TruncatedValue + Sign(MagnifiedValue);

  Result := TruncatedValue * PowersOfTen[ADigit];

end;

但我还没有完全测试它。(目前它通过了900+单元测试用例,但我认为测试套件还不够充分。)


可能将 RoundMidpAway(const X: Real): Integer; 内联化是个不错的主意。 - Andreas Rejbrand
1
RoundMidpAway(2.135, -2) results 2.13. should be 2.14 - zig
1
@zig:是的,浮点数确实很难处理,正如我所暗示的那样。为了辩护自己,我怀疑这种问题可能存在,因此发表了关于广泛测试的评论(我打算今晚进行)。在这种情况下,“RoundMidpAway(2.135, -2)”产生“MagnifiedValue = 213.5”和“Abs(Frac(X)) = 0.499999999999972”,而不是精确值“0.5”。这就是原因。我会尝试修复这个问题。 - Andreas Rejbrand
1
这就是浮点数的工作原理。如果你将一个无法精确表示的值存储在double中,你会失去永远无法恢复的信息。将其升级为extended也没有帮助。尝试d := 2.135; e := 2.135; Writeln(extended(d) = e); - Andreas Rejbrand
1
我的后续版本处理浮点数的方式是按照浮点数应被处理的方式:通过假定一些epsilon不确定性。因此,如果小数部分略低于0.5,我认为这是由于数值问题引起的,并仍将其视为 >= 0.5。常量1E-15特别适用于double - Andreas Rejbrand
显示剩余6条评论

14
你要找的是与SimpleRoundTo函数结合使用的SetRoundMode。正如文档所述: SimpleRoundTo返回具有指定十的幂的最接近的值。如果AValue恰好位于具有指定十的幂的两个最接近的值之间(上方和下方),则此函数返回:
  • 如果AValue为正,则朝正无穷大的值。

  • 如果AValue为负且FPU舍入模式未设置为rmUp,则向负无穷方向的值。

请注意,该函数的第二个参数是TRoundToRange,它是指数(10的幂),而不是.NET的Math.Round方法中的小数位数。因此,要将数字四舍五入到小数点后2位,您需要将其作为round-to范围使用-2。
uses Math, RTTI;

var
  LRoundingMode: TRoundingMode;
begin
  for LRoundingMode := Low(TRoundingMode) to High(TRoundingMode) do
  begin
    SetRoundMode(LRoundingMode);
    Writeln(TRttiEnumerationType.GetName(LRoundingMode));
    Writeln(SimpleRoundTo(2.125, -2).ToString);
    Writeln(SimpleRoundTo(-2.125, -2).ToString);
  end;
end;

rmNearest

2,13

-2,13

rmDown

2,13

-2,13

rmUp

2,13

-2,12

rmTruncate

2,13

-2,13


9
请注意,为此设置舍入模式是使用全局更改来解决本地问题。这可能会引起问题(多线程、库等)。 - Andreas Rejbrand
2
@AndreasRejbrand 这是正确的。Delphi 7(甚至更新的版本)受到这些全局状态依赖例程的困扰,正如评论和其他答案所指出的那样,应该小心使用。 - Peter Wolf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接