Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange results with currency value / constant value comparison

When compiled with Delphi 2009 and run, this console application writes "strange". The values on both sides of the "less than" operator are equal, but the code behaves as if they are not equal. What can I do to avoid this problem?

program Project5;

{$APPTYPE CONSOLE}

var
  C: Currency;
begin
  C := 1.32;

  if C < 1.32 then
  begin
    WriteLn('strange');
  end;

  ReadLn;
end.

p.s. code works fine with other values.

This answer by Barry Kelly explains that the Currency type "is not susceptible to precision issues in the same way that floating point code is."

like image 519
mjn Avatar asked Jan 16 '13 12:01

mjn


3 Answers

This would appear to be a regression in Delphi.

The output is 'strange' in Delphi 2010. But in XE2 there is no output, and so the bug is not present. I don't have XE at hand to test on, but thanks to @Sertac for confirming that XE also outputs 'strange'. Note that older versions of Delphi are also fine, so this was a regression around D2009 time.

On 2010 the code generated is:

Project106.dpr.10: if C < 1.32 then
004050D6 DB2D18514000     fld tbyte ptr [$00405118]
004050DC DF2D789B4000     fild qword ptr [$00409b78]
004050E2 DED9             fcompp 
004050E4 9B               wait 
004050E5 DFE0             fstsw ax
004050E7 9E               sahf 
004050E8 7319             jnb $00405103
Project106.dpr.12: WriteLn('strange');

The literal 1.32 is stored as a 10 byte floating point value that should have the value 13200. This is an exactly representable binary floating point value. The bit pattern for 13200 stored as 10 byte float is:

00 00 00 00 00 00 40 CE 0C 40

However, the bit pattern stored in the literal at $00405118 is different, and is slightly greater than 13200. The value is:

01 00 00 00 00 00 40 CE 0C 40

And that explains why C < 1.32 evaluates to True.

On XE2 the code generated is:

Project106.dpr.10: if C < 1.32 then
004060E6 DF2DA0AB4000     fild qword ptr [$0040aba0]
004060EC D81D28614000     fcomp dword ptr [$00406128]
004060F2 9B               wait 
004060F3 DFE0             fstsw ax
004060F5 9E               sahf 
004060F6 7319             jnb $00406111
Project106.dpr.12: WriteLn('strange');

Notice here that the literal is held in a 4 byte float. This can be seen by the fact that we compare against dword ptr [$00406128]. And if we look at the contents of the single precision float stored at $00406128 we find:

00 40 4E 46

And that is exactly 13200 as represented as a 4 byte float.

My guess is that the compiler in 2010 does the following when faced with 1.32:

  • Convert 1.32 to the nearest exactly representably 10 byte float.
  • Multiply that value by 10000.
  • Store the resulting 10 byte float away at $00405118.

Because 1.32 is not exactly representable, it turns out that the final 10 byte float is not exactly 13200. And presumably the regression came about when the compiler switch from storing these literals in 4 byte floats to storing them in 10 byte floats.

The fundamental problem is that Delphi's support for the Currency data type is founded on an utterly flawed design. Using binary floating point arithmetic to implement a decimal fixed point data type is simply asking for trouble. The only sane way to fix the design would be to completely re-engineer the compiler to use fixed point integer arithmetic. It's rather disappointing to note that the new 64 bit compiler uses the same design as the 32 bit compiler.

To be quite honest with you, I would stop the Delphi compiler doing any floating point work with Currency literals. It's just a complete minefield. I would do the 10,000 shift in my head like this:

function ShiftedInt64ToCurrency(Value: Int64): Currency;
begin
  PInt64(@Result)^ := Value;
end;

And then the calling code would be:

C := 1.32;
if C < ShiftedInt64ToCurrency(13200) then
  Writeln ('strange');

There's no way for the compiler to screw that up!

Humph!

like image 131
David Heffernan Avatar answered Nov 12 '22 02:11

David Heffernan


As fas as hard casting like Currency(1.32) is not possible, you could use the following for explicit casting

Function ToCurrency(d:Double):Currency;
    begin
       Result := d;
    end;

procedure TForm1.Button1Click(Sender: TObject);

var
  C: Currency;

begin
  C := 1.32;
  if C < ToCurrency(1.32) then
  begin
    Writeln ('strange');
  end;
end;

another way could by forcing the usage of curreny by usage of a const or variable

const
  comp:Currency=1.32;
var
  C: Currency;
begin
  C := 1.32;
  if C < comp then
  begin
    writeln ('strange');
  end;
end;
like image 23
bummi Avatar answered Nov 12 '22 02:11

bummi


To avoid this problem (bug in compiler) you can do as @bummi suggests, or try this run time cast:

if C < Currency(Variant(1.32)) then

To avoid the roundtrip into the FPU (and rounding errors), consider using this comparison function:

function CompCurrency(const A,B: Currency): Int64;
var
  A64: Int64 absolute A; // Currency maps internally as an Int64
  B64: Int64 absolute B;
begin
  result := A64-B64;
end;
...
if CompCurrency(C,1.32) < 0 then
begin
  WriteLn('strange');
end;

See this page for more information, Floating point and Currency fields.

like image 2
LU RD Avatar answered Nov 12 '22 03:11

LU RD