Forum Discussion
Spilled Array based on different Time Periods and applied periods - Help Needed
I have another issue that I can't solve. Most info is in the attached example sheet
I have a set of time periods start and end dates in rows across the top running from a few historic years to a few forecast years. These are the dates to work off.
In the left hand column I have the start and end dates of different expenses and a column showing applied periods. The idea being that the expenses will start on the start period, end on the end period and occur based on the applied period. SO one starting on 1st Feb 2022 with applied period of 3 would occur on 1st Feb 22, 1st May 22, 1st Aug 22 etc - once every 3 months from the starting period till the ending period.
These can be either flags - 1 or 0 or show the applied period of say 1,3,6 etc. Ultimately these will be multiplied by the monthly amount so numbers may be more useful as that will already have the number of months to multiply by the monthly amount. This calc will come later so only need the flag or counter at this stage.
Example sheet attached. I want this a single spilled array
Note the time periods at the top will vary
Many thanks in advance. There are some super smart contributors here.
18 Replies
- JohnwweeCopper Contributor
To handle this, you can create a spilled array formula that checks each time period and flags when the expense should occur based on the start date, end date, and applied period. Here's a general approach:
For each expense, use SEQUENCE() or similar to generate a series of applied dates (e.g., starting from the start date and repeating every X months).
Use IF() or FILTER() to check if each generated date falls within the range of the time periods at the top.
Populate the flags (1/0) based on the matches.
If you can share more specific details from the attached sheet (like columns used), I can provide a more detailed formula.- James_BuistBrass ContributorThanks for all the incredible feedback and seeing the intellectuals corresponding!! I'm on a biz trip this week and travelling lots so only short chances to review on my tiny screened laptop. I will study in detail all when I get back this coming weekend. Many thanks
- PeterBartholomew1Silver Contributor
I was going to leave this one to David, but somehow I got embroiled with some interesting results. Firstly, I do not think I have ever worked with a variable pitch timeline before so generating it was a challenge; I am still not sure what is the most economical way of specifying it.
Turning to the expenses, I wrote a function that returns the payment dates for a single expense
COUNTPMT位 = LAMBDA(start, end, interval, LET( duration, 1 + DATEDIF(start, end, "m"), nPmts, duration / interval, counter, SEQUENCE(1, nPmts, 0), pmtDate, EOMONTH(start, counter), TOROW(DROP(FREQUENCY(pmtDate, Start_of_Period),1)) ) )
I was planning to assign each payment to a time period and use GROUPBY to accumulate frequent payments over the longer reporting intervals. Then it occurred to me that using the timeline as FREQUENCY bins would achieve the same goal. Hence the function as it is.
My next issue was that there are multiple expense lines. The first thought is to use MAP to run over the various lines. Except, that returns a nested array error (in common with every other worthwhile problem I have ever seen - OK, so that's probably an exaggeration!) That is where I turned to a function MAP位 I had written earlier and posted to GitHub as a Gist.
The formula I finished with was
= ext.MAP位(Start_Date, End_Date, Applied_Periods, COUNTPMT位)
where ext is a module that I created on this occasion to hold Excel extensions.
cc djclements
- James_BuistBrass ContributorPeter, very cool stuff. Have seen many of your posts and always impressed. However, I haven't managed to apply this to my model yet. Obviously the example I sent was just a mock up so all my dates and everything are fully dynamic. But I'm having an issue testing your version when applying it. I have ported over all the lambda functions but getting an error so must have done something wrong. Somehow the function is seeing the dates along the top as it doesn鈥檛 work without them but I can鈥檛 see how it is referencing them. I need to spend more time on it.
- PeterBartholomew1Silver Contributor
The defined name Start_of_Period is hard-wired into the COUNTPMT位 function, where it is used to define the bins of the Excel FREQUENCY function. To avoid that would require Currying so that the worksheet formula becomes
= ext.MAP位(Start_Date, End_Date, Applied_Periods, COUNTPMT位(Start_of_Period))
and the Lambda function is
COUNTPMT位 = LAMBDA(timeline, LAMBDA(start, end, interval, LET( duration, 1 + DATEDIF(start, end, "m"), nPmts, duration / interval, counter, SEQUENCE(1, nPmts, 0), pmtDate, EOMONTH(start, counter), TOROW(DROP(FREQUENCY(pmtDate, timeline),1)) ) ))
The additional LAMBDA means that the function required two sets of arguments, ordered left to right, before it evaluates. The first is provided explicitly in the worksheet formula whilst the second set are provided term at a time by the ext.MAP位 helper function.
- djclementsBronze Contributor
James_Buist My first response used a brute-force method to compare the target periods against all dates between the earliest start date and the oldest end date, which can be quite labor intensive and may affect performance with a larger dataset/date range. It was also originally written as a generalized method to handle any start day, including the 30th and 31st, which is probably overkill for your needs.
Another approach could be to generate an array of thunks (uncalled lambda functions), which contain the required sequence of dates for each line number. Then, the array(s) can be broadcast across (and down) to fill the entire output grid for each period, and MAP can be used to loop through the resulting 3D array, calling each applicable thunk (function) as needed.
Assuming I've understood your request correctly, it could look something like this:
=LET( periodStart, G2:R2, periodEnd, G3:R3, interval, B5:B12, startDate, C5:C12, endDate, D5:D12+(interval>1), duration, ((YEAR(endDate)-YEAR(startDate))*12+MONTH(endDate)-MONTH(startDate)+1)/interval, seq位, MAP(startDate,duration,interval,LAMBDA(sd,d,i,LAMBDA(x,y, LET(a,EDATE(sd,SEQUENCE(d,,0,i)),--IFERROR(OR((a>=x)*(a<=y)),0))))), MAP(IF({1},periodStart,interval),IF({1},periodEnd,interval),IF({1},seq位,periodEnd),LAMBDA(ps,pe,位,位(ps,pe))) )
It's not the easiest method to understand, but the amount of data being handled is much less than in my first approach. 馃槈
EDIT: my first two attempts only checked if the start date landed between the period start and period ending dates, which was based on your manually entered "expected results"; however, after re-reading your instructions in the workbook where you said, "if a start or end date hits the middle of a time period... then it is fine just to apply it to the entire period", I'm thinking this might be more appropriate:
=LET( periodStart, G2:R2, periodEnd, G3:R3, interval, B5:B12, startDate, C5:C12, endDate, D5:D12, duration, ((YEAR(endDate)-YEAR(startDate))*12+MONTH(endDate)-MONTH(startDate)+1)/interval, seq位, MAP(startDate,duration,interval,LAMBDA(sd,d,i,LAMBDA(x,y, LET(a,EDATE(sd,SEQUENCE(d,,0,i)),b,EOMONTH(a,i-1),--IFERROR(OR(((a>=x)*(a<=y))+((b>=x)*(b<=y))),0))))), MAP(IF({1},periodStart,interval),IF({1},periodEnd,interval),IF({1},seq位,periodEnd),LAMBDA(ps,pe,位,位(ps,pe))) )
Check them out and see if either one does what you want...
- James_BuistBrass Contributor
Amazing. But I see this one was definitely more of a challenge than my last few queries.
Overall, Version 1 seemed to work. Again, there is a lot to test but I think it does.
V2 is definitely faster. I have rolled it out over 100 cols x around 10 rows. But will be replicating this around 8 times in the model.
It seems to work bar a small glitch. It drops off the last entry when the Applied Period is > 1. If the end date falls on the month of the final period then this final period flag does not show. Ie if End Date is Oct 22 then it doesn鈥檛 show flag in Oct 22 when that is the final due flag, but if end date is Nov 22 then it will show the Oct 22 flag. IT only seems to affect the last flag in the series whether it ends before the model end or not.
V3 has the same last flag issue but also creates and extra flag in the month before the actual flag when the applied period >1
Sample files attached- djclementsBronze Contributor
James_Buist It looks like v2 is what you needed after all. I just botched the duration calculation. Remove the +(interval>1) from the endDate and wrap the duration formula inside ROUNDUP(...,0), and it should work as expected:
=LET( periodStart, G2:R2, periodEnd, G3:R3, interval, B5:B12, startDate, C5:C12, endDate, D5:D12, duration, ROUNDUP(((YEAR(endDate)-YEAR(startDate))*12+MONTH(endDate)-MONTH(startDate)+1)/interval,0), seq位, MAP(startDate,duration,interval,LAMBDA(sd,d,i,LAMBDA(x,y, LET(a,EDATE(sd,SEQUENCE(d,,0,i)),--IFERROR(OR((a>=x)*(a<=y)),0))))), MAP(IF({1},periodStart,interval),IF({1},periodEnd,interval),IF({1},seq位,periodEnd),LAMBDA(ps,pe,位,位(ps,pe))) )
In the revised file (attached), I've also added an interactive drop-down where you can select the desired row_num and col_num, and it will show you what sequence of dates is returned for the intersecting cell in the output grid. If something doesn't look right, just let me know....
- JohnwweeCopper ContributorWhat help you need? elaborate
- PeterBartholomew1Silver Contributor
You might like to check out Craig Hatmaker 's 5G components for financial modelling.
5G functions for Excel for Financial Models: deprecated on 2024-05-29 (github.com)
At first sight TimelinePosition位 would appear to be relevant to the situation you describe but I will take a further look at your problem later.
- djclementsBronze Contributor
James_Buist Give this a try and see if it's what you're looking for...
=LET( colData, G2:R3, rowData, B5:D12, rowId, SEQUENCE(ROWS(rowData)), start, INDEX(rowData,,2), end, INDEX(rowData,,3), num, INDEX(rowData,,1), low, MIN(start), seq, SEQUENCE(,MAX(end)-low+1,low), dates1, DATE(YEAR(seq),MONTH(seq),DAY(start)), dates2, EOMONTH(seq,0), test, NOT(MOD(MONTH(seq)-MONTH(start)+(YEAR(seq)-YEAR(start))*12,num))*(seq=IF(dates1<dates2,dates1,dates2))*(seq>=start)*(seq<=end), ids, TOROW(IFS(test,rowId),2), dates, TOCOL(IFS(test,seq),2), --(MMULT(--(ids=rowId),(dates>=TAKE(colData,1))*(dates<=DROP(colData,1)))>0) )
It's virtually identical to my solution on another thread, with the additional MMULT formula used to flag the applicable periods.
Reference: Generating a list of Dates using nested array inside SEQUENCE function