Как устроены числа во встроенном языке 1С:Предприятия 8.3 и почему 0,1 + 0,2 там равно 0,3

· 9 мин

Содержание

Тип Число в 1С: Предприятие 8.3 — это десятичное число с фиксированной запятой, до 38 значащих разрядов. У него нет собственных свойств и методов, как у объектов: это примитивный тип, а работают с ним через операторы и глобальные функции Окр(), Цел(), Формат(), Число(), ЧислоПрописью() и математические функции.

Типичная ситуация: разработчик пишет обработку пересчёта цен, сравнивает две суммы через =, и условие не срабатывает на копейке. Или выгружает число в файл для банка и получает 1 234,50 с неразрывным пробелом вместо 1234.50. Почти все такие сюрпризы объясняются тремя вещами: как число хранится, как округляется и как превращается в строку.

Как платформа хранит число: разрядность, точность, знак

Во встроенном языке 1С число хранится в десятичном виде, а не в двоичном с плавающей точкой, как double в C# или JavaScript. Поэтому сложение и вычитание дробей не дают «хвостов»:

Результат = 0.1 + 0.2;
Сообщить(Результат = 0.3); // Да

В JavaScript то же выражение даёт 0.30000000000000004. В 1С — ровно 0,3. Это главное отличие, и оно хорошо подходит для бухгалтерских расчётов.

Квалификаторы числа в метаданных

У реквизита, ресурса или измерения с типом Число в конфигураторе задают квалификаторы:

КвалификаторЧто задаётПример
ДлинаОбщее число разрядов (целая + дробная часть)15
ТочностьСколько разрядов после запятой2
Допустимый знакЛюбой или НеотрицательныйНеотрицательный

Число(15,2) вмещает значения до 9 999 999 999 999,99. Для сумм в рублях этого хватает почти всегда. Для количества с дробями обычно берут Число(15,3), для курсов валют — Число(10,4).

💡 В СУБД такой реквизит хранится как numeric(15,2) (MS SQL Server и PostgreSQL). Поэтому длину не стоит раздувать «на всякий случай» до 38: растёт размер таблиц и индексов.

Описание типа в коде

Когда тип колонки или реквизита формы задают программно, квалификаторы передают через КвалификаторыЧисла:

КЧ = Новый КвалификаторыЧисла(15, 2, ДопустимыйЗнак.Неотрицательный);
ТипСумма = Новый ОписаниеТипов("Число", КЧ);

ТЗ = Новый ТаблицаЗначений;
ТЗ.Колонки.Добавить("Сумма", ТипСумма);

Типизированная колонка нужна, если таблицу значений потом передают в запрос как параметр и помещают во временную таблицу. С нетипизированной колонкой такой запрос завершится ошибкой.

Какие функции работают с числом во встроенном языке

Раз у Число нет методов вида Число.Округлить(), всё делается глобальными функциями. Вот рабочий набор:

ФункцияЧто делаетПример → результат
Окр(Ч, Разрядность, Режим)ОкруглениеОкр(12.345, 2) → 12,35
Цел(Ч)Отбрасывает дробную частьЦел(-2.7) → -2
Макс(...), Мин(...)Максимум и минимум из спискаМакс(3, 7, 5) → 7
Pow(Основание, Степень)Возведение в степеньPow(2, 10) → 1024
Sqrt(Ч)Квадратный кореньSqrt(16) → 4
Log(Ч), Log10(Ч), Exp(Ч)Логарифмы и экспонентаLog10(1000) → 3
Sin, Cos, Tan, ASin, ACos, ATanТригонометрия, аргумент в радианахCos(0) → 1
Формат(Ч, Строка)Число в строку по шаблонусм. ниже
ЧислоПрописью(Ч, ...)Сумма прописью«Сто рублей 00 копеек»
Число(Значение)Приведение к числуЧисло("15") → 15

Модуля в виде отдельной функции нет. Его пишут так: ?(Ч < 0, -Ч, Ч).

Режимы округления Окр

Третий параметр Окр() — значение системного перечисления РежимОкругления:

  • 🔼 РежимОкругления.Окр15как20 — пятёрка округляется вверх по модулю: 1,5 → 2, 2,5 → 3. Это режим по умолчанию.
  • 🔽 РежимОкругления.Окр15как10 — пятёрка округляется вниз по модулю: 1,5 → 1.

Отрицательная разрядность округляет до десятков, сотен и так далее:

Окр(1234.56, -2); // 1200
Окр(1250, -2);    // 1300 (режим по умолчанию)

Банковского округления «до чётного» среди штатных режимов нет. Если оно нужно, его пишут вручную.

Арифметика: операторы, приоритет, деление и остаток

Для чисел доступны операторы:

  • ➕ + сложение, ➖ - вычитание и унарный минус;
  • ✖️ * умножение, ➗ / деление;
  • 🔢 % остаток от деления: 7 % 3 → 1.

Порядок вычислений: сначала унарный минус, затем *, /, %, затем + и -. Операторы одного приоритета выполняются слева направо. Скобки снимают любые сомнения, и в расчётах сумм их лучше ставить явно:

Итого = Цена * Количество - Скидка;          // скидка вычитается из суммы
Итого = Цена * (Количество - Скидка);        // совсем другое число

Деление на ноль

Деление на ноль во встроенном языке — исключение Деление на 0, выполнение модуля прерывается. Проверку ставят до деления:

Если Количество = 0 Тогда
    Цена = 0;
Иначе
    Цена = Окр(Сумма / Количество, 2);
КонецЕсли;

В запросе деление на ноль приводит к ошибке СУБД. Там знаменатель проверяют через ВЫБОР КОГДА ... ТОГДА ... ИНАЧЕ ... КОНЕЦ.

Целочисленное деление

Отдельного оператора целочисленного деления нет. Его собирают из Цел():

Коробок  = Цел(Штук / 12);
Остаток  = Штук % 12;

Числа и даты

Разность двух дат — число секунд. Прибавление числа к дате сдвигает её на столько же секунд:

Дней = (ДатаКонца - ДатаНачала) / 86400;
Завтра = ТекущаяДата() + 86400;

Если даты содержат время, результат деления на 86 400 будет дробным. Для целого количества дней используют НачалоДня() у обеих дат.

Сравнение чисел и где всё-таки теряется точность

Сложение, вычитание и умножение конечных десятичных дробей в 1С точные. Потеря точности появляется при делении: 1/3 — бесконечная дробь, и платформа хранит её с ограниченным числом разрядов.

А = 1 / 3;
Б = А * 3;
// Б может оказаться 0,999... и не пройти проверку Б = 1

Правило простое: результат деления перед сравнением и записью округляйте до нужной точности.

Если Окр(Сумма1, 2) = Окр(Сумма2, 2) Тогда
    // суммы совпадают до копейки
КонецЕсли;

Распределение суммы без потери копейки

Классическая задача: распределить 100 рублей на 3 строки. Окр(100/3, 2) даёт 33,33, а в сумме — 99,99. Копейку добавляют в последнюю строку:

Всего = 100;
Строк = 3;
Распределено = 0;
Для Сч = 1 По Строк Цикл
    Если Сч = Строк Тогда
        Доля = Всего - Распределено;   // 33,34
    Иначе
        Доля = Окр(Всего / Строк, 2);   // 33,33
    КонецЕсли;
    Распределено = Распределено + Доля;
КонецЦикла;

Проверка: 33,33 + 33,33 + 33,34 = 100,00. В типовых конфигурациях для распределения пропорционально коэффициентам есть готовые служебные функции в общих модулях БСП; перед своей реализацией их стоит поискать.

Сравнение с другими типами

Число сравнивается только с числом. Выражение 5 = "5" вернёт Ложь, а 5 < "6" вызовет ошибку сравнения. Тип проверяют так:

Если ТипЗнч(Значение) = Тип("Число") Тогда
    // работаем как с числом
КонецЕсли;

Форматирование чисел: функция Формат и вывод на экран

Формат() превращает число в строку по форматной строке из параметров через точку с запятой. Основные параметры:

ПараметрЗначениеПример
ЧЦВсего разрядовЧЦ=10
ЧДЦРазрядов после запятойЧДЦ=2 → 1 234,50
ЧРДРазделитель дробной частиЧРД=. → 1 234.50
ЧРГРазделитель группЧРГ=' '
ЧГГруппировка; ЧГ=0 — без группЧГ=0 → 1234,50
ЧНЧто выводить при нулеЧН=0 или ЧН='-'
ЧВНВыводить лидирующие нулиЧЦ=6; ЧВН → 000042
ЧОКак показывать минусЧО=0 → (12,00)
ЧССдвиг на N разрядовЧС=3 → тысячи
ЧФШаблон с текстомЧФ='Ч руб.'

Три ловушки, на которых спотыкаются чаще всего:

  1. Ноль превращается в пустую строку. Формат(0, "ЧДЦ=2") вернёт "". Если в печатной форме нужна цифра, добавьте ЧН=0 или ЧН='0,00'.
  2. Неразрывный пробел в разделителе групп. По умолчанию тысячи отделяются неразрывным пробелом. Внешняя система, которая ждёт число, такую строку не разберёт.
  3. Склейка числа со строкой. "Документ № " + 1234 даст Документ № 1 234. Для кодов, номеров и годов используйте Формат(Ч, "ЧГ=0").

Число для файлов и обменов

Для выгрузки в XML, JSON и CSV для другой системы подходит XMLСтрока(). Она всегда даёт точку как разделитель и не ставит групп:

XMLСтрока(1234.5);   // "1234.5"
Строка(1234.5);      // "1 234,5" — для пользователя, не для обмена

Если формат требует ровно два знака после точки, используйте Формат(Ч, "ЧДЦ=2; ЧРД=.; ЧГ=0; ЧН=0.00").

Сумма прописью

Параметры = "рубль, рубля, рублей, м, копейка, копейки, копеек, ж, 2";
Текст = ЧислоПрописью(1250.40, "Л=ru_RU; ДП=Истина", Параметры);

Последняя цифра в параметрах предмета — количество разрядов дробной части. Род единицы задаётся буквой после форм: м, ж или с.

Преобразование к числу: строки, булево, NULL и Неопределено

Строка в число

Число("125") вернёт 125. Если строку разобрать нельзя, будет исключение Преобразование значения к типу Число не может быть выполнено. Мешают обычно пробелы-разделители, символ валюты, текст в ячейке Excel.

Два способа обойтись без падения:

// 1. Очистить и обработать исключение
Стр = СтрЗаменить(СокрЛП(Стр), Символы.НПП, "");
Стр = СтрЗаменить(Стр, " ", "");
Попытка
    Сумма = Число(Стр);
Исключение
    Сумма = 0;
КонецПопытки;

// 2. Привести через описание типов — без исключения
ОписаниеЧисла = Новый ОписаниеТипов("Число");
Сумма = ОписаниеЧисла.ПривестиЗначение(Стр); // 0, если строка не число

💡 Второй способ удобен при загрузке больших файлов: исключения в цикле на тысячах строк заметно замедляют обработку. Но он молча возвращает 0, поэтому строки с ошибкой лучше отдельно показать пользователю.

Булево и другие типы

Число(Истина) вернёт 1, Число(Ложь) — 0. Ссылку, дату или Неопределено функция Число() не преобразует: будет ошибка.

NULL и Неопределено в расчётах

Во встроенном языке NULL + 1 и Неопределено + 1 — ошибка выполнения. Чаще всего NULL приходит из запроса с левым соединением, где во второй таблице нет записи.

В запросе NULL заменяют на месте:

ВЫБРАТЬ
    Товары.Ссылка КАК Номенклатура,
    ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК Остаток
ИЗ
    Справочник.Номенклатура КАК Товары
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
    ПО Товары.Ссылка = Остатки.Номенклатура

В запросе арифметика с NULL не падает, а даёт NULL. Поэтому Цена * Количество, где одно из полей NULL, вернёт пустое значение, и строка выпадет из суммы. Для явного типа результата есть ВЫРАЗИТЬ(Поле КАК ЧИСЛО(15, 2)).

В коде проверяют до расчёта: Значение = ?(ТипЗнч(Значение) = Тип("Число"), Значение, 0).

Производительность числовых вычислений

Сама арифметика в 1С быстрая: сложение в цикле на сотнях тысяч итераций не станет узким местом. Время теряется в другом.

  • 🐢 Запрос в цикле ради одного числа. Остаток или цену по каждой строке получают одним запросом с соединением, а не сотней мелких.
  • 📊 Суммирование вручную. Для таблицы значений есть ТЗ.Итог("Сумма") и ТЗ.Свернуть("Номенклатура", "Количество, Сумма"). Для данных из базы — агрегат СУММА в запросе.
  • 🔁 Форматирование внутри цикла расчёта. Формат() и Строка() вызывайте один раз при выводе, а не на каждом шаге.
  • ⚠️ Исключения как способ проверки. Попытка ... Исключение вокруг Число() на большом файле медленнее, чем ПривестиЗначение().
  • 🧮 Лишнее округление. Округлять каждый промежуточный шаг не нужно и даже вредно: копейки накапливаются. Округляйте итог строки и итог документа.
Проверьте себя
Что вернёт выражение Формат(0, "ЧДЦ=2")?
Чему равно Цел(-2.7)?
Какой режим Окр используется, если третий параметр не указан?

Частые вопросы

Есть ли в 1С тип для целых чисел?
Отдельного нет. Целое число — это Число с точностью 0, например Число(10,0). В коде целым считается любое число без дробной части.
Какое максимальное число можно хранить в реквизите?
Длина реквизита ограничена 38 разрядами. Для денежных сумм обычно хватает Число(15,2), для курсов — Число(10,4).
Что будет, если записать в реквизит Число(15,2) значение с тремя знаками после запятой?
Значение приводится к квалификатору: дробная часть округляется до двух знаков. Чтобы результат не зависел от неявного приведения, округляйте сами через Окр().
Почему в отчёте СКД вместо нуля пусто?
Это тот же эффект, что и у Формат(): нулевые значения по умолчанию не выводятся. В оформлении поля задайте формат с параметром ЧН.
Как получить случайное число?
Через объект ГенераторСлучайныхЧисел: Генератор = Новый ГенераторСлучайныхЧисел; Ч = Генератор.СлучайноеЧисло(1, 100).