"Round()" error final Test

This forum is meant for questions and discussions about the X# language and tools
User avatar
lumberjack
Posts: 723
Joined: Fri Sep 25, 2015 3:11 pm
Location: South Africa

"Round()" error final Test

Post by lumberjack »

robert wrote:Johan,
I am not sure what exactly your idea behind this message is ?
- Are you suggesting that we replace the current Round() function with your VORound() ?
And your code tests the speed, but does it also test the correctness ?
Decimals are limited to 4 positions. Does your code work for Round() with more than 4 positions ?
Hi Robert,
Yes, I was suggesting that VORound be a substitute for Round(). If you look at my Test output you will see I have tested upto 8 decimal places and VORound() produce the "expected" AwayFromZero rounding result in all those cases, even where Math.Round chokes. It's speed is 2x that of Round() and about 4x slower than Math.Round, but I don't think anybody will have code that run as in my benchmark 90,000,000 Round() calls, which took about 10 secs for VORound() vs 5 for Round() and 3 for Math.Round.
______________________
Johan Nel
Boshof, South Africa
Frank Maraite
Posts: 176
Joined: Sat Dec 05, 2015 10:44 am
Location: Germany

"Round()" error final Test

Post by Frank Maraite »

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.
Karl-Heinz
Posts: 774
Joined: Wed May 17, 2017 8:50 am
Location: Germany

"Round()" error final Test

Post by Karl-Heinz »

Here´s my attempt.

The Round() below overrides the XSharp.RT.Round(), whereby most of the code was taken from the origin Round() sources. The "new" Round() overcomes the float round problem and works also with the DECIMAL type - at least, i hope so ;-)

regards
Karl-Heinz

Code: Select all


FUNCTION Round ( n AS USUAL,iDec AS INT) AS USUAL
 	
    LOCAL ret    AS USUAL
    LOCAL IsLong , IsDecimal  AS LOGIC
    LOCAL IsInt64  AS LOGIC
    LOCAL r8     AS REAL8 
    
     
    IF ! IsNumeric ( n ) //  
        THROW Error.ArgumentError( __ENTITY__, NAMEOF(n), "Argument is not numeric")
    ENDIF
    
 
    // For Integers we could round the numbers 
    // Round(12345, -1) -> 12350
    IsInt64 := IsInt64 ( n )   
    IsLong  := IsLong ( n )   
    IsDecimal := IsDecimal ( n )  
    
    r8  := n 
    

    IF iDec > 0
        // Round after decimal point
        IF iDec > MAX_DECIMALS
            iDec := MAX_DECIMALS
        ENDIF 
        
        // NOTE: Math.Round() is still used 
		r8 := Math.Round( (DECIMAL) r8, iDec, MidpointRounding.AwayFromZero ) 
	            
         
    ELSE   
        // Round before decimal point 
      	iDec := -iDec   
	    IF iDec > MAX_DECIMALS
    	    iDec := MAX_DECIMALS
        ENDIF
        
		// NOTE: Math.Round() is still used if iDec <= 0
	    r8 := r8 / ( 10 ^ iDec )
    	r8 := Math.Round( r8, 0, MidpointRounding.AwayFromZero ) 
        r8 := r8 * ( 10 ^ iDec )        
        
        IF ! IsDecimal 
        	
		    IF r8 < 0
    		    isLong	:= r8 >= Int32.MinValue 
        		isInt64 := r8 >= Int64.MinValue 
		    ELSE
    		    isLong  := r8 <= Int32.MaxValue
        		isInt64 := r8 <= Int64.MaxValue 
		    ENDIF
	        
    		iDec := 0 
        
        ENDIF
        
        
    ENDIF
    
     
    IF IsDecimal 
    	ret := (DECIMAL) r8	
    	
    ELSE	
    	
	    IF isLong .OR. IsInt64
    	    iDec := 0
	    ENDIF     	
    	
	    IF iDec == 0 
    	    IF IsLong
        	    ret := (INT) r8
	        ELSEIF IsInt64
    	        ret := (INT64) r8
        	ELSE
            ret := FLOAT{r8, 0}
	        ENDIF
    	ELSE
        	ret := FLOAT{r8, iDec}
	    ENDIF
    
    ENDIF 
    
RETURN ret 

FUNCTION Test_Round()  AS VOID 
 		LOCAL u AS USUAL 
 		LOCAL f AS FLOAT

 		SetDecimal ( 3 ) 
 		  		
        u := 65.4789
        f := 65.4741 
         
       
		? Round ( u , 3 )   //  65,479
		? Round ( f , 3 ) //  65,474
		? Round ( u , -5 ) //  0 
		? Round ( u , 5 ) //  65,47890
		
		?  
		? Round ( u , -1 ) // 70
		? Round ( f , -1)  // 70
        ?
 
		? Round ( 65.475m , 2 ) // 65,480
		? Round ( 65.475 , 2 ) // 65,48 	
		? Round ( 65.475m , 0 ) // 65,000
		? Round ( 65.475 , 0 ) // 65  
		? Round ( 65.875m , 0 ) // 66,000
		? Round ( 65.875 , 0 ) // 66		
		? Round ( 65.475m , -1 ) // 70,000
		? Round ( 65.475 , -1 ) // 70
		? Round ( 65675m , -3 ) // 66000,000			
 		? Round ( 65675 , -3 ) // 66000 
 		? Round ( 65675.4758m , 3) // 65675,476
 		? Round ( 65675.4753m , 3) // 65675,475  
 		
		setDecimal ( 2 ) 				
        ?
		? Round ( 65.475m , 2 ) // 65.48
		? Round ( 65.475m , -1 ) // 70,00 
		? Round ( 65.475m , -2 ) // 100,000  
		? Round ( 65675m , -3 ) // 66000,00
		? Round ( 65675m , -4 ) // 700000,00								
		? Round ( 65675m , -2) // 65700,00
		? Round ( 65675m , -1) // 65680,00				
		? Round ( 65675m , 2) // 65675,00
		? Round ( 65675.475m , 2) // 65675,48 
		? Round ( 65675.4758m , -3) // 66000,00
        ?
		? "done"
RETURN 


User avatar
Chris
Posts: 4606
Joined: Thu Oct 08, 2015 7:48 am
Location: Greece

"Round()" error final Test

Post by Chris »

Thanks Karl-Heinz, I think this is what we will be using in the end, but first we will review the Round() function from the beginning and then make sure that the results are always the same as in VO.

Btw, Math.Round() internally calls Decimal.Round() when you pass it a System.Decimal value, so there's no gain y not calling Decimal.Round() directly!
Chris Pyrgas

XSharp Development Team test
chris(at)xsharp.eu
User avatar
lumberjack
Posts: 723
Joined: Fri Sep 25, 2015 3:11 pm
Location: South Africa

"Round()" error final Test

Post by lumberjack »

Hi Frank,
Frank Maraite wrote:Hi Johan,
Decimal.Round does not work for negative decimals
Well here is my VORound that handles negative decimals, it has on my benchmarks only a 33% speed penalty compared to the current Round() function:

Code: Select all

FUNCTION VORound(r8 AS REAL8, iDec AS INT) AS REAL8
	LOCAL dRound AS Decimal
	VAR dec := Decimal{r8}
	IF iDec < 0
		dRound := Decimal.Multiply(dec, (Decimal)(10^iDec))
		dRound := Decimal.Round(dRound, 0, MidpointRounding.AwayFromZero)
		dRound := Decimal.Divide(dRound, (Decimal)(10^iDec))
	ELSE
		dRound := Decimal.Round(dec, iDec, MidpointRounding.AwayFromZero)
	ENDIF
RETURN (REAL8)dRound
______________________
Johan Nel
Boshof, South Africa
Jamal
Posts: 315
Joined: Mon Jul 03, 2017 7:02 pm

"Round()" error final Test

Post by Jamal »

Hi Johan,

Based on your VoRound(), here another version that handles u argument and return value as USUAL data type to match the current X# Round() function specs. Test results below.

Code: Select all

FUNCTION Round(u AS USUAL, iDec AS INT) AS USUAL
	LOCAL dRound AS Decimal
	LOCAL f := Float(u) AS Float
	IF iDec < 0                
		dRound := Decimal.Multiply(f, (Decimal)(10^iDec))
		dRound := Decimal.Round(dRound, 0, MidpointRounding.AwayFromZero)
		dRound := Decimal.Divide(dRound, (Decimal)(10^iDec))
	ELSE
                // Round after decimal point
                IF iDec > MAX_DECIMALS
                    iDec := MAX_DECIMALS
                ENDIF
		dRound := Decimal.Round(f, iDec, MidpointRounding.AwayFromZero)
	ENDIF
RETURN dRound  
Test results:

Code: Select all

FUNCTION Start() AS VOID STRICT
              
       ? Round(65.4758 , 2 )        // 65.48        
       ? Round(512.925000000, 2)    // 512.93          
       
        ? "Below from VO Round() docs. Results match"
        ? Round(10.4, 0)                                     // 10
        ? Round(10.5, 0)                                     // 11
        ? Round(10.51, 0)                                   // 11
        ? Round(10.49999999999999, 2)           // 10.50

       ? "Pass negative decimal" 
       ? Round(101.99, -1)   // 100.00

       ? Round(109.99, -1)   // 110.00

       ? Round(109.99, -2)   // 100.00
       
        Console.ReadKey()
        
 RETURN
Jamal

P.S. Hopefully this works as expected. Don't bite me if it doesn't :evil:
Post Reply