Как перебрать все колонки ТаблицыЗначений и не сломать данные

· 9 мин

Содержание

Колонки таблицы значений в 1С перебирают через коллекцию Колонки: Для Каждого Колонка Из ТЗ.Колонки Цикл ... КонецЦикла. Значение ячейки в строке берут так: Строка[Колонка.Имя]. Этого хватает в девяти задачах из десяти — пока внутри цикла не начинают удалять колонки или не забывают, что у колонки может не быть типа.

Частое заблуждение — что у таблицы значений есть метаданные, как у справочника. Их нет: это объект в памяти, и всё, что о нём известно, лежит в самой коллекции колонок. Метаданные пригодятся, только если таблица выгружена из табличной части или регистра. Ниже — все методы обхода с рабочими примерами кода.

Таблица значений доступна на сервере и в толстом клиенте. В тонком клиенте и в веб-клиенте её нет, поэтому весь код ниже пишите в серверных процедурах (&НаСервере) или в общих модулях.

Простой перебор колонок через коллекцию Колонки

Коллекция ТЗ.Колонки — это объект КоллекцияКолонокТаблицыЗначений. Каждый её элемент — КолонкаТаблицыЗначений с четырьмя полезными свойствами.

СвойствоЧто хранитПример
ИмяИдентификатор колонки, по нему обращаются к ячейкеКоличество
ЗаголовокТекст для показа пользователю, может быть пустымКол-во, шт.
ТипЗначенияОбъект ОписаниеТиповЧисло(15,3)
ШиринаШирина в символах при выводе10

Минимальный пример: вывести имена всех колонок и значения первой строки.

&НаСервере
Процедура ПоказатьКолонкиНаСервере()

    ТЗ = Новый ТаблицаЗначений;
    ТЗ.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("Строка"));
    ТЗ.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
    ТЗ.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));

    НоваяСтрока = ТЗ.Добавить();
    НоваяСтрока.Номенклатура = "Бумага А4";
    НоваяСтрока.Количество = 5;
    НоваяСтрока.Цена = 320;

    Для Каждого Колонка Из ТЗ.Колонки Цикл
        Сообщить(Колонка.Имя + " = " + Строка(ТЗ[0][Колонка.Имя]));
    КонецЦикла;

КонецПроцедуры

Чтобы пройти всю таблицу, нужен вложенный цикл: внешний по строкам, внутренний по колонкам.

Для Каждого СтрокаТЗ Из ТЗ Цикл
    Для Каждого Колонка Из ТЗ.Колонки Цикл
        Значение = СтрокаТЗ[Колонка.Имя];
        // обработка значения
    КонецЦикла;
КонецЦикла;

Другие методы коллекции, которые нужны при обходе:

  • 🔢 Количество() — число колонок;
  • 🔍 Найти("Имя") — колонка по имени или Неопределено, если её нет;
  • 📍 Индекс(Колонка) — позиция колонки, отсчёт с нуля;
  • 📥 Получить(Индекс) — колонка по номеру.

Перебор колонок с проверкой типа значения

Свойство ТипЗначения возвращает ОписаниеТипов, а не один тип. Поэтому сравнивать его с Тип("Число") напрямую бессмысленно. Для проверки есть метод СодержитТип().

Для Каждого Колонка Из ТЗ.Колонки Цикл
    Если Колонка.ТипЗначения.СодержитТип(Тип("Число")) Тогда
        Сообщить("Числовая колонка: " + Колонка.Имя
            + ", итог: " + Формат(ТЗ.Итог(Колонка.Имя), "ЧДЦ=2"));
    ИначеЕсли Колонка.ТипЗначения.СодержитТип(Тип("Дата")) Тогда
        Сообщить("Колонка даты: " + Колонка.Имя);
    КонецЕсли;
КонецЦикла;

Три ситуации, которые ломают такую проверку:

  1. Колонка добавлена без типа. Вызов ТЗ.Колонки.Добавить("Сумма") создаёт колонку с пустым описанием типов. Список Колонка.ТипЗначения.Типы() пуст, СодержитТип() вернёт Ложь для любого типа, а хранить в колонке можно что угодно.
  2. Таблица выгружена из запроса. Колонки результата запроса часто содержат в описании ещё и тип Null. СодержитТип(Тип("Число")) при этом вернёт Истина, но в ячейке может оказаться Null, и арифметика упадёт.
  3. Составной тип. Колонка может хранить и число, и строку. Тогда проверяйте тип конкретного значения: ТипЗнч(СтрокаТЗ[Колонка.Имя]) = Тип("Число").

Для пустых колонок без типа надёжнее смотреть на само значение, а не на описание колонки:

Значение = СтрокаТЗ[Колонка.Имя];
Если ТипЗнч(Значение) = Тип("Число") Тогда
    Сумма = Сумма + Значение;
КонецЕсли;

Имена и типы из метаданных: таблица из табличной части

У самой таблицы значений метаданных нет. Но если вы получили её методом Выгрузить() из табличной части документа, колонки совпадают с реквизитами этой табличной части. Плюс есть служебная колонка НомерСтроки.

Из метаданных удобно брать синоним реквизита — понятное название для заголовка отчёта или файла:

&НаСервере
Функция ЗаголовкиКолонок(ТЗ, МетаТабЧасть)

    Заголовки = Новый Соответствие;
    Для Каждого Колонка Из ТЗ.Колонки Цикл
        МетаРеквизит = МетаТабЧасть.Реквизиты.Найти(Колонка.Имя);
        Если МетаРеквизит <> Неопределено Тогда
            Заголовки.Вставить(Колонка.Имя, МетаРеквизит.Синоним);
        ИначеЕсли ЗначениеЗаполнено(Колонка.Заголовок) Тогда
            Заголовки.Вставить(Колонка.Имя, Колонка.Заголовок);
        Иначе
            Заголовки.Вставить(Колонка.Имя, Колонка.Имя);
        КонецЕсли;
    КонецЦикла;
    Возврат Заголовки;

КонецФункции

Вызов для табличной части документа, где Документ — ссылка или объект:

МетаТЧ = Документ.Метаданные().ТабличныеЧасти.Товары;
ТЗ = Документ.Товары.Выгрузить();
Заголовки = ЗаголовкиКолонок(ТЗ, МетаТЧ);

Если НомерСтроки в выгрузке не нужен, сразу удалите его: ТЗ.Колонки.Удалить("НомерСтроки"). Иначе он попадёт во все экспорты и проверки.

Тот же приём работает с регистрами: у метаданных регистра есть коллекции Измерения, Ресурсы и Реквизиты. Ищите имя колонки по очереди в каждой из них.

Цикл по индексам колонок вместо Для Каждого

Строка таблицы значений понимает обращение не только по имени, но и по индексу колонки: СтрокаТЗ[0] — значение первой колонки. Когда колонок много и нужна позиция, цикл по счётчику удобнее.

ЧислоКолонок = ТЗ.Колонки.Количество();
Для Каждого СтрокаТЗ Из ТЗ Цикл
    Для Инд = 0 По ЧислоКолонок - 1 Цикл
        Значение = СтрокаТЗ[Инд];
        // обработка значения
    КонецЦикла;
КонецЦикла;

Что здесь сделано ради скорости:

  • ⚡ количество колонок вычислено один раз до циклов, а не в каждой итерации;
  • ⚡ нет поиска колонки по имени на каждой ячейке;
  • ⚡ индексы совпадают с номерами колонок в табличном документе (со сдвигом на единицу), это удобно при выводе.

Ещё быстрее — не перебирать ячейки вообще, если задача решается целой колонкой:

ЗадачаВместо цикла по ячейкамЧто возвращает
Сумма по колонкеТЗ.Итог("Сумма")Число
Все значения колонкиТЗ.ВыгрузитьКолонку("Номенклатура")Массив
Записать массив в колонкуТЗ.ЗагрузитьКолонку(Массив, "Цена")—
Заполнить колонку одним значениемТЗ.ЗаполнитьЗначения(0, "Скидка")—
Группировка с суммамиТЗ.Свернуть("Номенклатура", "Количество, Сумма")—
Копия только нужных колонокТЗ.Скопировать(, "Номенклатура, Цена")ТаблицаЗначений

Колонки, которые добавляются в коде на лету

Типичный пример — колонки по месяцам или по складам: заранее неизвестно, сколько их будет. Имена собирают из префикса и номера.

Для НомерМесяца = 1 По 12 Цикл
    ИмяКолонки = "Месяц" + Формат(НомерМесяца, "ЧЦ=2; ЧВН=; ЧГ=0");
    Если ТЗ.Колонки.Найти(ИмяКолонки) = Неопределено Тогда
        ТЗ.Колонки.Добавить(ИмяКолонки, Новый ОписаниеТипов("Число"),
            Формат(Дата(2000, НомерМесяца, 1), "ДФ=ММММ"));
    КонецЕсли;
КонецЦикла;

Получатся колонки Месяц01 … Месяц12 с заголовками-названиями месяцев. Год в Дата(2000, ...) не важен — он нужен только для форматирования.

Правила для имён таких колонок:

  • ✅ начинаются с буквы или подчёркивания;
  • ✅ без пробелов, точек, дефисов;
  • ✅ числа в имени через Формат(..., "ЧГ=0"), иначе для 1000 и больше получится неразрывный пробел-разделитель и ошибка имени;
  • ✅ перед добавлением — проверка Найти(), чтобы не получить ошибку дубля.

Новые колонки появляются во всех уже существующих строках со значением по умолчанию для своего типа. У колонки без типа это Неопределено.

Обход только «динамической» части удобно делать по префиксу имени:

Для Каждого Колонка Из ТЗ.Колонки Цикл
    Если СтрНачинаетсяС(Колонка.Имя, "Месяц") Тогда
        Сообщить(Колонка.Заголовок + ": " + Формат(ТЗ.Итог(Колонка.Имя), "ЧГ=0"));
    КонецЕсли;
КонецЦикла;

Удаление колонок при обходе

Удалять колонки внутри Для Каждого по той же коллекции нельзя: перебор собьётся и часть колонок будет пропущена. Используйте обратный цикл:

Всего = ТЗ.Колонки.Количество();
Для Сч = 1 По Всего Цикл
    Колонка = ТЗ.Колонки[Всего - Сч];
    Если ТЗ.ВыгрузитьКолонку(Колонка).Найти(Неопределено) <> Неопределено Тогда
        ТЗ.Колонки.Удалить(Колонка);
    КонецЕсли;
КонецЦикла;

Пример удаляет колонки, где хотя бы в одной ячейке Неопределено. Условие замените на своё. Граница Всего вычислена до цикла, индекс идёт с конца, поэтому удаление не сдвигает ещё не проверенные колонки.

ОписаниеТипов в деле: приведение и разбор составных типов

Описание типов умеет не только отвечать на вопрос «содержит ли». Три метода, которые пригодятся при универсальной обработке колонок:

  • 📋 Типы() — массив всех типов колонки. Пустой массив — колонка без типа;
  • 🔄 ПривестиЗначение(Значение) — приводит значение к типу колонки. Если значение не подходит, вернётся значение по умолчанию: для числа — 0, для строки — пустая строка;
  • 📏 КвалификаторыЧисла, КвалификаторыСтроки, КвалификаторыДаты — разрядность, длина, состав даты.

Пример: вывести полное описание каждой колонки, включая составные типы.

Для Каждого Колонка Из ТЗ.Колонки Цикл
    ИменаТипов = Новый Массив;
    Для Каждого ТекТип Из Колонка.ТипЗначения.Типы() Цикл
        ИменаТипов.Добавить(Строка(ТекТип));
    КонецЦикла;
    Если ИменаТипов.Количество() = 0 Тогда
        ИменаТипов.Добавить("<любой тип>");
    КонецЕсли;
    Сообщить(Колонка.Имя + ": " + СтрСоединить(ИменаТипов, ", "));
КонецЦикла;

Приведение значений перед записью в таблицу защищает от «грязных» данных, например при загрузке из файла:

Для Каждого Колонка Из ТЗ.Колонки Цикл
    СтрокаТЗ[Колонка.Имя] = Колонка.ТипЗначения.ПривестиЗначение(СтрокаТЗ[Колонка.Имя]);
КонецЦикла;

У колонки без типа ПривестиЗначение() ничего не исправит. Задавайте тип колонкам, которые заполняются из внешних файлов, иначе ошибка всплывёт позже — при расчёте или записи в базу.

Готовые задачи: выгрузка в Excel и CSV, проверка заполнения

Таблица значений в табличный документ и Excel

Первая строка — заголовки, дальше — данные. Ячейки адресуются по имени области вида R1C1.

&НаСервере
Функция ТЗВТабличныйДокумент(ТЗ)

    ТабДок = Новый ТабличныйДокумент;
    НомКол = 0;
    Для Каждого Колонка Из ТЗ.Колонки Цикл
        НомКол = НомКол + 1;
        Ячейка = ТабДок.Область("R1C" + Формат(НомКол, "ЧГ=0"));
        Ячейка.Текст = ?(ЗначениеЗаполнено(Колонка.Заголовок), Колонка.Заголовок, Колонка.Имя);
    КонецЦикла;

    НомСтр = 1;
    Для Каждого СтрокаТЗ Из ТЗ Цикл
        НомСтр = НомСтр + 1;
        НомКол = 0;
        Для Каждого Колонка Из ТЗ.Колонки Цикл
            НомКол = НомКол + 1;
            Ячейка = ТабДок.Область("R" + Формат(НомСтр, "ЧГ=0") + "C" + Формат(НомКол, "ЧГ=0"));
            Ячейка.Текст = Строка(СтрокаТЗ[Колонка.Имя]);
        КонецЦикла;
    КонецЦикла;

    Возврат ТабДок;

КонецФункции

Сохранить результат в файл Excel: ТабДок.Записать(ИмяФайла, ТипФайлаТабличногоДокумента.XLSX). Код выполняется на сервере, поэтому и файл ляжет на сервер. Пользователю его передают через временное хранилище и получение файла на клиенте.

Выгрузка в CSV

CSV удобен, когда файл забирает другая программа. Значения строки собираются в массив и склеиваются разделителем.

Имена = Новый Массив;
Для Каждого Колонка Из ТЗ.Колонки Цикл
    Имена.Добавить(Колонка.Имя);
КонецЦикла;

Запись = Новый ЗаписьТекста(ИмяФайла, КодировкаТекста.UTF8);
Запись.ЗаписатьСтроку(СтрСоединить(Имена, ";"));

Для Каждого СтрокаТЗ Из ТЗ Цикл
    Значения = Новый Массив;
    Для Каждого Колонка Из ТЗ.Колонки Цикл
        Значения.Добавить(СтрЗаменить(Строка(СтрокаТЗ[Колонка.Имя]), ";", ","));
    КонецЦикла;
    Запись.ЗаписатьСтроку(СтрСоединить(Значения, ";"));
КонецЦикла;
Запись.Закрыть();

Замена ; на , внутри значений нужна, чтобы точка с запятой в названии товара не разорвала строку файла на лишние поля.

Проверка, что все обязательные колонки заполнены

Обязательные = СтрРазделить("Номенклатура,Количество,Цена", ",");
Для Каждого СтрокаТЗ Из ТЗ Цикл
    Для Каждого ИмяКолонки Из Обязательные Цикл
        Если ТЗ.Колонки.Найти(ИмяКолонки) = Неопределено Тогда
            ВызватьИсключение "В таблице нет колонки " + ИмяКолонки;
        КонецЕсли;
        Если Не ЗначениеЗаполнено(СтрокаТЗ[ИмяКолонки]) Тогда
            Сообщить(СтрШаблон("Строка %1: не заполнено поле %2",
                Формат(ТЗ.Индекс(СтрокаТЗ) + 1, "ЧГ=0"), ИмяКолонки));
        КонецЕсли;
    КонецЦикла;
КонецЦикла;

ЗначениеЗаполнено() считает незаполненными ноль, пустую строку, пустую дату, пустую ссылку и Неопределено. Если ноль в колонке допустим, проверяйте его отдельно через ТипЗнч.

Ошибки при обходе колонок и как их найти

Диагностика: выберите симптом

Отдельно про форму. Реквизит формы с типом ТаблицаЗначений на клиенте — это ДанныеФормыКоллекция, у неё нет коллекции Колонки. Чтобы перебрать колонки, на сервере преобразуйте реквизит: ТЗ = РеквизитФормыВЗначение("ТаблицаТоваров"), обработайте, затем верните ЗначениеВРеквизитФормы(ТЗ, "ТаблицаТоваров").

Частые вопросы

Как узнать, есть ли в таблице значений колонка с нужным именем?
ТЗ.Колонки.Найти("Имя") вернёт колонку или Неопределено. Сравните результат с Неопределено до обращения к ячейке.
Как переименовать колонку при обходе?
Присвойте новое значение свойству Имя: Колонка.Имя = "НовоеИмя". Данные в строках сохранятся, меняется только идентификатор.
Как поменять порядок колонок?
Метод ТЗ.Колонки.Сдвинуть(Колонка, Смещение): положительное смещение двигает колонку вправо, отрицательное — влево.
Можно ли получить значения строки сразу все, без цикла по колонкам?
Нет отдельного метода «строка в массив». Обычно строку копируют в структуру через ЗаполнитьЗначенияСвойств(Структура, СтрокаТЗ), но ключи структуры нужно создать заранее — тем же циклом по колонкам.
Чем обход колонок таблицы значений отличается от обхода дерева значений?
Колонки устроены так же, через Колонки. Разница в строках: у дерева они вложенные, и для полного обхода нужна рекурсия по Строки каждой строки.