Forum Discussion
PeterBartholomew1
Jan 17, 2026Silver Contributor
Excel Challenge - Pivoting poorly structured data
This is from an ExcelBI challenge. I thought it may be worth while posting my solution here as a demonstration of modern Excel methods. Challenge Like many of such challenges, the natural solu...
- Jan 18, 2026
This won't win the award for shortest function. I didn't set out to make a generalized function, but a modular approach using existing functions in my library allowed me to pull this together quickly.
//----------------------------------------------------------------------------------- //PivotbyCatλ //----------------------------------------------------------------------------------- // Author: Patrick H // Date: 1/18/2026 // Description: // Pivot by row fields (one of which is a date), categories, and values. // It's presumed the categories array must be split prior to aggregation so a // delimiter must be specified. With optional parameters for aggregation type (function) // and date display (date_format). // Parameters: // row_detail - 1D Array of non-dates. // row_dates - 1D Array of dates. // categories - 1D Array containing category information strung together with a delimiter. // values - 1D Array of numbers. // delimiter - Delimiter used in the categories array. // // Optional Parameters: // [date_format] - Format code for dates array. If omitted, defaults to "yyy". // [function] - ETA or Lambda function for aggregation. If omitted, defaults to SUM. // // Lambdas called: Streakλ, RepeatRowsλ PivotbyCatλ= LAMBDA( row_detail,row_dates,categories,values,delimiter,[date_format],[function], //Validate inputs LET( //Dimensions r₁, ROWS(row_detail), r₂, ROWS(row_dates), r₃, ROWS(categories), r₄, ROWS(values), //Halting scenarios ShapeMismatch, OR(r₁ <> r₂,r₁ <> r₃,r₁ <> r₄), InvalidDates, OR(AND(ISERROR(DATEVALUE(TEXT(row_dates,date_format))), ISERROR(VALUE(TEXT(row_dates,date_format)))), NOT(AND(ISNUMBER(row_dates)))), InvalidFn, AND(NOT(ISOMITTED(function)), ISERROR(BYROW({1,1;2,2},function))), scenarios, VSTACK(ShapeMismatch,InvalidDates,InvalidFn), errors, {"#DIMENSIONS!";"#DATES!";"#INVALID-FUNCTION!"}, //Logic gate FILTER(errors,scenarios, //Proceed LET( //Optional parameters fnλ, IF(ISOMITTED(function),SUM,function), date_format,IF(ISOMITTED(date_format),"yyy",date_format), //Local functions CountCatλ, LAMBDA(row,COUNTA(TEXTSPLIT(row,delimiter))), Parseλ, LAMBDA(array,instance, TEXTBEFORE(TEXTAFTER(delimiter&array&delimiter, delimiter,instance,,,array),delimiter,,,,categories)), //Categories in each row k, BYROW(categories,CountCatλ), //Indices i, RepeatRowsλ(SEQUENCE(r₁),k), j, Streakλ(i), //Row fields a, RepeatRowsλ(row_detail,k), detail, IF(a= 0, "Unknown",a), dates, RepeatRowsλ(TEXT(row_dates,date_format),k), sort_key, IFERROR(DATEVALUE(dates),VALUE(dates)), row_fields, HSTACK(sort_key,detail,dates), //Column fields unpack, RepeatRowsλ(categories,k), categories, Parseλ(unpack,j), //Value field values, RepeatRowsλ(values / k,k), //Aggregation agg, DROP(PIVOTBY(row_fields,categories,values,fnλ,,0,,0),,1), agg )))); //----------------------------------------------------------------------------------- //Streakλ //----------------------------------------------------------------------------------- // Author: Patrick H // Date: 12/22/25 // Description: // Calculate streaks of consecutive values within 1D or 2D arrays. // // Parameters: // array - Array (1D or 2D). // // Optional Parameters: // [Horizontal?] - For 1D arrays, calculate streaks column‑wise instead of row‑wise. // [Include_Symbol?] - TRUE to include the symbol with streak counts (e.g., W·1, W·2, L·1). Streakλ= LAMBDA( array,[Horizontal?],[Include_Symbol?], // Handle scalar input IF(AND(ROWS(array)=1,COLUMNS(array)=1),array, LET( //Dimensions i, ROWS(array), j,COLUMNS(array), Is2D?, AND(i>1,j>1), //Flatten if 2D array array, IF(Is2D?,TOCOL(array),array), //Direction for stacking NoDir?, ISOMITTED(Horizontal?), fn, IF(NoDir?,VSTACK,HSTACK), //Build true/false array for streak detection kept, IF(NoDir?,DROP(array,-1),DROP(array,,-1)), bool, array=fn("omit",kept), //Accumulate streak counts acc, SCAN(0,bool,LAMBDA(a,v,IF(a=0,1,IF(v,a+1,1)))), //Optionally include symbol with streak counts result, IF(ISOMITTED(Include_Symbol?),acc, array&UNICHAR(183)&acc), //Wrap result back to 2D if original array was 2D deliver,IF(Is2D?,WRAPROWS(result,j),result), deliver ))); //----------------------------------------------------------------------------------- //RepeatRowsλ //----------------------------------------------------------------------------------- //Author: Patrick H. //Date: 11/7/2025 //Version: 1.0 //Description: //Repeat each row in the supplied array by a corresponding repeat count. //Arrays and scalars are supported. //----------------------------------------------------------------------------------- //Parameters: //array - array to be repeated //repeat - 1D array of repeat integers RepeatRowsλ = LAMBDA( array, repeat, //Check inputs IF(OR(ISTEXT(repeat),repeat<=0),#VALUE!, LET( //Flatten input repeat, TOCOL(repeat), //Dimensions and row indexing A↕, ROWS(array), R↕,ROWS(repeat), r, IF(A↕<>R↕,EXPAND(repeat,A↕,,@TAKE(repeat,-1)), repeat), i, SEQUENCE(ROWS(r)), m, MAX(r), idx, LAMBDA(TOCOL(IF(SIGN(r-SEQUENCE(,m,0))=1,i,NA()),2)), //Unwrap idx but defer delivery until function invocation deliver, LAMBDA(CHOOSEROWS(array,idx())), deliver ))());Pivot by row fields (one of which is a date), categories, and values.
Patrick2788
Jan 18, 2026Silver Contributor
This won't win the award for shortest function. I didn't set out to make a generalized function, but a modular approach using existing functions in my library allowed me to pull this together quickly.
//-----------------------------------------------------------------------------------
//PivotbyCatλ
//-----------------------------------------------------------------------------------
// Author: Patrick H
// Date: 1/18/2026
// Description:
// Pivot by row fields (one of which is a date), categories, and values.
// It's presumed the categories array must be split prior to aggregation so a
// delimiter must be specified. With optional parameters for aggregation type (function)
// and date display (date_format).
// Parameters:
// row_detail - 1D Array of non-dates.
// row_dates - 1D Array of dates.
// categories - 1D Array containing category information strung together with a delimiter.
// values - 1D Array of numbers.
// delimiter - Delimiter used in the categories array.
//
// Optional Parameters:
// [date_format] - Format code for dates array. If omitted, defaults to "yyy".
// [function] - ETA or Lambda function for aggregation. If omitted, defaults to SUM.
//
// Lambdas called: Streakλ, RepeatRowsλ
PivotbyCatλ=
LAMBDA(
row_detail,row_dates,categories,values,delimiter,[date_format],[function],
//Validate inputs
LET(
//Dimensions
r₁, ROWS(row_detail), r₂, ROWS(row_dates),
r₃, ROWS(categories), r₄, ROWS(values),
//Halting scenarios
ShapeMismatch, OR(r₁ <> r₂,r₁ <> r₃,r₁ <> r₄),
InvalidDates, OR(AND(ISERROR(DATEVALUE(TEXT(row_dates,date_format))),
ISERROR(VALUE(TEXT(row_dates,date_format)))),
NOT(AND(ISNUMBER(row_dates)))),
InvalidFn, AND(NOT(ISOMITTED(function)),
ISERROR(BYROW({1,1;2,2},function))),
scenarios, VSTACK(ShapeMismatch,InvalidDates,InvalidFn),
errors, {"#DIMENSIONS!";"#DATES!";"#INVALID-FUNCTION!"},
//Logic gate
FILTER(errors,scenarios,
//Proceed
LET(
//Optional parameters
fnλ, IF(ISOMITTED(function),SUM,function),
date_format,IF(ISOMITTED(date_format),"yyy",date_format),
//Local functions
CountCatλ, LAMBDA(row,COUNTA(TEXTSPLIT(row,delimiter))),
Parseλ, LAMBDA(array,instance,
TEXTBEFORE(TEXTAFTER(delimiter&array&delimiter,
delimiter,instance,,,array),delimiter,,,,categories)),
//Categories in each row
k, BYROW(categories,CountCatλ),
//Indices
i, RepeatRowsλ(SEQUENCE(r₁),k),
j, Streakλ(i),
//Row fields
a, RepeatRowsλ(row_detail,k),
detail, IF(a= 0, "Unknown",a),
dates, RepeatRowsλ(TEXT(row_dates,date_format),k),
sort_key, IFERROR(DATEVALUE(dates),VALUE(dates)),
row_fields, HSTACK(sort_key,detail,dates),
//Column fields
unpack, RepeatRowsλ(categories,k),
categories, Parseλ(unpack,j),
//Value field
values, RepeatRowsλ(values / k,k),
//Aggregation
agg, DROP(PIVOTBY(row_fields,categories,values,fnλ,,0,,0),,1),
agg
))));
//-----------------------------------------------------------------------------------
//Streakλ
//-----------------------------------------------------------------------------------
// Author: Patrick H
// Date: 12/22/25
// Description:
// Calculate streaks of consecutive values within 1D or 2D arrays.
//
// Parameters:
// array - Array (1D or 2D).
//
// Optional Parameters:
// [Horizontal?] - For 1D arrays, calculate streaks column‑wise instead of row‑wise.
// [Include_Symbol?] - TRUE to include the symbol with streak counts (e.g., W·1, W·2, L·1).
Streakλ=
LAMBDA(
array,[Horizontal?],[Include_Symbol?],
// Handle scalar input
IF(AND(ROWS(array)=1,COLUMNS(array)=1),array,
LET(
//Dimensions
i, ROWS(array), j,COLUMNS(array),
Is2D?, AND(i>1,j>1),
//Flatten if 2D array
array, IF(Is2D?,TOCOL(array),array),
//Direction for stacking
NoDir?, ISOMITTED(Horizontal?),
fn, IF(NoDir?,VSTACK,HSTACK),
//Build true/false array for streak detection
kept, IF(NoDir?,DROP(array,-1),DROP(array,,-1)),
bool, array=fn("omit",kept),
//Accumulate streak counts
acc, SCAN(0,bool,LAMBDA(a,v,IF(a=0,1,IF(v,a+1,1)))),
//Optionally include symbol with streak counts
result, IF(ISOMITTED(Include_Symbol?),acc,
array&UNICHAR(183)&acc),
//Wrap result back to 2D if original array was 2D
deliver,IF(Is2D?,WRAPROWS(result,j),result),
deliver
)));
//-----------------------------------------------------------------------------------
//RepeatRowsλ
//-----------------------------------------------------------------------------------
//Author: Patrick H.
//Date: 11/7/2025
//Version: 1.0
//Description:
//Repeat each row in the supplied array by a corresponding repeat count.
//Arrays and scalars are supported.
//-----------------------------------------------------------------------------------
//Parameters:
//array - array to be repeated
//repeat - 1D array of repeat integers
RepeatRowsλ =
LAMBDA(
array,
repeat,
//Check inputs
IF(OR(ISTEXT(repeat),repeat<=0),#VALUE!,
LET(
//Flatten input
repeat, TOCOL(repeat),
//Dimensions and row indexing
A↕, ROWS(array), R↕,ROWS(repeat),
r, IF(A↕<>R↕,EXPAND(repeat,A↕,,@TAKE(repeat,-1)),
repeat),
i, SEQUENCE(ROWS(r)),
m, MAX(r),
idx, LAMBDA(TOCOL(IF(SIGN(r-SEQUENCE(,m,0))=1,i,NA()),2)),
//Unwrap idx but defer delivery until function invocation
deliver, LAMBDA(CHOOSEROWS(array,idx())),
deliver
))());Pivot by row fields (one of which is a date), categories, and values.
- LorenzoJan 19, 2026Silver Contributor
PQ option
- Abuses of Table.ReplaceValue (can cause perf. issue on large table)
- Doesn't use Table.Pivot
// TableSource let Source = Excel.CurrentWorkbook(){[Name="TableSource"]}[Content], ChangedType = Table.TransformColumnTypes( Source, { {"Amount", type number} } ) in ChangedType // TablePivoted let Source = TableSource, ReplacedOrderDateAndRegion = Table.ReplaceValue( Source, each [OrderDate], "Unknown", // Works with the given source table. Otherwise would also replace null with "Unknown" in [OrderDate] (x,y,z) => if x is null then z else Date.Year( DateTime.Date(x) ), {"OrderDate", "Region"} ), FlaggedMultiCategories = Table.AddColumn( ReplacedOrderDateAndRegion, "Flag", each Text.Contains( [Categories], ";" ), type logical ), SplittedCategoriesAndAmount = Table.Combine( { fxFilterTable( FlaggedMultiCategories, "Flag", false ), Table.ExpandListColumn( Table.ReplaceValue( fxFilterTable( FlaggedMultiCategories, "Flag", true ), each [Amount], each Text.Split( [Categories], ";" ), (x,y,z) => if Value.Type(x) = type number then x / List.Count(z) else z, {"Amount", "Categories"} ), "Categories" ) } ), TotalAmounts = Table.Group( SplittedCategoriesAndAmount, {"OrderDate", "Region", "Categories"}, {"Amount", each List.Sum( [Amount] ), type number} ), DistinctCategories = List.Buffer( List.Sort( List.Distinct( Table.Column( TotalAmounts, "Categories") ) ) ), PivotedCategories = Table.Group( TotalAmounts, {"Region", "OrderDate"}, { "Pivoted", (nested_table) => fxPivotTable( nested_table, "Categories", "Amount"), fxTypeForTable( DistinctCategories, type nullable number) } ), SortedRegionAndOrderDate = Table.Sort( PivotedCategories, { {"Region", Order.Ascending}, {"OrderDate", Order.Ascending} } ), ExpandedPivotedTables = Table.ExpandTableColumn( SortedRegionAndOrderDate, "Pivoted", DistinctCategories ), ReplacedNullAmounts = Table.ReplaceValue( ExpandedPivotedTables, null, 0, Replacer.ReplaceValue, DistinctCategories ), TypedOtherColumns = Table.TransformColumnTypes( ReplacedNullAmounts, { {"Region", type text}, {"OrderDate", Int64.Type} } ) in TypedOtherColumns // fxFilterTable (table as table, filter_field as text, filter_value as any) as table => Table.RemoveColumns( Table.SelectRows( table, each Record.Field( _, filter_field ) = filter_value ), filter_field ) // fxPivotTable ( table as table, optional attributeColumn as nullable text, optional valueColumn as nullable text ) as table => Table.Transpose( Table.SelectColumns( table, if valueColumn is null then "Value" else valueColumn ), Table.Column( table, if attributeColumn is null then "Attribute" else attributeColumn ) ) // fxTypeForTable (List as list, Type as type) as type => type table Type.ForRecord( List.Accumulate( List, [], (state,current) => Record.AddField( state, current, [Type = Type, Optional=false] ) ), false )