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

2

15

How do I use c# similar Math.Round with MidpointRounding.AwayFromZero in Delphi?

What will be the equivalent of:

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

Output: 2.13

In Delphi?

Sweeping answered 24/6, 2019 at 8:5 Comment(1)
I don't think there is an out-of-the-box function that does that. There is up, down, to-wards zero and banker's rounding, but no AwayFromZero, I'm afraid.Riti
B
14

What you're looking for is SimpleRoundTo function in combination with SetRoundMode. As the documentations says:

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

Note that the second parameter to the function is TRoundToRange which refers to exponent (power of 10) rather than number of fractional digis in .NET's Math.Round method. Therefore to round to 2 decimal places you use -2 as round-to range.

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

Boyt answered 24/6, 2019 at 8:48 Comment(2)
But beware that setting the rounding mode for this is using a global change to solve a local problem. This might cause problems (multi-threading, libraries, etc.).Ashworth
@AndreasRejbrand That's correct. Delphi 7 (and even newer versions) is plagued with these global state dependent routines and as pointed out in the comment and other answer, it should be used with care.Boyt
A
15

I believe the Delphi RTL's SimpleRoundTo function does essentially this, at least if the FPU rounding mode is "correct". Please read its documentation and implementation carefully, and then decide if it is good enough for your purposes.

But beware that setting the rounding mode for a single rounding operation like this is using a global change to solve a local problem. This might cause problems (multi-threading, libraries, etc.).

Bonus chatter: Had the question been about "regular" rounding (to an integer), I think I'd tried an approach like

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

instead.

Of course, it is possible to write a similar function even for the general case of n fractional digits. (But be careful to handle edge cases, overflows, floating-point issues, etc., correctly.)

Update: I believe the following does the trick (and is fast):

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;

Of course, if you'd use this function in production code, you'd also add at least 50 unit test cases that test its correctness (to be run daily).

Update: I believe the following version is more stable:

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;

but I haven't fully tested it. (Currently it passes 900+ unit test cases, but I don't consider the test suite quite sufficient yet.)

Ashworth answered 24/6, 2019 at 8:47 Comment(11)
Might be a good idea to inline RoundMidpAway(const X: Real): Integer;.Ashworth
RoundMidpAway(2.135, -2) results 2.13. should be 2.14Sweeping
@zig: Yeah, floating-point numbers are indeed difficult to work with, as hinted. In my defence, I suspected such issues could be present, thus my comment about extensive testing (which I intended to perform tonight). In this case, RoundMidpAway(2.135, -2), yields MagnifiedValue = 213.5 and Abs(Frac(X)) = 0.499999999999972, instead of the exact value 0.5. That's the reason. I'll try to fix this.Ashworth
thanks. may I ask why you use Real type instead of Double or Extended (like SimpleRoundTo does)?Sweeping
@zig: I saw (in CodeInsight and documentation) that both System.Round and System.Trunc use Real for the argument, and so I wanted to do the same. But, of course, currently (and likely in the future, too) Real is the same thing as Double. (You can see that I assume double precision in my choice of the fuzz constant.) You likely need to tweak the constants if you want to support Single and Extended (only present in 32-bit apps); then overload RoundMidpAway.Ashworth
The thing is, if I change the Real to Extended parameters, the fuzz logic is not needed for the 2.135 anomaly. however if I hold that number in a Double variable and pass it to SimpleRoundTo or RoundMidpAway (with Extended parameters) I get the wrong result again. and I'm scratching my head to understand why...Sweeping
But the above does not happen with your final version. both for Double and Extended variables. I didn't test deeply yet.Sweeping
That's how floating-point numbers work. If you store a value in a double, which cannot be represented exactly, you lose information that can never be recovered. It won't help to upgrade it to an extended later. Try d := 2.135; e := 2.135; Writeln(extended(d) = e);.Ashworth
Thanks. Do I need to create overloads for SimpleRoundTo and RoundMidpAway to work with Extended?Sweeping
My later version treats floating-points the way floating-points should be treated: by assuming some epsilon uncertainties. Thus, if the fractional part is a tiny bit below 0.5, I assume that's because of numerical issues, and still regard it as >= 0.5. The 1E-15 constant is especially suitable for double.Ashworth
@zig: Yes. And then I'd try ExtendedResolution = 1E-19 * FuzzFactor; instead of DoubleResolution = 1E-15 * FuzzFactor;. But beware! If you compile for 64-bit, extended is merely an alias for double!Ashworth
B
14

What you're looking for is SimpleRoundTo function in combination with SetRoundMode. As the documentations says:

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

Note that the second parameter to the function is TRoundToRange which refers to exponent (power of 10) rather than number of fractional digis in .NET's Math.Round method. Therefore to round to 2 decimal places you use -2 as round-to range.

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

Boyt answered 24/6, 2019 at 8:48 Comment(2)
But beware that setting the rounding mode for this is using a global change to solve a local problem. This might cause problems (multi-threading, libraries, etc.).Ashworth
@AndreasRejbrand That's correct. Delphi 7 (and even newer versions) is plagued with these global state dependent routines and as pointed out in the comment and other answer, it should be used with care.Boyt

© 2022 - 2024 — McMap. All rights reserved.