Как избежать проблем округления при сравнении значений валюты в Delphi?

AFAIK, тип валюты в Delphi Win32 зависит от точности процессора с плавающей запятой. Из-за этого у меня проблемы округления при сравнении двух значений Currency, возвращающих разные результаты в зависимости от машины.

На данный момент я использую функцию SameValue, передающую параметр Epsilon = 0.009, потому что мне нужна только 2 десятичные цифры.

Есть ли лучший способ избежать этой проблемы?

6 ответов

Тип валюты в Delphi - это 64-разрядное целое число, масштабируемое на 1/10 000; другими словами, его наименьший приращение эквивалентен 0,0001. Он не подвержен ошибкам точности так же, как и код с плавающей запятой.

Однако, если вы умножаете свои номера валют на типы с плавающей запятой или деля свои значения валюты, округление должно быть выработано так или иначе. FPU управляет этим механизмом (он называется "контрольным словом" ). Модуль Math содержит некоторые процедуры, которые контролируют этот механизм: SetRoundMode в частности. Вы можете увидеть эффекты в этой программе:

{$APPTYPE CONSOLE}
uses Math;
var
 x: Currency;
 y: Currency;
begin
 SetRoundMode(rmTruncate);
 x := 1;
 x := x / 6;
 SetRoundMode(rmNearest);
 y := 1;
 y := y / 6;
 Writeln(x = y); // false
 Writeln(x - y); // 0.0001; i.e. 0.1666 vs 0.1667
end.

Возможно, что сторонняя библиотека, которую вы используете, устанавливает управляющее слово на другое значение. Вы можете явно указать управляющее слово (то есть режим округления) в начальной точке ваших важных вычислений.

Кроме того, если ваши расчеты когда-либо переходят в обычную плавающую точку, а затем обратно в валюту, все ставки не могут быть слишком сложными для аудита. Убедитесь, что все ваши расчеты указаны в валюте.


Нет, валюта не является типом с плавающей точкой. Это десятичная дробь фиксированной точности, реализованная с целым хранилищем. Это можно сравнить точно и не имеет проблем округления, например, ******. Поэтому, если вы видите неточные значения в ваших валютных переменных, проблема заключается не в самом типе валюты, а в том, что вы вкладываете в нее. Скорее всего, у вас есть расчет с плавающей точкой где-то еще в вашем коде. Поскольку вы не показываете этот код, вам сложно помочь в этом вопросе. Но решение, вообще говоря, будет округлять ваши числа с плавающей точкой до правильной точности перед сохранением в переменной Currency, вместо того, чтобы делать неточное сравнение переменных в валюте.


Более быстрый и безопасный способ сравнения двух значений currency - это, безусловно, сопоставление переменных с их внутренним представлением Int64:

function CompCurrency(var A,B: currency): Int64;
var A64: Int64 absolute A;
 B64: Int64 absolute B;
begin
 result := A64-B64;
end;

Это позволит избежать ошибки округления во время сравнения (работает с целыми значениями * 10000) и будет быстрее, чем стандартная реализация на основе FPU (особенно в 64-разрядном компиляторе XE2).

Дополнительную информацию см. в этой статье.


См. раздел:

D7/DUnit: все тесты CheckEquals (Currency, Currency) неожиданно терпят неудачу...

https://forums.codegear.com/thread.jspa?threadID=16288

Похоже, что изменения на наших рабочих станциях разработки привели к тому, что сравнение валютных курсов завершилось неудачей. Мы не нашли основную причину, но на двух компьютерах под управлением Windows 2000 SP4 и независимо от версии gds32.dll(InterBase 7.5.1 или 2007) и Delphi (7 и 2009) эта строка

TIBDataBase.Create(nil);

изменяет значение управляющего слова до 8087 с $1372 до $1272.

И все валютные сравнения в модульных тестах не будут выполняться со смешными сообщениями типа

Expected: <12.34> - Found: <12.34>

gds32.dll не был изменен, поэтому я предполагаю, что в этой библиотеке есть зависимость от сторонней dll, которая модифицирует управляющее слово.


Если ваша ситуация похожа на мою, вы можете найти этот подход полезным. Я работаю в основном по зарплате. Если бизнес скажет 3 отдела и хочет равномерно распределить расходы на сотрудника среди этих трех отделов, существует много случаев, когда будут проблемы округления.

То, что я делал, - это цикл через отделы, взимающие каждую треть стоимости и добавив стоимость, взимаемую с промежуточной суммы (валюты). Но когда переменная цикла равна пределу, а не умножается на долю, я вычитаю промежуточную переменную из общей стоимости и помещаю ее в последний отдел. Поскольку записи журнала, которые являются результатом этого процесса, всегда должны быть сбалансированы, я считаю, что он всегда работал.


Чтобы избежать возможных проблем с округлением валюты в Delphi, используйте 4 десятичных знака.

Это гарантирует, что у вас никогда не будет проблем с округлением при выполнении вычислений с очень небольшими суммами.

"Been there. Done That. Written the unit tests."

licensed under cc by-sa 3.0 with attribution.