Для тех, кто в танке
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
Table.PositionOf – подстановка по нескольким условиям с диапазонами или когда FillDown бессилен
#АнатомияФункций - Table.PositionOf

Всем привет!
В чате подкинули интересную задачку – поэтому есть что обсудить.
Сначала рассмотрим лайтовый вариант – есть таблица соответствий категорий некоторому признаку, причём подразумевается некоторый диапазон от/до. Ну и есть таблица, в которую по значению признака надо подставить категорию. Рассмотрим классический подход к решению:
let
dct = #table({"ширина","категория"},{{0,"узко"},{100,"средне"},{250,"широко"}}),
tbl = #table({"что","ширина"},{{"стул",60},{"кресло",80},{"стол",150},{"диван",220},{"сервант",280}}),
cmb = tbl&dct,
srt = Table.Sort(cmb,{"ширина"}),
fll = Table.FillDown(srt,{"категория"}),
to = Table.SelectRows(fll,each [что]<>null)
in
to
dct – таблица с категориями, здесь для простоты оставлена только нижняя граница (т.е. ширина от…)
tbl – таблица, куда подставляем
cmb – объединили обе таблицы
srt – отсортировали по нашему признаку
fll – сделали заполнение вниз
to – оставили только интересующие нас строки.
Вместо ширины бывает производительность, предельное напряжение, дата производства и т.д.
В общем поковыряйте приём – он крайне полезен.

НО вчера было про другое – подстановка нужна по ДВУМ параметрам. И вот тут выясняется, что сортировка нам не поможет (в качестве упражнения пытливые умы могут проверить).
Приходится искать по исходной таблице по двум параметрам. И вот тут хочу предложить воспользоваться Table.PositionOf – она аналогична List.PositionOf , просто про таблицу )))
Поехали:
let
dct=#table({"длина","толщина","категория"},{{0,0,"фитинг детский"},{0,2,"фитинг"},{0,5,"фитинг усиленный"},{10,0,"патрубок учебный"},{10,2,"патрубок"},{10,8,"патрубок напорный"},{100,0,"труба китайская"},{100,5,"труба"},{100,12,"труба напорная"}}),
tbl=#table({"что","длина","толщина"},{{"деталь1",7,1.5},{"деталь2",24,2.2},{"деталь3",80,9.9},{"деталь4",120,15},{"деталь5",140,0.8}}),
lst=List.Buffer(dct[категория]),
f=(x)=>lst{Table.PositionOf(dct,x,Occurrence.Last,g)},
g=(x,y)=>x[длина]<=y[длина] and x[толщина]<=y[толщина],
to = Table.AddColumn(tbl,"категория",f)
in
to

dct, tbl – словарь и таблица соответственно
lst – сами категории забрали в отдельный список (обращение по индексу в списке быстрее, чем к таблице)
f – функция, обращающаяся к списку категорий по индексу. Индекс находим с помощью Table.PositionOf –берем последнюю строку, соответствующую условию (Occurrence.Last), а соответствие условию проверяем функцией g
g
– функция от двух аргументов – в данном случае это записи: x – искомая, y – запись из таблицы (т.е. конкретная строка в таблице для поиска). Само условие прописываем как сопоставление отдельных полей записей (главное не запутаться, что на первом месте - что ищем, а на втором где; ну и не забываем между условиями добавлять оператор and)
to –а дальше просто применили нашу функцию в момент добавления столбца к таблице.

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

Надеюсь, было полезно.
Всех благ!
@buchlotnik
1C - Новая рубрика на канале
#1C - Содержание
Дамы и господа, Вашему вниманию предлагается еще одно направление в «Танке» - обработка сохраненных файлов с отчетами из системы 1С с созданием нормализованной таблицы (таблиц).
На скрине (см. первый комментарий) – один из примеров выгрузки данных в xlsx-формате.
Отличительной особенностью таких отчетов в большинстве случаев является сложная иерархическая структура, которая не позволяет проводить нормальный анализ данных без предварительной обработки выгруженного файла, например: составить сводную таблицу, выгрузить информацию в модель данных для дальнейшей обработки и так далее.
Конечно, самое простое – это попросить программиста 1С в компании – выгрузить плоскую таблицу, но не всегда это удается (то программиста нет в компании, то ума у программиста) и простой пользователь выгружает данные (зачастую несколько десятков тысяч строк) в Excel, открывает PQ и клацая мышкой создает километры шагов для того, чтобы получить более-менее нужную информацию.
Циклом мини-статей по направлению #1C мы попробуем создать алгоритм обработки такого рода отчетов, предполагая, что правильно выстроенная последовательность операций позволит пользователю довольно быстро разобраться с выгруженными из 1С данными.
При разборе данных будем использовать как существующие решения из Танка, так и создавать собственные.

Планируемые мини-статьи:
1. Анализ отчета. Убираем пустые столбцы и строки. Очищаем текст
2. Разбираем многоуровневую шапку
3. Разбираем категории, работаем с иерархической структурой
4. Создаем справочники и связи
5. Загрузка отчетов 1С из папки и обработка данных

В финале же попробуем создать универсальную функцию, скормив которой исходный файл мы получим на выходе нормализованную таблицу.

Прошу прокомментировать данное предложение. Имеет ли оно право на существование в Танке или же это лишнее и разбор структуры отчета из 1С не имеет никакой сложности с решением?

PS^ Михаил @buchlotnik любезно согласился помочь в данном направлении своими рекомендациями и решениями. Думаю будет очень интересно и познавательно

С уважением к Вам. @CubRoot
1C. Анализ отчета. Поднимаем шапку, убираем пустые столбцы и строки. Очищаем текст.
#1C - Анализ отчета. Поднимаем шапку, убираем пустые столбцы и строки. Очищаем текст.

Всем привет. Этим постом мы начинаем серию постов по применению PQ к выгрузкам из 1C.
Для начала мы попробуем разобрать стандартный отчет "1с". Профессионалы М, могут улыбнуться – это все им знакомо, а те, кто постигает основы – рекомендую присмотреться к данному посту )

В большинстве случаев пользователи стараются сохранить отчеты из 1С в xls(xlsx)-формат для дальнейшей обработки данных, а не, например, в текстовый формат. Причина этого скорее всего лежит в плоскости привычки – получить сразу же на выходе какую-то таблицу в Excel.
Что же мы получаем в таблице отчета на выходе из 1С:
1. Шапка с фильтрами отчета (несколько строк, в которых информация чаще всего не нужна в дальнейшем)
2. Многоуровневая шапка (разбор данного пункта будет представлен в следующей теме)
3. Большое количество пустых столбцов
4. Присутствие пустых строк
5. Как бонус от 1С – частое наличие в полях непечатных символов

Поехали, разбираться с отчетом:
1-ый шаг:
удаляем шапку отчета. Для чего воспользуемся уже готовым решением из танка:
Table.Skip(tbl,(r)=>not Text.Contains(Text.Combine(List.Select(Record.ToList(r),(x)=>x is text)),TXT))

tbl - таблица данных
TXT – часть имени из шапки таблицы, до которой нужно удалить верхние строки

2-ой шаг: удаляем непечатные символы из текста. Как вариант, воспользуемся вот таким решением:
T= [txt= "#(lf)#(00A0)",k=(x)=> Text.Combine(List.RemoveMatchingItems(Text.SplitAny(Text.Clean(x), txt),{""})," "),to=Table.TransformColumns(t,{},k)][to]

3-ий шаг: удаляем пустые столбцы. Тут так же можно воспользоваться готовым решением из танка:
fnRemEmptyColumns = (tbl)=>
[a = List.Sort(Table.ColumnNames(tbl)),
b = Table.Profile(tbl,{{"tmp", (x)=>true,List.NonNullCount}})[tmp],
c = List.PositionOf(b,0,Occurrence.All),
d = List.Transform(c,(x)=>a{x}),
e = Table.RemoveColumns(tbl,d)][e]
Пояснение по работе кода - читаем в танке
4-ый шаг: удаляем пустые строки. Вариант решения может быть таким:
T= Table.SelectRows(cl, each not List.IsEmpty(List.RemoveMatchingItems(Record.FieldValues(_), {"", null})))
Давайте попробуем объединить все вышеуказанные подходы в одну функцию:.
fnClear=(tbl,optional shp,optional clearData, optional clearColumn, optional clearRows)=> [tr=Table.TransformColumns(tbl,{},Text.From),
t= if shp is null then tbl else Table.Skip(tr,(r)=>not Text.Contains(Text.Combine(List.Select(Record.ToList(r),(x)=>x is text)),shp)),
p= if clearData is null then t else [k=(x)=>if x=null then x else Text.Combine(List.RemoveMatchingItems(Text.SplitAny(Text.Clean(x),clearData),{""})," "),
to=Table.TransformColumns(t,{},k)][to],
cl=if clearColumn is null then p else
[a = List.Sort(Table.ColumnNames(p)),
b = Table.Profile(p,{{"tmp", (x)=>true,List.NonNullCount}})[tmp],
c = List.PositionOf(b,0,Occurrence.All),
d = List.Transform(c,(x)=>a{x}),
e = Table.RemoveColumns(p,d)][e],
to=if clearRows is null then cl else Table.SelectRows(cl, each not List.IsEmpty(List.RemoveMatchingItems(Record.FieldValues(_), {"", null})))][to]

Вызов функции: to=fnClear(from,"Артик","#(lf)#(00A0)",1,1) // таблица, удаление строк до указанных данных в поле, удалить непечатные, удалить пустые столбцы, удалить пустые строки
Обязательным параметром является передаваемая таблица, остальные параметры – необязательные.
К чему этот пост.
Все решения уже так или иначе были представлены или в танке или в чате PQ. Данный пост –попытка объединить некоторые решения в одну функцию. Конечно, нюансов в выгрузках 1С большое количество, но уловив смысл построения функций – можно самостоятельно создавать решения. Попробуйте провести эксперимент и дополнить функцию опцией удаления всех полей, которые содержат в имени «Column» или же добавить опцию поднятия заголовков. Для профессионалов это все давно пройденный этап, а для нас, начинающих познавать красоту языка М, будет очередной возможностью потренироваться.

С уважением к Вам. @CubRoot

PS^ Михаил, спасибо за помощь )
List.Union, List.Intersect, List.Difference или кольца Венна на списках
#ВесёлыеКартинки

Давненько не рисовал - а тут чат прям навеял )))

Надеюсь, было полезно.
Всех благ!
@buchlotnik
List.Union vs List.Combine
#ВесёлыеКартинки

По итогам обсуждения вчерашней картинки - закрепляем разницу между List.Union и List.Combine, а заодно - зачем же нам List.Distinct )))

Надеюсь, было полезно.
Всех благ!
@buchlotnik
Text.Split vs Text.BeforeDelimiter или причём тут списки и почему на них опять быстрее
#АнатомияФункций - Text.Split, Text.BeforeDelimiter

Всем привет!
Небольшая зарисовка вдогонку к сегодняшнему чату.
Итак, есть задача – вытянуть из текста фрагмент от начала до какого-то набора символов, который мы назовём «разделитель». Вроде несложно:
let
from = #table({"txt"},{{"раз"},{"раз; два"},{"раз; два; три"}}),
f=(x)=>Text.BeforeDelimiter(x,"; "),
to = Table.TransformColumns(from,{"txt",f})
in
to
Т.е. у нас есть специальная функция – Text.BeforeDelimiter (её мы уже разбирали тут ).
Код вышел простой и лаконичный, обращаем внимание, что не во всех значениях встретился разделитель, и в этом случае функция вернула нам значение целиком.

В общем всё круто, всё работает, но тогда о чём пост? Да вот, собственно, о чём:
let
from = #table({"txt"},{{"раз"},{"раз; два"},{"раз; два; три"}}),
f=(x)=>Text.Split(x,"; "){0},
to = Table.TransformColumns(from,{"txt",f})
in
to
Здесь мы используем Text.Split – она возвращает список. Соответственно мы вынимаем первый элемент. Идея в том, что при наличии разделителя, первый элемент списка и будет искомым значением; а при отсутствии разделителя функция вернёт список из одного элемента (исходного значения), который мы и получим на выходе.
Зачем такие сложности при наличии специализированной функции? Да просто так быстрее ))) Промеры будут в первом комментарии под постом – с ростом объема анализируемых данных выигрыш по скорости составляет полтора/два раза – неплохо, а?

Предполагаю, что выигрыш возникает из-за того, что Text.BeforeDelimiter слишком умная – у неё же ещё и третий аргумент есть – требуется время на оценку, передан он или нет, наверняка ещё и счётчик вхождений вшит, а в данной конкретной ситуации нам это всё не надо – и дубовая Text.Split справляется ощутимо быстрее.
Так что ещё раз подтверждается тезис, что не всегда специализированные функции решают задачу максимально эффективно )))

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

Всем привет!
Разберём вопрос переименования столбцов – для этого у нас есть Table.RenameColumns:
let
from = #table({"a","b","c"},{{1,2,3}}),
to = Table.RenameColumns(from,{"a","A"})
in
to
Тут всё просто – передали список {что, на что} поменять.

Если переименований нужно несколько – используем список списков:
let
from = #table({"a","b","c"},{{1,2,3}}),
to = Table.RenameColumns(from,{{"a","A"},{"b","B"}})
in
to
Тоже вроде несложно. Определённая сложность возникает, когда таких преобразований много и они у нас содержатся в отдельной таблице:
let
from = #table({"a","b","c"},{{1,2,3}}),
tbl = #table({"что","на что"},{{"a","A"},{"b","B"},{"c","C"}}),
to = Table.RenameColumns(from,Table.ToList(tbl,(x)=>x))
in
to
В этой ситуации мы просто превратили таблицу в список списков через Table.ToList (да, я помню про Table.ToRows, а вы помните, что она почему-то не столь шустрая).

Ну и наконец, зачем нам третий аргумент? Вариант первый – у вас не просто таблица переименования, а набор вариантов, которые встречаются в разных файлах:
let
from = #table({"a","b","c"},{{1,2,3}}),
tbl = #table({"что","на что"},{{"a","A"},{"aa","A"},{"aaa","A"},{"b","B"},{"c","C"}}),
to = Table.RenameColumns(from,Table.ToRows(tbl),MissingField.Ignore)
in
to
Здесь мы использовали MissingField.Ignore, чтобы не вылетала ошибка на отсутствующих столбцах.
В более специфичном варианте можно использовать MissingField.UseNull:
let
from = #table({"a","b","c"},{{1,2,3}}),
tbl = #table({"что","на что"},{{"a","A"},{"b","B"},{"c","C"},{"d","D"}}),
to = Table.RenameColumns(from,Table.ToRows(tbl),MissingField.UseNull)
in
to
Здесь не просто произошло переименование, но и был добавлен недостающий столбец.

Как-то так – третий аргумент необязателен, просто иногда упрощает программирование или жизнь )))

Надеюсь, было полезно.
Всех благ!
@buchlotnik
СЧЁТЕСЛИ, СУММЕСЛИ и прочие радости на M
#АнатомияФункций – приёмы

Всем привет! За последние пару недель уже несколько раз писал в чат один и тот же по сути код. Запрос обычно звучит «как написать СЧЁТЕСЛИ?» или «как сделать что-то вроде СУММЕСЛИ?». Ответ – «сгруппировать» как правило народ не устраивает, поскольку надо сохранить все строки. Хорошо, дадим «правильный» ответ – «сгруппировать, а потом словарь на записях» ))) Вот этот ответ и разберём:
let
from = #table({"A","B","C"},{{"x",1,2},{"x",3,4},{"y",5,6},{"y",7,8},{"y",9,10}}),
f=(x)=>Table.RowCount(x),
dict = Record.FromTable(Table.RenameColumns(Table.Group(from, "A", {"Value",f}),{"A","Name"})),
to = Table.AddColumn(from,"New",(r)=>Record.Field(dict,r[A]))
in
to

по шагам:
from - имеем таблицу, в которой по каждой позиции из столбца А нужно получить сколько раз она встречается в таблице
Чтобы получить агрегацию самое простое таблицу сгруппировать, поэтому
f – пишем функцию, которая заменит СЧЁТЕСЛИ – в данном случае Table.RowCount
и далее
dict – осуществляем группировку, причём агрегированный столбец называем Value, а тот, по которому группировали – Name – нам это нужно, чтобы получить словарь (разбиралось тут)
to – на последнем шаге используем словарь для добавления нового столбца

Думаю, возник вопрос – а зачем я вообще вынес шаг f? Да мне просто лень всё переписывать целиком, у нас же тут есть варианты:
f=(x)=>List.Sum(x[B]) // СУММЕСЛИ по столбцу В
f=(x)=>List.Sum(x[C]) // СУММЕСЛИ по столбцу С
f=(x)=>List.Max(x[C]) // МАКСЕСЛИ по столбцу С
f=(x)=>List.Min(x[B]) // МИНЕСЛИ по столбцу В
f=(x)=>List.Average(x[C]) // СРЗНАЧЕСЛИ по столбцу С
и так далее…

Код выходит вроде несложный, допускает доработку напильником и не только - экспериментируйте! )))

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

Всем привет! Недавно в чате подняли вопрос о вычислении скользящего среднего. Исходное решение через индексы безбожно тупило, поэтому было предложено вот такое:
let
from = #table({"a"},List.Zip({{1..100000}})),
lst = List.Buffer(from[a]),
m = 10,
n = List.Count(lst),
gen = List.Generate(()=>[i=0,l={},a=List.Average(l)],
(x)=>x[i]<n,
(x)=>[i=x[i]+1,l=if i<m-1 then {} else List.Range(lst,i-m+1,m),a=List.Average(l)],
(x)=>x[a]),
to = Table.FromColumns(Table.ToColumns(from)&{gen},Table.ColumnNames(from)&{"new"})
in
to

по шагам:
from – исходная таблица
lst – столбец, по которому считаем среднее (не забыли про буфер)
m – ширина окна усреднения
n – число элементов в списке (засунуть в генератор можно, но так быстрее)
gen – генерация скользящего среднего – в записи [i – счётчик, l – список для усреднения, вычисляемый через List.Range, a – собственно среднее]
to – добавляем новый столбец к имеющейся таблице.

Код логичный, отработал за приемлемое время и можно было успокоиться. Однако, спинной мозг в районе копчика напомнил, что решал я пару лет назад подобную задачу, причём совсем по-другому именно из-за проблемы быстродействия. Пришлось немножко подумать, понять, что в коде выше меня смущает практически всё и поэтому:
let
from = #table({"a"},List.Zip({{1..100000}})),
lst = List.Buffer(from[a]),
m = 10,
n = List.Count(lst),
gen = List.Generate(()=>[i=0,s=lst{i}],
(x)=>x[i]<n,
(x)=>[i=x[i]+1,s=x[s]+lst{i}],
(x)=>x[s]),
gen2 = List.Repeat({null},m-1)&{0}&List.RemoveLastN(gen,m),
zip = List.Transform(List.Zip({gen,gen2}),(x)=>(x{0}-x{1})/m),
to = Table.FromColumns(Table.ToColumns(from)&{zip},Table.ColumnNames(from)&{"new"})
in
to
Первые четыре шага такие же
gen – а вот здесь не выпендриваемся и просто рассчитываем накопленную сумму – такой генератор работает существенно быстрее
gen2 – добавляем в только что сгенерированный список null-ы и отрезаем последние m элементов
zip – а теперь суть – в первом генераторе мы хватали диапазон из списка и находили по нему среднее, т.е. находили сумму элементов, а потом делили на их количество, т.е. каждый элемент у нас участвовал в суммировании несколько раз. Вместо этого в данном случае мы просто сдвинули накопленную сумму на m элементов вправо (или вниз – кто как воспринимает одномерные массивы), объединили исходный и полученный списки через List.Zip и нашли разницы соответствующих элементов – это и есть сумма по конкретному диапазону усреднения (М – математика))), осталось только поделить на число элементов и примотать столбец к таблице как и в первом варианте.

Промеры в первом комментарии под постом – ускорение составляет от десятков процентов на малых объемах до порядка(!) на 100k строк. Как-то так, иногда стоит задумываться о вычислительной сложности.

Надеюсь, было полезно.
Всех благ!
@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
СЖПРОБЕЛЫ, только круче, или причём тут Text.SplitAny
#АнатомияФункций - Text.SplitAny

Всем привет!
В продолжение сегодняшнего обсуждения в чате решим одну задачку. Итак, как видно из названия поста, нам нужно реализовать функцию СЖПРОБЕЛЫ – мы раньше уже обсуждали очистку текста , но проблема в том, что Text.Trim, Text.TrimStart и Text.TrimEnd не удаляют лишние пробелы в середине текста, а СЖПРОБЕЛЫ – удаляет. Что ж, решим эту проблему:
let
tbl = #table({"txt"},{{" мама мыла раму"},{" мама #(lf) мыла #(tab)#(tab)#(lf) раму"}}),
to = Table.TransformColumns(tbl,{"txt",(x)=>Text.Combine(List.Select(Text.Split(x," "),(x)=>x<>"")," ")}),
to1 = Table.TransformColumns(tbl,{"txt",(x)=>Text.Combine(List.Select(Text.SplitAny(x," #(00A0)#(lf)#(tab)"),(x)=>x<>"")," ")}),
to2 = Table.TransformColumns(tbl,{"txt",(x)=>Text.Combine(List.Select(Text.SplitAny(x,""),(x)=>x<>"")," ")}),
f = (txt,optional splitby,optional combby)=>Text.Combine(List.Select(Text.SplitAny(txt,""&(splitby??"")),(x)=>x<>""),combby??" "),
to3 = Table.TransformColumns(tbl,{"txt",f}),
to4 = Table.TransformColumns(tbl,{"txt",(i)=>f(i," ")}),
to5 =Table.TransformColumns(tbl,{"txt",(i)=>f(i,null,"#(lf)")})
in
to5

По шагам:
tbl – исходная таблица с не очень чистым текстом
to – преобразовываем текстовый столбец. Логика простая – разделяем текст по пробелу (Text.Split), при этом получаем список, два пробела подряд также будут разделены и в список попадёт "" (пустая строка), убираем пустые строки (List.Select) и собираем обратно текст через пробел (Text.Combine). Для первой строки в таблице задача решена, но вторая содержит неразрывные пробелы, табуляции, переносы строк – можно ли избавиться и от них тоже? Можно:
to1 – просто используем Text.SplitAny, которой во втором аргументе передадим помимо пробела ещё и другие разделители
Но сегодня выяснилось, что можно и проще:
to2 – снова используем Text.SplitAny, просто в этот раз вторым аргументом передаём пустую строку – и в этой ситуации она сама прекрасно делит текст по пробельным символам и непечатным символам, что на мой вкус удобно
f – ну и подытожим написанием условно универсальной функции. Обязательный аргумент txt – обрабатываемый текст, необязательные – splitby и combby – разделители по которым надо делить текст и разделитель, через который надо обратно собрать текст соответственно.
to3 – просто используем функцию
to4 – делим только по пробелу
to5 – делим по пробельным и нечитаемым символам, а собираем через разрыв строки (обращаю внимание на синтаксис – второй аргумент передан как null)

Как-то так. Вроде несложно, но пришлось пописать, зато получилась СЖПРОБЕЛЫ с блекджеком и … необязательными аргументами. Пользуйтесь!

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

Всем привет! Несколько раз всплывал вопрос как вытащить из файла не только данные, но и уровни иерархии. Иерархия при этом может быть в виде структуры, а может быть и в виде отступов, которые непонятно как ловить (привет 1С). На самом деле нам в обоих случаях надо добраться до xml-разметки листа и получить оттуда нужную информацию. В первом комменте под постом будет пример файла (однолистовая выгрузка) с обоими вариантами, попробуем его спарсить:

let
unzip=Expression.Evaluate(Text.FromBinary(Web.Contents("https://raw.githubusercontent.com/buchlotnik/buchlotnik_functions/main/UnZip.pq")),#shared),
bin = Binary.Buffer(File.Contents("C:\Users\User\Downloads\Структура Item_.xlsx")),
sh=Excel.Workbook(bin,false){0}[Data],
xml=Xml.Document(unzip(bin){[FileName="xl/worksheets/sheet1.xml"]}[Content]){0}[Value]{[Name="sheetData"]}[Value],
f=(x)=>x{[Name="outlineLevel"]}?[Value]?,
g=(x)=> x{0}[Attributes]{[Name="s"]}?[Value]?,
tr=Table.TransformColumns(xml,{{"Attributes",f},{"Value",g}}),
lst=List.Sort(List.Distinct(tr[Value]),(x)=>Number.From(x)),
dict=Record.FromList(List.Positions(lst),lst),
tr1=Table.TransformColumns(tr,{"Value",(x)=>Record.Field(dict,x)}),
lst1=Table.ToList(tr1,(x)=>List.LastN(x,2)),
lst2=Table.ToList(sh,(x)=>x),
to=Table.FromList(List.Zip({lst2,lst1}),List.Combine,Table.ColumnNames(sh)&{"величина отступа","уровень иерархии"})
in
to

По шагам:
unzip – тащим функцию-разархиватор с моего гитхаба (принцип описан здесь, а если хотите держать функцию у себя – заберите её тут)
bin – подключаемся к бинарному содержимому файла и помещаем его в буфер. Суть в том, что нам нужно обратиться к нему дважды – отдельно за данными, отдельно – за иерархией
sh – получили данные с листа традиционным способом
xml – получили данные о том же листе, но через разметку., конкретно sheetdata – обратите внимание, что это таблица, содержащая информацию о строках – содержимое и атрибуты.

Теперь напишем две функции :
f – получает информацию об outlineLevel – это и есть уровень строки в иерархии, если она определена структурой
g – а здесь посложнее, дело в том, что 1С-ые отступы являются атрибутом не строки, а отдельной ячейки. В данном случае мы обращаемся к свойствам первого столбца конкретной строки и забираем оттуда s – величину отступа (если нужен не первый столбец заменяем {0} на другую нужную позицию)
tr – ну и применяем, функцию f к атрибутам строки, а функцию g – к значениям строки – на выходе имеем таблицу с соответствующей построчной информацией.

Следующие три шага не являются обязательными, но позволяют превратить величину отступа в уровень иерархии
lst – получили список уникальных значений и отсортировали по возрастанию
dict – собрали из него словарь
tr1 – заменили отступы на уровни через словарь

lst1, lst2, to - дело осталось за малым – данные и информацию об иерархии надо объединить - в данном случае сделано на списках через построчный zip

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

Надеюсь, было полезно.
Всех благ!
@buchlotnik
ДОбыча иерархии 2 – боремся с пустыми строками
#АнатомияФункций – Xml.Document

Всем привет! В предыдущем посте мы рассмотрели подход к чтению интересующей нас информации из xml-разметки файла. При этом возможно 2 проблемы:
1 – у любого листа есть его dimension (в xml этот параметр называется именно так) – область, в которой, по мнению Excel, находятся данные (в первом каменте пример – данные начинаются с D5, хотя здоровому человеку очевидно, что с D6) – при обращении к листу pq загружает именно эту область
2 – в данных возможны пустые строки, соответственно информация о них в разметке не хранится

Что ж, на примере вытаскивания структуры решим и эти проблемы:
let
unzip=Expression.Evaluate(Text.FromBinary(Web.Contents("https://raw.githubusercontent.com/buchlotnik/buchlotnik_functions/main/UnZip.pq")),#shared),
bin = Binary.Buffer(File.Contents("C:\Users\muzyk\Desktop\Структура Item_trouble.xlsx")),
xml=Xml.Document(unzip(bin){[FileName="xl/worksheets/sheet1.xml"]}[Content]){0}[Value]{[Name="sheetData"]}[Value][Attributes],
val=List.Transform(xml,(x)=>x{[Name="outlineLevel"]}?[Value]?),
nms=List.Transform(xml,(x)=>x{[Name="r"]}?[Value]?),
dict=Record.FromList(val,nms),
sh = Excel.Workbook(bin,false){0}[Data],
add = Table.AddIndexColumn(sh, "Уровень", Number.From(nms{0}),1),
to=Table.TransformColumns(add,{"Уровень",(x)=>Record.FieldOrDefault(dict,Text.From(x))})
in
to

По шагам:
unzip, bin – всё как в прошлый раз
xml – поскольку вынимаем только структуру, сразу загружаем таблицу Attributes для каждой строки
val – получаем для каждой строки значение её outlineLevel
nms – получаем для каждой строки её номер (посмотрите на этот шаг при парсинге файла-примера – можете убедиться, что нумерация не сквозная)
dict – соберем из номеров и уровней словарь на записях
sh – как и в прошлый раз получаем данные первого листа
add – а вот пересборку делаем по-другому – а именно – добавляем столбец индекса, но не с 1, а с первой строки в dimension
to – подцепляем данные из словаря через Record.FieldOrDefault, поскольку не обо всех строках у нас имеется информация

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

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

Всем привет!
Что-то уже несколько раз всплывал вопрос получения отфильтрованной таблицы из экселевского файла.
Общее замечание – при обращении к файлу он читается целиком, поэтому сократить время загрузки не получится, но получить на выходе данные только из отфильтрованных строк можно, для этого мы снова полезем в xml-разметку.

В этот раз напишем функцию для обработки файла:
(bin)=>
let
bin = Binary.Buffer(bin),
xml=Xml.Document(unzip(bin){[FileName="xl/worksheets/sheet1.xml"]}[Content]){0}[Value]{[Name="sheetData"]}[Value][Attributes],
val=List.Transform(xml,(x)=>x{[Name="hidden"]}?[Value]?),
nms=List.Transform(xml,(x)=>x{[Name="r"]}?[Value]?),
dict=Record.FromList(val,nms),
sh = Excel.Workbook(bin,false){0}[Data],
add = Table.AddIndexColumn(sh, "tmp", Number.From(nms{0}),1),
tbl=Table.TransformColumns(add,{"tmp",(x)=>Record.FieldOrDefault(dict,Text.From(x))}),
filtr = Table.SelectRows(tbl, each ([tmp] = null)),
to = Table.RemoveColumns(filtr,{"tmp"})
in
to

Собственно, мы по большей части это разбирали тут и тут
bin – забуферили бинарное содержимое (мы его читаем дважды)
xml – получили данные в виде списка об атрибутах строк
val – получили информацию о свойстве hidden
nms
– получили данные о номере строки
dict – собрали словарь
sh – теперь из бинарного содержимого получили данные с самого листа
add – добавили временный столбец индекса (не забыли учесть, что нумерация может быть не с единицы)
tbl – применили словарь к нашему столбцу индекса, т.е. заменили номера строк на информацию скрыта строка или нет
filtr – оставили только нескрытые строки
to – удалили временный столбец

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

Как-то так. Ковыряйте xml – там много полезного, причём не только в экселевских файлах.

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

Всем привет!
Последнее время несколько раз всплывала задача интерполяции значений: на входе имеем таблицу с пропусками по столбцу, которые необходимо заполнить, но не с помощью FillDown или замены null на среднее, а именно арифметической прогрессией, то есть вот такое:
{2,null,6,8,9,null,7,null,null,1}
должно превратиться в
{2,4,6,8,9,8,7,5,3,1}
Т.е. число пропусков может быть переменным, шаг тоже и вот «надо как-то решить».

Начнём с интерполяции одного списка. Для этого соорудил функцию ListInterpolate:
(lst)=>
[ lst=List.Buffer(lst),
n = List.Count(lst),
int=(x,y,z)=>List.Transform({1..x},(i)=>y+i*(z-y)/x),
gen = List.Generate(
()=>[i=0,l=lst{i},m=0,s=l,o={l}],
(x)=>x[i]<n,
(x)=>[ i=x[i]+1,l=lst{i},
m=if l = null then x[m]+1 else 0,
s=if l = null then x[s] else l,
o=if l=null then null else if x[m]>0 then int(x[m]+1,x[s],s) else {l}],
(x)=>x[o]),
to = List.Combine(List.RemoveNulls(gen))][to]

Функция принимает в качестве аргумента список.
Далее разбираем по шагам:
lst – забуферили исходный список
n – нашли число его элементов – это потребуется для генерации
int – функция интерполяции, принимает три аргумента:
x – число генерируемых элементов,
y – от какого числа ,
z – до какого (включительно);
здесь получаем элементы простой арифметикой

gen – а вот теперь генератор. Здесь оперируем записью с пятью полями:
i – счётчик элементов анализируемого списка,
l -i-ый элемент анализируемого списка,
m
– счётчик элементов для генерации,
s – последнее не null-овое числовое значение,
o – список, который отдаём в качестве результата генерации.
Принцип работы: проходим по всем элементам списка, на каждой итерации проверяем, не встретился ли нам null, если встретился – запускаем счётчик m и считаем число элементов, которые надо заполнить, всё это время в качестве результата отдаём null, как только возникает очередной не null – генерим список и обнуляем счётчик

to – очищаем полученный в ходе генерации список от null, объединяем полученные списки.

Ну и проверяем:
ListInterpolate({1,null,3,4,null,null,7,null,9})//{1,2,3,4,5,6,7,8,9}
ListInterpolate({1,2,null,2,null,2,1})//{1,2,2,2,2,2,1}
ListInterpolate({-1,null,1,null,-1}) //{-1,0,1,0,-1}

Работает, как и задумывалось )))
Собственно, всё. Функцию можно брать в работу, ну а если возникнут затруднения с применением – в следующий раз разберём как с её помощью пачкой заполнить несколько столбцов в таблице.

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

Всем привет!
В прошлый раз мы написали функцию ListInterpolate, которая позволила заполнить пропуски в списке с помощью прогрессии. Другое дело, что обычно пропуски встречаются в табличных данных (встречались данные со счётчиков электроэнергии, пробег автомобиля, вырабатываемая мощность агрегатов, даже биржевые котировки))), и требуется заполнение нескольких столбцов. По этому поводу напишем TableInterpolate:
(tbl,lst)=>
let
nms = List.Buffer(Table.ColumnNames(tbl)),
tr = List.Transform(lst,(x)=>List.PositionOf(nms,x)),
col=List.Buffer(Table.ToColumns(tbl)),
f=(x,y)=>List.ReplaceRange(x,y,1,{ListInterpolate(x{y})}),
acc=List.Accumulate(tr,col,f),
to = Table.FromColumns(acc,Value.Type(tbl))
in
to

Функция принимает на вход два аргумента:
tbl – исходная таблица,
lst – список столбцов для преобразования.

Разбираем по шагам:
nms – получили список названий столбцов исходной таблицы,
tr – список названий столбцов для преобразования превращаем в список их позиций в таблице
col – саму таблицу преобразуем в список списков по столбцам
f – ну и напишем функцию для аккумулятора – она заменяет конкретный элемент списка (по индексу) им же, но с применённой ListInterpolate
acc
– а теперь собираем все вышенаписанные шаги в аккумуляторе: идем по списку интересующих нас номеров столбцов и применяем к ним интерполятор
to – осталось только собрать таблицу обратно (обратите внимание на последний аргумент – данные о типах столбцов получаем из исходной таблицы, т.е. информация не будет потеряна).

Ну и как это применять:
let
tbl = #table(type table [a=date,b=number,c=text,d=number,e=text,f=number],
{{#date(2023,1,1),1,"мама",1,"папа",1},
{#date(2023,1,2),null,"мыла",null,"пил",null},
{#date(2023,1,3),null,"раму",3,"пиво",null},
{#date(2023,1,4),3,"мама",null,"папа",null},
{#date(2023,1,5),null,"мыла",null,"пил",3},
{#date(2023,1,6),5,"раму",5,"пиво",5}}),
lst = Table.ColumnsOfType(tbl,{type number}),
to = TableInterpolate(tbl,lst)
in
to
В данном случае
tbl – исходная таблица
lst – нужные столбцы – здесь я просто взял все числовые; вы же можете написать список руками или, например, использовать List.Select
to – применяем функцию

Как-то так – пара несложных функций и решена вполне себе заковыристая задача. Пользуйтесь!

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

Всем привет!
Всплыл в чате вопрос про сортировку таблицы. Ежу понятно, что нужно использовать Table.Sort. Заглянем в справку:
Table.Sort(table as table, comparisonCriteria as any) as table

Второй аргумент имеет тип any, а значит нас ждёт что-то интересное.
В целом мы уже разбирали вопрос сортировки списка и выяснили, что во второй аргумент можно запихивать функцию. Но то было про списки, а тут таблица. С другой стороны, мы разбирали Table.Max - и там тоже выясняли, как использовать функцию во втором аргументе.
Короче, как в одном анекдоте: «у бабочек и цветочков происходит примерно то же самое»:

let 
from = #table(type table [t=text,d=date,n=number],{{"A-001-2023",#date(2023,1,1),80},{"B-002-2023",#date(2023,1,2),88},{"A-003-2023",#date(2023,1,1),52},{"B-001-2023",#date(2023,1,2),99},{"A-002-2023",#date(2023,1,1),88},{"B-003-2023",#date(2023,1,2),80}}),
to0 = Table.Sort(from,"d"),
to1 = Table.Sort(from,{"d",Order.Descending}),
to2 = Table.Sort(from,{"d","n"}),
to3 = Table.Sort(from,{"n","d"}),
to4 = Table.Sort(from,{"d",{"n",Order.Descending}}),
to5 = Table.Sort(from,(x)=>x[t]),
to6 = Table.Sort(from,(x)=>Number.From(Text.Split(x[t],"-"){1})),
to7 = Table.Sort(from,{(x)=>Number.From(Text.Split(x[t],"-"){1}),"d"}),
to8 = Table.Sort(from,{{(x)=>Number.From(Text.Split(x[t],"-"){1}),Order.Descending},"d"})
in
to8

По шагам:
from – исходная таблица
to0 – простая сортировка по столбцу d, порядок сортировки по умолчанию, т.е. по возрастанию
to1 – то же самое, но по убыванию – обращаем внимание, что теперь мы передаём список {название_столбца,порядок_сортировки}
to2
– сортировать можно по нескольким столбцам, в данном случае сначала по столбцу d, потом по столбцу n
to3
– обращаю внимание – порядок перечисления имеет значение, в данном случае сортировка сначала по столбцу n, а потом по d
to4
– ну и при сортировке по нескольким столбцам также можно задать порядок – в данном случае сначала по d по возрастанию, а затем по n по убыванию – не запутайтесь в фигурных скобках )))

Ну ОК, всё вышеизложенное и так было в справке, а вот чего там нет:
to5 – вместо названия столбца пишем функцию (мы же помним, что одно из представлений таблиц – это список записей), сортируем по полю t. Оно работает
to6 – ну и раз работает, значит можно немножко усложнить функцию – отсортируем не по префиксу в номере документа, а по самому номеру
to7 – функция вместо названия столбца точно также может быть скомбинирована с другими условиями сортировки
to8 – и точно также можно заодно задать порядок сортировки, в данном случае из строкового значения в столбце t вынимается номер документа и сортировка по убыванию, а затем по столбцу d по возрастанию

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

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

Всем привет!
Уже несколько раз всплывал вопрос фильтрации таблицы по списку значений. Пожалуй, стоит дать развернутый комментарий. Сразу немножко кода, а потом разберём:
let
from = #table(type table [a=text,b=number,c=number],{{"a",1,1},{"b",2,2},{"c",3,3},{"d",4,4},{"e",5,5},{"f",6,6},{"g",7,7}}),
lst={"a","c","e","g"},
to = Table.SelectRows(from,(x)=>List.Contains(lst,x[a])),
tbl = Table.FromList(lst,(x)=>{x},{"a"}),
to1 = Table.Join(from,"a",tbl,"a"),
dict = Record.FromList(List.Repeat({true},List.Count(lst)),lst),
to2 = Table.SelectRows(from,(x)=>Record.FieldOrDefault(dict,x[a],false))
in
to2

Итак, имеем:
from – фильтруемая таблица,
lst – список искомых значений
to – классическое решение через List.Contains, на котором можно было бы и закончить пост… НО, хорошо, если список действительно состоит из буквально нескольких значений, а встречаются варианты, когда имеется список контрагентов из нескольких десятков позиций, или список пользователей на несколько сотен или вообще одна таблица в десятки или сотни тысяч строк фильтруется по другой таблице в десятки или сотни тысяч строк – и вот тут List.Contains начинает «тупить».

Попробуем найти альтернативу:
tbl – соберём из списка таблицу в один столбец, причём название столбца должно совпадать с названием столбца в фильтруемой таблице (если у вас сразу на входе нужная таблица – просто радуемся, а так возможно потребуется Table.RenameColumns)
to1 – а теперь делаем Table.Join – поскольку имена столбцов совпадали, ничего лишнего в таблицу не добавилось, пятый аргумент не указан, поэтому по умолчанию JoinKind.Inner, т.е. только совпадающие строки из обеих таблиц, короче сразу получили отфильтрованную таблицу, вообще без лишних телодвижений. В первом комментарии будут промеры – это реально шустрый вариант

Ну а вишенкой на торте буде вариант, заявленный в теме поста – словари на записях )))
dict – превратим наш список в запись, где интересующие нас значения – названия полей, а в самих полях напишем просто true
to2
– ну и теперь фильтруем через Record.FieldOrDefault, где дефолтом будет false – логика простая – если в записи есть соответствующее поле – сохраняем строку, иначе – выкидываем. На больших массивах данный вариант показывает наилучшие результаты.

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

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

Всем привет!
Не созрел я пока до полноценного канала, но кое-что выкладывать буду.
Посему - мануал по Мерке:
https://www.youtube.com/watch?v=pScYcnbsG2c&t=5s
А там посмотрим, может и попишу код на камеру )))

Надеюсь, было полезно.
Всех благ!
@buchlotnik
PQ тизер курса
#ПолезныеСсылки - Код

Всем привет!
Осенью запускаем курс по pq - по ссылке анонс того, чем будем заниматься )))
https://www.youtube.com/watch?v=Rgy4vXWV1jU

Версия режиссерская, т.е. моя - тут мало про курс и много того, как я пишу, по мне - это самое интересное.
Маркетологи зарубили, а мне нравится, так что смотрите - может полезностей каких подсмотрите )))

Пы.Сы. я знаю, что там можно без FillDown сразу локальной группировкой, но всё в одно видео не вместить, вот )))

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