Math.Round() 与 MidpointRounding.AwayFromZero 在 Delphi 中的等价物是什么?

What is the equivalent of Math.Round() with MidpointRounding.AwayFromZero in Delphi?

如何在 Delphi 中使用类似于 Math.RoundMidpointRounding.AwayFromZero 的 c#?

相当于:

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

输出:2.13

在Delphi?

我相信 Delphi RTL 的 SimpleRoundTo 函数本质上就是这样做的,至少如果 FPU 舍入模式是 "correct"。请仔细阅读其文档和实现,然后确定它是否足以满足您的目的。

但要注意设置像这样的单次舍入操作的舍入模式是用全局变化来解决局部问题。这可能会导致问题(multi-threading、库等)。

奖金喋喋不休:如果问题是关于 "regular" 四舍五入(到整数),我想我已经尝试过

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

相反。

当然,即使对于n小数位的一般情况,也可以编写类似的函数。 (但要注意正确处理边缘情况、溢出、floating-point 问题等。)

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

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+ unit test cases,但我认为测试套件还不够。)

您要找的是SimpleRoundTo function in combination with SetRoundMode。正如文档所说:

SimpleRoundTo returns the nearest value that has the specified power of ten. In case AValue is exactly in the middle of the two nearest values that have the specified power of ten (above and below), this function returns:

  • The value toward plus infinity if AValue is positive.

  • The value toward minus infinity if AValue is negative and the FPU rounding mode is not set to rmUp

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

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