Странные результаты с сопоставлением стоимости валюты/постоянного значения

При компиляции с Delphi 2009 и запуском это консольное приложение пишет "странно". Значения с обеих сторон оператора "меньше" равны, но код ведет себя так, как будто они не равны. Что я могу сделать, чтобы избежать этой проблемы?

program Project5;
{$APPTYPE CONSOLE}
var
 C: Currency;
begin
 C := 1.32;
 if C < 1.32 then
 begin
 WriteLn('strange');
 end;
 ReadLn;
end.

p.s. код отлично работает с другими значениями.

Этот ответ Барри Келли объясняет, что тип валюты "не подвержен ошибкам точности так же, как и код с плавающей запятой".

4 ответа

Так как жесткое литье, такое как Currency (1.32), невозможно, вы можете использовать следующее для явного литья

Function ToCurrency(d:******):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;

другой способ мог бы заставлять использование curreny использовать константу или переменную

const
 comp:Currency=1.32;
var
 C: Currency;
begin
 C := 1.32;
 if C < comp then
 begin
 writeln ('strange');
 end;
end;


Это будет регрессия в Delphi.

Вывод "странный" в Delphi 2010. Но в XE2 нет выхода, поэтому ошибка отсутствует. У меня нет XE для проверки, но благодаря @Sertac для подтверждения того, что XE также выводит "странно". Обратите внимание, что более старые версии Delphi также прекрасны, поэтому это был регресс вокруг D2009.

В 2010 году сгенерированный код:

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');

Литерал 1.32 хранится как 10-байтовое значение с плавающей запятой, которое должно иметь значение 13200. Это точно представляемое двоичное значение с плавающей запятой. Битовая диаграмма для 13200, хранящаяся как 10-байтовый поплавок, составляет:

00 00 00 00 00 00 40 CE 0C 40

Однако битовая диаграмма, хранящаяся в литерале в $00405118, отличается и немного больше, чем 13200. Значение:

01 00 00 00 00 00 40 CE 0C 40

И это объясняет, почему C < 1.32 оценивается как True.

В XE2 сгенерированный код:

Project106.dpr.10: if C < 1.32 then
004060** 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');

Обратите внимание на то, что литерал хранится в полях с 4 байтами. Это видно из того, что мы сравниваем с dword ptr [$00406128]. И если мы посмотрим на содержимое одинарного прецизионного поплавка, хранящегося в $00406128, мы найдем:

00 40 4E 46

И это точно 13200, как показано в виде 4 байтового поплавка.

Моя догадка заключается в том, что компилятор в 2010 году выполняет следующие действия при 1.32:

  • Преобразуйте 1.32 в ближайший точно представляемый по 10 байтов float.
  • Умножьте это значение на 10000.
  • Сохраните полученный 10-байтовый поплавок в $00405118.

Поскольку 1.32 не является точно представимым, получается, что окончательный 10-байтовый поплавок не является точно 13200. И, предположительно, регрессия возникла, когда компилятор переключился с хранения этих литералов в 4 байтовых поплавках на хранение их в полях с байтом по 10 байтов.

Основная проблема заключается в том, что поддержка Delphi для типа данных Currency основана на совершенно ошибочной конструкции. Использование двоичной арифметики с плавающей запятой для реализации десятичного типа данных с фиксированной точкой просто требует неприятностей. Единственный разумный способ исправить дизайн - это полностью перекомпилировать компилятор для использования целочисленной арифметики с фиксированной точкой. Удивительно отметить, что новый 64-битный компилятор использует тот же дизайн, что и 32-битный компилятор.

Чтобы быть честным с вами, я бы остановил компилятор Delphi, делающий любую работу с плавающей точкой с Currency литералами. Это просто полное минное поле. Я бы сделал 10 000 изменений в моей голове следующим образом:

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

И тогда код вызова будет:

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

Нет никакого способа, чтобы компилятор смог это сделать!

Гм!


Чтобы избежать этой проблемы (ошибка в компиляторе), вы можете сделать, как предлагает @bummi, или попробуйте выполнить это время выполнения:

if C < Currency(Variant(1.32)) then

Чтобы избежать обратного перехода в FPU (и ошибки округления), рассмотрите возможность использования этой функции сравнения:

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;

Смотрите эту страницу для получения дополнительной информации, Floating point and Currency fields.


Чтобы добавить к Дэвиду ответ - следующий код не странный, хотя он эквивалентен OP-коду:

program Project2;
{$APPTYPE CONSOLE}
var
 I: Int64;
 E: Extended;
begin
 I:= 13200;
 E:= 13200;
 if I < E then
 begin
 WriteLn('strange');
 end;
 ReadLn;
end.

Теперь компилятор генерирует правильное двоичное значение для Extended (13200), поэтому проблема, похоже, связана с неудачной реализацией типа Currency в компиляторе Delphi.

licensed under cc by-sa 3.0 with attribution.