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