Forum Discussion

ahmerac's avatar
ahmerac
Copper Contributor
Dec 28, 2024
Solved

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

  • Patrick2788's avatar
    Patrick2788
    Silver 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))))

     

    • ahmerac's avatar
      ahmerac
      Copper Contributor

      Thank you, Patrick. It worked perfectly.

  • 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)
    )

     

    • ahmerac's avatar
      ahmerac
      Copper Contributor

      Very insightful! Thank you for the solutions and the detailed explanation regarding formula performance.

  • djclements's avatar
    djclements
    Silver 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))
    )
    • ahmerac's avatar
      ahmerac
      Copper Contributor

      Both options worked perfectly. Thank you, djclements.

  • =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?

     

    • ahmerac's avatar
      ahmerac
      Copper 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

       

    1. Assume your data is in columns A, B, and C:
      • StartDate in column A
      • EndDate in column B
      • Amount in column C
    2. Create a list of all dates within the ranges:
      • In a new column, use the following formula to generate a list of all dates:
        =SEQUENCE(MAX(B:B) - MIN(A:A) + 1, 1, MIN(A:A), 1)
        This formula creates a sequence of dates from the earliest start date to the latest end date.
    3. 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.

    4. 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.
    • ahmerac's avatar
      ahmerac
      Copper Contributor
      1. 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:

        1. The date list will show those dates that are not part of the provided ranges.
        2. The amount column is not dynamic.

Resources