Hi Johan,
Decimal.Round does not work for negative decimals, so we need some tricks. I cannot test the VO version of Round() anymore, because I do not have VO installed.
Talking about rounding could be a never ending story, Rounding is not precise by definition, so to say. We loose precision when we round. So rounding a value should be be the last step whenever possible. From my experience are in math environments other than currency issues like you described very rare. There are mostly digits after the '5'.
Rounding floating point with the .NET builtin functions is limited. System.Decimal allows 0 to 28 as decimals. Math.Round( System.Double ) allows from 0 to 15. To work around these limitations I worked on a family of roundings. Here is what I did for my needs. I use overloads and designed them as extension methods. See the tests right after the code how to use them. I do not use USUALS because USUALS are evil.
Code: Select all
STATIC PUBLIC METHOD VM_Rnd( SELF value AS REAL8, decimals AS INT ) AS REAL8
RETURN VM_Round( value, decimals, System.MidpointRounding.AwayFromZero )
STATIC PUBLIC METHOD VM_Round( SELF value AS REAL8 ) AS REAL8
RETURN VM_Round( value, 0, System.MidpointRounding.ToEven )
STATIC PUBLIC METHOD VM_Round( SELF value AS REAL8, decimals AS INT ) AS REAL8
RETURN VM_Round( value, decimals, System.MidpointRounding.ToEven )
STATIC PUBLIC METHOD VM_Round( SELF value AS REAL8, mode AS System.MidpointRounding ) AS REAL8
RETURN VM_Round( value, 0, mode )
STATIC PUBLIC METHOD VM_Round( SELF value AS REAL8, decimals AS INT, mode AS System.MidpointRounding ) AS REAL8
LOCAL RetValue AS REAL8
IF Math.Abs( decimals ) > 28
RetValue := ExtendedRound( value, decimals, mode )
ELSE
RetValue := (REAL8)VM_Round( (Decimal)value, decimals, mode )
ENDIF
RETURN RetValue
STATIC PUBLIC METHOD ExtendedRound( SELF value AS REAL8, decimals AS INT ) AS REAL8
RETURN ExtendedRound( value, decimals, System.MidpointRounding.AwayFromZero )
STATIC PUBLIC METHOD ExtendedRound( SELF value AS REAL8, decimals AS INT, mode AS System.MidpointRounding ) AS REAL8
LOCAL RetValue AS REAL8
DO CASE
CASE decimals < -323
THROW System.OverflowException{ "Decimals must not be < -323"}
CASE decimals > 308
THROW System.OverflowException{ "Decimals must not be > 308"}
OTHERWISE
LOCAL isSign AS LOGIC
IF value < 0.0
isSign := TRUE
value := -value // Now Value is positive 1.23351
ENDIF
LOCAL PowDeci AS REAL8
PowDeci := 10.0^decimals // 1000
LOCAL Intermediate AS REAL8
Intermediate := Value*PowDeci // Vor dem Komma 1233.51
LOCAL LeftDecimals AS REAL8
IF mode == System.MidpointRounding.AwayFromZero
LeftDecimals := RoundHandleAwayFromZero( Intermediate )
ELSE
LeftDecimals := RoundHandleToEven( Intermediate )
ENDIF
RetValue := LeftDecimals / ( PowDeci ) // Wieder durch 10erpotenz
IF isSign
RetValue := -RetValue
// This is,when you like negative zero.
IF Value == 0.0
Value := -double.Epsilon // Would like to have a -0.0
ENDIF
ENDIF
ENDCASE
RETURN RetValue
STATIC PRIVATE METHOD RoundHandleAwayFromZero( intermediate AS REAL8 ) AS REAL8
// 1233.51
RETURN Math.Truncate( intermediate+0.5 ) // 1234
STATIC PRIVATE METHOD RoundHandleToEven( intermediate AS REAL8 ) AS REAL8
// 1233.51
LOCAL LeftDecimals AS REAL8
LeftDecimals := Math.Truncate( intermediate ) // left of decimal separator 1233
LOCAL RightDecimals AS REAL8
RightDecimals := intermediate - LeftDecimals // right of decimal separator 0.51
IF Math.Abs(RightDecimals - 0.5 ) <= Double.Epsilon
IF Math.Truncate( LeftDecimals / 2.0 ) * 2.0 != LeftDecimals // odd
LeftDecimals := LeftDecimals + 1.0 // then upround
ENDIF
ELSE // 0.01 right of decimal separator
LeftDecimals := Math.Truncate( Intermediate + 0.5 ) // 1234
ENDIF
RETURN LeftDecimals
STATIC PUBLIC METHOD VM_Round( SELF value AS System.Decimal, decimals AS INT, mode AS System.MidpointRounding ) AS Decimal
LOCAL RetValue := 0.0m AS Decimal
DO CASE
CASE decimals < -28
THROW System.OverflowException{ "Decimals must not be < -28"}
CASE decimals > 28
THROW System.OverflowException{ "Decimals must not be > 28"}
OTHERWISE
IF decimals >= 0
RetValue := Decimal.Round( value, decimals, mode )
ELSE
LOCAL isSign := FALSE AS LOGIC
IF value < 0.0m
isSign := TRUE
value := -value // Now Value is positive 1.23351
ENDIF
LOCAL PowDeci AS Decimal
PowDeci := FMMath.DecimalPow( 10.0m, decimals ) // 1000
LOCAL Intermediate AS Decimal
Intermediate := value * PowDeci // Vor dem Komma 1233.51
LOCAL LeftDecimals AS Decimal
IF mode == System.MidpointRounding.AwayFromZero
LeftDecimals := RoundHandleAwayFromZero( Intermediate ) // 1234
ELSE
LeftDecimals := RoundHandleToEven( Intermediate ) // 1234
ENDIF
RetValue := LeftDecimals / ( PowDeci ) // divide by 10erpotenz
IF isSign
RetValue := - RetValue
ENDIF
ENDIF
ENDCASE
RETURN RetValue
STATIC PRIVATE METHOD RoundHandleAwayFromZero( intermediate AS Decimal ) AS Decimal
// 1233.51
RETURN Math.Truncate( intermediate+0.5m ) // 1234
STATIC PRIVATE METHOD RoundHandleToEven( intermediate AS Decimal ) AS Decimal
// 1233.51
LOCAL LeftDecimals AS Decimal
LeftDecimals := Math.Truncate( intermediate ) // left of decimal separator 1233
LOCAL RightDecimals AS Decimal
RightDecimals := intermediate - LeftDecimals // right of decimal separator 0.51
IF ( RightDecimals - 0.5m ) == 0.0m
IF Math.Truncate( LeftDecimals / 2.0m ) * 2.0m != LeftDecimals // odd
LeftDecimals := LeftDecimals + 1.0m // then upround
ENDIF
ELSE // 0.01 right of decimal separator
LeftDecimals := Math.Truncate(Intermediate+0.5m) // 1234
ENDIF
RETURN LeftDecimals
This is a part of my test code. I use NUnit for my unit tests.
Code: Select all
[Test] ;
METHOD VM_Rnd_AwayFromZero( ) AS VOID // allgemein runden
LOCAL TestValue AS REAL8
TestValue := 1.234567
Expect( TestValue:VM_Rnd( 1 ), Is.EqualTo( 1.2 ) ) // Usuage as extension Method
Expect( VM_Rnd(TestValue, 1 ), Is.EqualTo( 1.2 ) ) // usuage as function
TestValue := (1.234+1.235)*0.5
Expect( TestValue:VM_Rnd( 3 ), Is.EqualTo( 1.235 ) )
TestValue := (1.233+1.234)*0.5
Expect( TestValue:VM_Rnd( 3 ), Is.EqualTo( 1.234 ) )
TestValue := (1.232+1.233)*0.5
Expect( TestValue:VM_Rnd( 3 ), Is.EqualTo( 1.233 ) )
TestValue := 512123
Expect( TestValue:VM_Rnd( -1 ), Is.EqualTo( 512120 ) )
Expect( TestValue:VM_Rnd( -2 ), Is.EqualTo( 512100 ) )
Expect( TestValue:VM_Rnd( -3 ), Is.EqualTo( 512000 ) )
TestValue := 513500
Expect( TestValue:VM_Rnd( -1 ), Is.EqualTo( 513500 ) )
Expect( TestValue:VM_Rnd( -2 ), Is.EqualTo( 513500 ) )
Expect( TestValue:VM_Rnd( -3 ), Is.EqualTo( 514000 ) )
TestValue := 514500
Expect( TestValue:VM_Rnd( -3 ), Is.EqualTo( 515000 ) )
Expect( (0.5*10^32):VM_Rnd( -32 ), Is.EqualTo( 9.9999999999999987e31 ) )
Expect( (0.5*10^22):VM_Rnd( -22 ), Is.EqualTo( 1.0e22 ) ) // Excact because of Deimal.Round internally
Expect( 50.00000000:VM_Rnd( -2 ), Is.EqualTo( 100.00000000 ) )
Expect( 5.000000000:VM_Rnd( -1 ), Is.EqualTo( 10.000000000 ) )
Expect( 0.500000000:VM_Rnd( 0 ), Is.EqualTo( 1.000000000 ) )
Expect( 0.050000000:VM_Rnd( 1 ), Is.EqualTo( 0.100000000 ) )
Expect( 0.005000000:VM_Rnd( 2 ), Is.EqualTo( 0.010000000 ) )
Expect( 0.000500000:VM_Rnd( 3 ), Is.EqualTo( 0.001000000 ) )
Expect( 0.000050000:VM_Rnd( 4 ), Is.EqualTo( 0.000100000 ) )
Expect( 0.000005000:VM_Rnd( 5 ), Is.EqualTo( 0.000010000 ) )
Expect( 0.000000500:VM_Rnd( 6 ), Is.EqualTo( 0.000001000 ) )
Expect( 0.000000050:VM_Rnd( 7 ), Is.EqualTo( 0.000000100 ) )
Expect( 0.000000005:VM_Rnd( 8 ), Is.EqualTo( 0.000000010 ) )
Expect( 1.500000000:VM_Rnd( 0 ), Is.EqualTo( 2.000000000 ) )
Expect( 1.050000000:VM_Rnd( 1 ), Is.EqualTo( 1.100000000 ) )
Expect( 1.005000000:VM_Rnd( 2 ), Is.EqualTo( 1.010000000 ) )
Expect( 1.000500000:VM_Rnd( 3 ), Is.EqualTo( 1.001000000 ) )
Expect( 1.000050000:VM_Rnd( 4 ), Is.EqualTo( 1.000100000 ) )
Expect( 1.000005000:VM_Rnd( 5 ), Is.EqualTo( 1.000010000 ) )
Expect( 1.000000500:VM_Rnd( 6 ), Is.EqualTo( 1.000001000 ) )
Expect( 1.000000050:VM_Rnd( 7 ), Is.EqualTo( 1.000000100 ) )
Expect( 1.000000005:VM_Rnd( 8 ), Is.EqualTo( 1.000000010 ) )
Expect( 2.500000000:VM_Rnd( 0 ), Is.EqualTo( 3.000000000 ) )
Expect( 2.050000000:VM_Rnd( 1 ), Is.EqualTo( 2.100000000 ) )
Expect( 2.005000000:VM_Rnd( 2 ), Is.EqualTo( 2.010000000 ) )
Expect( 2.000500000:VM_Rnd( 3 ), Is.EqualTo( 2.001000000 ) )
Expect( 2.000050000:VM_Rnd( 4 ), Is.EqualTo( 2.000100000 ) )
Expect( 2.000005000:VM_Rnd( 5 ), Is.EqualTo( 2.000010000 ) )
Expect( 2.000000500:VM_Rnd( 6 ), Is.EqualTo( 2.000001000 ) )
Expect( 2.000000050:VM_Rnd( 7 ), Is.EqualTo( 2.000000100 ) )
Expect( 2.000000000000000005:VM_Rnd( 17 ), Is.EqualTo( 2.000000000000000010 ) )
Expect( 0.00000000000000000000000005:VM_Rnd( 25 ), Is.EqualTo( 0.00000000000000000000000010m ) )
Expect( 0.00000000000000000000000005:VM_Round( 25, MidpointRounding.AwayFromZero ), Is.EqualTo( 0.00000000000000000000000010m ) )
Expect( 0.0000000000000000000000000000005:VM_Rnd( 30 ), Is.EqualTo( 9.9999999999999991e-31 ) )
Expect( 0.000000000000000000000000000000005:VM_Rnd( 32 ), Is.EqualTo( 9.9999999999999991e-33 ) )
Hope this helps a little bit.