Forum Discussion
Transform multiple date ranges into continuous dates list through Dynamic Array
Hi Excel experts,
I have data in a table format with StartDate, EndDate, and Amount columns. The data needs to be transformed into an output format that consists of Date and Amount Per Day columns, i.e., date ranges need to be flattened into a single column and Amounts need to be divided into the count of days within a range for each row in the input table.
I was able to achieve this result through Power Query (M language). I would really appreciate it if you could submit only dynamic array formula solutions for this challenge.
Link to the example file:
https://1drv.ms/x/c/100048db520b4028/EW2ahANa201LnsqY5ECpoXYBLYvYoaFnp0E2hHYWuzfZZg?e=c4v817
Thanks
=DROP(REDUCE("",SEQUENCE(ROWS(Input)),LAMBDA(u,v,
LET(
_days,INDEX(Input,v,2)-INDEX(Input,v,1)+1,
_seq,SEQUENCE(_days,1,INDEX(Input,v,1)),
_amt,EXPAND(INDEX(Input,v,3)/_days,_days,1,INDEX(Input,v,3)/_days),
VSTACK(u,HSTACK(_seq,_amt))))),1)This returns the expected result in my Excel for the web sheet. The name of the input table is "Input".
Can you share your M-code that achieves the same result?
11 Replies
- ahmeracCopper Contributor
djclements, PeterBartholomew1 Patrick2788 thank you all for the answers provided. If it were possible, I would have tagged all of them as "Marked as Solution".
- Patrick2788Silver Contributor
I offer a recursive solution. I enjoy the challenge of coming up with recursive solutions because there's not a lot technical documentation from Microsoft so it often feels like the wild west of Excel. The limit of this function is noted below. The limit can be increased even more if I nest functions and eliminate parameters but I can't forsee unpackings needing to be done with tables encompassing more than a few thousand rows (I could be wrong!).
//Unpack a table by recursion. Function presumes table //includes col1: Start Date, col2: End Date, col3: Amount. //Recursion limit: 2,047 rows in packed table. UnPackλ = LAMBDA(table, LET( next,TAKE(table,1), start,TAKE(next,,1), diff,1+INDEX(next,,2)-start, dates,SEQUENCE(@diff,,@start), amount,TAKE(next,,-1)/SEQUENCE(@diff,,@diff,0), resize,VSTACK(DROP(table,1),HSTACK(dates,amount)), IF(diff<0,DROP(table,,-1),UnPackλ(resize))))- ahmeracCopper Contributor
Thank you, Patrick. It worked perfectly.
- PeterBartholomew1Silver Contributor
It is reasonably easy to distribute one payment over a period, but life gets much harder when, as in this case, there are multiple amounts. This is because Microsoft implemented the helper functions such as MAP and SCAN on the assumption that a scalar value would be returned at each step. Whenever you need to return an array at each step, Excel will throw a #CALC! error. ☹️
OliverScheurichhas addressed this by calling REDUCE to accumulate a growing array of array results using VSTACK (his calculation is included within attached the workbook). I have used a more complicated approach, but the complexity is hidden within the function MAPλ. For small problems my approach shows little advantage except, perhaps, by hiding complexity. For larger problems ~1000+ values, the approach has been found to be 10x faster than REDUCE/VSTACK.
The final calculation takes things a step further by grouping amounts by date and summing any overlaps.
=LET( DISTRIBUTEλ, LAMBDA(startLst, endLst, amount, LET( duration, 1 + endLst - startLst, day, SEQUENCE(duration, 1, startLst), amt, IF(day, amount / duration), HSTACK(day, amt) ) ), stacked, MAPλ(Table1[StartDate], Table1[EndDate], Table1[Amount], DISTRIBUTEλ), dates, TAKE(stacked, , 1), amounts, TAKE(stacked, , -1), GROUPBY(dates, amounts, SUM) )- ahmeracCopper Contributor
Very insightful! Thank you for the solutions and the detailed explanation regarding formula performance.
- djclementsSilver Contributor
Another option you could try with your sample file:
=LET( beg, Table1[StartDate], end, IF(ISBLANK(Table1[EndDate]),beg,Table1[EndDate]), dur, end-beg+1, amt, Table1[Amount]/IF(dur>0,dur,1), seq, SEQUENCE(,MAX(end)-MIN(beg)+1,MIN(beg)), tst, (seq>=beg)*(seq<=end), HSTACK(TOCOL(IFS(tst,seq),2),TOCOL(IFS(tst,amt),2)) )Or perhaps:
=LET( beg, Table1[StartDate], end, IF(ISBLANK(Table1[EndDate]),beg,Table1[EndDate]), dur, end-beg+1, amt, Table1[Amount]/IF(dur>0,dur,1), seq, SEQUENCE(,MAX(dur),0), tst, dur>seq, HSTACK(TOCOL(IFS(tst,beg+seq),2),TOCOL(IFS(tst,amt),2)) )- ahmeracCopper Contributor
Both options worked perfectly. Thank you, djclements.
- OliverScheurichGold Contributor
=DROP(REDUCE("",SEQUENCE(ROWS(Input)),LAMBDA(u,v,
LET(
_days,INDEX(Input,v,2)-INDEX(Input,v,1)+1,
_seq,SEQUENCE(_days,1,INDEX(Input,v,1)),
_amt,EXPAND(INDEX(Input,v,3)/_days,_days,1,INDEX(Input,v,3)/_days),
VSTACK(u,HSTACK(_seq,_amt))))),1)This returns the expected result in my Excel for the web sheet. The name of the input table is "Input".
Can you share your M-code that achieves the same result?
- ahmeracCopper Contributor
Your solution worked flawlessly. Thank you, Oliver.
Here is my M code:let Source = Excel.CurrentWorkbook(){[Name="Table1"]}[Content], //transformDates a = Table.CombineColumns( Source, { "StartDate" , "EndDate" }, each { Number.From( _{0} ) .. Number.From( _{1} ) }, "Dates" ), // transformAmounts b = Table.ReplaceValue( a, each [Amount] , each [Amount] / List.Count( [Dates] ) , Replacer.ReplaceValue, {"Amount"} ), //expandDates c = Table.ExpandListColumn(b, "Dates"), //changeTypes d = Table.TransformColumnTypes(c, {{"Dates", type date}, {"Amount", type number}} ) in d
- Assume your data is in columns A, B, and C:
- StartDate in column A
- EndDate in column B
- Amount in column C
- Create a list of all dates within the ranges:
- In a new column, use the following formula to generate a list of all dates:
This formula creates a sequence of dates from the earliest start date to the latest end date.=SEQUENCE(MAX(B:B) - MIN(A:A) + 1, 1, MIN(A:A), 1)
- In a new column, use the following formula to generate a list of all dates:
- Calculate the amount per day for each date:
- In another new column, use the following formula to calculate the amount per day for each date:
=SUMPRODUCT((A:A<=D2)*(B:B>=D2)*(C:C/(B:B-A:A+1)))Here, D2 is the cell reference for the first date in your sequence. This formula checks if each date falls within the start and end dates and then divides the amount by the number of days in the range.
- In another new column, use the following formula to calculate the amount per day for each date:
- Combine the results:
- You can now have a table with the dates and the corresponding amounts per day.
Here's a more detailed example:
StartDate EndDate Amount Date Amount Per Day 2024-01-01 2024-01-03 300 2024-01-01 100 2024-01-02 2024-01-04 200 2024-01-02 150 2024-01-03 150 2024-01-04 50 - Column D (Date) is generated using the SEQUENCE formula.
- Column E (Amount Per Day) is calculated using the SUMPRODUCT formula.
- ahmeracCopper Contributor
I appreciate your answer, Kidd_Ip. In general, it works, but it will require some additional steps from the user to achieve the expected result, namely:
- The date list will show those dates that are not part of the provided ranges.
- The amount column is not dynamic.
- Assume your data is in columns A, B, and C: