Как устроены числа во встроенном языке 1С:Предприятия 8.3 и почему 0,1 + 0,2 там равно 0,3
Содержание
Тип Число в 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 → тысячи |
ЧФ | Шаблон с текстом | ЧФ='Ч руб.' |
Три ловушки, на которых спотыкаются чаще всего:
- Ноль превращается в пустую строку.
Формат(0, "ЧДЦ=2")вернёт"". Если в печатной форме нужна цифра, добавьтеЧН=0илиЧН='0,00'. - Неразрывный пробел в разделителе групп. По умолчанию тысячи отделяются неразрывным пробелом. Внешняя система, которая ждёт число, такую строку не разберёт.
- Склейка числа со строкой.
"Документ № " + 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С быстрая: сложение в цикле на сотнях тысяч итераций не станет узким местом. Время теряется в другом.
- 🐢 Запрос в цикле ради одного числа. Остаток или цену по каждой строке получают одним запросом с соединением, а не сотней мелких.
- 📊 Суммирование вручную. Для таблицы значений есть
ТЗ.Итог("Сумма")иТЗ.Свернуть("Номенклатура", "Количество, Сумма"). Для данных из базы — агрегатСУММАв запросе. - 🔁 Форматирование внутри цикла расчёта.
Формат()иСтрока()вызывайте один раз при выводе, а не на каждом шаге. - ⚠️ Исключения как способ проверки.
Попытка ... ИсключениевокругЧисло()на большом файле медленнее, чемПривестиЗначение(). - 🧮 Лишнее округление. Округлять каждый промежуточный шаг не нужно и даже вредно: копейки накапливаются. Округляйте итог строки и итог документа.