Округление вещественных чисел

Вещественные числа, в отличие от целых чисел, хранят лишь приблизительное значение, и за рубежом используются в основном для хранения научных данных. Для хранения денежных величин обычно используются целочисленные типы данных. Однако integer как правило не хватает для хранения наших денег (особенно остро стоит эта проблема в турции, где зарплату получают миллионами турецких лир). Поэтому для денег приходится использовать вещественные числа (начиная с InterBase 6.0 и в последующих версиях InterBase/Firebird/Yaffil есть поддержка int64 или bigint в третьем диалекте).

InterBase имеет 2 типа данных для хранения вещественных чисел: float и double precision. Эти типы эквивалентны дельфийским single и double, соответственно. Single имеет весьма низкую точность (длина всего 4 байта), в то время как double precision – вполне достаточную (8 байт).

Для "виртуальных" типов данных NUMERIC(precision, scale) и DECIMAL (precision, scale) при объявлении поля анализируется его точность, и IB пытается "втиснуть" его в один из физических типов – в диалекте 1: smallint, integer, float, double precision – в соответствии с указанными precision и scale, в диалекте 3: smallint, integer, in64. Таблица соответствия типов для конкретных precision и scale указана в Data Definition Guide в главе 4 Specifying Datatypes, разделе Defining numeric datatypes.

В общем правило простое: если вам нужно хранить вещественное число, то объявляйте его как NUMERIC(15, 2) или как DECIMAL(15, 2) (в диалекте 3 precision может быть до 18 знаков вместо 15). В первом диалекте при выполнении вычислений обязательно будет погрешность (из-за того, что число хранится в вещественном типе), а в третьем диалекте погрешностей не будет (число хранится как целое), но могут быть проблемы переполнения (см. подробно о реализации int64).

Для проверки точности вещественных чисел в IB необязательно создавать таблицы и выполнять SQL-операторы. Вы можете поэкспериментировать с числами double в Delphi или в C++Builder. Одновременно, можно использовать любой приведенный ниже код в качестве UDF для округления вещественных чисел как банковского, так и обычного. Одним из самых популярных решений на сегодняшний день является UDF FormatFloat.

Далее приведены примеры потери точности с разными типами вещественных чисел, и способы борьбы с этим. Информация собрана из конференций fido7.ru.delphi.db, fido7.su.dbms, fido7.su.dbms.interbase, interbase@mers.com. Авторство некоторых писем, к сожалению, утеряно.
 

KDV (support@ibase.ru)

Появление погрешностей можно проиллюстрировать на примерах прямо в Delphi:
procedure TForm1.Button1Click(Sender: TObject);
var a, b, c: double;
begin
   a:=1190.35;
   b:=1234.29;
   c:=a - b;
   Label1.Caption:=FloatToStr(c);
end;

Результат – -43,9400000000001. Если вместо double использовать single (эквивалент FLOAT), то результат будет еще хуже: -43,9400634765625.

Другой пример:
procedure TForm1.Button1Click(Sender: TObject);
var a: double;
begin
   a:=1.88;
   Label1.Caption:=FloatToStr(a);
end;

В этом случае результат будет нормальным (1.88), а вот single не справляется с точностью, и получается 1,87999999523163. Выводы делайте сами.
 

Subject: Round(2.5) = 2 !!!

Hi Eugeny!

Пон Сен 14 1998, Eugeny Ivanoff == Gleb V. Ufimtsev:
==============================

Я приведу переписку из эхи по бухгалтерии, уж извини.

===== начало =========
Если я купил 100 тетрадей за 12.10 и в месяце продал 5 штук, то в оборотке появляется лишняя копейка
на начало   .00
куплено   12.10
продано     .61
на конец  11.50
=====
1. Вести учет с десятыми долями копейки.
2. Разделить пpиобpетенную паpтию на 2 комплекта: 10 штук – по 13 коп, 90 штук – по 12 коп. Тогда 10 х 0,13 + 90 х 0,12 = 12,10.
=====
 AB> 1. Вести учет с десятыми долями копейки.

При тех обемах учет ведется с 6 знаками от гривны, иначе налоговая не будет корректной (12 200 шт. по 0.083333 грн без ндс).
не спасает – итоговая цифра в строчке должна быть в копейках

 AB> 2. Разделить пpиобpетенную паpтию на 2 комплекта: 10 штук – по 13 коп, 90 штук – по 12 коп. Тогда 10 х 0,13 + 90 х 0,12 = 12,10.

Обясни это фирме, от которой мы привезли налоговые накладные
======
 SV>> Веди учет тетрадей в тыс. шт. Если мало – в десяти тысячах штук. Проблема с копейками уйдет – проверено.

 VK>    Вiрно. Коли я налаштовував облiк швацького виробництва, менi намагались нав'язати облiк ниток у метрах.;-)
 VK>    З того часу – тiльки в умовних "катушках" по 200 м. (хоч iнодi iдуть кiлометровi бобiни.

Особенно приятно выдать на руки накладную или чек на 0.006 тыс.шт.
Люди покупают партии до 1000 000 шт., и продают поштучно!
====
Ребяты, прошу прощения, что вмешиваюсь, но, я уже писал Игорю мылом, а не легче ли пользоваться (я, кстати, уже давно пользуюсь) некоей багофичей (это, по-моему, из буржуйской банковской системы) системой округления к _ближайшему_ _четному_ – фор екзампл (из примера Игоря):
  на начало     .00
  куплено     12.10
                  отсюда цена 0.121
  продано      0.60     (0.121 * 5 = 0.605 -> 0.60 а не 0.61 (1 – нечетное))
  на конец    11.50     (12.10 - 0.605 = 11.495 -> 11.50 (0 – четное))

И нет никаких проблем с копейками.

IC> Имеется ввиду складской учет и оборотная ведомость, и чтоб считалась автоматом.

Все считается автоматом. Единственное – но. Hадо иметь прямые руки для написания некоей функции – обеспечивающей данное округление :)))
=============   Конец   ========

>>>> Это правило бухгалтерского округления.
>>>> Половинки до ближайшего четного.
>>>> Так как эта фича у дельфей была заявлена давно, то, думаю, будет обнаружена в любой версии.

EI> наpод, помему это баг, я не фича, или у амеpиканцев бухгалтеpия совсем неpуская, я эти гpабли обошел используя пеpеменную
EI> var x: real; i: integer;
EI> x:=2.5; i:=round(x);
EI> в pезультате i = 3, я думаю, что это давно можно было и фак поместить, каждый месяц на них кто-нибудь наступает :)

Bye.  Igor
 

Павел Зотов

Пример округления в 1 и 3 диалекте (sql для Firebird 2.1-2.5)
SQL> create database 'tx3.fdb'; commit;
SQL> show sql dialect;
        Client SQL dialect is set to: 3 and database SQL dialect is: 3
SQL> select m, round(m,2) m_rounded_2, round(round(m,4),2) m_rounded_4_then_2
CON> from (select 318985.3 * .95 m from rdb$database);
                    M           M_ROUNDED_2    M_ROUNDED_4_THEN_2
===================== ===================== =====================
          303036.0350           303036.0400           303036.0400
SQL> select m, round(m,2) m_rounded_2, round(round(m,4),2) m_rounded_4_then_2 from
CON> from (select 318985.30 * .95 m from rdb$database);
                    M           M_ROUNDED_2    M_ROUNDED_4_THEN_2
===================== ===================== =====================
           303036.035            303036.040            303036.040
SQL> set sql dialect 1;
WARNING: Client SQL dialect has been set to 1 when connecting to Database SQL dialect 3 database.
SQL> select m, round(m,2) m_rounded_2, round(round(m,4),2) m_rounded_4_then_2
CON> from (select 318985.3 * .95 m from rdb$database);
                      M             M_ROUNDED_2      M_ROUNDED_4_THEN_2
======================= ======================= =======================
      303036.0350000000       303036.0400000000       303036.0400000000
SQL> select m, round(m,2) m_rounded_2, round(round(m,4),2) m_rounded_4_then_2
CON> from (select 318985.30 * .95 m from rdb$database);
-- общее кол-во цифр в первом и втором числах 8+2 = 10 - превышает максимум (9),
-- при котором в 1-м диалекте еще будет хранение с фиксированной точкой.
-- Результат будет преобразован в double precision со всеми присущими ему граблями:
                      M             M_ROUNDED_2      M_ROUNDED_4_THEN_2
======================= ======================= =======================
      303036.0350000000       303036.0300000000       303036.0400000000
 

Andrew Bagirov (7034.g23@g23.relcom.ru)

Функция ROUND(X) в Delphi работает абсолютно корректно (округление по условию 'ближайшее целое'), когда ближайшее максимальное целое является четным числом. В книге "Использование Delphi 3" ,Тодд Миллер и др., приводится следующее: Условие "ближайшее целое" не работает, если верхнее и нижнее значения оказываются равноудаленными (например, если дробная часть точно равна 0.5). В этих случаях Delphi перекладывает решение на операционную систему. Обычно процессоры Intel решают эту задачу в соответствии с рекомендацией IEEE – округлять в сторону ближайшего целого четного числа. Иногда такой подход называется "банкирским округлением". cтр.63. Ниже приведена функция RND(X) которая использует свойство ROUND(X) округлять в сторону ближайшего целого четного числа и реализует корректное округление по условию 'ближайшее целое' для любых чисел:
function RND(x:extended):longInt;
   var
      Flag:Boolean;
      c:longint;
   begin
      if x<0 then
         begin              // определение знака округляемого числа
            Flag:=True;x:=-x;
         end;
      c:=Trunc(x);
      result:=c-1+Round(x-c+1);      // Используем вышеупомянутое св-во ROUND(X)
      if Flag then Result:=-Result;  // в диапазоне от 1 до 2
   end;

Пожалуй надо скорректировать свою UDF следующим образом:
function OKR_2(Var X:double):Double;
Var s:string;
begin
   s:=FormatFloat('############.###', X);
   if StrToInt(s[Pos(',', s)+3])=5 then
      if X>=0 then X:=X+0.001
      else X:=X-0.001;
   result:=Round(X*100)/100;
end;

Спасибо, Евгений. Без тебя не досчитался бы я копейки. :)

С уважением, Сергей Белов.
 

Документ с www.borland.com/devsupport/

Since Delphi's Round() function uses "bankers rounding" where the value is rounded to the nearest even number, how can I round a floating point number using the more  traditional means, where fractional values less than .5 round down, and fractional values of .5 and greater round up?

Answer:
The following function demonstrates rounding down numbers with  fractional values of less than .5, and rounding up numbers with  fractional values of .5 and greater.

Example:
function RoundUp(X: Extended): Extended;
begin
   Result := Trunc(X) + Trunc (Frac(X) * 2);
end;
 

Paul Bonnette

I have been using   x = CAST ( ((x+0.00001)*100) AS INTEGER) /100; to fight round-off error in double precision decimals.

It forces a number into an integer representing pennies then divides back down to decimal form, removing creaping millipennies.  The 10,000th of a penny (or whatever you would like to use) is added so that if the decimal representation rounds up, not truncate down.

This technique is not thoroughly tested but has worked so far without problem.  Tell me if anyone finds a better technique or tests this one thoroughly.

Подпишитесь на новости Firebird в России

Подписаться