Рассмотрение темы предлагается начать "от обратного".
Вниманию читателя предлагается вызов функции чтения табличных данных (xls,csv)
Функция мПрочитатьВходнойФайл(пКонтекстДокумента, пФайлЗагрузки, пРольАгентскойСхемы)
лРасшФормы = глРасшФормы(пКонтекстДокумента.Форма);
лТаблица = глСчитатьТабличныеДанные(пФайлЗагрузки
, глСписок_("Признак-итога", 2, "ВидЗатрат", 6,"Счет", 6, "Сумма", 11, "Объект", 4, "РольАгентСхемы", 1)
, глСписок_(
"Сумма", глСписок("->", глСписок("глЗаменить", "\.", "", ",", "."), глСписок("Число"))
, "Объект", глСписок("->>", глСписок("глЭлементСправочника", "СПП"))
, "ВидЗатрат", глСписок("->>", глСписок("глЭлементСправочника", "IBSВидыЗатрат"))
, "Счет", глСписок("->>", глСписок("глЭлементСправочника", "IBSВидыЗатрат"), глСписок("глВызов_", глСписок("Реквизит", "БухСчет")))
, "РольАгентСхемы", глСписок("->>", глСписок("глЗнач", пРольАгентскойСхемы))
)
, глСписок_("Признак-итога", глСписок("ПустоеЗначение"))
, глСписок("мНоваяСтрокаДокументаОстатки", пКонтекстДокумента)
, 4);
КонецФункции // ПрочитатьВходнойФайл
Функция мПрочитатьВходнойФайл вызывается из документа "Ввод остатков" конфигурации БУ. При вызове происходит передача контекста документа, который используется для заполнения табличной части вызывающего документа содержимым excel или csv файла.
Таким образом, функция мПрочитатьВходнойФайл дублируется в нескольких внешних обработках, реализуя множество способов построения документа "Ввод остатков" через интерфейсную функцию мПрочитатьВходнойФайл, каждая из которых производит чтение, преобразование и проверку разнородных табличных данных (паттерн "Строитель")
Используемая функция глСчитатьТабличныеДанные является реализацией паттерна стратегия. Производится чтение произвольного файла табличных данных, указание целевых полей, функций проебразования, проверки и конечной обработки строки данных исходя из целесообразности решаемой бизнес-функции, строка, с которой следует начинать чтение.
Предлагается рассмотреть используемые функции-обертки подробней. Сначала фигурирующие в мПрочитатьВходнойФайл и далее те, что находятся "под капотом"
глСписок
Функция глСписок( пЗнач1 = "дефолт", пЗнач2 = "дефолт", пЗнач3 = "дефолт", ..., пЗнач43 = "дефолт") Экспорт
Функция глСписок является оберткой для создания объекта "СписокЗначений" в одну строку, также поддерживается хранение типизированных пустых значений.
Пример:
лСписок = глСписок(1, 2, 4, "stuf stuf stuf",, ПолучитьПустоеЗначение("Справочник.Номенклатура"));
глСписок_
Функция глСписок_(знач пСтрока1 = 0, пЗнач1 = "дефолт", знач пСтрока2 = 0, пЗнач2 = "дефолт", ..., знач пСтрока43 = 0, пЗнач43 = "дефолт") Экспорт
Функция глСписок является оберткой для создания объекта "СписокЗначений" в одну строку. Дополнительно указываются строковые представления хранимых в списке значений.
Пример:
лСписок = глСписок_("фамилия", "Рожков", "имя", "Дмитрий", "отчество", "Сергеевич", "возраст", 30)
глВызов_
Функция глВызов_(знач пФунктор, знач пАргумент, знач пВКонец = 0)
Реализует отложенный вызов функции, переданной в первом параметре. На второй позиции передается аргумент, располагающийся в вызываемом функторе в качестве первого параметра по умолчанию. Если пВКонец не равно нулю, то пАргумент будет размещен в конце списка параметров пФуктор.
Функтор
Функтором в представленной организации исполнительной среды является объект типа "СписокЗначений", на первой позиции которого хранится строковое наименование вызываемой функции, в оставшейся части списка поочередно размещаются параметры указанной на первой позиции функции. Вызов алгоритмов обработки любых коллекций ("СписокЗначений", "ТаблицаЗначений", файл excel, "Справочник.ХХХ" и др.) подразумевает передачу в обобщенный алгоритм функтора и дальнейшую последовательную подстановку в него i-того элемента коллекции по аналогии с функциями стандартной библиотеки высокоуровневых языков foreach, accumulate, map_reduce, sort, filter, transform, search и др. Как правило, производится подстановка на вторую позицию в списке, сразу после имени функции, при вызове подставленное значение окажется на первой позиции списка аргументов функции. Почти во всех таких абстракциях присутствует вызов глВызов_. Например функция глВызов (без знака подчеркивания) реализована при её помощи.
глВызов
Функция глВызов(знач пФунктор) Экспорт
Перем лРезульт;
Если ТипЗначенияСтр(пФунктор) <> "СписокЗначений" Тогда
Возврат пФунктор;
КонецЕсли;
Шаблон("[глПрисвоить(лРезульт, " + глРазвернутьФунктор(пФунктор,, "пФунктор", 1) + ")]");
Возврат лРезульт;
КонецФункции // глВызов
Функция глРазвернутьФунктор(пФунктор, знач пПервыеАргументы = "", знач пИмяФунктора, знач пБезСкобок = 0) Экспорт
лРазмер = пФунктор.РазмерСписка();
лНаимФункц = пФунктор.ПолучитьЗначение(1);
Если "->" = лНаимФункц Тогда
пФунктор.УстановитьЗначение(1, "Композиция");
КонецЕсли;
Если "->>" = лНаимФункц Тогда
пФунктор.УстановитьЗначение(1, "Композиция_");
КонецЕсли;
лВызовУсловия = ?(пБезСкобок <> 0, "", "[") + пФунктор.ПолучитьЗначение(1) + "(" + пПервыеАргументы
+ ?(лРазмер > 1, ?(ПустаяСтрока(пПервыеАргументы) = 0, ", ", ""), "");
лАргументы = глСписок();
Если пФунктор.РазмерСписка() > 1 Тогда
пФунктор.Выгрузить(лАргументы, 2);
КонецЕсли;
Возврат лВызовУсловия + глРазвернутьАргументы(лАргументы, пИмяФунктора, 1) + ")" + ?(пБезСкобок <> 0, "", "]");
КонецФункции // глРазвернутьФунктор
Функция глРазвернутьАргументы(знач пСписок, знач пИмяСписка, знач пСмещение = 0) Экспорт
лРазмер = пСписок.РазмерСписка();
лСтрВызова = "";
Для ит = 1 по лРазмер Цикл
лСтрока = "";
лЗначение = глЗначениеСписка(пСписок, ит, лСтрока);
лСтрВызова = лСтрВызова + ", " + ?(НепустоеЗначение(лЗначение) = 0, "Пуст(""" + лСтрока + """)"
, Шаблон("глЗначениеСписка([пИмяСписка], [ит + пСмещение])"));
КонецЦикла;
Возврат Сред(лСтрВызова, 3);
КонецФункции // глРазвернутАргументы
Функция глВызов производит "развертывание" функтора в строковое представление, интерпретируемое впоследствии вызовом Шаблон
Шаблон("[глПрисвоить(лРезульт, " + глРазвернутьФунктор(пФунктор,, "пФунктор", 1) + ")]");
Считается, что аргументы функтора, как и её название, хранятся в переменной пФунктор. Исходя из этого знания происходит развертывание. В глРазвернутьФунктор передается не только его содержание, но и наименование переменной, представленное в контексте исполнения. После работы функций глРазвернутьФунктор и глРазвернутьАргументы в контексте глВызов оказывается пригодная для выполнения в Шаблоне строка вызова. Например, подстановка в табло операций развёртывания функторов даст следующие результаты:
глРазвернутьФунктор(глСписок("ок", "Привет мир!!!"),, "пФунктор", 1) = ок(глЗначениеСписка(пФунктор, 2))
глРазвернутьФунктор(глСписок("->>", глСписок("глЭлементСправочника", "СтавкиНДС", "V1")),, "пФунктор", 1)
= Композиция_(глЗначениеСписка(пФунктор, 2))
Композиция, Композиция_ ("->", "->>")
Функция Композиция создана исключительно для "заворачивания" и хранения её в функторе. Прямой вызов этой функции вне функций обхода коллекций нецелесообразен.
Отложенный вызвов функций Композиция ("->") и Композиция_ ("->>") подразумевает уже упомянутую передачу в них в качестве первого аргумента i-го элемента коллекции. Функция композиция производит последовательное выполнение функторов переданных в качестве параметров. На вход первого функтора подается i-й элемент обрабатываемой в текущий момент коллекции.
Композиция ("->") отличается от Композиция_ ("->>") тем, что в первом случае переданный начальный параметр всегда размещается как первый аргумент цепочки функторов, а во втором как последний. Очень редко может понадобиться размещение входного параметра в цепочке функторов на иной позиции - для этого следует использовать функции глСместАргСлева(Справа). Сказанное наглядно иллюстрирует исходный код функции композиция и пример.
Функция Композиция(знач пИскомое, знач пф1 = 0, знач пф2 = 0, знач пф3 = 0, знач пф4 =0, знач пф5 = 0) Экспорт
лРезультат = пИскомое;
лФункторы = глСписок();
Если пф1 <> 0 Тогда лФункторы.ДобавитьЗначение(пф1); КонецЕсли;
Если пф2 <> 0 Тогда лФункторы.ДобавитьЗначение(пф2); КонецЕсли;
Если пф3 <> 0 Тогда лФункторы.ДобавитьЗначение(пф3); КонецЕсли;
Если пф4 <> 0 Тогда лФункторы.ДобавитьЗначение(пф4); КонецЕсли;
Если пф5 <> 0 Тогда лФункторы.ДобавитьЗначение(пф5); КонецЕсли;
лРазмер = лФункторы.РазмерСписка();
Для ит = 1 по лРазмер Цикл
лФунктор = лФункторы.ПолучитьЗначение(ит);
Если лФунктор = 0 Тогда Прервать; КонецЕсли;
лРезультат = глВызов(глВставитьВСписок(лФунктор, лРезультат, 2));
КонецЦикла;
Возврат лРезультат;
КонецФункции // Композиция
Пример:
Установлено, что в справочнике "СПП" по коду "test-test-test" хранится строка, в которой среди прочего мусора хранится дата ("Элемент справочника СПП - дата х . 17.09.2015 --"), которую необходимо изъять и преобразовать в тип Дата:
глВызов_(
глСписок("->>"
, глСписок("глЭлементСправочника", "СПП")
, глСписок("Строка")
, глСписок("глСместАргСправа" // сместить аргумент справа в указанную позицию (1)
, глСписок("глЗаменить", ".*(\d{2}\.\d{2}\.\d{2,4}).*", "$1"), 1)
, глСписок("Дата"))
, "test-test-test") = 17.09.15
Таким образом происходит "отложенный" (при вызове из Табло отложенный совсем чуть-чуть) вызов цепочки функций глЭлементСправочника,Строка,глЗаменить,Дата. Пример является надуманным и в реальной практике такие сложные вызовы встречаются примерно никогда и скорее вредны с точки зрения возможности поддержания кода коллегами. Оптимальным решением является обработка коллекций при помощи несложных простых или более сложных составных функторов на основе композиции.
В заключение
Поводом для статьи послужили несколько причин.
К сожалению, некоторые разработчики на 1С и не только придерживаются в своей практике принципа простоты, считая что для создания хороших програм достаточно операторов Если...ИначеЕсли...Иначе...Тогда, Для..Цикл..КонецЦикла. Практика показала, что исходные коды, созданые без применения минимальных абстракций, стремятся в к высоким величинам по показателям длина кода функции и вложенность условий. Любовь к ремеслу вызывает стремление показать заинтересованному читателю, что можно писать на 1С лаконичней, интересней и продуктивней.
Синтаксис 1С не предоставляет указанных абстракций, так сильно облегчающих жизнь программиста.
Нежелание хоронить результат своей работы ...
Весь описанный функционал сохранен в приложенном к публикации zip архиве, буду безмерно рад, если кому-нибудь пригодится.
Еще примеры:
Объединение списков периодов
В примере рассматривается объединение списков периодов, являющихся списком троек хранящих начальную и конечную даты. Период также характеризуется вещественным коэффициентом, применяемым в вычислениях на списке периодов. В простейшем случае коэффициент периода равен единице. При объединении списка периодов производится удаление дублей по пересекающимся участкам с последующей сортировкой.
Функция глОбъединитьСпискиПериодов(знач пА, знач пБ, знач пМетодУчетаКоэфПересеч = 1) Экспорт
лРазнАБ = глРазностьСписковПериодов(пА, пБ);
лРазнБА = глРазностьСписковПериодов(пБ, пА);
лПересАБ = глПересеченияСписковПериодов(пА, пБ, пМетодУчетаКоэфПересеч);
лОбъединение = глСортироватьСписокЗначений(глОбъединитьСписки(глОбъединитьСписки(лРазнАБ, лРазнБА), лПересАБ), глСписок("глПериодРаньше"));
//... обработка коэффициентов
КонецФункции
Функция глПериодРаньше(знач пВрПер1, знач пВрПер2) Экспорт
Возврат ?(пВрПер1.ПолучитьЗначение(1) < пВрПер2.ПолучитьЗначение(1), 1, 0);
КонецФункции // глПериодРаньше
Обход справочника, копирование стажей
Рассмотрено копирование стажей по всему справочнику сотрудников при помощи вызова глОбходСправочника. Стажи хранятся в отдельном подчиненном сотрудникам справочнике.
глФильтрСправочника("Сотрудники"
, глСписок("глСкопироватьСтажи", Перечисление.ВидСтажа.ВОтрасли, 1, 0.85, Перечисление.ВидСтажа.МММ),,,0);
Функция глСкопироватьСтажи(знач пСотрудник, знач пВидСтажаИЗ, знач пРучнСтажиИз, знач пКоэф, знач пВидСтажаВ, знач пОчист = 0) Экспорт ...
По типу стажа источника выбираются записи с указанным, если он задан, коэффициентом из соответсвующего справочника. Система разделяет стажи ручные и производные (вычиляемые из других), поэтому дополнительно указывается производить ли поиск ручных записей в справочнике стажей или же принять производные стажи по указанному типу. Производится копирование записей по указанному источнику в справочник стажей по целевому типу с возможностью предварительной очистки.
Фильтр списка сотрудников
Интересный пример выборки сотрудников с достаточным стажем и не перерботавших более полугода после возраста права гос. пенсии для назначения негосударственной пенсии.
Возврат глФильтрСпискаПоИ(лСостав
, глСписок("->", глСписок("СтажГазпромРКСПриУвольнении"), глСписок("больше", 14))
, глСписок("НеПереработал"));
Фильтр таблицы, получение строк таблицы по типу документа
Пример заключается в фильтрации строчек таблицы значений, имеющей колонку "Документ", по типу "НачислениеОтпуска". Сразу оговорюсь, что большие возможности по работе с таблицами нам открывает индексированная таблица. Однако, при некритических требованиях к производительности подобные конструкции вполне жизнеспособны.
Функция ПечатьРасходов_ФСС_НС(знач пРасходыЗаСчет_ФСС_НС)
лТаблица = глТаблица("4ФСС_Расходы_ФССНС");
лТаблица.Вывести();
// ... предварительная подготовка
лРасходыЗаСчет_ФСС_НС = глФильтрТаблциы(пРасходыЗаСчет_ФСС_НС, глСписок("Композиция"
, глСписок("глЗначениеКолонки", "Документ")
, глСписок("глВидДокумента")
, глСписок("равно", "НачислениеОтпуска")));
// ... обработка отфильтрованной таблицы
КонецФункции