Функция M: один контракт для сотни файлов

Функция нужна не тогда, когда код выглядит умно, а когда одно и то же правило должно честно применяться к разным входам.

В папке лежат книги филиалов. В каждой нужен лист Data, четыре обязательных столбца и код филиала из имени файла. Можно скопировать запрос десять раз, но любое исправление придётся повторять. Функция M превращает это правило в контракт: получает бинарный файл и его имя, возвращает типизированную таблицу или объясняет, почему не может.

Сигнатура рассказывает половину истории

(content as binary, fileName as text) as table =>
let
    Workbook = Excel.Workbook(content, null, true),
    DataRows = Table.SelectRows(
        Workbook,
        each [Kind] = "Sheet" and [Item] = "Data"
    ),
    Sheet =
        if Table.RowCount(DataRows) = 1
        then DataRows{0}[Data]
        else error Error.Record(
            "MissingSheet",
            "В книге нет единственного листа Data",
            [File = fileName]
        ),
    Headers = Table.PromoteHeaders(Sheet, [PromoteAllScalars=true]),
    Required = {"OrderID", "OrderDate", "SKU", "Amount"},
    Missing = List.Difference(Required, Table.ColumnNames(Headers)),
    Checked =
        if List.IsEmpty(Missing)
        then Headers
        else error Error.Record(
            "MissingColumns",
            "В файле отсутствуют обязательные столбцы",
            [File = fileName, Columns = Missing]
        ),
    Typed = Table.TransformColumnTypes(
        Checked,
        {{"OrderID", type text}, {"OrderDate", type date},
         {"SKU", type text}, {"Amount", Currency.Type}},
        "ru-RU"
    ),
    WithSource = Table.AddColumn(Typed, "SourceFile", each fileName, type text)
in
    WithSource

Явные типы параметров ловят неправильный вызов раньше. Результат as table обещает вызывающему запросу таблицу, а не иногда список, иногда null. M вычисляет выражения по зависимостям, поэтому названия шагов важнее их визуального порядка: Typed ссылается на Checked и не может возникнуть раньше.

Ошибка тоже является результатом контракта

Обращение DataRows{0} без проверки породило бы техническую ошибку «ключ не соответствует строкам таблицы». Она ничего не говорит владельцу файла. Мы сначала проверили ровно один лист и создали Error.Record с кодом, сообщением и именем. Вызывающий запрос может применить try fxReadSales(...) и собрать ошибки по файлам.

Не следует превращать функцию в универсальный комбайн с двадцатью логическими параметрами. Если два источника имеют разную семантику, им нужны две небольшие функции и, возможно, общий вспомогательный шаг. Параметр уместен, когда меняется значение, а не весь алгоритм.

Проверяем функцию без папки

Создайте три крошечных файла: корректный, без Amount и с двумя листами Data. В отдельных запросах вызовите функцию напрямую через File.Contents. Такой набор быстрее полного обновления папки и остаётся регрессионной проверкой после изменения функции.

Проверка должна охватывать типы. Текст «неизвестно» в Amount обязан стать видимой ошибкой, а не нулём. Дата 03/04/2026 должна читаться по договорённой локали, иначе апрель и март поменяются местами без ошибки.

Метаданные функции для коллеги

В Power Query можно добавить описание функции и параметров через тип с метаданными. Даже без этого оставьте рядом запрос-пример с одним вызовом и таблицей ожидаемых столбцов. Пользователь функции не должен открывать Advanced Editor, чтобы понять, что передавать.

Критерий готовности: новый разработчик получает функцию, три тестовых файла и описание результата. Он должен суметь добавить ещё один филиал, не меняя тело функции и не спрашивая автора о скрытых предположениях.

Функция M ценна не сокращением строк, а централизацией договора. Один вход, один результат, понятные отказы — и изменение формата исправляется в одном месте.

Проверьте себя
1. Что даёт явный тип параметра content as binary?
AАвтоматически скачивает файл
BРаньше обнаруживает вызов с неверным типом
CУдаляет ошибки из таблицы
DОбъединяет все листы
2. Зачем создавать Error.Record с именем файла и списком столбцов?
AЧтобы сделать файл больше
BЧтобы ошибка была диагностируемой вызывающим запросом
CЧтобы скрыть проблему
DЧтобы изменить локаль Excel
3. Когда лучше создать две функции вместо одного универсального комбайна?
AКогда источники имеют различную семантику и алгоритм
BКогда имена файлов отличаются
CКогда есть два пользователя
DВсегда после третьей строки M