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?
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?
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
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.)
RoundMidpAway(const X: Real): Integer;
. –
Ashworth RoundMidpAway(2.135, -2)
results 2.13
. should be 2.14
–
Sweeping 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 Real
type instead of Double
or Extended
(like SimpleRoundTo
does)? –
Sweeping 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 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 Double
and Extended
variables. I didn't test deeply yet. –
Sweeping 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 SimpleRoundTo
and RoundMidpAway
to work with Extended
? –
Sweeping 1E-15
constant is especially suitable for double
. –
Ashworth 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 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
© 2022 - 2024 — McMap. All rights reserved.