Подтвердить что ты не робот

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

При компиляции с 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. код отлично работает с другими значениями.

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

4b9b3361

Ответ 1

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

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;

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

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

Ответ 2

Это будет регрессия в 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
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');

Обратите внимание на то, что литерал хранится в полях с 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');

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

Гм!

Ответ 3

Чтобы избежать этой проблемы (ошибка в компиляторе), вы можете сделать, как предлагает @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.

Ответ 4

Чтобы добавить к Дэвиду ответ - следующий код не странный, хотя он эквивалентен 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.