Для тех, кто в танке
3.38K subscribers
8 photos
2 videos
3 files
196 links
Канал создан для себя, обсуждаем вопросы использования языка M и шарим всякие полезные ссылки.
На вопросы отвечаем в комментах и тут - t.me/pbi_pq_from_tank_chat

Для желающих поддержать канал - https://sponsr.ru/pq_m_buchlotnik/
Download Telegram
Excel.Workbook - собираем все листы из файла
#АнатомияФункций – Excel.Workbook, Table.Combine, Table.ExpandTableColumn

Всем привет!
В продолжение этой темы давайте разберем вопрос – как собрать информацию со всех листов в файле. Мы помним, что для этого нам понадобится Excel.Workbook, а вот дальше есть варианты:

Вариант 0 – нужна только информация с листов
let
from = Excel.Workbook(File.Contents("путь\файл.xlsx"), true),
sheets = Table.SelectRows(from, each [Kind]="Sheet"),
to = Table.Combine(sheets[Data])
in
to
from – подключение к файлу, второй аргумент true – использование первой строки в качестве заголовков
sheets – выбрали только листы (если вам нужны таблицы - [Kind]="Table")
to – ну и соединили таблицы воедино

Вариант 1 – нужно сохранить названия листов, данные на листах типовые
let
from = Excel.Workbook(File.Contents("путь\файл.xlsx "), true),
sheets = Table.SelectRows(from, each [Kind]="Sheet"),
cols = Table.SelectColumns(sheets,{"Name", "Data"}),
nms = Table.ColumnNames(cols{0}[Data]),
to = Table.ExpandTableColumn(cols, "Data",nms)
in
to
первые два шага те же
cols – выбрали столбцы с названиями листов и данными
nms – получили заголовки столбцов из первой таблицы
to – раскрыли табличный столбец

Вариант 2 - нужно сохранить названия листов, шапки таблиц могут различаться
let
from = Excel.Workbook(File.Contents("путь\файл.xlsx "), true),
sheets = Table.SelectRows(from, each [Kind]="Sheet"),
cols = Table.SelectColumns(sheets,{"Name", "Data"}),
nms = List.Distinct(List.Combine(List.Transform(cols[Data], Table.ColumnNames))),
to = Table.ExpandTableColumn(cols, "Data",nms)
in
to

меняем только шаг nms – в этой ситуации мы получаем списки заголовков со всех таблиц, собираем в один список и оставляем только уникальные

Как бы и всё. Если таблицы не требуют дополнительных преобразований, количество кода минимально (особенно в варианте 0), а пользы приносит много.

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Table.FromList + GroupKind.Local – шустрое подобие регулярок
#АнатомияФункций - Table.FromList, GroupKind.Local

Всем привет!
Опять зачастили вопросы по регуляркам, которые не подвезли. Посему хочу разобрать небольшой пример и предложить принцип решения - разбиваем текст посимвольно, а затем группируем их в соответствии с заданным условием.
Ну поехали:
let
ld = List.Buffer({"0".."9"}),
ldp = List.Buffer(ld&{"(",")"," ","-"}),

f=(x)=>[a = Table.FromList(Text.ToList(x),(x)=>{x,if List.Contains(ldp,x) then 0 else 1},{"y","z"}),
b = Table.Group(a,"z",{"i",each Text.Combine([y])},GroupKind.Local)[i],
c = List.Transform(b,(x)=>Text.Select(x,ld)),
d = List.Select(c,(x)=>x<>"")][d],

lst = { " +7 (495) 617-61-16б доб. 1503",
"тел: +7-913-728-64-87 Владимир",
"(4152), доб. 23-81-47 +7962 215 17 17",
"8 (423) 2 60-84-84, 143(внутренний)",
"8 (495) 229-49-69, доб. 114",
"+7 (383) 286-87-08 ext.24120"},
to = Table.FromList(lst,(x)=>{x}&f(x),4)
in
to

lst – наш источник, для простоты в виде списка – задача вынуть номера телефонов
to – собрали из списка таблицу с использованием функции f

Ну а теперь о функции.
Для начала заведём два вспомогательных списка – ld и ldp
ld
– список цифр (именно символов, кавычки не забываем) - это список символов, которые нас интересуют в конечном результате
ldp – к списку ld добавили символы, которые могут встречаться в номере телефона – скобки, пробелы, дефис - это список символов, которые могут быть объединены в одну группу
Обращаю внимание – списки вынесены в отдельные шаги и помещены в List.Buffer – это важно для ускорения работы

Теперь сами шаги функции:
a – собираем таблицу из списка символов анализируемого значения. На выход получим таблицу из двух столбцов – y это сам символ, а z – будет нулевым, если символ может содержаться в номере и единица – если не может
b – группируем по z, локально, пятый аргумент не пишем (подробно про пятый аргумент разбиралось тут), агрегируем в текст. Таким образом все потенциальные символы из номера телефона будут сгруппированы в отдельные текстовые значения
c – чистим полученные тексты, оставляя только символы из списка ld
d
– выбираем не пустые тексты
Собственно, всё. Тонкую настройку работы функции можно осуществить меняя списки ld и ldp, экспериментируйте )))

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Table.Skip – отделяем лишнее в таблице
#АнатомияФункций - Table.Skip

Всем привет!
По мотивам недавнего обсуждения в чате обсудим вопрос пропуска лишних строк в таблице.
Основной пример:
let
from = Table.FromRecords(Json.Document(Binary.Decompress(Binary.FromText("3VO7DsIwDPwV5LlDHTuEsvIZhaEVMIUyZUL8O6qDVAeUSK0YgOXk6+Nsn+32BrurD5cBYTsE76snNSmllHJKbUrXkd6rP9CGfaiZziMySmwFjSALnuTtRuJ6+p66+AR+se+yNqjuY8fHCbmPPoHKjh9OLUPgZkSLq7nVmC8xOfbiVOE0LRT1KjYq7vRCLZRAeLN0tobRli7UIFB2gboa+YPjZTlQJi5MZCFxHvMjdfl5IpWmaZSmze8IFm4BXaJI+SqbQpX2tcrDAw=="),Compression.Deflate))),
skip=(x)=>Table.Skip(x),
to = skip(from)
in
to
Далее будем менять только функцию skip.

Итак, в примере выше функции Table.Skip передан только один аргумент – сама таблица, поэтому будет пропущена только первая строка. Результат не впечатляющий, пропустить нужно несколько, поэтому:
skip=(x)=>Table.Skip(x,8)
Так уже лучше, но мы задали количество строк в явном виде. Но что если мы не знаем сколько строк необходимо пропустиь? Почему-то многие начинают именно вычислять число пропускаемых строк, сооружая жуткие конструкции с индексацией и прочим.

Фишка состоит в том, что во второй аргумент можно передать условие:
skip=(x)=>Table.Skip(x,(r)=>r[Column1]=null)
Обращаю внимание – функцию во втором аргументе пишем от записи. Будут пропущены все строки, которые СООТВЕТСТВУЮТ условию. В данном случае пока в Column1 будет встречаться null.
Результат не очень – давайте просто сменим столбец:
skip=(x)=>Table.Skip(x,(r)=>r[Column2]=null)
Теперь результат как надо.
Понятно, что мы не знали сколько строк пропустить, но знали в каком столбце искать. Причём искать можно не только null:
skip=(x)=>Table.Skip(x,(r)=>r[Column1]<>"заголовок")
в данном случае поиск по Column1 закончится на строке, содержащей «заголовок».

Но что, если мы знаем какое слово искать, но не знаем ни строки, ни столбца? Да пожалуйста:
skip=(x)=>Table.Skip(x,(r)=>not List.Contains(Record.ToList(r),"ключ"))
в этой ситуации каждую строку мы превращаем в список и проверяем в нём наличие конкретного значения.

Наконец, в самом плохом случае, мы можем даже значение целиком не знать, только его фрагмент. Но тоже не проблема:
skip=(x)=>Table.Skip(x,(r)=>not Text.Contains(Text.Combine(List.Select(Record.ToList(r),(x)=>x is text)),"клю"))
Тут финт ушами состоит в том, что превратив строку в список, мы выбираем только текстовые значения (is text), сцепляем все в одну строку (при этом пропускаем все null) и уже её проверяем на наличие фрагмента. Выходит достаточно шустро.

Как-то так. Простая функция, несложные приёмы, но очень лаконично можно получить нужный результат в одну строку.

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Обновление навигации - второе меню у PQfromtankbot

Всем привет! Наш главный админ - @PQfromtankbot
- уже несколько месяцев активно трудится на благо помощи в навигации по каналу.
Анализ запросов показал, что все предпочитают жмакать кнопки, а не набирать текст. Поэтому немного обновил меню /help
Теперь доступны две команды -
/category - вызывает уже многим привычное меню категорий
/key - вызывает меню выбора ключевого слова для поиска

если новое меню будет пользоваться популярностью, будем его развивать и расширять

Также напоминаю, что боту можно отсылать любой текст - для него это сигнал для поиска по базе. Если какой-то запрос окажется в лидерах, добавим его в новое меню. Как-то так.

Надеюсь, будет полезно.
Всех благ!
@buchlotnik
Propis - сумма прописью
#АнатомияФункций - custom

Всем привет! Всплыл скелет из шкафа - когда-то давно писал функцию для суммы прописью, там нашлась бага. Багу исправил, выкладываю как есть, код хоть и старый но вполне годный:
Propis=(num)=>
[ ln = { {"десять ","одиннадцать ","двенадцать ","тринадцать ","четырнадцать ","пятнадцать ","шестнадцать ","семнадцать ","восемнадцать ","девятнадцать "},
{"","сто ","двести ","триста ","четыреста ","пятьсот ","шетьсот ","семьсот ","восемьсот ","девятьсот "},
{"","","двадцать ","тридцать ","сорок ","пятьдесят ","шестьдесят ","семьдесят ","восемьдесят ","девяносто "},
{"","один ","два ","три ","четыре ","пять ","шесть ","семь ","восемь ","девять "},
{"","одна ","две ","три ","четыре ","пять ","шесть ","семь ","восемь ","девять "}},
lr = { {"","миллиард ","миллиарда ","миллиарда ","миллиарда ","миллиардов "},
{"","миллион ","миллиона ","миллиона ","миллиона ","миллионов "},
{"","тысяча ","тысячи ","тысячи ","тысячи ","тысяч "},
{""," "," "," "," "," "}},
fn = (n,r)=> [ t = List.Transform(Text.ToList(n), each Number.From(_)),
x = ln{1}{t{0}} & (if t{1}<>1 then ln{2}{t{1}} & (if r = 2 then ln{4}{t{2}} else ln{3}{t{2}}) else ln{0}{t{2}}),
y = lr{r}{if t{1}=1 or t{2}=0 then 5 else List.Min({t{2},5})},
z = if Number.From(n)=0 then "" else x&y][z],
t = Splitter.SplitTextByRepeatedLengths(3)(Number.ToText(num,"000000000000")),
z = if num = 0 then "ноль" else Text.Combine(List.Transform({0..3},each fn(t{_},_)),"")][z]

Работает с целыми числами от нуля до 999 999 999 999. Кому нужны триллионы - завидую, но допиливайте самостоятельно (уговорили - допилил)😉

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Зачем нужны вспомогательные запросы при объединении файлов из папки?
#НеВсеЙогуртыОдинаковоПолезны
Всем добра! Регулярно то тут, то там всплывает проблема по сборке данных из кучи файлов, которые сначала нужно причесать.
Сегодня расскажу про механизм автоматической генерации запросов интерфейсом редактора.
Итак, когда делаете запрос к папке и разворачиваете структуру бинарников как на картинке☝️, то автоматически генерится вот такая структура из запросов:
где
123 - это собственно запрос к папке с названием "123"
Запрос "Пример файла" - это запрос, который по заданному в нем алгоритму выбирает тот единственный файл, из папки по образцу которого будет формироваться шаблонный запрос обработки и функция на его основе.
Запарос "Параметр1" - это собственно параметр, значением которого является бинарное содержимое файла из запроса "Пример файла"
Запрос "Преобразовать пример файла" - это шаблонный запрос, в который в качестве параметра передается бинарник из предыдущего шага и в нем же формируется алгоритм обработки единичного файла из папки перед сборкой в единый массив
Функция "Преобразовать файл" - это функция, которая связана с запросом "Преобразовать пример файла", Что это значит? Это значит что код функции берется напрямую из этого запроса, т.е. если изменить запрос-шаблон, то автоматически изменится и функция. Эта самая функция как раз и вызывается для обработки бинарников в запросе "123".
Что из вышеизложенного следует? Лезем в этот самый запрос-шаблон (он же "Преобразовать пример файла") и в нем производим причесывание по выбранному вами алгоритму, а далее, когда будем разворачивать содержимое файлов в единый массив, там уже будут все нужные вам строки и столбцы. Такая обработка всегда работает гораздо быстрее.
Зачем все это надо? Для обеспечения юзерам возможности легкого редактирования шаблонного запроса после подключения к папке: нет мороки с написанием этого запроса вручную, с переделыванием ее в функцию, с отлавливанием багов, если ошибка скрывается именно в этой функции, и много чего еще.
Продолжение следует...
Table.ReplaceErrorValues, Table.RemoveRowsWithErrors – чистим входящие данные
#АнатомияФункций - Table.ReplaceErrorValues, Table.RemoveRowsWithErrors

Всем привет!
Дважды за последние сутки всплывал вопрос как поступать с ошибками данных в исходнике. Имеются ввиду ошибки на листах - #ДЕЛ/0!, #ССЫЛКА!, и т.д. Понятное дело, что наилучшее решение – влезть в исходник и ошибки исправить. Но если у нас доступ только на чтение или, например, в файле протянуты формулы, а ячейки не все заполнены, что и вызывает ошибки – в такой ситуации приходится заниматься обработкой при загрузке.

Здесь два варианта – заменить или удалить.
Начнём с замены – нам в помощь Table.ReplaceErrorValues.
Тут всё просто:
Table.ReplaceErrorValues(table, {"Column2", null})
Вторым аргументом – {имя столбца,на что заменить ошибку}
Хотя если мышкоклацать, код будет выглядеть так:
Table.ReplaceErrorValues(table, {{"Column2", null}})
Т.е. автоматом вводится список списков - ведь за один раз можно обрабатывать более чем один столбец:
Table.ReplaceErrorValues(table, {{"Column2", null},{"Column4", null}})
Причём в разных столбцах значение для замены может быть разным:
Table.ReplaceErrorValues(table, {{"Column2", null},{"Column4", 0}})

Но что, если мы хотим обработать сразу все столбцы:
Table.ReplaceErrorValues(table, List.Transform(Table.ColumnNames(table),(x)=>{x,null}))
Т.е. получили список всех столбцов и превратили его в список списков вида {имя, null}

Теперь с удалением строк. Тут работает Table.RemoveRowsWithErrors.
Синтаксис ещё проще:
Table.RemoveRowsWithErrors(table, {"Column2"})
Но обращаю внимание – функция ищет ошибки только в указанном столбце, хотя удаляет всю строку с ошибкой. Для нескольких столбцов всё также несложно:
Table.RemoveRowsWithErrors(table, {"Column1", "Column2", "Column3"})

А как удалить строки с ошибками сразу по всем столбцам? И вот тут мы не будем сооружать ничего:
Table.RemoveRowsWithErrors(table)
Т.е. по умолчанию без второго аргумента функция обрабатывает все столбцы (обратите на это внимание – через мышкоклац второй аргумент всегда добавляется и может порождать неприятные сюрпризы).

Как-то так – ошибки в исходных данных можно очистить, но всегда важно понимать – не повлечет ли такое удаление или замена искажения аналитики, может все-таки лучше поправить исходник.

Надеюсь, было полезно.
Всех благ!
@buchlotnik
UnFill или как учесть значения из предыдущих или последующих строк
#АнатомияФункций – Table.ToColumns, Table.FromColumns

Всем привет! В чате подкинули задачку – сделать «FillDown наоборот». Сложность задачи состоит в том, что при преобразовании столбца нам необходимо учесть значения в предыдущих строках. Напрямую M так не умеет, решения через индексы – это в категорию #НеВсеЙогуртыОдинаковоПолезны; но есть приём, который и используем.
Сразу код:
let 
UnFill =(tbl,col,optional up)=>
[ a = Table.ColumnNames(tbl),
b = List.PositionOf(a,col),
c = Table.ToColumns(tbl),
d = c{b},
e = if up is null or up = 0 then {null}&List.RemoveLastN(d,1) else List.Skip(d),
f = List.Transform(List.Zip({d,e}),(x)=>if x{0}=x{1} then null else x{0}),
g = List.ReplaceRange(c,b,1,{f}),
h = Table.FromColumns(g,a)
][h],

from = Table.FromRecords(Json.Document(Binary.Decompress(Binary.FromText("xdExCoAwDAXQu3R2aNJUoVdRF6tOxa2TeHcxESq4qENcPh1+8qBpV9NlS0BHOss5lqSB394EruF8KWAZIcloKq4RcEph4pQ9YAJslTaJ+uSSU9JX3VtSar6Qgp2Dw2OY/oK//POPNsTbXfGB5HWYWodpdBi987DU7w=="),1))),
unfilldown = UnFill(from,"Данные"),
unfillup = UnFill(from,"Данные",1),
unfilldown2 = UnFill(from,"Данные",0)
in
unfilldown2

Видим функцию UnFill – я немного обобщил задачу, поэтому функция принимает анализируемую таблицу, столбец для преобразования и опциональный аргумент, который отвечает за выбор FillDown или FillUp мы хотим «отменить».
По шагам:
a – получили список имен столбцов
b – нашли номер интересующего нас столбца
c – получили список списков – список, где каждый элемент – это список значений конкретного столбца
d – взяли интересующий нас столбец
e – а вот и начинается магия: если у нас Down – мы берем пустой элемент и добавляем к нему наш список без последнего значения, т.е. получаем список предыдущих значений; если Up – наоборот, удаляем первый элемент и получаем список последующих значений
f
– попарно объединяем элементы исходного и преобразованного списков и сравниваем их – если одинаковые – возвращаем null, иначе сохраняем элемент исходного списка
g – теперь дело за малым – подсовываем результат наших преобразований на место исходного столбца (обратите внимание на конструкцию {f} – нам нужно передать элемент списка)
h – ну и собираем таблицу обратно

Всё, задача решена. unfilldown, unfillup, unfilldown2 – просто примеры корректного синтаксиса при использовании.
Вот так, курите списки, чтобы шаманить таблицы 😉

Надеюсь, было полезно.
Всех благ!
@buchlotnik
PropisRub - сумма прописью (дополнение)
#АнатомияФункций – custom
Всем привет!
Начали тут. Не думал, что добавить триллионы рублей такая проблема, но раз уж надо:
PropisRub=(x)=>
[ ln = { {"десять ","одиннадцать ","двенадцать ","тринадцать ","четырнадцать ","пятнадцать ","шестнадцать ","семнадцать ","восемнадцать ","девятнадцать "},
{"","сто ","двести ","триста ","четыреста ","пятьсот ","шестьсот ","семьсот ","восемьсот ","девятьсот "},
{"","","двадцать ","тридцать ","сорок ","пятьдесят ","шестьдесят ","семьдесят ","восемьдесят ","девяносто "},
{"","один ","два ","три ","четыре ","пять ","шесть ","семь ","восемь ","девять "},
{"","одна ","две ","три ","четыре ","пять ","шесть ","семь ","восемь ","девять "}},
lr = { {"","триллион ","триллиона ","триллиона ","триллиона ","триллионов "},
{"","миллиард ","миллиарда ","миллиарда ","миллиарда ","миллиардов "},
{"","миллион ","миллиона ","миллиона ","миллиона ","миллионов "},
{"","тысяча ","тысячи ","тысячи ","тысячи ","тысяч "},
{"","","","","",""}},
lc = { {"лей","ль","ля","руб"},
{"ек","йка","йки","копе"}},
txt=(n,r)=>[ a = List.Transform(Text.ToList(n), each Number.From(_)),
b = ln{1}{a{0}} & (if a{1}<>1 then ln{2}{a{1}} & (if r = 3 then ln{4}{a{2}} else ln{3}{a{2}}) else ln{0}{a{2}}),
c = lr{r}{if a{1}=1 or a{2}=0 then 5 else List.Min({a{2},5})},
d = if Number.From(n)=0 then "" else b&c][d],
cur=(x,y)=>[ a = Number.From(x),
b = if Number.IntegerDivide(a,10)=1 or Number.Mod(a+9,10)>3 then lc{y}{0} else if Number.Mod(a,10)=1 then lc{y}{1} else lc{y}{2},
c = lc{y}{3}&b][c],
prp=(x)=>[ a = Splitter.SplitTextByRepeatedLengths(3)(x),
b = Text.Combine(List.Transform({0..4},each txt(a{_},_)),""),
c = if Number.From(x)=0 then "ноль " else b][c],
lst = Text.Split(Number.ToText(Number.Round(x,2),"000000000000000.00","en-US"),"."),
res = prp(lst{0})&cur(Text.End(lst{0},2),0)&" "&lst{1}&" "&cur(lst{1},1)][res]

Расширен диапазон, рубли и копейки склоняются. Юзайте на здоровье. Багрепорты по использованию приветствуются, а вот перламутровых пуговиц с расправленными крыльями не держим.

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Buffer 1 – List.Buffer
#АнатомияФункций – List.Buffer

Всем привет! Что-то назрело обсуждение буферов в M. Начнем с самого, на мой взгляд, простого – List.Buffer. В качестве аргумента функция принимает список и … возвращает список. Суть в том, что список помещается в память и является стабильным (stable). Иными словами, после помещения в буфер список заново не вычисляется.
Зачем оно надо?
Возьмем такой пример:
let
from = #table({"fio"},{{"Иван ИвановичИванов"},{"Пётр ПетровичПетров"},{"Евлампий ЕвлампиевичЕвлампиев"},{"JohnSmith"}}),
f=(x)=>Text.Combine(Splitter.SplitTextByCharacterTransition({"a".."z"}&{"а".."я"},{"A".."Z"}&{"А".."Я"})(x)," "),
to = Table.TransformColumns(from,{"fio",f})
in
to

Фамилия прилипла к имени и отчеству. Разделяем через Splitter.SplitTextByCharacterTransition и обратно собираем, но уже через пробел. Всё просто, но не совсем.
Дело в том, что функция f будет вызываться для каждого элемента столбца. А в качестве аргументов у нас указаны последовательности ({"a".."z"} - это не сокращённая запись, это именно последовательность, которая вычисляется на основании кодов символов) – таким образом аргументы будут вычисляться при каждом вызове. И вот чтобы этого избежать пишем так:
let
from = #table({"fio"},{{"Иван ИвановичИванов"},{"Пётр ПетровичПетров"},{"Евлампий ЕвлампиевичЕвлампиев"},{"JohnSmith"}}),
a = List.Buffer({"a".."z"}&{"а".."я"}),
b = List.Buffer({"A".."Z"}&{"А".."Я"}),
f=(x)=>Text.Combine(Splitter.SplitTextByCharacterTransition(a,b)(x)," "),
to = Table.TransformColumns(from,{"fio",f})
in
to

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

Надеюсь, было полезно.
Всех благ!
@buchlotnik
? ?? – Field Access и Coalesce – или зачем задавать вопросы в коде
#АнатомияФункций – основы

Всем привет!
Давайте поставим точку в вопросе знаков вопроса 😉. Уже разбирали тут, поэтому просто напомню:
{1,2,3,4}{7} //ошибка – элементов в перечислении недостаточно
[a= "Вася", b= "Петя", c="Коля"][d]//ошибка – поле записи не найдено
Excel.CurrentWorkbook(){[Name="Таблица124"]}[Content]//ошибка, если нет такой таблицы

Проблема решается путем использования ? (знак вопроса):
{1,2,3,4}{7}? //null
[a= "Вася", b= "Петя", c="Коля"][d]? //null
Excel.CurrentWorkbook(){[Name="Таблица124"]}?[Content]? //null
Т.е. при обращении к элементу списка или полю таблицы, в случае их отсутствия оператор ? вернёт нам null – это лучше, чем ошибка.

Но что если нам не нужен null? Ведь результат может использоваться далее в расчётах и нам может понадобиться 0, {}, [], "" (ноль, пустой список, пустая запись, пустая строка и т.п.) или просто служебное сообщение вроде «не найдено».
Во тут нам на помощь и приходит оператор ??объединение (coalesce). В общем виде это выглядит так:
x??y
если x не null – будет возвращён x, иначе – y.
Ну а в нашем случае:
{1,2,3,4}{7}? ??0 //0 – ноль
[a= "Вася", b= "Петя", c="Коля"][d]? ?? ""//пустая строка
Excel.CurrentWorkbook(){[Name="Таблица124"]}?[Content]? ??"данные не найдены"// данные не найдены
возвращается нужное значение - главное не забыть поставить пробел между ? и ?? .

Вот так – никаких try, никаких if then else, просто знаки вопроса, поставленные в правильном месте.
В чате можно глянуть на боевой пример – берите на вооружение.

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Text.Combine – собираем текст воедино
#АнатомияФункций – Text.Combine

Всем привет! Проанализировал свои сообщения из чата – в лидерах используемых функций List.Transform (потому что на списках быстрее), далее Excel.CurrentWorkbook (без комментариев), далее Table.ColumnNames (её я не разбирал, но тут и разбирать нечего – даёт список имён столбцов таблицы), а вот четвертой оказалась Text.Combine – если честно, удивлён – но думается тут есть о чём поговорить.
Итак, справка:
Text.Combine(texts as list, optional separator as nullable text) as text

texts – список текстовых значений, проверяем:
Text.Combine({"мама","мыла","раму"}) //мамамылараму
Text.Combine({"a".."f"}) //abcdef
Text.Combine({"1".."5"}) //12345
Text.Combine({1..5}) //Expression.Error: Не удается преобразовать значение 1 в тип Text
Последний вариант - это одна из типичных ошибок, объединять можно только текст.

separator – необязательный аргумент, без него объединение идет через «ничего», а точнее пустую строку:
Text.Combine({"мама","мыла","раму"},"") //мамамылараму
Получили то же самое, что и выше.
Ну а если использовать не нулевую строку:
Text.Combine({"мама","мыла","раму"}," ") // мама мыла раму
Text.Combine({"мама","мыла","раму"},"-") // мама-мыла-раму
Text.Combine({"мама","мыла","раму"},"-&-") // мама-&-мыла-&-раму
Т.е. строка произвольной длины просто подставляется между элементами.

Ещё одна типичная задача – собрать текстовые значения, но чтобы каждое было с новой строки:
Text.Combine({"мама","мыла","раму"},"#(lf)")
Здесь "#(lf)"- escape-последовательность для обозначения непечатаемого символа - перевода строки (кому нужна табуляция - "#(tab) " – правда Excel её не отобразит, но если вы потом копируете текст в другое место – табуляция там будет).

Escape-последовательности можно комбинировать с обычными символами:
Text.Combine({"мама","мыла","раму"},";#(lf)")

Наконец, символ не обязательно должен быть непечатаемым:
Text.Combine({"мама","мыла","раму"},"#(00A0)")
Кто не узнал – это неразрывный пробел (A0 - это 160 в шестнадцатеричной системе). Т.е. пишем "#(шестнадцатеричный код Юникода) " – и подставляем любой код по вкусу. Ну а если у вас плохо с шестнадцатеричной системой, символы можно вызвать и по старинке:
Text.Combine({"мама","мыла","раму"},Character.FromNumber(160))
Character.FromNumber понимает обычное (десятичное) представление.

Как-то так. Ничего сложного – главное всё преобразовать в текст и выяснить код нужного разделителя.

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Date + Time = ? получаем датувремя из даты и времени
#АнатомияФункций - DateTime.From, DateTime.FromText

Всем привет! Что-то последнее время несколько раз проскочила задачка по объединению даты и времени в датувремя. Зачем оно надо? Да просто реально частенько дата и время хранятся в отдельных столбцах, а надо анализировать периоды – т.е. разницы между строками и можно заморочиться на вычисление разницы дат и отдельно времени, а можно получить датувремя и просто считать разницу – реально удобнее.
Ну поехали. Пример будет предельно простой:
let
date = #date(2022,8,6),
time = #time(1,41,26),
datetime0 = date + time, // Не удается применить оператор + к типам Date и Time
datetime1 = DateTime.From(Number.From(date) + Number.From(time)),//06.08.2022 1:41:26
datetime2 = DateTime.From(Text.From(date)&" "&Text.From(time)), //06.08.2022 1:41:00
datetime3 = DateTime.From(Text.From(date)&" "&Time.ToText(time,"hh:mm:ss")),//06.08.2022 1:41:26
datetime4 = DateTime.From(Text.From(date)&Time.ToText(time,[Format=" hh:mm:ss"])),//06.08.2022 1:41:26
datetime5 = DateTime.From(date) + Duration.From("0."&Time.ToText(time,[Format="T"])),//06.08.2022 1:41:26
datetime6 = DateTime.FromText("06.08.2022"&"01:41:26",[Format="dd.MM.yyyyhh:mm:ss"]),//06.08.2022 1:41:26
datetime7 = DateTime.FromText("2022-08-06T01:41:26",[Format="yyyy-MM-ddThh:mm:ss"]),//06.08.2022 1:41:26
datetime8 = date & time //06.08.2022 1:41:26

in
datetime8

Для начала – date и time – исходные дата и время. Обращаю внимание date – это просто имя, а #date() – функция, создающая значение даты, с time аналогично – не забываем и не путаем – диез решает )))
Теперь по шагам:
datetime0 – самое простое и естественное, что может придумать эксельщик – попробовать сложить два значения. Но так не выйдет – здесь нет автоматического преобразования типов, а дата и время – это разные типы данных. Поэтому придется колдовать.

datetime1 – раз нельзя просто сложить – пробуем привести типы – и дату, и время превращаем в числа, суммируем, из результата получаем датувремя – работает, прям как в Excel – и надо сказать это самый быстрый (с точки зрения производительности) способ

datetime2 – альтернативный способ – превратить оба значения в текст, сконкатенить и получить датувремя из текста – здесь есть проблема – Text.From "съедает" секунды.

datetime3 – решение проблемы секунд – использовать Time.ToText – в этом шаге представлен старый синтаксис, который работает в любой версии – вторым аргументом указан формат

datetime4 – то же самое, но в новом синтаксисе – здесь вторым аргументом идёт запись, одно из полей которой – формат. Обращаю внимание, что пробел можно "загнать" в формат

datetime5 – ещё один способ, который особенно хорош, если у вас дата и время уже на входе в виде текста – превратить дату в датувремя, а время – в длительность и сложить (этот способ быстрее, чем через конкатенацию текста, но медленнее чем через Number.From)

Ну и обсуждая DateTime.From, нельзя не вспомнить про DateTime.FromText – специализированная функция для вынимания датывремени из текста.
datetime6 – оказывается можно не заморачиваться с пробелами, а просто "объяснить" в каком формате данные находятся на входе. Причём формат может быть самый разный.

datetime7 – кто получал такие значения в запросах и потом парился со строковыми преобразованиями и локалями сейчас наверное прослезились от умиления – так тоже можно.

UPD datetime8 - так увлекся преобразованиями, что не написал главное (спасибо @sboy_ko - напомнил) - дату и время нельзя складывать, но можно конкатенировать (всё-таки и то и другое - особая форма record )

Собственно, всё. Вроде ничего запредельного – главное помнить про типы данных и форматы. Через текст нагляднее, через числа быстрее – но всё зависит от конкретной задачи и типов данных на входе.

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Table.AddIndexColumn – снова получаем значения из предыдущих или последующих строк
#АнатомияФункций – Table.AddIndexColumn, List.Buffer

Всем привет! Сегодня в чате немножко без подготовки и с пол оборота случился челленж на тему получения значения из предыдущей строки. Мы уже решали такую задачу в ходе реализации UnFill. Сегодня же выяснилось, что во-первых, можно и через Table.Join (очень шустрый вариант от @MaximZelensky), ну а во-вторых… можно просто через Table.AddIndexColumn и список.

Собственно код:
let
tbl = Table.FromColumns({{"a".."i"},{1..9}},{"a","b"}),
lst = List.Buffer({null}&tbl[b]),
add = Table.AddIndexColumn(tbl,"c",0,1),
to = Table.TransformColumns(add,{"c",(x)=>lst{x}})
in
to
tbl – сама таблица,
lst – список значений нужного столбца, с добавленным в начало нулевым значением (и не забываем про буфер)
add – добавили столбец индекса
to – трансформируем индекс в значение из списка – поскольку мы добавили в начало списка нулевое значение, по индексу мы получим значение из предыдущей строки, что и требовалось )))

Сделаем другой вариант (он медленнее буквально на пару процентов, но интереснее):
let
tbl = Table.FromColumns({{"a".."i"},{1..9}},{"a","b"}),
lst = List.Buffer(tbl[b]),
add = Table.AddIndexColumn(tbl,"c",-1,1),
to = Table.TransformColumns(add,{"c",(x)=>if x<0 then null else lst{x}})
in
to
Здесь мы просто получили список, а вот нумерацию начали с -1. Т. е. точно также сдвинули позицию на 1 назад, правда пришлось предотвратить ошибку отрицательного индекса: (x)=>if x<0 then null else lst{x}
Зачем было усложнять? - да всё дело в том, что просто играя с третьим аргументом AddIndexColumn можно сдвигаться на любое число позиций вверх или вниз:
let
tbl = Table.FromColumns({{"a".."i"},{1..9}},{"a","b"}),
lst = List.Buffer(tbl[b]),
add = Table.AddIndexColumn(tbl,"c",1,1),
to = Table.TransformColumns(add,{"c",(x)=>lst{x}?})
in
to
Всё тоже самое, но сдвиг теперь вперед на 1 – так мы получаем следующую строку (ну и не забыли вопросик)

А теперь объединяем всё в одну функцию:
let
tbl = Table.FromColumns({{"a".."i"},{1..9}},{"a","b"}),
AddOtherRowColumn = (tbl,col,newcol,index) =>
[ lst = List.Buffer(Table.Column(tbl,col)),
add = Table.AddIndexColumn(tbl,newcol,index,1),
f = if index <0 then (x)=> if x <0 then null else lst{x}
else (x)=>lst{x}?,
to = Table.TransformColumns(add,{newcol,f})][to],
to = AddOtherRowColumn(tbl,"b","c",-1),
to1 = AddOtherRowColumn(tbl,"b","c",-3),
to2 = AddOtherRowColumn(tbl,"b","c",2)
in
to2
По аргументам всё думаю понятно – таблица, столбец, имя нового столбца и на сколько строк сдвинуть.
Просто, лаконично и главное шустро! Юзайте на здоровье.

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Измерение скорости запросов - Мерка 1.7
#ПолезныеСсылки

Всем привет!
Вышла новая версия Мерки - 1.7
Добавлена возможность выбора числа холостых прогонов, в окно теперь выводится информация о запросе - тип, число диапазонов в книге, находится ли запрос в модели. Обновлена строка состояния - отображается процент выполнения, текущий запрос и номер итерации.
Как всегда, сложена на гитхаб.

Надеюсь, будет полезна.
Всех благ!
@buchlotnik
List.Accumulate + Record.Field против Join-ов или словарь на записях
#АнатомияФункций - Record.Field, List.Accumulate

Всем привет!
Периодически в чате всплывает приём, по которому задают вопрос «что за магия»?
На самом деле приём широко известен в узких кругах, бывает эффективен, поэтому разберем.
Итак задача: есть таблица ФИО/должность, есть таблица Должность/оклад/премия, на выходе хотим таблицу ФИО/должность/оклад/премия. Понятное дело, что это решается через Join:
let
a = #table({"ФИО","Профессия"},{{"Вася","Игрок"},{"Петя","Игрок"},{"Коля","Тренер"},{"Евлампий","Врач"}}),
b = #table({"Должность","Оклад","Премия"},{{"Игрок",1000,100},{"Тренер",2000,5000},{"Врач",100,1}}),
c = Table.Join(a,"Профессия",b,"Должность"),
d = Table.RemoveColumns(c,"Должность")
in
d
Но сразу можно сказать – жульничество – там Должность, там – Профессия, где заморочки с переименованием? Где удаление лишних столбцов, которые всегда есть? И т.д. Ну ОК, тогда NestedJoin:
let
a = #table({"ФИО","Профессия"},{{"Вася","Игрок"},{"Петя","Игрок"},{"Коля","Тренер"},{"Евлампий","Врач"}}),
b = #table({"Должность","Оклад","Премия"},{{"Игрок",1000,100},{"Тренер",2000,5000},{"Врач",100,1}}),
c = Table.NestedJoin(a,"Профессия",b,"Должность","tmp"),
d = Table.ExpandTableColumn(c, "tmp", {"Оклад","Премия"})
in
d
Здесь, думаю ситуация и решение многим знакомы.

Тогда о чём пост? Да просто хочу предложить альтернативу:
let
a = #table({"ФИО","Профессия"},{{"Вася","Игрок"},{"Петя","Игрок"},{"Коля","Тренер"},{"Евлампий","Врач"}}),
b = #table({"Должность","Оклад","Премия"},{{"Игрок",1000,100},{"Тренер",2000,5000},{"Врач",100,1}}),
с = List.Accumulate(Table.ToRows(b),[],(s,c)=>Record.AddField(s,c{0},List.Skip(c))),
d = (x)=>x&Record.Field(с,x{1}),
e = Table.FromList(Table.ToRows(a),d,{"ФИО","Профессия","Оклад","Премия"})
in
e
Что поменялось?
Возник странный шаг c – с аккумулятором и пустой записью. Здесь мы таблицу соответствий разбили на строки (Table.ToRows) и аккумулируем полученные значения – первый столбец превращаем в название поля записи, остальной список – в значение данного поля. Т.е. на выходе получили запись, где поля содержат нужные значения (словарь). Зачем оно надо? Да просто при обращении к записи будет сразу выниматься значение нужного нам поля, без поиска.
Далее функция d – её аргументом будет список - мы увидим это на следующем шаге – она берет сам список и добавляет к нему значение нужного поля из словаря. Т.е. к списку значений по строке добавили список значений из таблицы подстановок
Ну и на шаге e – мы эту функцию подсовываем в мой любимый FromList.
Как бы всё. Наверняка немножко непривычно, но так может оказаться быстрее, особенно на больших таблицах. Пробуйте )))

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Record.FieldOrDefault, Record.FromTable – немножко оптимизации в словари на записях
#АнатомияФункций - Record.FieldOrDefault, Record.FromTable

Всем привет!
В прошлый раз мы посмотрели, как можно реализовать подстановку данных из другой таблицы через записи.
Возникает закономерный вопрос – а что делать, если в словаре есть не все соответствия?
Мы знаем про вот такой синтаксис
[a=1,b=2,c=3][d]? // null
И даже такой:
[a=1,b=2,c=3][d]? ?? "не найдено"// не найдено
Но они применимы при обращении к элементам записи или списка, а вот функция Record.Field выдаст ошибку и вопросик не спасет.

Зато спасёт другая, не менее полезная функция - Record.FieldOrDefault.
Record.FieldOrDefault([a=1,b=2,c=3], "d") //null
Record.FieldOrDefault([a=1,b=2,c=3], "d","не найдено") // не найдено

Т.е. без третьего аргумента имеем эквивалент [ ]?, с третьим аргументом – эквивалент [ ]? ??
Ну и применяем это к коду из прошлого поста:
let
a = #table({"ФИО","Профессия"},{{"Вася","Игрок"},{"Петя","Игрок"},{"Коля","Тренер"},{"Евлампий","Врач"}}),
b = #table({"Должность","Оклад","Премия"},{{"Игрок",1000,100},{"Тренер",2000,5000}}),
с = Record.FromTable(Table.FromList(Table.ToRows(b),(x)=>{x{0},List.Skip(x)},{"Name","Value"})),
d = (x)=>x&Record.FieldOrDefault(с,x{1},{}),
e = Table.FromList(Table.ToRows(a),d,{"ФИО","Профессия","Оклад","Премия"})
in
e

Смотрим на шаг d – в качестве альтернативного значения указан не null, а пустой список {} – это важно, иначе будет ошибка несовпадения типов при конкатенации (ну а если нужно возвращать не null, а значение по умолчанию – также пишем список, но уже со значениями: {"не найдено","не положено"})

Таже обращаю внимание на шаг с. В прошлый раз мы использовали Accumulate, но если таблица соответствий большая, он может замедлять работу (на тестовом словаре в 50k строк сегодня просто умер). В этой ситуации есть более элегантный способ собрать большую запись - Record.FromTable. Но есть нюанс - на вход нельзя просто подать исходную таблицу, таблица обязательно должна состоять из двух столбцов - "Name" и "Value". Ну и в примере показано как это можно сделать через Table.ToRows+Table.FromList.

Собственно, всё - с отсутствующими совпадениями разобрались, большие таблицы в записи превратили, дело за малым – попробовать на практике. Успехов!

Надеюсь, было полезно.
Всех благ!
@buchlotnik
TableRenameColumnsByPositions или как переименовать столбцы по номеру
#АнатомияФункций – custom functions

Всем привет! Сегодня очередной раз был поднят вопрос о переименовании столбцов в таблице по номеру (позиции). По этому поводу функция:

[func =(table,list) =>
[ a = List.Buffer(Table.ColumnNames(table)),
b = List.Count(a),
c = (x) =>{a{if x{0}>0 then x{0}-1 else b+x{0}},x{1}},
d = if list{0} is list then List.Transform(list,c) else c(list),
e = Table.RenameColumns(table,d)
][e],
typ = type function (
table as (type table meta [Documentation.FieldCaption = "исходная таблица"]),
list as (type list meta [Documentation.FieldCaption = "параметры переименования"])
)
as table meta [
Documentation.Name = "TableRenameColumnsByPositions> (@buchlotnik)",
Documentation.LongDescription = "Функция переименовывает столбцы таблицы по их номеру. Параметры переименования задаются в виде списка {номер, новое название} либо списка списков {{номер1, новое название1},{номер2, новое название2}}. Положительное значение номера задает нумерацию с начала таблицы, отрицательное - с конца.",
Documentation.Examples = {
[Description = "переименовать первый столбец", Code = "=TableRenameColumnsByPositions(#table({""a"",""b"",""c""},{}),{1,""первый""})",Result="#table({""первый"",""b"",""c""},{})"],
[Description = "перименовать первый с конца столбец", Code = "=TableRenameColumnsByPositions(#table({""a"",""b"",""c""},{}),{-1,""первый с конца""})",Result="#table({""a"",""b"",""первый с конца""},{})"],
[Description = "переименовать первый и последний столбцы", Code = "=TableRenameColumnsByPositions(#table({""a"",""b"",""c""},{}),{{1,""первый""},{-1,""последний""}})",Result="#table({""первый"",""b"",""последний""},{})"]
}
],
result = Value.ReplaceType(func,typ)
][result]

Глобально по шагам:
func – сама функция
typ – описание её типа – что-то подобное мы уже смотрели в постах про типизацию и документацию
result – возвращаем функцию со всеми метаданными, чтоб при вызове глаз радовался)))

Ну а теперь по функции.
Общая идея состоит в том, что обращаться к столбцам (полям) можно только по имени, но у нас есть возможность получить список имен столбцов, а вот со списком можно прекрасно работать по номерам элементов. Поехали:
a – получили список имен столбцов
b – нашли их количество, нам это нужно, если нумерация идёт с конца
c – функция преобразования, которая превращает номер в имя столбца и возвращает пару {изменяемое_имя,новое_имя}. Логика такая: если номер положительный – берем просто нужный элемент списка (там -1 поскольку нумерация с нуля), а если отрицательный – берем с конца, т.е. складываем общее число столбцов с тем, на сколько столбцов от конца нужно отступить
d – поскольку преобразование одного столбца – это просто список, а нескольких – список списков, то проверяем, чем у нас является первый элемент переданного аргумента – если он список, то преобразованию нужно подвергать список списков с использованием List.Transform, в противном случае однократно применяем функцию преобразования c к аргументу list
e
– подсовываем список преобразования штатной Table.RenameColumns

Собственно, всё. Немножко пришлось пошаманить для универсальности, но в целом, надеюсь, сложностей с пониманием возникнуть не должно. Юзайте на здоровье!

Надеюсь, было полезно.
Всех благ!
@buchlotnik
(x)=>x или зачем нужна функция, которая ничего не делает?
#АнатомияФункций – общие вопросы

Всем привет!
В свое время мы разбирали как написать «пустой аргумент» - нам это было нужно, поскольку различные операции осуществляются с разными типами данных и важно задать правильный. Но точно так же бывают ситуации, когда в качестве аргумента мы должны передать функцию, а на самом деле никаких преобразований не требуется.
Вот в таких ситуациях нас и выручает
(x)=>x
Это функция, принимающая аргумент x и возвращающая этот самый аргумент. Фактически мы написали функцию, которая ничего не делает (но при этом является функцией!).
Зачем оно надо? Смотрим пример и ниже разберём по шагам:
let
tbl = #table({"n","a".."f"},{{"i",1..5,{1,2,3}},{"j",2..6,{1,2}},{"k",3..7,{1}},{"i",4..8,{1,2}},{"j",5..9,{1}}}),
group = Table.Group(tbl,"n",{"tmp",(x)=>x}),
add = Table.AddColumn(tbl,"tmp",(x)=>x),
pivot = Table.Pivot(tbl[[n],[a]], {"i".."k"}, "n", "a",(x)=>x),
comb = Table.CombineColumns(tbl,List.Skip(Table.ColumnNames(tbl)),(x)=>x,"tmp"),
splt = Table.SplitColumn(tbl,"f",(x)=>x),
from = Table.FromList(tbl[f],(x)=>x)
in
from

tbl – входные данные
group – мы группируем по столбцу n и хотим получить все данные, поэтому функция агрегации нам и возвращает их все. Заодно мы узнали, что при группировке получается таблица (table), т.е. если вы заранее не знаете от какого типа данных писать агрегатор – напишите (x)=>x и увидев результат, делайте выводы

add – добавление столбца к таблице, аналогично – функция возвращает всё, что поступило на вход – и в данном случае это запись (record). Т.е. мы выяснили, что на вход функции подается строка целиком в виде записи

pivot – не люблю эту функцию, но сказать необходимо – при сведении данных от нас также хотят функцию агрегации, которой на вход приходит… список (list). Мышкоклацем так не сделать – если выбрать «не агрегировать» - функция будет записана без пятого аргумента и выдаст ошибку, а так всё работает.

Все примеры выше больше направлены на понимание того, с каким типом данных мы работаем, но есть вполне прикладные таблично-списочные операции:

comb – хотим собрать значения из ряда столбцов в список, от нас требуют Combiner как функцию – только у нас на входе и так список значений, и теперь мы знаем что с этим делать

split – этот пример уже разбирали - нам вроде как нужно разделить столбец, только он уже разделен, поэтому не делаем ничего

from – ну и для меня наиболее частый случай – сбор таблицы из списков – «почему не просто через Table.FromRows?» спросите вы – «потому что попробуйте прям на этом примере» отвечу я – FromRows требует списков одинаковой длины, а FromList – это функция пяти аргументов и с ней можно договориться, просто в данном случае её аргумент Splitter не должен делать ничего.

Как-то так. В М крайне важно использовать/передавать/обрабатывать корректные типы данных. Функция – это тоже тип данных и сегодня мы выяснили, как её написать, чтобы она была, но «ничего не делала».

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Table.SelectColumns, Table.RemoveColumns, Table.ReorderColumns – или жонглируем столбцами таблицы
#АнатомияФункций - Table.SelectColumns, Table.RemoveColumns, Table.ReorderColumns

Всем привет!
В М существует ряд функций для работы со столбцами, но писать по каждой отдельный пост как-то скучно, а вот если объединить – может будет информативно.
Итак SelectColumns, RemoveColumns, ReorderColumns – имеют два обязательных и один необязательный аргумент:
table – таблица, которую преобразуем
columns/columnsOrder – имя столбца или список имен столбцов (выбирать или удалять можно и один столбец, а вот менять порядок – только у списка)
missingfield – необязательный аргумент, говорящий что делать с отсутствующими столбцами – как водится имеет два варианта - MissingField.UseNull или MissingField.Ignore

Смотрим код и ниже разбираем шаги:
let 
from = #table({"a".."h"},{{1..8}}),

lst1 = Table.ColumnNames(from),
select1 = Table.SelectColumns(from,lst1),
remove11 = Table.RemoveColumns(from,lst1),
reorder1 = Table.ReorderColumns(from,lst1),

lst2 = {List.Last(lst1),List.First(lst1)},
select2 = Table.SelectColumns(from,lst2),
remove2 = Table.RemoveColumns(from,lst2),
reorder2 = Table.ReorderColumns(from,lst2),

lst3 = List.Reverse(List.Alternate(lst1,1,1,1)),
select3 = Table.SelectColumns(from,lst3),
remove3 = Table.RemoveColumns(from,lst3),
reorder3 = Table.ReorderColumns(from,lst3),

lst4 = {"k","j","i"}&List.Skip(lst1),
select41 = Table.SelectColumns(from,lst4,MissingField.Ignore),
remove41 = Table.RemoveColumns(from,lst4,MissingField.Ignore),
reorder41 = Table.ReorderColumns(from,lst4,MissingField.Ignore),

select42 = Table.SelectColumns(from,lst4,MissingField.UseNull),
remove42 = Table.RemoveColumns(from,lst4,MissingField.UseNull),
reorder42 = Table.ReorderColumns(from,lst4,MissingField.UseNull)
in
reorder42

lst1 – список имён столбцов таблицы
select1 и reorder1 – ничего не поменялось – одна функция выбрала столбцы, другая расположила столбцы в том порядке, в котором они и шли, а remove1 – вернула пустую таблицу – поскольку все столбцы пришлось удалить. Всё логично, но как-то не показательно. Другое дело, что со списком столбцов можно поиграть, и вот тут начинается интересное

lst2 – взяли только последнее и первое имена из списка
select2 – получили только последний и первый столбцы
remove2 – таблица без первого и последнего столбцов
reorder2 – таблица, в которой первый и последний столбцы поменяны местами. Обращаю внимание – Select просто берет те столбцы, которые указали, в том порядке, в котором указали, а вот Reorder – столбцы из второго аргумента берет в нужном порядке, но остальные оставляет на местах

lst3
– более сложный пример – List.Alternate даст нам все столбцы на нечётных позициях, а Reverse – расположит их в обратном порядке (иначе не будет понятно зачем нам reorder)
select3 – все нечётные столбцы в обратном порядке
remove3 – все четные столбцы (нечетные были удалены)
reorder3 – чётные столбцы на местах, нечётные – в обратном порядке

lst4 – ну и типичный вариант при сборе из кучи файлов – надо собрать/удалить столбцы, а не во всех файлах они присутствуют – чтобы не было ошибки прописываем MissingField
remove41, remove42
– вернулась таблица со столбцом а – остальные удалены, лишние были проигнорированы
select41 – таблица без первого столбца, лишние проигнорированы
reorder41 – исходная таблица (лишние проигнорированы, первый столбец был пропущен в списке и поэтому остался на месте)
select42 – таблица со всеми столбцами из второго аргумента, отсутствующие столбцы заполнены null
reorder42 – а вот это самое интересное – столбец a остался на месте, а далее все столбцы из второго аргумента

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

Надеюсь, было полезно.
Всех благ!
@buchlotnik