В практике разработчика 1С часто возникает задача агрегации данных. Стандартный метод Свернуть() объекта ТаблицаЗначений отлично справляется с суммированием, но как быть, если бизнес-логика требует не складывать числа, а вычитать их? Например, когда нужно найти разницу между приходом и расходом или вычислить отклонение между двумя версиями одного и того же документа.
В этой статье мы подробно разберем несколько способов реализации этой задачи, проанализируем ситуацию с использованием запросов и программного перебора таблиц, а также рассмотрим нюансы работы с модулем числа.
Самый простой и логичный способ, который можно реализовать прямо в объекте ТаблицаЗначений, основывается на базовых правилах арифметики. Мы знаем, что A - B — это то же самое, что A + (-B). Следовательно, чтобы метод Свернуть() выдал нам разность, нам нужно предварительно изменить знак у тех значений, которые должны выступать в роли «вычитаемого».
Рассмотрим ситуацию, когда в нашей таблице строки идут парами для каждого ключевого поля. Разберем по шагам алгоритм действий:
Свернуть().Посмотрим на пример кода, реализующего этот подход:
// Предположим, ТЗ_Данные уже заполнена и имеет колонки "Ключ" и "Объем"
// В каждой паре строк вторая строка должна вычитаться из первой
Счетчик = 0;
Для Каждого СтрокаТЗ Из ТЗ_Данные Цикл
// Если мы точно знаем, что строки идут парами (1-я плюс, 2-я минус)
Если Счетчик % 2 <> 0 Тогда
СтрокаТЗ.Объем = -СтрокаТЗ.Объем;
КонецЕсли;
Счетчик = Счетчик + 1;
КонецЦикла;
// Теперь стандартная свертка выполнит вычитание (сложение с отрицательным числом)
ТЗ_Данные.Свернуть("Ключ", "Объем");
Если же в вашей задаче, как упоминал автор темы, неважно, что из чего вычитается (так как итоговый результат берется по модулю), можно использовать признак четности строк или просто инвертировать каждую вторую строку в группе.
Проанализируем ситуацию с точки зрения производительности. Если данных много, работа с объектом ТаблицаЗначений в памяти может быть медленной. Использование механизма запросов — это «золотой стандарт» в 1С. В запросе мы можем реализовать вычитание гораздо элегантнее с помощью конструкции ВЫБОР (в SQL — CASE).
Рассмотрим, как это выглядит на практике. Допустим, у нас есть временная таблица ВТ_Данные с колонками Номенклатура, ТипОперации и Количество. Мы хотим получить итоговый остаток (Приход - Расход).
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВТ.Номенклатура КАК Номенклатура,
| СУММА(ВЫБОР
| КОГДА ВТ.ТипОперации = ЗНАЧЕНИЕ(Перечисление.ТипыОпераций.Расход)
| ТОГДА -ВТ.Количество
| ИНАЧЕ ВТ.Количество
| КОНЕЦ) КАК ИтоговыйОбъем
|ИЗ
| ВТ_Данные КАК ВТ
|ГРУППИРОВАТЬ ПО
| ВТ.Номенклатура";
В данном примере функция СУММА фактически выполняет роль вычитания для строк расхода. Это наиболее надежный метод, так как он позволяет четко контролировать логику вычислений и легко масштабируется на любое количество строк (не только пары).
Если мы имеем дело со строгими парами данных (например, «План» и «Факт» для каждого объекта), мы можем применить метод соединения. Выясним причину, почему это удобно: такой подход позволяет получить уменьшаемое и вычитаемое в одной строке в разных колонках, что делает расчет прозрачным.
Разберем пример запроса, где мы соединяем таблицу с самой собой:
Запрос.Текст =
"ВЫБРАТЬ
| Т1.Объект КАК Объект,
| Т1.Объем - ЕСТЬNULL(Т2.Объем, 0) КАК Разность
|ИЗ
| ВТ_Данные КАК Т1
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Данные КАК Т2
| ПО Т1.Объект = Т2.Объект
| И Т1.НомерСтроки < Т2.НомерСтроки
|ГДЕ
| Т1.НомерСтроки % 2 <> 0"; // Берем только нечетные как базу
Здесь мы используем ЛЕВОЕ СОЕДИНЕНИЕ, чтобы гарантировать, что даже при отсутствии второй строки мы получим результат. Использование функции ЕСТЬNULL критически важно, так как вычитание NULL из числа всегда дает NULL, что может привести к потере данных в отчете.
Как отметил автор обсуждения, иногда порядок вычитания не важен, если итоговое число нужно взять по модулю. В 1С (как в языке запросов, так и во встроенном языке) для этого используется функция abs() или abs().
Если вы используете способ с инверсией знака в ТаблицаЗначений, не забудьте финальный проход для приведения результата к положительному виду:
ТЗ_Данные.Свернуть("Ключ", "Объем");
Для Каждого Строка Из ТЗ_Данные Цикл
Строка.Объем = ?(Строка.Объем < 0, -Строка.Объем, Строка.Объем);
// Или более изящно:
// Строка.Объем = Макс(Строка.Объем, -Строка.Объем);
КонецЦикла;
При выборе метода стоит учитывать следующие моменты:
Для Каждого.ДопустимыйЗнак.Неотрицательный, вы получите ошибку при попытке записать -1.Таким образом, хотя прямого метода ТЗ.Вычесть() в платформе не существует, комбинация базовой математики и стандартных инструментов агрегации позволяет решить задачу вычитания строк при сворачивании любым удобным для разработчика способом.