Для тех, кто в танке
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
List.Select и все все все
#АнатомияФункций – List.Select

Всем привет! Последнее время было несколько вопросов про отбор данных по условию. По этому поводу хочу разобрать List.Select, поскольку к пониманию работы этой функции всё в общем и сводится.
Читаем справку
List.Select(list as list, selection as function) as list
Всё как обычно – список (list) и функция выбора (selection). Пример даётся простой:
List.Select({1..100}, each _>90)
Сразу вспоминаем, что это можно переписать как функцию
List.Select({1..100}, (x)=>x>90)
где x – элемент анализируемого списка, нам это ещё пригодится.
Как водится, функция может быть любой (главное, чтобы в результате она давала true или false).

Например, выводим числа, кратные пяти:
List.Select({1..100}, (x)=>Number.Mod(x,5)=0)

или выводим даты только рабочих дней (с понедельника по пятницу):
List.Select(List.Dates(#date(2022,1,1),365,#duration(1,0,0,0)),(x)=>Date.DayOfWeek(x,1)<5)

Также можно комбинировать условия.
Например, выводим числа, кратные трём ИЛИ пяти:
List.Select({1..100}, (x)=>Number.Mod(x,3)=0 or Number.Mod(x,5)=0)

А так кратные трём И кратные пяти :
List.Select({1..100}, (x)=>Number.Mod(x,3)=0 and Number.Mod(x,5)
(я знаком с математикой, можно было просто проверить делимость на пятнадцать, но это просто пример)

Ну и важно помнить, что списки могут состоять не только из отдельных значений.
Например, обращение к списку списков:
List.Select({{1,1},{1,2},{2,1},{2,2}},(x)=>x{0}>1)
В данном случае x – это список и мы проверяем его первый элемент x{0}

Аналогично можно обращаться к списку записей:
List.Select({[a=1,b=1],[a=1,b=2],[a=2,b=1],[a=2,b=2]},(x)=>x[a]>1)
В этом случае x – это запись, и мы проверяем конкретное её поле – x[a].

Остальное всё то же самое, например,комбинируем условия:
List.Select({[a=1,b=1],[a=1,b=2],[a=2,b=1],[a=2,b=2]},(x)=>x[a]>1 or x[b]<2)

Ничего не напоминает? Ровно так и работает Table.SelectRows:
Table.SelectRows(Table.FromRecords({[a=1,b=1],[a=1,b=2],[a=2,b=1],[a=2,b=2]}),(x)=>x[a]>1 or x[b]<2)
При выборе строк проверяется каждая из них, но строка – это запись, фактически это и есть работа со списком записей.

Да и в случае Table.SelectColumns без List.Select бывает не обойтись:
let
from = #table({"имя","a1","a2","итого a","b1","b2","b3","итого b"},{{"вася",1,2,3,4,5,6,15},{"петя",1,3,4,5,7,9,21},{"коля",2,4,6,8,10,12,30}}),
lst = List.Select(Table.ColumnNames(from),(x)=>x="имя" or Text.Contains(x,"итого")),
to = Table.SelectColumns(from,lst)
in
to
т.е. lst - из таблицы мы получаем полный список названий столбцов и выбираем «имя» или те, которые содержат «итого».
to - полученный список использовали в SelectColumns.

Как-то так – определяем анализируемую структуру, чтобы понимать, от какого аргумента следует писать функцию, прописываем условие (или комбинацию условий) и получаем нужный результат. List.Select редко используется сам по себе, а вот в связке с другими функциями - весьма полезная штука.

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

Всем привет!
Пару дней назад в чате всплыла задачка, которая периодически возникает у пользователей – преобразовать время (точнее текст) вида "35:40:06" в собственно время.
Нужно сказать, что M не позволяет подобных вольностей – #time, #date, #datetime, #datetimezone – требуют передачи аргументов в заданных диапазонах – часов не может быть больше 23, секунд и минут больше 59 и т.д. Другое дело #duration (длительность) – эта функция переваривает что угодно и выдаёт нужный формат:
#duration(0,0,0,1000000) //11.13:46:40
Т.е. миллион секунд даёт нам 11 дней 13 часов 46 минут и 40 секунд – штука удобная и часто используется при переводе Unix-времени в человеческое:
#datetime(1970,1,1,0,0,0)+#duration(0,0,0,1671278969) //17.12.2022 12:09:29

Ок, инструмент найден и теперь давайте его применять.
Попробуем написать функцию, превращающую текст выше в длительность:
f=(x)=>let
h = Number.From(Text.BeforeDelimiter(x,":")),
m = Number.From(Text.BetweenDelimiters(x,":",":")),
s = Number.From(Text.AfterDelimiter(x,":",1))
in
#duration(0,h,m,s)
Т.е. взяли текст до двоеточия, между двоеточиями и после второго двоеточия, преобразовали в числа и запихнули в #duration – так оно работает, но слишком много телодвижений (найдутся, конечно, те, кто скажет - "зато прозрачно и понятно" - но получать медленный запрос просто из-за удобства восприятия на мой взгляд сомнительно).
ОК, как бы ускориться? Давайте просто разделим текст по разделителю:
f=(x)=>[a=List.Transform(Text.Split(x,":"),Number.From),b=#duration(0,a{0},a{1},a{2})][b]
Такой вариант работает существенно (в разы) быстрее, поскольку мы один раз получили список чисел, а потом его элементы передали в #duration (аналогичное решение через мышкоклац – разделить столбец по разделителю, добавить вычисляемый, удалить лишнее – всё то же самое, просто медленнее раза в полтора).

Но хочется красоты – как бы не делать два шага в функции, а вот сразу передать результат Text.Split в #duration? Дело было утром и я как-то сразу и не сообразил, но в чат стремительно ворвался @IlyaNazarov и явил миру вот это:
f=(x)=>Function.Invoke(#duration,{0}&List.Transform(Text.Split(x,":"),Number.From))
Ну сказка, а не код! - Function.Invoke получает первым аргументом функцию (в данном случае #duration), а вторым – список передаваемых аргументов – это результат нашего Text.Split, но с добавленным в начале {0}- -потому что в тексте нет информации о количестве дней, а аргумент обязательный.
Ну и как бы всё – на лету полученный список передан в функцию. Прирост по скорости не космический, но присутствует. Все промеры традиционно в первом комментарии под постом.

Илья, спасибо за напоминание!
А в целом обращаю внимание, что есть такой класс функций – Function. – и периодически они бывают полезны. Курите стандартную библиотеку – в ней много интересного )))

Надеюсь, было полезно.
Всех благ!
@buchlotnik
List.Generate + Date.Add* - получаем списки дат и времён
#АнатомияФункций - List.Generate

Всем привет!
Сегодня в очередной раз написал в чат генератор списка времён. Видимо вопрос актуальный, поэтому разбираем:

let
// List.Generate(()=>дата_начала,(x)=>x<=дата_окончания,(x)=>соответствующая_функция_добавления)
// #date(year,month,day)
// #datetime(year,month,day,hour,minute,second)
// #duration(days,hours,minutes,seconds)

y = List.Generate(()=>#date(2021,1,1),(x)=>x<=#date(2025,12,31),(x)=>Date.AddYears(x,1)),
q = List.Generate(()=>#date(2021,1,11),(x)=>x<=#date(2025,12,31),(x)=>Date.AddQuarters(x,1)),
qm = List.Generate(()=>#date(2021,1,11),(x)=>x<=#date(2025,12,31),(x)=>Date.AddMonths(x,3)),
m = List.Generate(()=>#date(2021,1,1),(x)=>x<=#date(2025,12,31),(x)=>Date.AddMonths(x,1)),
w = List.Generate(()=>#date(2021,1,1),(x)=>x<=#date(2025,12,31),(x)=>Date.AddWeeks(x,1)),
wd = List.Generate(()=>#date(2021,1,1),(x)=>x<=#date(2025,12,31),(x)=>Date.AddDays(x,7)),
d = List.Generate(()=>#date(2021,1,1),(x)=>x<=#date(2025,12,31),(x)=>Date.AddDays(x,1)),
d1 = List.Generate(()=>#date(2021,1,1),(x)=>x<=#date(2025,12,31),(x)=>x+#duration(1,0,0,0)),
h = List.Generate(()=>#datetime(2023,4,16,0,0,0),(x)=>x<=#datetime(2023,4,18,0,0,0),(x)=>x+#duration(0,1,0,0)),
m15 = List.Generate(()=>#datetime(2023,4,16,0,0,0),(x)=>x<=#datetime(2023,4,18,0,0,0),(x)=>x+#duration(0,0,15,0))
in
m15

Сначала комменты в коде – сам List.Generate мы уже разбирали тут. Принципиально ничего сложного – имеем дату/датувремя начала, на каждой итерации проверяем не превышение даты/датывремени окончания, функция добавления прибавляет к дате/датевремени нужный период.

Сначала рассмотрим семейство Date.Add*.
y - Date.AddYears добавляет годы, вторым аргументом регулируем шаг в годах (мало ли, вам пятилетки нужны)
q - Date.AddQuarters добавляет кварталы
qmDate.AddQuarters можно заменить Date.AddMonths с шагом 3 месяца
m – так выглядит генерация с шагом в месяц
w – с шагом в неделю
wd - Date.AddWeeks можно заменить Date.AddDays с шагом в 7 дней
d – ну и генерация с шагом в 1 день соответственно

Теперь рассмотрим альтернативу:
d1 – вместо Date.AddDays можно использовать +#duration(1,0,0,0) – т.е. добавление длительности в 1 день
Соответственно использование #duration позволяет генерить датувремя с шагом, меньше суток:
h – генерим с шагом в 1 час
m15 – шаг 15 минут

Как видите схема генерации принципиально не меняется, главное помнить про List.Generate – он есть, и он хороший )))

Надеюсь, было полезно.
Всех благ!
@buchlotnik
ЧИСТРАБДНИ на М или практическое применение List.Generate
#АнатомияФункций - List.Generate
Всем привет!

В продолжение прошлого поста про генерацию дат давайте обсудим вопрос генерации списка только рабочих дней, ну и соответственно реализации экселевской ЧИСТРАБДНИ (NETWORKDAYS)

Комплексный пример выглядит так:
let
f=(ot as date,do as date, optional holidays as list) as number=>
[ a=List.Generate(()=>ot,(x)=>x<=do,(x)=>Date.AddDays(x,1)),
b=List.Select(a,(x)=>Date.DayOfWeek(x,Day.Monday)<5),
c=List.RemoveMatchingItems(b,holidays),
d=List.Count(if holidays=null then b else c)][d],

hol = {#date(2023,2,24),#date(2023,1,2),#date(2023,1,3),#date(2023,1,4),#date(2023,1,5),#date(2023,1,6),#date(2023,1,7),#date(2023,5,8),#date(2023,2,23),#date(2023,3,8),#date(2023,5,1),#date(2023,5,9),#date(2023,6,12),#date(2023,11,6)},
from=#table(type table [ot=date,do=date],{{#date(2023,1,1),#date(2023,12,31)},{#date(2023,1,1),#date(2023,3,31)},{#date(2023,4,1),#date(2023,6,30)},{#date(2023,7,1),#date(2023,9,30)},{#date(2023,10,1),#date(2023,12,31)}}),
to = Table.AddColumn(from,"networkdays",(r)=>f(r[ot],r[do],hol),Int64.Type)
in
to

Разбираем по шагам:
f – функция, эквивалентная ЧИСТРАБДНИ – о ней чуть ниже
hol – список праздников,
from – исходная таблица
to – результирующая – вроде всё прозрачно.

Теперь сама функция f.
Вводим три аргумента: ot – дата начала, do – дата окончания, holidays – необязательный аргумент, список праздников

Шаги внутри функции:
a – генерация списка дней от даты начала до даты окончания – сделано через AddDays – это эффективнее, чем +#duration(1,0,0,0)
b
– выбираем только рабочие (номер дня в неделе меньше 5, считая с понедельника, напоминаю, что нумерация идёт с нуля)
c – удаление из полученного списка праздничных дней – сделано через List.RemoveMatchingItems – это шустрее, чем List.Difference
d
– последний шаг, возвращающий число дней – обращаю внимание – идёт проверка на наличие третьего аргумента: если передан список выходных – подсчитываем список из шага c, иначе – подсчитываем список из шага b
В принципе, если вам нужно именно сгенерировать список дат рабочих дней – останавливайтесь на шаге c.

Как-то так. Код достаточно короткий и на мой вкус логичный – пользуйтесь!

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