【问题标题】:What is the equivalent of Math.Round() with MidpointRounding.AwayFromZero in Delphi?在 Delphi 中,Math.Round() 和 MidpointRounding.AwayFromZero 的等价物是什么?
【发布时间】:2019-06-24 08:05:53
【问题描述】:

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

相当于:

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

输出:2.13

在德尔福?

【问题讨论】:

  • 我不认为有一个开箱即用的功能可以做到这一点。有向上、向下、接近零和银行家的四舍五入,但恐怕没有 AwayFromZero。

标签: c# delphi delphi-7


【解决方案1】:

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

【讨论】:

  • 内联RoundMidpAway(const X: Real): Integer; 可能是个好主意。
  • RoundMidpAway(2.135, -2) 结果2.13。应该是2.14
  • @zig:是的,浮点数 确实难以处理,正如所暗示的那样。在我的辩护中,我怀疑可能存在此类问题,因此我对广泛测试(我打算今晚进行)发表评论。在这种情况下,RoundMidpAway(2.135, -2) 产生 MagnifiedValue = 213.5Abs(Frac(X)) = 0.499999999999972,而不是精确值 0.5。这就是原因。我会努力解决这个问题。
  • 浮点数就是这样工作的。如果您在 double 中存储一个无法准确表示的值,您将丢失永远无法恢复的信息。稍后将其升级到extended 也无济于事。试试d := 2.135; e := 2.135; Writeln(extended(d) = e);
  • 我的更高版本以处理浮点的方式处理浮点:假设一些 epsilon 不确定性。因此,如果小数部分略低于 0.5,我认为这是因为数值问题,并且仍然认为它 >= 0.5。 1E-15 常量特别适用于double
【解决方案2】:

您正在寻找的是 SimpleRoundTo 函数与 SetRoundMode 的组合。正如文件所说:

SimpleRoundTo 返回具有指定 10 次幂的最接近的值。如果AValue 恰好位于具有指定十次幂的两个最接近值的中间(高于和低于),则此函数返回:

  • 如果AValue 是正数,则向正无穷大的值。

  • 如果AValue 为负且 FPU 舍入模式未设置为 rmUp,则值朝负无穷大

请注意,函数的第二个参数是TRoundToRange,它指的是指数(10 的幂),而不是 .NET 的Math.Round 方法中的小数位数。因此,要舍入到小数点后 2 位,请使用 -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;

rm最近的

2,13

-2,13

rmDown

2,13

-2,13

rmUp

2,13

-2,12

rm截断

2,13

-2,13

【讨论】:

  • 但请注意,为此设置舍入模式是使用全局更改来解决本地问题。这可能会导致问题(多线程、库等)。
  • @AndreasRejbrand 没错。 Delphi 7(甚至更新版本)受到这些全局状态相关例程的困扰,正如评论和其他答案中所指出的,应该谨慎使用。
猜你喜欢
  • 2019-03-09
  • 2019-05-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多