Почему функция округления Окр() в 1С 7.7 работает странно и выдает разные результаты при расчете НДС?

Программист 1С v7.7 Бухгалтерский учет IT и автоматизация бизнеса
← На главную

Разработчики, работающие со старыми конфигурациями на платформе 1С:Предприятие 7.7, часто сталкиваются с крайне неприятным поведением арифметических операций. Самый яркий и болезненный пример — это расчет налога на добавленную стоимость (НДС) по расчетной ставке 20/120 или 18/118. Вдруг выясняется, что две абсолютно идентичные с точки зрения математики формулы дают разные результаты при округлении копеек. В одном случае функция Окр() возвращает правильную сумму, а во втором — срезает копейку в меньшую сторону.

Давайте подробно разберем эту проблему по шагам, выясним глубинную техническую причину такого поведения процессора и платформы, а также рассмотрим практические способы ее решения, выработанные годами поддержки «семерочных» конфигураций.

Суть проблемы: магия скобок

Проанализируем простую ситуацию. У нас есть сумма с НДС, равная 482.37 руб. Нам необходимо вычислить сумму НДС по ставке 20/120 и округлить результат до двух знаков после запятой. Напишем два варианта вычислений:


// Вариант 1: со скобками
Результат1 = Окр(482.37 * (20 / 120), 2); // Возвращает 80.39

// Вариант 2: без скобок
Результат2 = Окр(482.37 * 20 / 120, 2); // Возвращает 80.40

С точки зрения школьной математики 5-го класса, оба выражения тождественны. Результатом деления 20 на 120 является дробь 1/6 (или 0.166666...). При умножении 482.37 на 1/6 мы получаем математически точное значение 80.395. По правилам математического округления, если первая отбрасываемая цифра равна или больше 5, округление должно происходить в большую сторону. То есть мы ожидаем получить 80.40 в обоих случаях. Тем не менее, 1С 7.7 в первом варианте упорно выдает 80.39.

Самое интересное начинается, если мы попытаемся вывести неокругленные результаты в отладчик или через процедуру Сообщить():


а = 482.37 * (20 / 120);
б = 482.37 * 20 / 120;
Сообщить("а = " + а); // Выводит: а = 80.395
Сообщить("б = " + б); // Выводит: б = 80.395

Оба числа отображаются абсолютно одинаково — 80.395. Но если мы попробуем их сравнить, программа скажет, что они не равны!


Если а = б Тогда
    Сообщить("Равны");
Иначе
    Сообщить("Не равны"); // Сработает эта ветка!
КонецЕсли;

Давайте выясним, почему так происходит.

Техническая причина: IEEE 754 против Decimal

Разница в результатах вычислений и странное поведение функции Окр() кроются в архитектуре хранения вещественных чисел в оперативной памяти компьютера.

В платформе 1С:Предприятие 7.7 все промежуточные вычисления в переменных выполняются с использованием стандартного для процессоров типа данных двойной точности с плавающей запятой (стандарт IEEE 754, известный как тип double). Компьютер оперирует числами в двоичной системе счисления. В ней невозможно абсолютно точно представить некоторые десятичные дроби. Дробь 20/120 (или 1/6) в двоичной системе превращается в бесконечную периодическую дробь.

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

Рассмотрим, как процессор вычисляет оба варианта:

  1. Вычисление со скобками (Вариант 1): Сначала выполняется деление в скобках. Операция 20 / 120 дает периодическую дробь, которая усекается и сохраняется в памяти как 0.16666666666666666 (чуть меньше реальной 1/6). При последующем умножении на 482.37 мы получаем число, реальное значение которого в памяти равно 80.394999999999996... Это значение строго меньше, чем 80.395. Функция Окр() «честно» анализирует этот двоичный хвост, видит, что число не дотягивает до границы округления, и округляет его вниз — до 80.39.
  2. Вычисление без скобок (Вариант 2): Операции выполняются слева направо. Сначала происходит точное умножение 482.37 * 20, что дает ровно 9647.4. Затем это число делится на 120. Из-за особенностей двоичного представления результата деления в памяти компьютера оказывается число 80.39500000000001... (микроскопически больше, чем 80.395). Функция Окр() видит, что значение превысило порог, и округляет его вверх — до 80.40.

Разницу между этими двумя числами в 1С 7.7 невозможно увидеть стандартными средствами вывода, так как функция Сообщить() и табло отладчика принудительно округляют выводимое строковое представление числа для удобства пользователя. Но разницу легко выявить искусственным умножением:


а = 482.37 * (20 / 120);
б = 482.37 * 20 / 120;
Сообщить((а - б) * 1000000000000000); // Выведет отрицательное число (около -0.32)

В платформе 1С:Предприятие 8 эта проблема была полностью решена. Разработчики отказались от использования аппаратного типа double для финансовых расчетов. Тип «Число» в 1С 8 базируется на собственной десятичной арифметике высокой точности (аналог типа Decimal / BCD с фиксированной точкой до 38 знаков). В этой системе вычислений десятичные дроби не имеют двоичных погрешностей представления, поэтому в 1С 8 оба примера работают одинаково и дают точный результат 80.40.

Практические приемы решения проблемы в 1С 7.7

Если вам приходится поддерживать или дорабатывать конфигурации на платформе 1С 7.7, необходимо знать стандарты написания кода, которые гарантируют отсутствие «потерянных» копеек при расчете налогов.

Способ 1. Строгий порядок операций (умножение перед делением)

Самое простое и эффективное правило — никогда не вычислять промежуточные коэффициенты делением, если в формуле присутствует последующее умножение. Деление всегда должно идти последней операцией в цепочке расчетов. Забудьте про скобки вокруг ставок!


// Неправильно:
СуммаНДС = Окр(Всего * (Ставка / (100 + Ставка)), 2);

// Правильно:
СуммаНДС = Окр(Всего * Ставка / (100 + Ставка), 2);

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

Способ 2. Использование микро-погрешности (эпсилон)

Если формула сложная или вы не можете гарантировать порядок выполнения операций, классическим решением является добавление к рассчитываемому числу ничтожно малой величины (эпсилон) перед его округлением. Эта величина должна быть достаточно малой, чтобы не повлиять на реальные копейки, но достаточно большой, чтобы «вытолкнуть» двоичный хвост типа 80.394999999999996 за пределы математического порога 80.395.

Обычно в качестве такой микро-погрешности используют число 0.000000001 (или $10^{-9}$):


ЧислоДляОкругления = 482.37 * (20 / 120);
Результат = Окр(ЧислоДляОкругления + 0.000000001, 2); // Вернет точные 80.40

Этот способ гарантирует, что любые погрешности, возникшие из-за преобразования периодических дробей в формат IEEE 754, будут успешно нивелированы.

Способ 3. Использование типовой функции глОкруглить()

В типовых конфигурациях 1С 7.7 (например, в «Торговля и Склад» или «Бухгалтерский учет») разработчики фирмы «1С» давно внедрили глобальные функции для безопасного округления. Настоятельно рекомендуется использовать именно их вместо прямого вызова встроенной функции Окр().

Пример использования глобальной функции:


// В типовых конфигурациях определена функция глОкруглить()
СуммаНДС = глОкруглить(Сумма * 20 / 120, 0.01);

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

Недокументированная особенность функции Окр() в 1С 7.7

Разбирая особенности работы округления, нельзя не упомянуть интересную недокументированную возможность встроенной функции Окр() в «семерке». Согласно официальной документации, синтаксис функции выглядит следующим образом:

Окр(Число, Разрядность, Способ)

Параметр Способ отвечает за округление граничных значений (когда последняя значащая цифра равна 5):

Однако в платформе 1С 7.7 существует скрытый, недокументированный третий параметр — 2. Если вы передадите двойку в качестве способа округления, функция выполнит не математическое округление, а простое отсечение (усечение) всех «лишних» знаков после указанной разрядности, полностью игнорируя их значения.

Проанализируем пример:


Значение = 13.519;

РезультатОбычный = Окр(Значение, 2, 1); // Вернет 13.52 (математическое округление)
РезультатСреза   = Окр(Значение, 2, 2); // Вернет ровно 13.51 (хвост просто отсекается!)

Использование недокументированного параметра 2 избавляет программиста от необходимости писать громоздкие математические конструкции с использованием функции Цел(), умножением на 100 и последующим делением, когда по бизнес-логике требуется просто отрезать копейки без округления.

Резюме

Мы выяснили, что «странное» поведение функции округления в 1С 7.7 — это не ошибка самой платформы, а прямое следствие ограничений компьютерной архитектуры при работе с числами двойной точности с плавающей запятой. При разработке финансовых блоков в 1С 7.7 всегда придерживайтесь правила: сначала выполняйте умножение, а деление оставляйте на самый конец вычислений. В спорных ситуациях используйте добавление микро-погрешности 0.000000001 или встроенные функции типовых конфигураций.

← На главную