При разработке обработок загрузки или обмена данными программисты часто используют чтение файлов формата DBF с помощью встроенного объекта платформы XBase. На первый взгляд, это простой и надежный механизм. Однако при попытке записать считанные числа в строго типизированную колонку таблицы значений можно столкнуться с крайне неприятным поведением: в отладчике переменная содержит корректное число (например, 1), но после записи в ячейку таблицы значений там оказывается обыкновенный 0.
В этой статье мы подробно разберем, почему возникает этот феномен, как особенности представления чисел с плавающей точкой влияют на платформу 1С:Предприятие, и рассмотрим несколько надежных способов обхода этой проблемы.
Рассмотрим классический пример кода, в котором воспроизводится данное поведение. Мы создаем таблицу значений, жестко типизируем ее колонку под однозначное целое число и пытаемся заполнить строку данными из DBF-файла:
КЧ_1_ = Новый КвалификаторыЧисла(1, 0); // Длина 1, точность 0 (целое число от 0 до 9)
Массив = Новый Массив;
Массив.Добавить(Тип("Число"));
ОписаниеТиповЧ_1_ = Новый ОписаниеТипов(Массив, , , КЧ_1_);
ТЗ_ = Новый ТаблицаЗначений;
ТЗ_.Колонки.Добавить("OBRAZ", ОписаниеТиповЧ_1_);
НС = ТЗ_.Добавить();
НС.OBRAZ = Число(ДБФ1.OBRAZ); // Здесь в ТЗ записывается 0, хотя в DBF лежит 1
При анализе ситуации разработчики обычно проверяют следующие моменты:
ДБФ1.OBRAZ равно 1, тип значения — Число.Число(ДБФ1.OBRAZ) не меняет ситуацию — в таблице значений все равно остается 0.НС.OBRAZ = 1;, то значение успешно записывается.Ситуация кажется парадоксальной: и там, и там число 1, но в одном случае оно записывается в колонку таблицы значений, а в другом — игнорируется платформой и сбрасывается в дефолтное значение ячейки (ноль).
Корни этой проблемы лежат на стыке двух систем: внутреннего представления вещественных чисел в драйвере XBase и строгого контроля типов со стороны платформы 1С:Предприятие.
1. Особенности представления чисел в двоичной системе (IEEE 754)
Объект XBase считывает числовые поля (тип numeric в DBF) как числа двойной точности с плавающей точкой (double precision float). В двоичной системе счисления невозможно абсолютно точно представить большинство десятичных дробей. Из-за этого число, которое физически записано в файле DBF как целая единица 1, после считывания в оперативную память драйвером базы данных превращается в бесконечную дробь вида 0.9999999999999999 или 1.0000000000000001.
2. Иллюзия отладчика 1С
Почему же в табло отладчика мы видим красивую и точную единицу? Дело в том, что отладчик 1С производит автоматическое дружественное округление числовых значений перед выводом их на экран программисту. Он скрывает микроскопическую погрешность, чтобы не перегружать интерфейс. В итоге разработчик уверен, что оперирует чистой единицей, хотя в системной памяти переменная содержит "хвост" из девяток.
3. Поведение функции Число()
Функция Число() предназначена для преобразования типов (например, из строки в число). Если на вход функции передается значение, которое уже имеет тип Число, платформа возвращает его "как есть" без каких-либо математических округлений или модификаций. Таким образом, конструкция Число(ДБФ1.OBRAZ) возвращает все то же неточное значение 0.9999999999999999.
4. Жесткие ограничения Квалификаторов Числа
Когда мы создаем описание типов с квалификаторами КвалификаторыЧисла(1, 0), мы принудительно заставляем ячейку таблицы значений принимать только числа, состоящие максимум из одного разряда и не имеющие дробной части. При попытке присвоить этой ячейке значение 0.9999999999999999 происходит следующее:
0 (целое число) платформа отсекает дробную часть (производит операцию, аналогичную функции Цел()).0.9999999999999999 остается только целая часть, то есть 0. Именно этот ноль мы и наблюдаем в таблице.Если бы считанное число имело погрешность в большую сторону (например, 1.0000000000000001), то его общая длина превысила бы заданный лимит в 1 символ (так как знаков после запятой в типе не предусмотрено, но физическая длина числа с "хвостом" больше разрешенного лимита). В таком случае платформа 1С также не может корректно разместить его в ячейке и сбрасывает значение до дефолтного нуля.
Для того чтобы обойти особенности работы с плавающей точкой и заставить платформу корректно записывать данные в типизированную таблицу значений, мы можем использовать несколько проверенных подходов. Рассмотрим их по шагам.
Самый простой, быстрый и логически правильный способ — принудительно округлить считанное из DBF значение до целого числа с помощью встроенной функции 1С Окр(). Математическое округление превратит дробь 0.9999999999999999 в честную целую единицу 1, которая без проблем запишется в ячейку с жестким ограничением типа.
Изменим строку записи значения в нашем коде следующим образом:
// Вместо НС.OBRAZ = Число(ДБФ1.OBRAZ);
НС.OBRAZ = Окр(ДБФ1.OBRAZ, 0);
Этот способ является наиболее производительным, так как не требует преобразования типов в строку и обратно, а работает напрямую в рамках числового процессора платформы.
Если по каким-то причинам вы хотите гарантированно избавиться от любых признаков дробной части и привести значение к текстовому виду перед записью, можно воспользоваться промежуточным преобразованием в строку. При переводе числа в строковый тип платформа автоматически форматирует его, отбрасывая микроскопическую погрешность плавающей точки.
Рассмотрим два варианта реализации этого способа:
Вариант А. Простое приведение к строке
// Приводим число из DBF к строке "1", а затем функция Число() делает из нее точное число 1
НС.OBRAZ = Число(Строка(ДБФ1.OBRAZ));
Вариант Б. Применение функции Формат
Использование функции Формат() более надежно, так как позволяет жестко зафиксировать отсутствие разделителей групп и точно определить количество знаков, исключая непредвиденное поведение при региональных настройках операционной системы:
// Преобразуем число в строку без разделителей групп и дробной части
СтрокаБезПогрешности = Формат(ДБФ1.OBRAZ, "ЧГ=0");
НС.OBRAZ = Число(СтрокаБезПогрешности);
Если структура вашей таблицы значений создается динамически и допускает изменения, вы можете расширить длину квалификатора числа. Например, вместо длины 1 выделим под число 3 или более знаков. Платформа 1С выделит в памяти больше места под хранение значения ячейки, что позволит ей успешно принять число с плавающей точкой, а при отображении пользователю или выгрузке оно округлится автоматически.
Изменим инициализацию квалификатора числа:
// Задаем длину 3 знака вместо 1, точность оставляем 0
КЧ_1_ = Новый КвалификаторыЧисла(3, 0);
Массив = Новый Массив;
Массив.Добавить(Тип("Число"));
ОписаниеТиповЧ_1_ = Новый ОписаниеТипов(Массив, , , КЧ_1_);
ТЗ_ = Новый ТаблицаЗначений;
ТЗ_.Колонки.Добавить("OBRAZ", ОписаниеТиповЧ_1_);
НС = ТЗ_.Добавить();
// Теперь значение запишется корректно, так как платформе хватит буфера для размещения числа
НС.OBRAZ = ДБФ1.OBRAZ;
Важное примечание: данный способ решает следствие, но не причину проблемы. Рекомендуется использовать его только в тех случаях, когда вы действительно предполагаете, что в DBF-файле могут появиться числа большей разрядности.
Если создаваемая таблица значений используется исключительно локально внутри обработки (например, для вывода информации на форму или промежуточного накопления данных), вы можете полностью отказаться от типизации колонки при ее добавлении. Без указания квалификаторов ячейка таблицы значений будет иметь тип Произвольный и сможет хранить абсолютно любые числа без потери точности.
ТЗ_ = Новый ТаблицаЗначений;
// Добавляем колонку без явного указания ОписанияТипов
ТЗ_.Колонки.Добавить("OBRAZ");
НС = ТЗ_.Добавить();
// Значение запишется без ошибок, сохранив внутреннюю точность
НС.OBRAZ = ДБФ1.OBRAZ;
Однако помните, что у этого способа есть существенный недостаток. Если в дальнейшем вы планируете передавать эту таблицу значений в качестве параметра в запрос (механизм Новый ОписаниеТипов...), отсутствие жесткой типизации колонок вызовет ошибку выполнения запроса. Поэтому для построения надежных алгоритмов обмена всегда лучше использовать Способ 1 (Округление).
Мы проанализировали неочевидное поведение платформы 1С при работе с файлами DBF. Столкнувшись с ситуацией, когда числовые данные сбрасываются в ноль при записи в таблицу значений, придерживайтесь следующих правил:
Число() не округляет переданный ей числовой аргумент, а возвращает его исходное неточное представление.Окр() при переносе числовых данных из объектов XBase во внутренние типизированные структуры 1С:Предприятия.