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."
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
:
$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!
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;
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With