"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

"Round()" error final Test

Post by lumberjack »

Hi DevTeam,
I make this a new topic since I played a bit with the Round() function and it seems there is a problem with the conversion of float to to Real8. Here is the sample code showing the result:

Code: Select all

FUNCTION Start() AS VOID
	LOCAL u := 5.0 AS USUAL
	LOCAL y AS USUAL
	SetFloatDelta(10)
	SetDecimal(9)
	FOR LOCAL x := 0 TO 9
		FOR LOCAL i := 1 UPTO 9
			y := x + ( u * 10^(-i))
			? y,    i - 1, ;
			   PadL(Round(y, i - 1), 10), Decimal.Round((Decimal)(REAL8)y, i - 1, ;
			   MidpointRounding.AwayFromZero), Math.Round((REAL8)y, i - 1, MidpointRounding.AwayFromZero)
		NEXT
	NEXT
RETURN
//The results:
0.500000000 0          1 1.000000000 1.000000000
0.050000000 1        0.1 0.100000000 0.100000000
0.005000000 2       0.01 0.010000000 0.010000000
0.000500000 3      0.001 0.001000000 0.001000000
0.000050000 4     0.0001 0.000100000 0.000100000
0.000005000 5    0.00000 0.000010000 0.000000000 // Note
0.000000500 6   0.000001 0.000001000 0.000001000
0.000000050 7  0.0000001 0.000000100 0.000000100
0.000000005 8 0.00000001 0.000000010 0.000000010
1.500000000 0          2 2.000000000 2.000000000
1.050000000 1        1.1 1.100000000 1.100000000
1.005000000 2       1.00 1.010000000 1.000000000 // Note
1.000500000 3      1.001 1.001000000 1.001000000
1.000050000 4     1.0001 1.000100000 1.000100000
1.000005000 5    1.00001 1.000010000 1.000010000
1.000000500 6   1.000001 1.000001000 1.000001000
1.000000050 7  1.0000001 1.000000100 1.000000100
1.000000005 8 1.00000001 1.000000010 1.000000010
2.500000000 0          3 3.000000000 3.000000000
2.050000000 1        2.1 2.100000000 2.100000000
2.005000000 2       2.01 2.010000000 2.010000000
2.000500000 3      2.001 2.001000000 2.001000000
2.000050000 4     2.0001 2.000100000 2.000100000
2.000005000 5    2.00000 2.000010000 2.000000000 // Note
2.000000500 6   2.000001 2.000001000 2.000001000
2.000000050 7  2.0000001 2.000000100 2.000000100
2.000000005 8 2.00000001 2.000000010 2.000000010
3.500000000 0          4 4.000000000 4.000000000
3.050000000 1        3.1 3.100000000 3.100000000
3.005000000 2       3.01 3.010000000 3.010000000
3.000500000 3      3.001 3.001000000 3.001000000
3.000050000 4     3.0001 3.000100000 3.000100000
3.000005000 5    3.00001 3.000010000 3.000010000
3.000000500 6   3.000001 3.000001000 3.000001000
3.000000050 7  3.0000001 3.000000100 3.000000100
3.000000005 8 3.00000001 3.000000010 3.000000010
4.500000000 0          5 5.000000000 5.000000000
4.050000000 1        4.1 4.100000000 4.100000000
4.005000000 2       4.01 4.010000000 4.010000000
4.000500000 3      4.000 4.001000000 4.000000000 // Note
4.000050000 4     4.0001 4.000100000 4.000100000
4.000005000 5    4.00001 4.000010000 4.000010000
4.000000500 6   4.000000 4.000001000 4.000000000 // Note
4.000000050 7  4.0000001 4.000000100 4.000000100
4.000000005 8 4.00000001 4.000000010 4.000000010
5.500000000 0          6 6.000000000 6.000000000
5.050000000 1        5.1 5.100000000 5.100000000
5.005000000 2       5.01 5.010000000 5.010000000
5.000500000 3      5.001 5.001000000 5.001000000
5.000050000 4     5.0001 5.000100000 5.000100000
5.000005000 5    5.00001 5.000010000 5.000010000
5.000000500 6   5.000001 5.000001000 5.000001000
5.000000050 7  5.0000001 5.000000100 5.000000100
5.000000005 8 5.00000001 5.000000010 5.000000010
6.500000000 0          7 7.000000000 7.000000000
6.050000000 1        6.1 6.100000000 6.100000000
6.005000000 2       6.01 6.010000000 6.010000000
6.000500000 3      6.001 6.001000000 6.001000000
6.000050000 4     6.0001 6.000100000 6.000100000
6.000005000 5    6.00001 6.000010000 6.000010000
6.000000500 6   6.000001 6.000001000 6.000001000
6.000000050 7  6.0000001 6.000000100 6.000000100
6.000000005 8 6.00000001 6.000000010 6.000000010
7.500000000 0          8 8.000000000 8.000000000
7.050000000 1        7.1 7.100000000 7.100000000
7.005000000 2       7.01 7.010000000 7.010000000
7.000500000 3      7.001 7.001000000 7.001000000
7.000050000 4     7.0001 7.000100000 7.000100000
7.000005000 5    7.00001 7.000010000 7.000010000
7.000000500 6   7.000001 7.000001000 7.000001000
7.000000050 7  7.0000001 7.000000100 7.000000100
7.000000005 8 7.00000001 7.000000010 7.000000010
8.500000000 0          9 9.000000000 9.000000000
8.050000000 1        8.1 8.100000000 8.100000000
8.005000000 2       8.01 8.010000000 8.010000000
8.000500000 3      8.001 8.001000000 8.001000000
8.000050000 4     8.0001 8.000100000 8.000100000
8.000005000 5    8.00001 8.000010000 8.000010000
8.000000500 6   8.000001 8.000001000 8.000001000
8.000000050 7  8.0000001 8.000000100 8.000000100
8.000000005 8 8.00000001 8.000000010 8.000000010
9.500000000 0         10 10.000000000 10.000000000
9.050000000 1        9.1 9.100000000 9.100000000
9.005000000 2       9.01 9.010000000 9.010000000
9.000500000 3      9.001 9.001000000 9.001000000
9.000050000 4     9.0001 9.000100000 9.000100000
9.000005000 5    9.00001 9.000010000 9.000010000
9.000000500 6   9.000001 9.000001000 9.000001000
9.000000050 7  9.0000001 9.000000100 9.000000100
9.000000005 8 9.00000001 9.000000010 9.000000010
Press any key to continue . . .
User avatar
Chris
Posts: 4562
Joined: Thu Oct 08, 2015 7:48 am
Location: Greece

"Round()" error, probably a bug in USUAL/FLOAT to Real8 conversion rather?

Post by Chris »

Hi Johan,

Why are you saying there's a conversion problem? Is it because of the different results you are showing in the output? This happens because you use two different methods (Decimal.Round() and Math.Round()).
Chris Pyrgas

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

"Round()" error, probably a bug in USUAL/FLOAT to Real8 conversion rather?

Post by lumberjack »

Chris wrote:Why are you saying there's a conversion problem? Is it because of the different results you are showing in the output? This happens because you use two different methods (Decimal.Round() and Math.Round()).
Chris, look at the First Round() that is the X# implementation, see how this match the Math.Round() function on the "//Note lines, it seems to ignore the fraction of the number...
User avatar
Chris
Posts: 4562
Joined: Thu Oct 08, 2015 7:48 am
Location: Greece

"Round()" error, probably a bug in USUAL/FLOAT to Real8 conversion rather?

Post by Chris »

Exactly, as mentioned before, the Round() function uses internally the Math.Round() method, so it has the same problems that Math.Round() has, as we are discussing in the other thread.
Chris Pyrgas

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

"Round()" error, probably a bug in USUAL/FLOAT to Real8 conversion rather?

Post by lumberjack »

Hi Chris,
Chris wrote:Exactly, as mentioned before, the Round() function uses internally the Math.Round() method, so it has the same problems that Math.Round() has, as we are discussing in the other thread.
Well, is it therefore not time to consider using Decimal rather, as it seems to give the desired result in all cases? I am sure the "speed" problem will not grind all applications to a halt...
Jamal
Posts: 314
Joined: Mon Jul 03, 2017 7:02 pm

"Round()" error, probably a bug in USUAL/FLOAT to Real8 conversion rather?

Post by Jamal »

Chris,

The documentation says that REAL8 = (System.Double) and I think this is the cause of the Round() function not returning the precise value or expected value.

Partial code snippet of the x# Round() function

Code: Select all

FUNCTION Round(n AS USUAL,iDec AS INT) AS USUAL
    LOCAL ret    AS USUAL
    LOCAL IsLong   AS LOGIC
    LOCAL IsInt64  AS LOGIC
    LOCAL r8     AS REAL8    //  <=========== HERE 

Instead of using REAL8 inside Round() function, using System.Decimal would definitely resolve the issue. Of course, the ELSE condition has to be modified when iDec is =< 0, i.e. (Round before decimal point)

Now compare C# vs X# with 2 decimal places

C#

Code: Select all

           // next line uses the m suffix to donate Decimal number,
            Console.WriteLine(Math.Round(65.475m, 2));    // 65.48  decimal value passed   
            Console.WriteLine(Math.Round(512.925000000m, 2, MidpointRounding.AwayFromZero));  returns 512.93  (which is correct)
            Console.WriteLine(Math.Round(65.475d, 2));     // 65.47  double value passed
            Console.WriteLine(Math.Round(65.475, 2));       // 65.47  double value passed
            Console.Read();
X#

Code: Select all

           // next line uses the m suffix to donate Decimal number, 
           // but Round returns 65.47 even though decimal value passed due to REAL8 conversion 
           // inside the X# Round() function

            Console.WriteLine(Round(65.475m, 2));    // returns 65.47. C# returned 65.48

            // X# uses MidpointRounding.AwayFromZero internally
            Console.WriteLine(Round(512.925000000m, 2));    // returns 512.92 which is incorrect,  C# returned 512.93 as well as VO
            

            Console.WriteLine(Math.Round(65.475d, 2));     // 65.47  double value passed
            Console.WriteLine(Math.Round(65.475, 2));       // 65.47  double value passed
            Console.Read();
The point here is that Round is using REAL8 which is not as precise as System.Decimal. According to many developers, System.Decimal is the recommend type to use with financial calculations.

Jamal

P.S. For my info, may be someone can tell what situation(s) one may one use a negative number in the decimal places parameter in the Round() function. I've never seen it or needed it.
User avatar
lumberjack
Posts: 723
Joined: Fri Sep 25, 2015 3:11 pm

"Round()" error, time to resolve this forever

Post by lumberjack »

Hi Chris/Robert,
Chris wrote:Exactly, as mentioned before, the Round() function uses internally the Math.Round() method, so it has the same problems that Math.Round() has, as we are discussing in the other thread.
Further to what I have said and also what Jamal is saying. It is unfortunately an "internal" Computer binary issue, certain

Code: Select all

5*10^-i
cannot be exactly represented, hence banker's rounding is applied (also called statistical rounding i.e. ToEven) that needs to be resolved when AwayFromZero is specified.

The only way of ensuring "expected" rounding is to make use of Decimal.Round(). See this discussion.

Just my 2c,
User avatar
Chris
Posts: 4562
Joined: Thu Oct 08, 2015 7:48 am
Location: Greece

"Round()" error, time to resolve this forever

Post by Chris »

Guys, yes, using System.Decimal instead of FLOAT/REAL would solve the issues. But, remember, VO does not have a decimal type, but still returns the expected results in this area, with REAL and FLOAT data types and of course there is also so much existing code that we need to support.

One option is to indeed use Decimal.Round(), (converting values to Decimal first) but we need to check if the speed penalty is too much and if it indeed works well in all cases. Another option is to emulate exactly what VO is doing. One way or the other this will be probably fixed for the next build.
Chris Pyrgas

XSharp Development Team test
chris(at)xsharp.eu
User avatar
robert
Posts: 4225
Joined: Fri Aug 21, 2015 10:57 am
Location: Netherlands

"Round()" error, time to resolve this forever

Post by robert »

Chris, Johan,

One of the problem with emulating the VO behavior is that the VO runtime was using REAL8 for storage format, but for calculations it was using the numeric coprocessor which uses a 80 bits floating point. In the VO runtime the former devteam used inline assembly to directly execute 80 bits numeric instructions, also as part of the Round() logic. Instructions like FLD QWORD PTR <address>, FRNDINT and FMULP. There is no 80bits float type in .Net. We will NEVER be able to do it 100% the same.
This is also the reason why VO has runtime functions that save/restore floats to/from 80 bit strings

And w.r.t. "conversion problems" between Float and Real8: both in VO and in X# the float type uses a Real8 for storage. So there are no conversion problems between the two. The difference is that the float also "remembers" the # of decimals and its size, and the routines that convert a float to a string will uses this information unless told otherwise.

Robert
XSharp Development Team
The Netherlands
robert@xsharp.eu
User avatar
lumberjack
Posts: 723
Joined: Fri Sep 25, 2015 3:11 pm

"Round()" error, time to resolve this forever

Post by lumberjack »

Chris,
Chris wrote:But, remember, VO does not have a decimal type, but still returns the expected results in this area, with REAL and FLOAT data types and of course there is also so much existing code that we need to support.
I am a bit confused, why: "VO does not have a decimal type"? Surely we now in X# with VO language support, and the aim is to not use VO anymore, but X#?

I can't imagine anybody wanting a banker's rounding when RoundAwayFromZero is specified when a number cannot be exactly represented...
Post Reply