В практике разработки на платформе «1С:Предприятие» регулярно возникают задачи, связанные с расчетом периодических событий на будущие периоды. Это могут быть графики платежей, регламентные сервисные работы, расписания дежурств или, как в нашем игровом, но очень показательном примере, график полива декоративных растений. У нас есть два объекта: Дерево 1 (требует 3 литра полива каждые 5 дней) и Дерево 2 (требует 2 литра полива каждые 3 дня). Нам необходимо вывести график полива на определенный будущий период, например, с 1 по 10 августа.
В данной статье мы подробно рассмотрим, как правильно спроектировать архитектуру хранения таких данных, разберем алгоритмы расчета на встроенном языке и внутри запросов, а также научимся обходить технические ограничения платформы 1С, такие как отсутствие оператора остатка от деления в языке запросов и проблему банковского округления.
Прежде чем приступать к написанию кода, мы должны определиться с бизнес-логикой системы. Существует два принципиально разных подхода к расчету периодических графиков:
ПараметрыПолива (Периодичность, Дата начала, Объем), а сам график рассчитывать динамически «на лету» при выводе отчета или печатной формы.ПоливРастений (фиксирующий факт выполнения) и регистр сведений ИсторияПолива. Плановый график в этом случае будет рассчитываться по формуле: ДатаПоследнегоФактическогоПолива + ПериодичностьДней.Проанализируем обе схемы реализации подробнее.
Если нам требуется просто вывести печатную форму или показать пользователю график на небольшой период без сохранения в базу данных, самым простым и надежным способом будет расчет на встроенном языке программирования 1С с использованием цикла по дням.
Рассмотрим пример функции, которая принимает параметры и возвращает заполненную таблицу значений с датами и объемами полива:
Функция СформироватьТаблицуПолива(Знач ДатаНачалаПериода, Знач ДатаКонцаПериода, ПараметрыДеревьев) Экспорт
ТаблицаГрафика = Новый ТаблицаЗначений;
ТаблицаГрафика.Колонки.Добавить("Дата", Новый ОписаниеТипов("Дата"));
ТаблицаГрафика.Колонки.Добавить("Дерево", Новый ОписаниеТипов("СправочникСсылка.Деревья"));
ТаблицаГрафика.Колонки.Добавить("ОбъемПолива", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(10, 2)));
// Перебираем каждый день из указанного периода
ТекущаяДата = НачалоДня(ДатаНачалаПериода);
КонечнаяДата = НачалоДня(ДатаКонцаПериода);
Пока ТекущаяДата <= КонечнаяДата Цикл
Для Каждого Настройка Из ПараметрыДеревьев Цикл
// Вычисляем разницу в днях между текущим днем и точкой отсчета (датой старта)
РазностьВДнях = (ТекущаяДата - НачалоДня(Настройка.ДатаСтарта)) / 86400;
Если РазностьВДнях >= 0 Тогда
// Проверяем кратность периода с помощью оператора остатка от деления %
Если РазностьВДнях % Настройка.ПериодичностьДней = 0 Тогда
НоваяСтрока = ТаблицаГрафика.Добавить();
НоваяСтрока.Дата = ТекущаяДата;
НоваяСтрока.Дерево = Настройка.Дерево;
НоваяСтрока.ОбъемПолива = Настройка.ОбъемЛитров;
КонецЕсли;
КонецЕсли;
КонецЦикла;
ТекущаяДата = ТекущаяДата + 86400; // Переходим к следующему дню
КонецЦикла;
Возврат ТаблицаГрафика;
КонецФункции
Этот алгоритм работает быстро на небольших периодах (до года) и чрезвычайно прост в отладке. Однако, если деревьев тысячи, а период расчета составляет несколько лет, выполнение этой операции на встроенном языке может снизить производительность системы. В таких случаях расчет необходимо переносить на сторону СУБД с помощью запросов.
При попытке перенести этот расчет в запрос мы сталкиваемся с серьезным ограничением: в языке запросов 1С отсутствует оператор получения остатка от деления (аналог % или MOD). Нам придется воссоздать этот математический инструмент вручную.
Выясним причину, почему простая конструкция ВЫРАЗИТЬ(Число1 / Число2 КАК ЧИСЛО(15, 0)) не подходит для вычисления целой части от деления. В СУБД и платформе 1С применяется так называемое банковское округление. При его использовании число, оканчивающееся на 0.5, округляется до ближайшего четного числа. Например, 1.5 превратится в 2, а 2.5 тоже превратится в 2. Это приведет к фатальным ошибкам при расчете дат.
Чтобы получить математически точную целую часть от деления в запросе 1С, мы используем следующую формулу:
ЦелаяЧасть = ВЫРАЗИТЬ(Делимое / Делитель КАК ЧИСЛО(15, 0)) - ВЫБОР КОГДА Делимое / Делитель - ВЫРАЗИТЬ(Делимое / Делитель КАК ЧИСЛО(15, 0)) < 0 ТОГДА 1 ИНАЧЕ 0 КОНЕЦ
Зная целую часть, мы легко можем вычислить остаток от деления по формуле: Остаток = Делимое - (Делитель * ЦелаяЧасть). Если этот остаток равен нулю, значит, день является расчетным.
Посмотрим на пример практической реализации такого запроса. Предположим, что параметры у нас хранятся во временной таблице ВТ_Параметры, а список дат мы получаем из производственного календаря:
ВЫБРАТЬ
Параметры.Дерево КАК Дерево,
Параметры.ДатаСтарта КАК ДатаСтарта,
Параметры.ПериодичностьДней КАК ПериодичностьДней,
Параметры.ОбъемЛитров КАК ОбъемЛитров,
Календарь.ДатаКалендаря КАК ДатаКалендаря,
РАЗНОСТЬДАТ(Параметры.ДатаСтарта, Календарь.ДатаКалендаря, ДЕНЬ) КАК ВсегоДней
ПОМЕСТИТЬ ВТ_ПодготовкаДанных
ИЗ
ВТ_Параметры КАК Параметры
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК Календарь
ПО Календарь.ДатаКалендаря МЕЖДУ &ДатаНачала И &ДатаОкончания
И Календарь.ДатаКалендаря >= Параметры.ДатаСтарта
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ.Дерево КАК Дерево,
ВТ.ДатаКалендаря КАК ДатаКалендаря,
ВТ.ОбъемЛитров КАК ОбъемЛитров,
ВТ.ВсегоДней КАК ВсегоДней,
ВТ.ПериодичностьДней КАК ПериодичностьДней,
// Вычисляем математически точную целую часть деления
ВЫРАЗИТЬ(ВТ.ВсегоДней / ВТ.ПериодичностьДней КАК ЧИСЛО(15, 0)) -
ВЫБОР
КОГДА ВТ.ВсегоДней / ВТ.ПериодичностьДней - ВЫРАЗИТЬ(ВТ.ВсегоДней / ВТ.ПериодичностьДней КАК ЧИСЛО(15, 0)) < 0
ТОГДА 1
ИНАЧЕ 0
КОНЕЦ КАК ЦелаяЧасть
ПОМЕСТИТЬ ВТ_РасчетЦелойЧасти
ИЗ
ВТ_ПодготовкаДанных КАК ВТ
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ.Дерево КАК Дерево,
ВТ.ДатаКалендаря КАК ДатаКалендаря,
ВТ.ОбъемЛитров КАК ОбъемЛитров
ИЗ
ВТ_РасчетЦелойЧасти КАК ВТ
ГДЕ
// Условие на равенство остатка от деления нулю
(ВТ.ВсегоДней - (ВТ.ПериодичностьДней * ВТ.ЦелаяЧасть)) = 0
Предыдущий запрос опирается на физическую таблицу РегистрСведений.РегламентированныйПроизводственныйКалендарь. Это хорошее решение, но оно имеет очевидные недостатки: производственный календарь может отсутствовать в вашей конфигурации, либо он может быть банально не заполнен пользователями на будущий год.
Рассмотрим продвинутый прием генерации непрерывной последовательности дат непосредственно в транзакции запроса без обращения к реальным таблицам базы данных. Для этого мы применим кросс-соединение (декартово произведение) статических таблиц цифр.
ВЫБРАТЬ 0 КАК Цифра ПОМЕСТИТЬ ВТ_Цифры ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 4 ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 5 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 9
;
////////////////////////////////////////////////////////////////////////////////
// Генерируем числа от 0 до 999
ВЫБРАТЬ
Т1.Цифра + Т2.Цифра * 10 + Т3.Цифра * 100 КАК ЧислоДней
ПОМЕСТИТЬ ВТ_ПоследовательностьЧисел
ИЗ
ВТ_Цифры КАК Т1
КРОСС СОЕДИНЕНИЕ ВТ_Цифры КАК Т2
КРОСС СОЕДИНЕНИЕ ВТ_Цифры КАК Т3
;
////////////////////////////////////////////////////////////////////////////////
// Превращаем последовательность чисел в последовательность дат
ВЫБРАТЬ
ДОБАВИТЬКДАТЕ(&ДатаНачала, ДЕНЬ, ВТ.ЧислоДней) КАК ДатаКалендаря
ПОМЕСТИТЬ ВТ_ГенерируемыйКалендарь
ИЗ
ВТ_ПоследовательностьЧисел КАК ВТ
ГДЕ
ДОБАВИТЬКДАТЕ(&ДатаНачала, ДЕНЬ, ВТ.ЧислоДней) <= &ДатаОкончания
Соединяя полученную виртуальную таблицу ВТ_ГенерируемыйКалендарь с нашими параметрами полива, мы получим полностью независимый алгоритм, который будет работать стабильно на любой конфигурации 1С и СУБД.
Если перед вами стоит задача разработать отчет со сложной структурой вывода (например, календарная сетка, где в строках — деревья, в колонках — даты, а на пересечении — литры), идеальным инструментом станет Система компоновки данных (СКД):