Last active
December 1, 2025 20:23
-
-
Save CHatmaker/472f65269519a0937b03f741e9271334 to your computer and use it in GitHub Desktop.
BXL 5g Functions LAMBDA Financial Models 2025
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // BXL 5g Functions: Financial Library 2025 | |
| /* FUNCTION NAME: Aboutλ | |
| DESCRIPTION:*//**Displays the URL to this module's Gist which includes documentation*/ | |
| /* REVISIONS: Date Developer Description | |
| Jul 13 2023 Craig Hatmaker Original Development | |
| Aug 07 2025 Craig Hatmaker Version 2 | |
| Sep 27 2025 Craig Hatmaker See Growthλ | |
| Oct 06 2025 Craig Hatmaker Added CorkscrewAλ | |
| Oct 08 2025 Craig Hatmaker Prep for loading to gist | |
| Nov 15 2025 Craig Hatmaker Added Inflationλ | |
| Nov 24 2025 Craig Hatmaker See Depreciateλ | |
| Dec 01 2025 Craig Hatmaker See Corkscrewλ | |
| */ | |
| Aboutλ = TRIM(TEXTSPLIT( | |
| "ABOUT: →BXL's Financial Models module. Suggested module name: BXF¶" & | |
| "VERSION: →BXL: Nov 24 2025¶" & | |
| "GIST URL: →https://gist.github.com/CHatmaker/472f65269519a0937b03f741e9271334 ¶" & | |
| "WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-component-libraries/5g-financial-library ¶" & | |
| "→¶" & | |
| "FUNCTION →DESCRIPTION¶" & | |
| " Aboutλ →Produces this table¶" & | |
| "→¶" & | |
| " Allocateλ →Divide values by smaller periods and distribute evenly.¶" & | |
| " AvgBalIntλ →Calculate interest on average balance.¶" & | |
| " Cumulativeλ →Creates a row or column of cumulative totals from a total row or column¶" & | |
| " Growthλ →Project growth or decline factors that can be applied to values to increase or decrease them. ¶" & | |
| " Inflationλ →Create period factors for adjusting amounts by inflation.¶" & | |
| " IntOnIntλ →Calculate Interest on Interest. Use to determine amount needed to cover debt plus interest on debt¶" & | |
| " IRRλ →Calculates IRR, correcting for when the first investment is not in the first period¶" & | |
| " Movementλ →Create a row of differences from column to column¶" & | |
| " PMTAλ →Determine monthly payment amount for Actual/360, Actual/365, or Actual/Actual¶" & | |
| " Reversalλ →Create a row that reverses input values in the next period.¶" & | |
| " SumContainsλ →Creates a row of totals for each row in an array where its labels contain a unique letter, word, or phrase.¶" & | |
| " SumPeriodsλ →Groups and totals all columns in a Values array by period resulting in one column for each period.¶" & | |
| "→¶" & | |
| "AMORTIZATION SUITE→¶" & | |
| " Amortizeλ →Creates a corkscrew amortization schedule.¶" & | |
| " AmortizeLabelsλ →Create row labels for Amortizeλ result¶" & | |
| " AmortizeSumλ →Create row totals for payments, interest, and principal portion in Amortizeλ results¶" & | |
| "→¶" & | |
| "CORKSCREW SUITE→¶" & | |
| " Corkscrewλ →Creates a corkscrew section from inflows (additions) and outflows (subtractions).¶" & | |
| " CorkscrewAλ →Creates a simple corkscrew that derives opening balances from actual periods.¶" & | |
| " CorkscrewRxOλ →Corkscrew-Rate-Times-Open-Balance multiplies the opening balance by a rate and adds results to one or more flows.¶" & | |
| " CorkScrewReversalλ→Create a corkscrew where the opening balance is reversed in the next period based on a timing row¶" & | |
| " CorkScrewAvgBalIntλ→Create a corkscrew that applies a rate to the average of opening and closing balances¶" & | |
| "→¶" & | |
| "DEPRECIATION SUITE→¶" & | |
| " Depreciateλ →Create a block of CAPEX, Opening Balance, Depreciation Values, and Book Value for each asset¶" & | |
| " DepreciateLabelsλ →Create row labels for Depreciateλ result¶" & | |
| " DepreciateSumsλ →Create row totals for CAPEX, Depreciation, Book Value, Salvage Value, and Disposal costs in Depreciateλ results¶" & | |
| " SUB FUNCTIONS →Functions used by Depreciateλ. Not intended for standalone use.¶" & | |
| " fnAllocateλ →Divide one or more amounts equally equally across smaller periods¶" & | |
| " SLNλ →Straight Line depreciation method for one asset or asset class.¶" & | |
| " SYDλ →Sum-of-years' digits depreciation method for one asset or asset class.¶" & | |
| " DBλ →Fixed declining balance depreciation method for one asset or asset class.¶" & | |
| " DDBλ →Double-declining balance depreciation method for one asset or asset class.¶" & | |
| " VDBλ →Variable declining balance depreciation method for one asset or asset class.¶" & | |
| "→¶" & | |
| "SUB FUNCTIONS →Functions used by other functions or general purpose functions.¶" & | |
| " FitInTimelineλ →Positions and adjusts an array to fit in a model's timeline.¶" & | |
| " TimelineOffsetλ →Determines how many columns a date is offset from a timeline's first date¶" & | |
| " TimelinePositionλ →Places an array or value appropriately within a model's timeline.¶" & | |
| " PeriodDiffλ →Determine the number of periods between two dates.", | |
| "→","¶" | |
| ) | |
| ); | |
| /* FUNCTION NAME: Allocateλ | |
| DESCRIPTION:*//**→Divide one or more amounts equally equally across smaller periods*/ | |
| /* REVISIONS: Date Developer Description | |
| Apr 17 2024 Craig Hatmaker Copyright | |
| Jun 25 2025 Craig Hatmaker Added ability to handle multiple amounts in our out of a timeline | |
| */ | |
| Allocateλ = LAMBDA( | |
| // Parameter Declaration | |
| [Amounts], | |
| [Timeline], | |
| [FromInterval], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →Allocateλ( Amounts, Timeline, [FromInterval])¶" & | |
| "DESCRIPTION: →Divide one or more amounts equally equally across smaller periods¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-allocate%CE%BB¶" & | |
| "VERSION: →BXL: Jun 25, 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " Amounts →(Required) One or more amounts to allocate in a row. The amount's position¶" & | |
| "→ under the model's timeline indicates when the amount is incurred.¶" & | |
| " Timeline →(Required) The model's timeline.¶" & | |
| " FromInterval →(Optional Default=Y) Y=amounts are annual, Q=quarterly.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Amounts), | |
| ISOMITTED( Timeline)), | |
| // Set defaults for optional values | |
| FromInterval, IF( ISOMITTED( FromInterval), "Y", FromInterval), | |
| // Get Starts and Ends | |
| Timelines, DeriveTimelinesλ( Timeline, 1), | |
| ToInterval, INDEX( Timelines, 3, 1), | |
| // Pre Calcs | |
| From, SWITCH(FromInterval, "A", 1, "Y", 1, "Q", 4, "M", 12, "W", 52), | |
| To, SWITCH(ToInterval, "A", 1, "Y", 1, "Q", 4, "M", 12, "W", 52), | |
| AmountCount, COLUMNS( Amounts), | |
| Counter, SEQUENCE( AmountCount), | |
| // Allocate Function | |
| Allocate, LAMBDA( Amount, From, To, AmountIndex, | |
| LET( | |
| Allocation, Amount * From / To, | |
| Array, EXPAND( Allocation, 1, To/From, Allocation), | |
| Result, IF( AmountIndex > 1, | |
| HSTACK( EXPAND( 0, 1, AmountIndex - 1 , 0), Array), | |
| HSTACK( Array) | |
| ), | |
| Result | |
| ) | |
| ), | |
| // Procedure | |
| Block, REDUCE( Timeline, Counter, | |
| LAMBDA( Acc, n, | |
| LET( | |
| Amount, INDEX( Amounts, n), | |
| IF( Amount = 0, | |
| Acc, | |
| VSTACK( Acc, Allocate( Amount, From, to, n)) | |
| ) | |
| ) | |
| ) | |
| ), | |
| Result, IFNA( TAKE( DROP( Block, 1), , COLUMNS( Timeline)), 0), | |
| // Return Result or Help | |
| CHOOSE( Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: AvgBalIntλ | |
| DESCRIPTION:*//**Calculates interest on the average of opening and closing balances*/ | |
| /* REVISIONS: Date Developer Description | |
| Jun 31 2025 Craig Hatmaker Copyright | |
| */ | |
| AvgBalIntλ = LAMBDA( | |
| [InterestRate], | |
| [OpeningBalance], | |
| [Additions], | |
| Let( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →AvgBalIntλ( InterestRate, [OpeningBalance], Additions)¶" & | |
| "DESCRIPTION: →Calculates interest on the average of opening and closing balances. ¶" & | |
| "→Closing balance includes interest.¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-avgbalint%CE%BB¶" & | |
| "VERSION: →BXL: Jun 31 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " InterestRate →(Required) The APR divided by the number of periods in a year.¶" & | |
| " OpeningBalance →(Required) The opening balance.¶" & | |
| " Additions, →(Optional) Total of amounts, other than interest, to add/sub to closing balance. Default = 0.", | |
| "→", "¶" ) | |
| ), | |
| Help?, OR( ISOMITTED( InterestRate), | |
| ISOMITTED( OpeningBalance)), | |
| // Defaults | |
| Additions, IF( ISOMITTED( Additions), 0, Additions), | |
| // Recursive function (fnInterest) definition | |
| fnInterest, LAMBDA(fn, InterestRate, Open, Additions, Close, | |
| LET( | |
| AvgBalance, Average(Open, Close), | |
| Interest, AvgBalance * InterestRate, | |
| Closing, Open + Additions + Interest, | |
| Difference, AvgBalance - Average( Open, Closing), | |
| Result, IF( ABS(Difference) < .0001, | |
| Interest, | |
| fn( fn, InterestRate, Open, Additions, Closing) | |
| ), | |
| Result | |
| ) | |
| ), | |
| // Procedure | |
| Result, fnInterest( fnInterest, InterestRate, OpeningBalance, Additions, 0), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: Cumulativeλ | |
| DESCRIPTION:*//**Creates a row or column of cumulative totals from a total row or column*/ | |
| /* REVISIONS: Date Developer Description | |
| Jan 05 2024 Craig Hatmaker Original Development | |
| */ | |
| Cumulativeλ = LAMBDA( | |
| // Parameter Declarations | |
| [Totals], | |
| [OpeningBalance], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →Cumulativeλ( Totals, [OpeningBalance])¶" & | |
| "DESCRIPTION: →Creates a row or column of cumulative totals from a total row or column¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-cumulative%CE%BB¶" & | |
| "VERSION: →BXL: Jan 05 2023¶" & | |
| "PARAMETERS: →¶" & | |
| " Totals →(Required) A row or column of totals from which to run cumulative totals¶" & | |
| "OpeningBalance →(Optional) A starting balance. This defaults to 0.", | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, ISOMITTED( Totals), | |
| // Procedure | |
| Result, SCAN( OpeningBalance, Totals, | |
| LAMBDA( PreviousAmount, CurrentAmount, | |
| PreviousAmount + CurrentAmount)), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: Growthλ | |
| DESCRIPTION:*//**Project growth or decline factors that can be applied to values to increase or decrease them. | |
| Uses include applying inflation to amounts or to ramp up sales projections. | |
| NOTE! GrowthPercents are not applied to Actuals. */ | |
| /* REVISIONS: Date Developer Description | |
| Jul 24 2025 Craig Hatmaker Copyright | |
| Sep 27 2025 Craig Hatmaker Timeline made optional | |
| */ | |
| Growthλ = LAMBDA( | |
| // Parameter Declarations | |
| [Actuals], | |
| [Percentages], | |
| [Timeline], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →Growthλ( Actuals, Percentages, [Timeline])¶" & | |
| "DESCRIPTION: →Projects percentage increases/(decreases) over the last Actual amount.¶" & | |
| "→ Uses include applying inflation to amounts or to ramp up sales projections. ¶" & | |
| "→ NOTE! Percents are not applied to Actuals.¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-growth%CE%BB¶" & | |
| "VERSION: →BXL: Jul 24 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " Actuals →(Required) A single number or an array of actuals placed under the timeline.¶" & | |
| "→ NOTE! If a single number, it is the first period's actual (not forecast) value.¶" & | |
| "→ If an array of actuals, select only actuals columns. Exclude forecast columns.¶" & | |
| "→ Increases or decreases are not applied to actuals columns, only forecast.¶" & | |
| " Percentages →(Required) A single number or an array of change percentages placed under the timeline.¶" & | |
| "→ NOTE! Positive percentages are increases, negative percentages are decreases.¶" & | |
| "→ NOTE! Use monthly % for monthly timelines and annual % for annual timelines.¶" & | |
| " Timeline →(Optional) The model's timeline. If omitted, number of periods is derived from percentages.", | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Actuals), | |
| ISOMITTED( Percentages) | |
| ), | |
| // Set defaults | |
| ActCols, COLUMNS( Actuals), | |
| PctCols, COLUMNS( Percentages), | |
| Cols, IF( ISOMITTED( Timeline), PctCols, COLUMNS( Timeline)), | |
| Percentages, IF( PctCols = 1, | |
| EXPAND( Percentages, 1, Cols, Percentages), | |
| EXPAND( Percentages, 1, Cols, 0) | |
| ), | |
| Periods, SEQUENCE( , Cols), | |
| Result, Scan( 0, Periods, | |
| LAMBDA( Acc, Period, | |
| LET(Factor, INDEX( Percentages, Period), | |
| Result, IF( Period <= ActCols , | |
| INDEX( Actuals, Period), | |
| Acc * ( 1 + Factor) | |
| ), | |
| Result | |
| ) | |
| ) | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: Inflationλ | |
| DESCRIPTION:*/ /**Create inflation factors for adjusting amounts*/ | |
| /* REVISIONS: Date Developer Description | |
| Nov 15 2025 Craig Hatmaker Copyright on LAMBDA | |
| */ | |
| Inflationλ = LAMBDA( | |
| [PercentageRates], | |
| [EffectiveDates], | |
| [Timeline], | |
| [EndDates?], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →IntOnIntλ( PercentageRates, EffectiveDates, Timeline)¶" & | |
| "DESCRIPTION: →Create period factors for adjusting amounts to inflation¶" & | |
| "WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-inflation%CE%BB¶" & | |
| "VERSION: →BXL: Nov 15 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " PercentageRates→(Required) Forecasted annual inflation percentage¶" & | |
| "→ NOTE! This converts rates for monthly and quarterly timelines using:¶" & | |
| "→ ( 1 + Annual % Rate) ^ (1 / number of periods in year) - 1¶" & | |
| " EffectiveDates→(Required) Date when the percentage rate is applied¶" & | |
| "→ NOTE! To prevent inflating amounts in the first period, set the first¶" & | |
| "→ effective date to the second period start date.¶" & | |
| " Timeline →(Required) Model's timeline¶" & | |
| " EndDates? →(Optional) If TRUE, timeline displays period end dates, else, start dates.¶" & | |
| "→ Default=TRUE.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( PercentageRates), | |
| ISOMITTED( EffectiveDates), | |
| ISOMITTED( Timeline) | |
| ), | |
| Result, IF( Help?, | |
| Help, | |
| LET( | |
| // Set Defaults | |
| EndDates, IF(ISOMITTED( EndDates?), TRUE, EndDates? ), | |
| // Get Timeline attributes | |
| Timelines, DeriveTimelinesλ( Timeline, EndDates?), | |
| Ends, CHOOSEROWS( Timelines, 2), | |
| Interval, TAKE( Timelines, -1, -1), | |
| Periods, SWITCH( Interval, "M", 12, "Q", 4, "Y", 1), | |
| // Create rates | |
| AnnualRates,XLOOKUP( Ends, EffectiveDates, PercentageRates, 0,-1), | |
| PeriodRates,( 1 + AnnualRates) ^ ( 1 / Periods) - 1, | |
| Result, SCAN( 1, PeriodRates, LAMBDA( Acc, Val, Acc + Acc * Val)), | |
| Result | |
| ) | |
| ), | |
| Result | |
| ) | |
| ); | |
| /* FUNCTION NAME: IntOnIntλ | |
| DESCRIPTION:*/ /**Calculate Interest on Interest. Use to determine amount needed to cover debt plus interest on debt*/ | |
| /* NOTE: Math source from Diarmuid Early's post: https://www.youtube.com/watch?v=k5rG_MvIWWs&t=787s | |
| REVISIONS: Date Developer Description | |
| Dec 29 2023 Craig Hatmaker Copyright on LAMBDA | |
| */ | |
| IntOnIntλ = LAMBDA( | |
| [Principal], | |
| [Rate], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →IntOnIntλ( Principal, Rate)¶" & | |
| "DESCRIPTION: →Calculate Interest on Interest (Interest only). Use to determine amount ¶" & | |
| "→ needed to cover borrowed interest¶" & | |
| "WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-intonint%CE%BB¶" & | |
| "VERSION: →BXL: Dec 29 2023¶" & | |
| "PARAMETERS: →¶" & | |
| " Principal →(Required) Principal amount¶" & | |
| " Rate →(Required) Interest percentage rate. For years, APR. For months, APR/12", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Principal), | |
| ISOMITTED( Rate) | |
| ), | |
| // Procedure | |
| EffectiveRate, Rate/(1-(Rate/2)), | |
| Result, EffectiveRate * Principal , | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: IRRλ | |
| DESCRIPTION:*/ /**Calculates IRR, correcting for when the first investment is not in the first period */ | |
| /* REVISIONS: Date Developer Description | |
| Oct 07 2023 Craig Hatmaker Initial development | |
| */ | |
| IRRλ = LAMBDA( | |
| [Values], | |
| [Dates], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →IRRλ( Values, Dates)¶" & | |
| "DESCRIPTION: →Calculates IRR, correcting for when the first investment is not in the first period¶" & | |
| "WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-irr%CE%BB¶" & | |
| "VERSION: →BXL: Dec 07 2023¶" & | |
| "PARAMETERS: →¶" & | |
| " Values →(Required) A row of investments (negative) and distributions (positive)¶" & | |
| "→ The first entry, whether 0 or an amount, must be for the first period in the timeline¶" & | |
| "→ and subsequent entries must correspond to periods in the timeline; however,¶" & | |
| "→ the timeline may extend beyond the end of values.¶" & | |
| " Dates →(Required) A timeline for when each investment is made", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Values), | |
| ISOMITTED( Dates) | |
| ), | |
| // Set Defaults | |
| Values, EXPAND(VALUES, 1, COUNTA(Dates), 0), | |
| // Procedure | |
| FilterBy, Values <> 0, | |
| FilteredValues, FILTER( Values, FilterBy), | |
| FilteredDates, FILTER( Dates, FilterBy), | |
| Result, XIRR( FilteredValues, FilteredDates), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: Movementλ | |
| DESCRIPTION:*//**Create a row of differences from column to column*/ | |
| /* REVISIONS: Date Developer Description | |
| Jun 21 2023 Craig Hatmaker Copyright | |
| Apr 16 2024 Craig Hatmaker Handles multiple rows | |
| */ | |
| Movementλ = LAMBDA( | |
| // Parameter Declarations | |
| [BeginningValues], | |
| [Values], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →Movementλ( [BeginningValue], Values)¶" & | |
| "DESCRIPTION: →Create a row of differences from column to column¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-movement%CE%BB¶" & | |
| "VERSION: →BXL: Jun 21 2023¶" & | |
| "PARAMETERS: →¶" & | |
| " BeginningValues→(Optional) Defaults to 0¶" & | |
| " Values →(Required) A row of values to compare column to column", | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, ISOMITTED( Values), | |
| // Set Defaults | |
| BeginningValues, IF(OR(ISOMITTED(BeginningValues), BeginningValues=""), 0, BeginningValues), | |
| // Procedure | |
| Rows, ROWS( Values), | |
| Cols, Columns( Values), | |
| Seq, SEQUENCE(, Cols), | |
| Result, REDUCE(0, SEQUENCE( Rows), | |
| LAMBDA( Acc, R, | |
| LET( | |
| Begin, If( ROWS(BeginningValues) = 1, BeginningValues, INDEX( BeginningValues, R)), | |
| Row, CHOOSEROWS( Values, R), | |
| NewRow, SCAN( Begin, Seq, | |
| LAMBDA( A, C, | |
| INDEX( Row, C ) - IF( C = 1, A, INDEX( Row, C - 1)) | |
| ) | |
| ), | |
| Result, IF( R = 1, NewRow, VSTACK(Acc, NewRow)), | |
| Result | |
| ) | |
| ) | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| PMTAλ = LAMBDA( | |
| [APR], | |
| [TermInMonths], | |
| [DebtAmount], | |
| [StartDate], | |
| [AccrualMethod], | |
| [Tolerance], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →PMTAλ( APR, TermInMonths, DebtAmount, StartDate, AccrualMethod, [Tolerance])¶" & | |
| "DESCRIPTION: →Determine monthly payment amount for Actual/360, Actual/365, or Actual/Actual¶" & | |
| "→ This does the same thing for commercial loans that PMT() does for consumer loans.¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-pmta%CE%BB¶"& | |
| "VERSION: →BXL: Jul 30 2025¶"& | |
| "PARAMETERS:→¶"& | |
| " APR →(Required) Annual interest rate¶"& | |
| " TermInMonths →(Required) Count of months from first payment due to end of term (inclusive).¶"& | |
| " DebtAmount →(Required) Debt amount when first payment is due.¶"& | |
| " StartDate →(Required) Date when first payment is due¶"& | |
| " AccrualMethod →(Required) Select: 1=30/360, 2=Actual/360, 3=Actual/365, or 4=Actual/Actual¶"& | |
| " Tolerance →(Optional) Returns a result within this tolerance. Default = .001.", | |
| "→","¶") | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( APR), | |
| ISOMITTED( TermInMonths), | |
| ISOMITTED( DebtAmount), | |
| ISOMITTED( StartDate), | |
| ISOMITTED( AccrualMethod) | |
| ), | |
| // Defaults | |
| Tolerance, IF( OR(ISOMITTED( Tolerance), Tolerance = 0), .001, Tolerance), | |
| // Pre Calculations | |
| Counter, SEQUENCE( TermInMonths), | |
| DatesArray, EOMONTH( StartDate, Counter - 1), | |
| MonthDays, IF( AccrualMethod = 1, IF( DatesArray, 30), DAY( DatesArray)), | |
| LeapYears, MONTH( DATE( YEAR( DatesArray), 2, 29)) = 2, | |
| YearDays, CHOOSE( AccrualMethod, | |
| IF( DatesArray, 360), | |
| IF( DatesArray, 360), | |
| IF( DatesArray, 365), | |
| IF( DatesArray, 365) + LeapYears | |
| ), | |
| // Initial guesses for the bisection method to find the correct payment. | |
| LoGuess, DebtAmount / TermInMonths, | |
| HiGuess, DebtAmount * (1 + APR), | |
| // Define function to apply payment to all months and determine remaining balance | |
| fnGetBal, LAMBDA( | |
| Payment, | |
| REDUCE( DebtAmount, Counter, | |
| LAMBDA( Acc, n, | |
| LET( | |
| MDays, INDEX( MonthDays, n, 1), | |
| YDays, INDEX( YearDays, n, 1), | |
| Acc + Acc * APR / YDays * MDays - Payment | |
| ) | |
| ) | |
| ) | |
| ), | |
| // Define solver-like function to find equal monthly payments | |
| fnPMT, LAMBDA( | |
| fn, LoGuess, HiGuess, | |
| LET( | |
| MidPayment, AVERAGE(LoGuess, HiGuess), | |
| Balance, fnGetBal( MidPayment), | |
| IF( ABS( Balance) < Tolerance, | |
| MidPayment, | |
| IF( Balance > 0, | |
| // If balance is positive, payment guess was too low. Increase lower bound | |
| fn( fn, MidPayment, HiGuess), | |
| // If balance is zero or negative, payment guess was too high or just right. Decrease upper bound | |
| fn( fn, LoGuess, MidPayment) | |
| ) | |
| ) | |
| ) | |
| ), | |
| // Run solver-like function to find equal monthly payments | |
| Result, IF( NOT(Help?), fnPMT( fnPMT, LoGuess, HiGuess)), | |
| // Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: Reversalλ | |
| DESCRIPTION:*//**Create a row that reverses input values in the next period.*/ | |
| /* REVISIONS: Date Developer Description | |
| Mar 25 2024 Craig Hatmaker Copyright | |
| */ | |
| Reversalλ = LAMBDA( | |
| // Parameter Declarations | |
| [OpeningValue], | |
| [RowToReverse], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →Reversalλ( [OpeningValue], RowToReverse)¶" & | |
| "DESCRIPTION: →Create a row that reverses input values in the next period.¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5greversal%CE%BB¶" & | |
| "VERSION: →BXL: Mar 25 2024¶" & | |
| "PARAMETERS: →¶" & | |
| " OpeningValue →(Optional) Defaults to 0¶" & | |
| " RowToReverse →(Required) A row of values to reverse the following periods", | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, ISOMITTED( RowToReverse), | |
| // Set Defaults | |
| BeginningValue, IF( ISOMITTED( OpeningValue), 0, OpeningValue), | |
| // Procedure | |
| Counter, SEQUENCE( , COLUMNS(RowToReverse)), | |
| Result, MAP( Counter, | |
| LAMBDA(Col, IF(Col = 1, -OpeningValue, -CHOOSECOLS( RowToReverse, Col-1))) | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: →SumContainsλ | |
| DESCRIPTION:*//**→Creates a row of totals for each row in an array where | |
| its labels contain a letter, word, or phrase*/ | |
| /* REVISIONS: Date Developer Description | |
| Jan 19 2024 Craig Hatmaker Copyright | |
| May 28 2024 Craig Hatmaker Added: Labels, CHOOSECOLS(ValueLabels, 1) | |
| */ | |
| SumContainsλ = LAMBDA( | |
| // Parameter Declarations | |
| [ValuesToFilter], | |
| [ValueLabels], | |
| [FilterByText], | |
| [IgnoreCase?], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →SumContainsλ(ValuesToFilter, ValueLabels, FilterByText, [IgnoreCase?])¶" & | |
| "DESCRIPTION: →Creates a row of column totals for each row in an array where¶" & | |
| "→its labels contain a unique letter, word, or phrase.¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-sumcontains%CE%BB¶" & | |
| "VERSION: →BXL: Feb 22 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " ValuesToFilter→(Required) An array of values to be filtered¶" & | |
| " ValueLabels →(Required) An array containing the values' labels¶" & | |
| " FilterByText →(Required) A unique letter, word or phrase to find contained in ValueLabels¶" & | |
| " IgnoreCase? →(Optional) A flag to determine if Text's case is important. Default is TRUE.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( ValuesToFilter), | |
| ISOMITTED( ValueLabels), | |
| ISOMITTED( FilterByText)), | |
| Result, IF( Help?, | |
| Help, | |
| LET( | |
| // Check inputs - Set defaults | |
| IgnoreCase?, IF( ISLOGICAL( IgnoreCase?), IgnoreCase?, TRUE), | |
| Labels, CHOOSECOLS(ValueLabels, 1), | |
| // Procedure | |
| Matches, IF( IgnoreCase?, | |
| SEARCH( FilterByText, Labels), | |
| FIND( FilterByText, Labels) | |
| ), | |
| Filtered, FILTER( ValuesToFilter, ISNUMBER( Matches)), | |
| Result, BYCOL( Filtered, SUM), | |
| Result | |
| ) | |
| ), | |
| Result | |
| ) | |
| ); | |
| /* FUNCTION NAME: SumPeriodsλ | |
| DESCRIPTION:*//**Groups and totals all columns in a Values array by period resulting in one column for each period*/ | |
| /* REVISIONS: Date Developer Description | |
| Mar 04 2022 Craig Hatmaker Original Development | |
| Apr 10 2023 Craig Hatmaker Remove SUMIFS to be compatible with arrays. Added Help. | |
| Feb 22 2025 Craig Hatmaker Leveraged DeriveTimelinesλ | |
| May 29 2025 Craig Hatmaker Pro Pack Prep | |
| */ | |
| SumPeriodsλ = LAMBDA( | |
| // Parameter Declarations | |
| [Values], | |
| [ValuesTimeline], | |
| [ModelTimeline], | |
| [EndDates?], | |
| // Procedure | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →SumPeriodsλ( Values, ValuesTimeline, ModelTimeline, [EndDates?])¶" & | |
| "DESCRIPTION: →Summarizes values where each value column is for one day ¶" & | |
| "→into one column for each period. ¶" & | |
| "VERSION: →BXL: May 29 2025¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-sumperiods%CE%BB¶" & | |
| "PARAMETERS:→¶" & | |
| " Values →(Required) A two dimensional array/range containing values to be summed where" & | |
| "each column is for a specific day¶" & | |
| " ValuesTimeline→(Required) The Values' timeline¶" & | |
| " ModelTimeline →(Required) The model's timeline¶" & | |
| " EndDates? →(Optional) TRUE=The model's timeline displays period end dates. Default = TRUE.", | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Values), | |
| ISOMITTED( ValuesTimeline), | |
| ISOMITTED( ModelTimeline) | |
| ), | |
| // Set Defaults | |
| EndDates?, IF( ISOMITTED( EndDates?), TRUE, EndDates?), | |
| // Get Starts and Ends | |
| Timelines, DeriveTimelinesλ( ModelTimeline, EndDates?), | |
| Starts, CHOOSEROWS( Timelines, 1), | |
| Ends, CHOOSEROWS( Timelines, 2), | |
| // Procedure | |
| MatchStarts, IFNA( XMATCH( ValuesTimeline, Starts, -1 ), 0), | |
| MatchEnds, IFNA( XMATCH( ValuesTimeline, Ends, 1, -1 ), 0), | |
| InRange, N( NOT( BYCOL( VSTACK( MatchStarts, MatchEnds), XOR))), | |
| PeriodNumber, InRange * MatchStarts, | |
| Result, MAKEARRAY( | |
| ROWS( Values), | |
| COLUMNS( ModelTimeline), | |
| LAMBDA( Row, Col, | |
| SUM( INDEX( Values, Row, ) * ( Col = PeriodNumber)) | |
| ) | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| // Amortization Suite | |
| /* FUNCTION NAME: Amortizeλ | |
| DESCRIPTION:*//**Creates a corkscrew amortization schedule*/ | |
| /* REVISIONS: Date Developer Description | |
| Jul 13 2023 Craig Hatmaker Original Development | |
| Dec 20 2023 Craig Hatmaker Payments are assumed to be paid monthly | |
| Jan 18 2024 Craig Hatmaker Added Debt Issuance | |
| May 02 2024 Craig Hatmaker Converted to anonymous recursion | |
| May 11 2024 Craig Hatmaker Corrected issue with month to year aggregation | |
| */ | |
| Amortizeλ = LAMBDA( | |
| // Parameter Declarations | |
| [Principals], | |
| [APRs], | |
| [Terms], | |
| [StartDates], | |
| [Timeline], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →Amortizeλ( Principals, APRs, Terms, StartDates, [Timeline])¶" & | |
| "DESCRIPTION: →Creates an amortization schedule array for one or more loans.¶" & | |
| "→It assumes all payments are made monthly.¶" & | |
| "→Use LableAmortizeλ() to apply labels to this function's results:¶" & | |
| "→Use SumAmortizeλ() to apply totals to this function's results:¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-amortize%CE%BB¶" & | |
| "VERSION: →BXL: May 11 2024¶" & | |
| "PARAMETERS:→¶" & | |
| " Principals →(Required) Loan/debt amounts.¶" & | |
| " APRs →(Required) Annual Percentage Rates for interest.¶" & | |
| " Terms →(Required) Number of months for loans/debts.¶" & | |
| " StartDates →(Required) Dates debts are issued.¶" & | |
| " Timeline →(Optional) An array of dates. If omitted, a row of months starting on start date will be added.", | |
| "→", "¶" ) | |
| ), | |
| // Constants | |
| MpY, 12, //Months Per Year | |
| ADpM, 30.42, //Average Days Per Month | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Principals), | |
| ISOMITTED( APRs), | |
| ISOMITTED( Terms), | |
| ISOMITTED( StartDates) | |
| ), | |
| // Set defaults and apply convenience conversions | |
| Timeline, IF( ISOMITTED( Timeline), | |
| LET(Counter, ROWS( Principals), | |
| MaxDate, MAX(MAP( Counter, | |
| LAMBDA( n, | |
| EDATE( INDEX( StartDates, n), INDEX( Terms, n) - 1)))), | |
| MinDate, MIN( StartDates), | |
| MaxTerm, DATEDIF(MinDate, MaxDate, "M") + 1, | |
| Result, EDATE( MinDate, Sequence(1, MaxTerm, 0)), | |
| Result), | |
| Timeline | |
| ), | |
| // Determine timeline's interval (length of periods) and orientation (horizontal or vertical) | |
| FirstPeriod, INDEX( Timeline, 1), | |
| SecondPeriod, INDEX( TImeline, 2), | |
| MpP, @ROUND(( SecondPeriod - FirstPeriod) / ADpM, 0), | |
| PpY, MpY / MpP, | |
| // Recursive function (fn) definition | |
| Recursion, | |
| LAMBDA( fn, Principals, APRs, Terms, StartDates, Timeline, Asset, | |
| LET( | |
| // Create individual Values from array | |
| Principal, INDEX( Principals, Asset), | |
| APR, INDEX( APRs, Asset), | |
| Term, INDEX( Terms, Asset), | |
| StartDate, INDEX( StartDates, Asset), | |
| // Adjust inputs to match timeline | |
| Periods, ROUNDUP(Term / MpP, 0), | |
| Rows, 1, | |
| Cols, Periods, | |
| // Calculate for each month | |
| Rate, APR / MpY, | |
| MonthlyPayment, PMT( Rate, Term, Principal), | |
| MonthlyPrincipal, -FV(Rate, SEQUENCE( 1, Term, 0), MonthlyPayment, Principal), | |
| MonthlyInterest, MonthlyPrincipal * Rate, | |
| // Convert to Timeline Periods | |
| DebtIssueArray, EXPAND(Principal, 1, Periods, 0), | |
| PrincipalArray, IF( MpP = 1, | |
| MonthlyPrincipal, | |
| MAKEARRAY( 1, Periods, | |
| LAMBDA(R, C, | |
| LET(Idx, (C - 1) * MpP + 1, | |
| INDEX( MonthlyPrincipal, Idx) | |
| ) | |
| ) | |
| ) | |
| ), | |
| InterestArray, IF( MpP = 1, | |
| MonthlyInterest, | |
| LET( | |
| MoInt, EXPAND( MonthlyInterest, 1, ROUNDUP( Term/ Mpp, 0) * Mpp, 0), | |
| MAKEARRAY( 1, Periods, | |
| LAMBDA(R, C, | |
| LET(Idx, (C - 1) * MpP + 1, | |
| SUM( CHOOSECOLS(MoInt, SEQUENCE(1, MpP, Idx))) | |
| ) | |
| ) | |
| ) | |
| ) | |
| ), | |
| PaymentArray, LET(MoPmt, EXPAND( SEQUENCE(1, Term, MonthlyPayment, 0), 1, ROUNDUP( Term/ Mpp, 0) * Mpp, 0), | |
| MAKEARRAY( 1, Periods, | |
| LAMBDA(R, C, | |
| LET(Idx, (C - 1) * MpP + 1, | |
| SUM( CHOOSECOLS(MoPmt, SEQUENCE(1, MpP, Idx))) | |
| ) | |
| ) | |
| ) | |
| ), | |
| ClosingArray, PrincipalArray + InterestArray + PaymentArray, | |
| PortionArray, -PaymentArray - InterestArray, | |
| AmortizationSched, VSTACK( | |
| DebtIssueArray, | |
| PrincipalArray, | |
| InterestArray, | |
| PaymentArray, | |
| ClosingArray, | |
| PortionArray), | |
| // Position in timeline | |
| Offset, TimelineOffsetλ(StartDate, Timeline), | |
| NewBlock, TimelinePositionλ(AmortizationSched, Timeline, Offset), | |
| Result, IF( Asset = ROWS( Principals), | |
| NewBlock, | |
| VSTACK( | |
| NewBlock, | |
| EXPAND({" "}, 1, COUNTA(Timeline), " "), | |
| fn( fn, Principals, APRs, Terms, StartDates, Timeline, Asset + 1) | |
| ) | |
| ), | |
| Result | |
| ) | |
| ), | |
| Result, Recursion( Recursion, Principals, APRs, Terms, StartDates, Timeline, 1), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: AmortizeLabelsλ | |
| DESCRIPTION:*//**Create labels for Amortizeλ*/ | |
| /* REVISIONS: Date Developer Description | |
| Dec 19 2023 Craig Hatmaker Original Development | |
| Jan 18 2024 Craig Hatmaker Added Debt Issuance | |
| May 29 2024 Craig Hatmaker Added optional parameters | |
| Nov 24 2025 Craig Hatmaker Renamed | |
| */ | |
| AmortizeLabelsλ = LAMBDA( | |
| // Parameter Declarations | |
| [LoanNames], | |
| [LoanAmounts], | |
| [LoanAPRs], | |
| [LoanTerms], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →AmortizeLabelsλ([LoanNames])¶" & | |
| "DESCRIPTION: →Create row labels for the amortization schedule array produced by Amortizeλ¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-amortize%CE%BB¶" & | |
| "VERSION: →BXL: Nov 24 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " LoanNames →(Required) Names or descriptions of each loan in the amortization schedule produced by Amortizeλ¶" & | |
| " LoanAmounts →(Optional) Amount borrowed¶" & | |
| " LoanAPR →(Optional) Annual Percentage Interest Rate¶" & | |
| " LoanTerm →(Optional) Number of months to repay.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check required inputs - Omitted required arguments | |
| Help?, ISOMITTED( LoanNames), | |
| // Check optional inputs | |
| Optionals, NOT( OR( | |
| ISOMITTED( LoanAmounts), | |
| ISOMITTED( LoanAPRs), | |
| ISOMITTED( LoanTerms) | |
| )), | |
| // Procedure | |
| Result, REDUCE( "", SEQUENCE( ROWS( LoanNames) * 7 - 1), | |
| LAMBDA(Acc, Row, | |
| LET(Idx, QUOTIENT( Row - 1, 7) + 1, | |
| Line, MOD( Row - 1, 7) + 1, | |
| LoanName, INDEX( LoanNames, Idx), | |
| LoanAmount, IF(ISOMITTED( LoanAmounts), 0, INDEX( LoanAmounts, Idx)), | |
| LoanAPR, IF(ISOMITTED( LoanAPRs), 0, INDEX( LoanAPRs, Idx)), | |
| LoanTerm, IF(ISOMITTED( LoanTerms), 0, INDEX( LoanTerms, Idx)), | |
| Description, SWITCH( Line, | |
| 1, HSTACK(LoanName & " Debt Issuance", LoanAmount, ""), | |
| 2, HSTACK(LoanName & " Debt Balance", "",""), | |
| 3, HSTACK(LoanName & " Interest", Text(LoanAPR, "0.00%"), "APR"), | |
| 4, HSTACK(LoanName & " Debt Services", LoanTerm, "Months"), | |
| 5, HSTACK(LoanName & " Closing Balance","",""), | |
| 6, HSTACK(LoanName & " Debt Repayment", "", ""), | |
| 7, HSTACK("","","") | |
| ), | |
| Values, IF( Optionals, Description, TAKE( Description, , 1)), | |
| Result, IF( Row = 1, Values, VSTACK(Acc, Values)), | |
| Result | |
| ) | |
| ) | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: AmortizeSumλ | |
| DESCRIPTION:*//**Create row totals for Amortizeλ*/ | |
| /* REVISIONS: Date Developer Description | |
| Dec 19 2023 Craig Hatmaker Original Development | |
| Jan 18 2024 Craig Hatmaker Added Debt Issuance | |
| Nov 24 2025 Craig Hatmaker Renamed | |
| */ | |
| AmortizeSumλ = LAMBDA( | |
| // Parameter Declarations | |
| [AmoritizationSchedule], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →AmortizeSumλ(AmoritizationSchedule)¶" & | |
| "DESCRIPTION: →Create row totals for Amortizeλ¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-amortize%CE%BB¶" & | |
| "VERSION: →BXL: Nov 24 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " AmoritizationSchedule→(Required) An array produced by Amoritizeλ.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, ISOMITTED( AmoritizationSchedule), | |
| // Procedure | |
| Result, MAKEARRAY(ROWS(AmoritizationSchedule), 1, | |
| LAMBDA( R, C, | |
| SWITCH( MOD( R, 7), | |
| 3, SUM(CHOOSEROWS( AmoritizationSchedule, R)), | |
| 4, SUM(CHOOSEROWS( AmoritizationSchedule, R)), | |
| 6, SUM(CHOOSEROWS( AmoritizationSchedule, R)), | |
| 0 | |
| ) | |
| ) | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| // Corkscrew suite | |
| /* FUNCTION NAME: Corkscrewλ | |
| DESCRIPTION:*//**Creates a corkscrew section from inflows (additions) and outflows (subtractions).*/ | |
| /* REVISIONS: Date Developer Description | |
| Dec 01 2025 Craig Hatmaker Rewrite | |
| */ | |
| Corkscrewλ = LAMBDA( | |
| // Parameter Declarations | |
| [Opening], | |
| [InFlows], | |
| [OutFlows], | |
| [PositiveOnly?], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →Corkscrewλ( [Opening], InFlows, OutFlows, [PositiveOnly?])¶" & | |
| "DESCRIPTION: →Creates a corkscrew section from inflows (additions) and outflows (subtractions).¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-corkscrew%CE%BB¶" & | |
| "VERSION: →BXL: Dec 01 2025¶" & | |
| "PARAMETERS:→¶" & | |
| " Opening →(Optional) First period's opening balance. This defaults to 0. ¶" & | |
| " InFlows →(Required/optional) A row, or several rows HSTACKed to be added to the balance¶" & | |
| "→ NOTE! If this is omitted, OutFlows must be present.¶" & | |
| " OutFlows →(Required/Optional) A row, or several rows HSTACKed to be subtracted from the balance¶" & | |
| "→ NOTE! These must be positive values.¶" & | |
| "→ NOTE! If this is omitted, InFlows must be present.¶" & | |
| " PositiveOnly? →(Optional) TRUE = display flows as positive values. Opens and Closes are not affected.¶" & | |
| "→ Default is FALSE.¶" & | |
| "→ NOTE! If PositiveOnly?=TRUE, indicate which flow(s) are subtracted in corkscrew line labels.", | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, AND( ISOMITTED( InFlows), ISOMITTED( OutFlows)), | |
| Result, IF( Help?, | |
| Help, | |
| // Processing | |
| LET( | |
| // Set defaults | |
| Opening, IF( ISOMITTED( Opening), 0, Opening), | |
| PosOnly?, IF( ISOMITTED( PositiveOnly?), FALSE, PositiveOnly?), | |
| Flows, IF( ISOMITTED( InFlows), | |
| -OutFlows, | |
| IF( ISOMITTED( OutFlows), | |
| InFlows, | |
| VSTACK( InFlows, -OutFlows) | |
| ) | |
| ), | |
| // Process | |
| Totals, BYCOL( Flows, SUM), | |
| Close, SCAN( Opening, Totals, SUM), | |
| Open, DROP( HSTACK( Opening, Close), , -1), | |
| PosFlows, IF( ISOMITTED( InFlows), | |
| OutFlows, | |
| IF( ISOMITTED( OutFlows), | |
| InFlows, | |
| VSTACK( InFlows, OutFlows) | |
| ) | |
| ), | |
| Result, VSTACK(Open, IF( PosOnly?, PosFlows, Flows), Close), | |
| Result | |
| ) | |
| ), | |
| Result | |
| ) | |
| ); | |
| /* FUNCTION NAME: CorkscrewAλ | |
| DESCRIPTION:*/ /**Create a corkscrew account that handles actuals*/ | |
| /* REVISIONS: Date Developer Description | |
| Oct 01 2025 Craig Hatmaker Copyright | |
| */ | |
| CorkscrewAλ = LAMBDA( | |
| // Arguments | |
| [Actuals], | |
| [ActualsPeriods], | |
| [Inflows], | |
| [Outflows], | |
| LET( | |
| // Help | |
| Help, TRIM( TEXTSPLIT( | |
| "FUNCTION: →CorkscrewAλ( Actuals, [ActualsPeriods],¶" & | |
| "→ [Inflows], [Outflows]) ¶" & | |
| "DESCRIPTION: →Create a corkscrew account that handles actuals¶" & | |
| "WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-corkscrewa%CE%BB¶" & | |
| "VERSION: →BXL: Oct 01 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " Actuals →(Required) Closing balances from historic periods.¶" & | |
| "→ NOTE! To treat this as an opening balance, set ActualPeriods to 0¶" & | |
| "→ in which case, this should be a single number¶" & | |
| " ActualsPeriods →(Optional) The number of periods (columns) occupied by Actuals.¶" & | |
| "→ Set to zero to treat Actuals as the opening balance.¶" & | |
| "→ This defaults to the number of columns in Actuals.¶" & | |
| "→ NOTE! If this is omitted then Actuals must not include ¶" & | |
| "→ columns reserved for forecasted values.¶" & | |
| " InFlows, →(Optional) One or more rows of values to add ¶" & | |
| "→ to the opening balance. NOTE! If omitted, Outflows are required.¶" & | |
| " OutFlows, →(Optional) One or more rows of values to subtract ¶" & | |
| "→ from the opening balance. NOTE! If omitted, Inflows are required." , | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Actuals), | |
| ActualsPeriods < 0, | |
| AND( ISOMITTED( InFlows), ISOMITTED( OutFlows)) | |
| ), | |
| // Handle defaults | |
| ActualsPeriods, IF( ISOMITTED( ActualsPeriods), COLUMNS( Actuals) , ActualsPeriods), | |
| // Procedure | |
| Count, IF( ISOMITTED( Inflows), COLUMNS( Outflows), Columns( Inflows)), | |
| Counter, SEQUENCE( , Count), | |
| Closing, SCAN( IF( ActualsPeriods = 0, TAKE( Actuals, 1, 1), 0), Counter, | |
| LAMBDA( Acc, Period, | |
| LET( | |
| InFlow, IF( ISOMITTED( InFlows), 0, CHOOSECOLS( Inflows, Period)), | |
| OutFlow, IF( ISOMITTED( OutFlows), 0, CHOOSECOLS( Outflows, Period)), | |
| Result, IF( Period <= ActualsPeriods, | |
| INDEX( Actuals, Period), | |
| Acc + SUM(Inflow) - SUM(Outflow) | |
| ), | |
| Result | |
| ) | |
| ) | |
| ), | |
| Opening, HSTACK( | |
| SUM(INDEX( Closing, 1) - | |
| IF( ISOMITTED( Inflows), 0, SUM( CHOOSECOLS( Inflows, 1))), | |
| IF( ISOMITTED( Outflows), 0, -SUM( CHOOSECOLS( Outflows, 1)))), | |
| TAKE( Closing, 1, Count - 1) | |
| ), | |
| Result, IF( ISOMITTED( InFlows), | |
| VSTACK( Opening, Outflows, Closing), | |
| IF( ISOMITTED( OutFlows), | |
| VSTACK( Opening, InFlows, Closing), | |
| VSTACK( Opening, Inflows, OutFlows, Closing) | |
| ) | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: CorkscrewRxOλ | |
| DESCRIPTION:*//**Corkscrew-Rate-Times-Open-Balance multiplies the opening balance by a rate and adds results to one or more flows.*/ | |
| /* REVISIONS: Date Developer Description | |
| Feb 21 2025 Craig Hatmaker Original Development | |
| May 16 2025 Craig Hatmaker Replace REDUCE and STACK with SCAN | |
| */ | |
| CorkscrewRxOλ = LAMBDA( | |
| // Parameter Declarations | |
| [Opening], | |
| [Rates], | |
| [Flows], | |
| [PositiveOnly], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →CorkscrewRxOλ( Opening, Rate, Flows, [PositiveOnly])¶" & | |
| "DESCRIPTION: →Corkscrew-Rate-Times-Open-Balance multiplies the opening balance by a rate¶" & | |
| "→and adds results to one or more flows. If more than one flow is desired, ¶" & | |
| "→VSTACK them into Flows.¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-corkscrewrxo%CE%BB¶" & | |
| "VERSION: →BXL: May 16 2025¶" & | |
| "PARAMETERS:→¶" & | |
| " Opening →(Required) First periods opening balance.¶" & | |
| " Rates →(Required) a sinble percent or rate or row of rates to multiple opening balance by.¶" & | |
| "→ If the result is to be subtracted from the opening balance make the rate negative.¶" & | |
| " Flows →(Required) A row, or VSTACKed rows of values to add to opening and applied rate result.¶" & | |
| "→ If a row in Flows is to be subtracted make the row negative.¶" & | |
| " PositiveOnly →(Optional) TRUE = display flows as positive values. Opens and Closes are not affected.¶" & | |
| "→ Default is FALSE.", | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Opening), | |
| ISOMITTED( Rates), | |
| ISOMITTED( Flows) | |
| ), | |
| // Processing | |
| Flow, BYCOL( Flows, SUM), | |
| Rates, EXPAND( Rates, 1, COLUMNS( Flow), TAKE( Rates, -1, -1)), | |
| PeriodArray, IF( PositiveOnly, ABS( Flows), Flows), | |
| Thunk, LAMBDA( x, LAMBDA( x)), | |
| // Load Thunks | |
| Thunks, BYCOL( VSTACK(Rates, Flow), Thunk), | |
| Result, LET( | |
| Close, SCAN( Opening, Thunks, | |
| LAMBDA( Acc, Val, Acc + Acc * INDEX( Val(), 1) + INDEX( Val(), 2)) | |
| ), | |
| Open, HSTACK( Opening, DROP( Close, , -1)), | |
| Rated, Open * Rates, | |
| Result, VSTACK( Open, Rated, PeriodArray, Close), | |
| Result | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: CorkScrewReversalλ | |
| DESCRIPTION:*//**Create a corkscrew where the opening balance is reversed in the next period based on a timing parameter.*/ | |
| /* REVISIONS: Date Developer Description | |
| Apr 19 2024 Craig Hatmaker Copyright - Thank you Finomatics for the suggestion | |
| Jul 21 2025 Craig Hatmaker Changed ReversalFlags to indicate which prior period to reverse | |
| Jul 23 2025 Craig Hatmaker Added PositiveOnly | |
| */ | |
| CorkScrewReversalλ=LAMBDA( | |
| // Parameter Declarations | |
| [Opening], | |
| [ReversalFlags], | |
| [Flow1], | |
| [Flow2], | |
| [Flow3], | |
| [Flow4], | |
| [PositiveOnly], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →CorkscrewReversalλ( Opening, FLow1, [Flow2], [Flow3], [Flow4], [PositiveOnly])¶" & | |
| "DESCRIPTION: →Create a corkscrew where the opening balance is reversed in the next period based on a timing parameter¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-corkscrewreversal%CE%BB¶" & | |
| "VERSION: →BXL: Apr 19 2024¶" & | |
| "PARAMETERS:→¶" & | |
| " Opening →(Required) First periods opening balance.¶" & | |
| " ReversalFlags →(Required) A row of numbers indicating how many periods back from current to reverse.¶" & | |
| "→ 0=no reversal this period. 1=Reverse close 1 period prior. 2=Reverse close 2 periods prior, etc.¶" & | |
| " Flow1 →(Required) First row (or array of rows) of numbers to include in period totals.¶" & | |
| " Flow2 →(Optional) Second row (or array of rows) of numbers to include in period totals.¶" & | |
| " Flow3 →(Optional) Third row (or array of rows) of numbers to include in period totals.¶" & | |
| " Flow4 →(Optional) Fourth row (or array of rows) of numbers to include in period totals.¶" & | |
| " PositiveOnly →(Optional) TRUE = display flows as positive values. Opens and Closes are not affected.¶" & | |
| "→ Default is FALSE.¶" & | |
| "NOTE! →All flows are added. To subtract a flow make it negative.", | |
| "→", "¶")), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Opening), | |
| ISOMITTED( ReversalFlags), | |
| ISOMITTED( Flow1)), | |
| // Set defaults and constants | |
| Rows, IF(ISOMITTED(Flow2), 1, | |
| IF(ISOMITTED(Flow3), 2, | |
| IF(ISOMITTED(Flow4), 3, 4) | |
| ) | |
| ), | |
| Count, COLUMNS(Flow1), | |
| Periods, SEQUENCE( , Count), | |
| // Procedure | |
| FlowArray, SWITCH(Rows, | |
| 1, Flow1, | |
| 2, VSTACK(Flow1, Flow2), | |
| 3, VSTACK(Flow1, Flow2, Flow3), | |
| 4, VSTACK(Flow1, Flow2, Flow3, Flow4) | |
| ), | |
| PeriodArray, IF( PositiveOnly, ABS( FlowArray), FlowArray), | |
| PeriodTotal, BYCOL(PeriodArray, LAMBDA(Col, SUM(Col))), | |
| Result, REDUCE( Opening, Periods, | |
| LAMBDA( Acc, Period, | |
| LET( | |
| Open, TAKE( Acc, -1, -1), | |
| ReversePeriod, INDEX( ReversalFlags, Period), | |
| ReverseAmount, IF( VALUE( ReversePeriod) = 0, 0, INDEX( TAKE( Acc, -1), Period - ReversePeriod) ), | |
| DisplayColumn, CHOOSECOLS( PeriodArray, Period), | |
| CalcColumn, CHOOSECOLS( FlowArray, Period), | |
| Column, VSTACK( | |
| Open, | |
| ReverseAmount, | |
| DisplayColumn, | |
| SUM(Open, -ReverseAmount, CalcColumn)), | |
| Result, IF(Period = 1, Column, HSTACK( Acc, Column)), | |
| Result | |
| ) | |
| ) | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: CorkScrewAvgBalIntλ | |
| DESCRIPTION:*//**Creates a corkscrew account applying an interest rate to the average of opening and closing balances.*/ | |
| /* REVISIONS: Date Developer Description | |
| Jul 31 2025 Craig Hatmaker Copyright | |
| */ | |
| CorkScrewAvgBalIntλ = LAMBDA( | |
| [InterestRate], | |
| [OpeningBalance], | |
| [Additions], | |
| [PositiveOnly], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →CorkScrewAvgBalIntλ( InterestRate, OpeningBalance, [Additions], [PositiveOnly])¶" & | |
| "DESCRIPTION: →Calculates interest on the average of opening and closing plus interest balances ¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-financial-library/5g-corkscrewavgbalint%CE%BB¶" & | |
| "VERSION: →BXL: Jun 31 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " InterestRate →(Required) The APR divided by the number of periods in a year.¶" & | |
| " OpeningBalance→(Required) The first period's opening balance.¶" & | |
| " Additions, →(Optional) One or more rows (VSTACKed if more than 1 row) of period values to add to close. Default = 0¶" & | |
| " PositiveOnly →(Optional) TRUE = display flows as positive values. Opens and Closes are not affected.¶" & | |
| "→ Default is FALSE.¶" & | |
| "NOTE! →All flows are added. To subtract a flow make it negative.", | |
| "→", "¶" ) | |
| ), | |
| Help?, OR( ISOMITTED( InterestRate), | |
| ISOMITTED( OpeningBalance)), | |
| // Defaults | |
| Additions, IF( ISOMITTED( Additions), 0, Additions), | |
| PositiveOnly, IF( ISOMITTED( PositiveOnly), FALSE, PositiveOnly), | |
| // Pre-Calcs | |
| Count, COLUMNS( Additions), | |
| Periods, SEQUENCE( , Count), | |
| // Recursive function (fnInterest) definition | |
| fnInterest, LAMBDA(fn, InterestRate, Open, Additions, Close, | |
| LET( | |
| AvgBalance, Average(Open, Close), | |
| Interest, AvgBalance * InterestRate, | |
| Closing, Open + Additions + Interest, | |
| Difference, AvgBalance - Average( Open, Closing), | |
| Result, IF( ABS(Difference) < .001, | |
| Interest, | |
| fn( fn, InterestRate, Open, Additions, Closing) | |
| ), | |
| Result | |
| ) | |
| ), | |
| // Procedure | |
| Result, REDUCE( OpeningBalance, Periods, | |
| LAMBDA( Acc, Period, | |
| LET( | |
| // Get Opening | |
| Open, TAKE( Acc, -1, -1), | |
| // Get Current Period Values | |
| TotalAdds, CHOOSECOLS( Additions, Period), | |
| TotalAdd, SUM( TotalAdds), | |
| Interest, fnInterest( fnInterest, InterestRate, Open, TotalAdd, 0), | |
| // Create Period Column | |
| Stack, VSTACK( | |
| Open, | |
| TotalAdds, | |
| Interest, | |
| Open + TotalAdd + Interest | |
| ), | |
| Column, IF( PositiveOnly, ABS(Stack), Stack), | |
| Result, IF( Period = 1, | |
| Column, | |
| HSTACK( Acc, Column) | |
| ), | |
| Result | |
| ) | |
| ) | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| // Depreciation Suite | |
| /* FUNCTION NAME: Depreciateλ | |
| DESCRIPTION:*//**Create a block of MCost, Opening Balance, Depreciation Values, and Book Value for each asset*/ | |
| /* REVISIONS: Date Developer Description | |
| Oct 24 2023 Craig Hatmaker Original Development | |
| May 13 2024 Craig Hatmaker Spring Version | |
| Nov 24 2025 Craig Hatmaker 2025 rewrite | |
| */ | |
| Depreciateλ = LAMBDA( | |
| // Parameter Declarations | |
| [Timeline], | |
| [EndDates?], | |
| [AssetCosts], | |
| [InServiceDates], | |
| [LifeInYears], | |
| [SalvageValues], | |
| [DisposalDates], | |
| [NetDisposals], | |
| [Methods], | |
| [Factors], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →Depreciateλ( Timeline, [EndDates?], AssetCosts, InServiceDates, LifeInYears, ¶" & | |
| " → [SalvageValues], [DisposalDates], [NetDisposals], [Methods], [Factor])¶" & | |
| "DESCRIPTION: →Creates an array of period costs, depreciations, and book values for each asset¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-depreciate%CE%BB ¶" & | |
| "VERSION: →BXL: Nov 22 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " Timeline →(Required) The model's timeline or a row of period start dates. ¶" & | |
| " → Timeline can be in Months, Quarters, or Years¶" & | |
| " EndDates? →(Optional) TRUE=Model's timeline displays period end dates. ¶" & | |
| " → FALSE=Start dates. Default=TRUE¶" & | |
| " AssetCosts →(Required) The acquisition cost of each asset to depreciate¶" & | |
| " InServiceDates→(Required) The dates from which to start depreciating each asset¶" & | |
| " LifeInYears →(Required) The number of years with which to depreciate each asset¶" & | |
| " SalvageValues →(Optional) The book values of each asset after fully depreciated. Default = 0¶" & | |
| " DisposalDates →(Optional) The dates on which the AssetCount are removed from the books. Default = End of life¶" & | |
| " NetDisposals →(Optional) The net of the asset sale less disposal costs. Default = 0¶" & | |
| " Methods →(Optional) The desired depreciation method. Must be one of these Excel function names: ¶" & | |
| "→SLN=Straight Line Depreciation (Default)¶" & | |
| "→SYD=Sum of Year's Digits¶" & | |
| "→DB =Declining Balance¶" & | |
| "→DDB=Double Declining Balance¶" & | |
| "→VDB=Variable Declining Balance (same as DDB)¶" & | |
| " Factors →(Optional) Only applicable to DDB and VDB methods. Default = 2¶" & | |
| "RESULTS: →This function produces the following lines for each asset¶" & | |
| " Asset Cost →¶" & | |
| " Opening Value →¶" & | |
| " Less Depreciation→¶" & | |
| " Book Value →¶" & | |
| " Net Disposal Sale/Cost→¶" & | |
| " Gain/(Loss) →", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Timeline), | |
| ISOMITTED( AssetCosts), | |
| ISOMITTED( InServiceDates), | |
| ISOMITTED( LifeInYears) | |
| ), | |
| IF( Help?, | |
| Help, | |
| LET( | |
| // Set Constants | |
| MpY, 12, | |
| AssetCount, ROWS( AssetCosts), | |
| AssetCounter, SEQUENCE( AssetCount), | |
| BlankLine, IF( Timeline, ""), | |
| // Set optional argument defaults | |
| SalvageValues, IF( ISOMITTED( SalvageValues), | |
| IF( AssetCosts, 0), | |
| MAP( SalvageValues, LAMBDA(v, IFERROR( VALUE( v), 0)))), | |
| InServiceDates, IF( NOT( ISOMITTED( InServiceDates)), | |
| IF( ISNUMBER( InServiceDates), | |
| InServiceDates, | |
| DATEVALUE( InServiceDates))), | |
| DisposalDates, IF( ISOMITTED( DisposalDates), | |
| EDATE( InServiceDates, LifeinYears * MpY), | |
| MAP( AssetCounter, | |
| LAMBDA( Asset, | |
| IF( INDEX( DisposalDates, Asset) = "", | |
| EDATE( INDEX( InServiceDates, Asset), INDEX(LifeinYears, Asset) * MpY), | |
| INDEX( DisposalDates, Asset) | |
| ) | |
| ) | |
| ) | |
| ), | |
| NetDisposals, IF( ISOMITTED( NetDisposals), | |
| SalvageValues, | |
| MAP( NetDisposals, SalvageValues, | |
| LAMBDA(NetDisposal, SalvageValue, | |
| IF( TRIM( NetDisposal) = "", SalvageValue, NetDisposal)))), | |
| Methods, IF( ISOMITTED( Methods), | |
| IF( AssetCosts, "SLN"), | |
| MAP( Methods, LAMBDA(Method, IF( TRIM( Method) = "", "SLN", Method)))), | |
| Factors, IF( ISOMITTED( Factors), | |
| IF( AssetCosts, 2), | |
| MAP( Factors, LAMBDA(Factor, IF( TRIM( Factor) = "", 2, Factor)))), | |
| // Set up model timeline starts and ends | |
| Timelines, DeriveTimelinesλ( Timeline, EndDates?), | |
| MStarts, CHOOSEROWS( Timelines, 1), | |
| MEnds, CHOOSEROWS( Timelines, 2), | |
| // Process each asset, | |
| Result, REDUCE( 0, AssetCounter, | |
| LAMBDA( Block, Asset, | |
| LET( | |
| // Get individual asset's values | |
| Cost, @INDEX( AssetCosts, Asset), | |
| InService, @INDEX( InServiceDates, Asset), | |
| Life, @INDEX( LifeInYears, Asset), | |
| Disposal, @INDEX( DisposalDates, Asset), | |
| Salvage, @INDEX( SalvageValues, Asset), | |
| Net, @INDEX( NetDisposals, Asset), | |
| Method, @INDEX( Methods, Asset), | |
| Factor, @INDEX( Factors, Asset), | |
| // Create asset's timeline | |
| APeriods, DATEDIF(Inservice, Disposal, "M") + 1, | |
| AStarts, EDATE( InService, SEQUENCE( , APeriods, 0)), | |
| AEnds, EDATE( Inservice, SEQUENCE( , APeriods, 1)) - 1, | |
| // Create timing flags and counters | |
| LastPeriodFlag, N( AEnds >= Disposal), | |
| DeprYears, SEQUENCE( , Life), | |
| Prd2DeprYear, TAKE( QUOTIENT( SEQUENCE( , APeriods, 0), MpY) + 1, 1, APeriods), | |
| // Calculate asset's annual depreciation | |
| AnnualDepr, SWITCH(Method, | |
| "SLN", SLNλ( Cost, Salvage, Life), | |
| "SYD", SYDλ( Cost, Salvage, Life), | |
| "DB", DBλ( Cost, Salvage, Life), | |
| "DDB", DDBλ( Cost, Salvage, Life, Factor), | |
| "VDB", VDBλ( Cost, Salvage, Life, Factor, FALSE) | |
| ), | |
| // Create rows for each asset's period values | |
| ADepr, TAKE( XLOOKUP( Prd2DeprYear, DeprYears, AnnualDepr / MpY, 0), 1, APeriods), | |
| ASalvage, Salvage * LastPeriodFlag, | |
| ABookValue, SCAN( Cost, -(ADepr + ASalvage), SUM), | |
| AOpening, DROP( HSTACK( Cost, ABookValue), 0, -1), | |
| ANet, Net * LastPeriodFlag, | |
| AGainLoss, ( ANet - ABookValue) * LastPeriodFlag, | |
| // Define function to aggregate asset periods by model periods | |
| fnAggregate, LAMBDA( Values, | |
| MAP( MStarts, MEnds, | |
| LAMBDA( MStart, MEnd, | |
| SUM(( MStart <= AEnds) * (MEnd >= AEnds) * Values)))), | |
| // Aggregate asset's period values (monthly) under model's timeline | |
| EffFlags, ( Inservice <= MEnds) * ( Disposal >= MStarts), | |
| MCost, Cost * ( MStarts <= InService) * ( MEnds >= InService), | |
| MOpening, XLOOKUP( MStarts, AStarts, AOpening, 0, 1) * EffFlags, | |
| MDepr, fnAggregate( ADepr), | |
| MSalvage, fnAggregate( ASalvage), | |
| MBookValue, MOpening - MDepr - MSalvage, | |
| MNet, fnAggregate( ANet), | |
| MGainLoss, fnAggregate( AGainLoss), | |
| // Create new assets block | |
| NewBlock, VSTACK( MCost, MOpening, MDepr, MSalvage, MBookValue, MNet, MGainLoss), | |
| // Add new asset block to prior blocks | |
| Result, IF( Asset = 1, | |
| NewBlock, | |
| VSTACK( Block, BlankLine, NewBlock)), | |
| Result | |
| ) | |
| ) | |
| ), | |
| Result | |
| ) | |
| ) | |
| ) | |
| ); | |
| /* FUNCTION NAME: DepreciateLabelsλ | |
| DESCRIPTION:*//**Create labels for Depreciateλ*/ | |
| /* REVISIONS: Date Developer Description | |
| Oct 24 2023 Craig Hatmaker Original Development | |
| */ | |
| DepreciateLabelsλ = LAMBDA( | |
| // Parameter Declarations | |
| [AssetNames], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →DepreciateLabelsλ( AssetNames)¶" & | |
| "DESCRIPTION: →Create row labels for the depreciation schedule array produced by Depreciateλ¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-depreciate%CE%BB¶" & | |
| "VERSION: →BXL: Nov 22 2025¶" & | |
| "PARAMETERS: →¶" & | |
| "AssetNames → (Required) The names or descriptions of each asset in teh depreciation schedule produced by Depreciateλ.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, ISOMITTED( AssetNames), | |
| IF( Help?, | |
| Help, | |
| LET( | |
| Result, MAKEARRAY( ROWS(AssetNames) * 8 - 1, 1, | |
| LAMBDA(R, C, | |
| LET(AssetName, INDEX( AssetNames, QUOTIENT(R-1, 8) + 1), | |
| SWITCH( MOD(R-1, 8) + 1, | |
| 1, AssetName & " Asset Cost", | |
| 2, AssetName & " Opening Value", | |
| 3, AssetName & " Less Depreciation", | |
| 4, AssetName & " Less Salvage", | |
| 5, AssetName & " Book Value", | |
| 6, AssetName & " Net Disposal Sale/(Cost)", | |
| 7, AssetName & " Gain/(Loss)", | |
| 8, "" | |
| ) | |
| ) | |
| ) | |
| ), | |
| Result | |
| ) | |
| ) | |
| ) | |
| ); | |
| /* FUNCTION NAME: DepreciateSumsλ | |
| DESCRIPTION:*//**Create row totals for Depreciateλ*/ | |
| /* REVISIONS: Date Developer Description | |
| Dec 21 2023 Craig Hatmaker Original Development | |
| */ | |
| DepreciateSumsλ = LAMBDA( | |
| // Parameter Declarations | |
| [DepreciationSchedule], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →DepreciateSumsλ(DepreciationSchedule)¶" & | |
| "DESCRIPTION: →Create row totals for Depreciateλ¶" & | |
| "WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/5g-depreciate%CE%BB¶" & | |
| "VERSION: →BXL: Dec 21 2023¶" & | |
| "PARAMETERS: →¶" & | |
| " DepreciationSchedule→(Required) An array produced by Depreciateλ.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, ISOMITTED( DepreciationSchedule), | |
| // Procedure | |
| Result, MAKEARRAY(ROWS(DepreciationSchedule), 1, | |
| LAMBDA( R, C, | |
| SWITCH( MOD( R, 8), | |
| 1, SUM(CHOOSEROWS( DepreciationSchedule, R)), | |
| 3, SUM(CHOOSEROWS( DepreciationSchedule, R)), | |
| 4, SUM(CHOOSEROWS( DepreciationSchedule, R)), | |
| 6, SUM(CHOOSEROWS( DepreciationSchedule, R)), | |
| 7, SUM(CHOOSEROWS( DepreciationSchedule, R)), | |
| 0 | |
| ) | |
| ) | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: SLNλ | |
| DESCRIPTION:*//**Straight Line depreciation method for one asset or asset class.*/ | |
| /* REVISIONS: Date Developer Description | |
| Apr 17 2024 Craig Hatmaker Copyright | |
| */ | |
| SLNλ = LAMBDA( | |
| // Parameter Declaration | |
| [Cost], | |
| [Salvage], | |
| [Life], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →SLNλ(Cost, Salvage, Life)¶" & | |
| "DESCRIPTION: →Straight Line Depreciation Method for one asset or asset class.¶" & | |
| "WEBPAGE: →<coming soon>¶" & | |
| "VERSION: →BXL: Apr 17 2024¶" & | |
| "PARAMETERS: →¶" & | |
| " Cost →(Required) Asset's initial cost.¶" & | |
| " Salvage →(Required) Asset's value and end of life¶" & | |
| " Life →(Required) Asset's useful life in years.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Cost), | |
| ISOMITTED( Salvage), | |
| ISOMITTED( Life) | |
| ), | |
| // Procedure | |
| Depn, SLN(Cost, Salvage, Life), | |
| Result, EXPAND(Depn, 1, Life, Depn), | |
| // Return Result or Help | |
| CHOOSE( Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: SYDλ | |
| DESCRIPTION:*//**Sum-of-years' digits depreciation method for one asset or asset class.*/ | |
| /* REVISIONS: Date Developer Description | |
| Apr 17 2024 Craig Hatmaker Copyright | |
| */ | |
| SYDλ = LAMBDA( | |
| // Parameter Declaration | |
| [Cost], | |
| [Salvage], | |
| [Life], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →SYDλ(Cost, Salvage, Life)¶" & | |
| "DESCRIPTION: →Sum-of-years' digits depreciation method for one asset or asset class.¶" & | |
| "WEBPAGE: →<coming soon>¶" & | |
| "VERSION: →BXL: Apr 17 2024¶" & | |
| "PARAMETERS: →¶" & | |
| " Cost →(Required) Asset's initial cost.¶" & | |
| " Salvage →(Required) Asset's value and end of life¶" & | |
| " Life →(Required) Asset's useful life in years.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Cost), | |
| ISOMITTED( Salvage), | |
| ISOMITTED( Life) | |
| ), | |
| // Procedure | |
| Result, SYD(Cost, Salvage, Life, SEQUENCE( , Life)), | |
| // Return Result or Help | |
| CHOOSE( Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: DBλ | |
| DESCRIPTION:*//**Fixed declining balance depreciation method for one asset or asset class.*/ | |
| /* REVISIONS: Date Developer Description | |
| Apr 17 2024 Craig Hatmaker Copyright | |
| */ | |
| DBλ = LAMBDA( | |
| // Parameter Declaration | |
| [Cost], | |
| [Salvage], | |
| [Life], | |
| [Months], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →DBλ(Cost, Salvage, Life, [Month])¶" & | |
| "DESCRIPTION: →Fixed declining balance depreciation method for one asset or asset class¶" & | |
| "WEBPAGE: →<coming soon>¶" & | |
| "VERSION: →BXL: Apr 17 2024¶" & | |
| "PARAMETERS: →¶" & | |
| " Cost →(Required) Asset's initial cost.¶" & | |
| " Salvage →(Required) Asset's value and end of life. Salvage cannot be 0.¶" & | |
| " Life →(Required) Asset's useful life in years.¶" & | |
| " Months →(Optional) Number of months in the first year to depreciate.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Cost), | |
| ISOMITTED( Salvage), | |
| ISOMITTED( Life) | |
| ), | |
| // Set defaults for optional values | |
| Months, IF( ISOMITTED( Months), 12, Months), | |
| // Procedure | |
| Depn, DB( Cost, Salvage, Life, SEQUENCE( , Life - 1 ), Months), | |
| Result, HSTACK( Depn, Cost - Sum(Depn) - Salvage), | |
| // Return Result or Help | |
| CHOOSE( Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: DDBλ | |
| DESCRIPTION:*//**Double-declining balance depreciation method for one asset or asset class.*/ | |
| /* REVISIONS: Date Developer Description | |
| Apr 17 2024 Craig Hatmaker Copyright | |
| */ | |
| DDBλ = LAMBDA( | |
| // Parameter Declaration | |
| [Cost], | |
| [Salvage], | |
| [Life], | |
| [Factor], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →DBλ(Cost, Salvage, Life, [Factor])¶" & | |
| "DESCRIPTION: →Double-declining balance depreciation method for one asset or asset class.¶" & | |
| "WEBPAGE: →<coming soon>¶" & | |
| "VERSION: →BXL: Apr 17 2024¶" & | |
| "PARAMETERS: →¶" & | |
| " Cost →(Required) Asset's initial cost.¶" & | |
| " Salvage →(Required) Asset's value and end of life. Salvage cannot be 0.¶" & | |
| " Life →(Required) Asset's useful life in years.¶" & | |
| " Factor →(Optional Default=2) Rate at which the balance declines.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Cost), | |
| ISOMITTED( Salvage), | |
| ISOMITTED( Life) | |
| ), | |
| // Set defaults for optional values | |
| Factor, IF( ISOMITTED( Factor), 2, Factor), | |
| // Procedure | |
| Depn, DDB( Cost, Salvage, Life, SEQUENCE( , Life -1), Factor), | |
| Result, HSTACK( Depn, Cost - Sum(Depn) - Salvage), | |
| // Return Result or Help | |
| CHOOSE( Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: VDBλ | |
| DESCRIPTION:*//**Variable declining balance depreciation method for one asset or asset class.*/ | |
| /* REVISIONS: Date Developer Description | |
| Apr 17 2024 Craig Hatmaker Copyright | |
| */ | |
| VDBλ = LAMBDA( | |
| // Parameter Declaration | |
| [Cost], | |
| [Salvage], | |
| [Life], | |
| [Factor], | |
| [No_Switch], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →VDBλ(Cost, Salvage, Life, [Factor], [No_Switch])¶" & | |
| "DESCRIPTION: →Variable declining balance depreciation method for one asset or asset class.¶" & | |
| "WEBPAGE: →<coming soon>¶" & | |
| "VERSION: →BXL: Apr 17 2024¶" & | |
| "PARAMETERS: →¶" & | |
| " Cost →(Required) Asset's initial cost.¶" & | |
| " Salvage →(Required) Asset's value and end of life. Salvage cannot be 0.¶" & | |
| " Life →(Required) Asset's useful life in years.¶" & | |
| " Factor →(Optional Default=2) Rate at which the balance declines.¶" & | |
| " No_Switch →(Optional Default=FALSE) Switch to straight-line depreciation when¶" & | |
| "→ depreciation is greater than the declining balance calculation.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Cost), | |
| ISOMITTED( Salvage), | |
| ISOMITTED( Life) | |
| ), | |
| // Set defaults for optional values | |
| Factor, IF( ISOMITTED( Factor), 2, Factor), | |
| No_Switch, IF( ISOMITTED( No_Switch), FALSE, No_Switch), | |
| // Procedure | |
| Result, VDB( Cost, Salvage, Life, SEQUENCE( , Life, 0), SEQUENCE( , Life), Factor), | |
| // Return Result or Help | |
| CHOOSE( Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: MACRSλ | |
| DESCRIPTION:*//**Modified Accelerated Cost Recovery System depreciation method for one asset or asset class.*/ | |
| /* REVISIONS: Date Developer Description | |
| Apr 17 2024 Craig Hatmaker Copyright | |
| */ | |
| MACRSλ = LAMBDA( | |
| // Parameter Declaration | |
| [Cost], | |
| [Life], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →MACRSλ(Cost, Life)¶" & | |
| "DESCRIPTION: →Modified Accelerated Cost Recovery System depreciation method for one asset or asset class.¶" & | |
| "WEBPAGE: →<coming soon>¶" & | |
| "VERSION: →BXL: Apr 17 2024¶" & | |
| "PARAMETERS: →¶" & | |
| " Cost →(Required) Asset's initial cost.¶" & | |
| " Life →(Required) Asset's useful life in years.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Cost), | |
| ISOMITTED( Life) | |
| ), | |
| // Set defaults for optional values | |
| Percentage, SWITCH( | |
| Life, | |
| 03, {33.33,44.45,14.81,7.41}, | |
| 05, {20.00,32.00,19.20,11.52,11.52,5.76}, | |
| 07, {14.29,24.49,17.49,12.49,8.93,8.92,8.93,4.46}, | |
| 10, {10.00,18.00,14.40,11.52,9.22,7.37,6.555,6.55,6.56,6.55,3.25}, | |
| 15, {5.00,9.50,8.55,7.70,6.93,6.23,5.90,5.90,5.91,5.90,5.91,5.90,5.91,5.90,5.91,2.95}, | |
| 20, {3.75,7.22,6.68,6.18,5.71,5.29,4.89,4.52,4.46,4.46,4.46,4.46,4.46,4.46,4.46,4.46,4.46,2.23} | |
| )/100, | |
| // Procedure | |
| Result, Cost * INDEX(Percentage, SEQUENCE( , Life + 1)), | |
| // Return Result or Help | |
| CHOOSE( Help? + 1, Result, Help) | |
| ) | |
| ); | |
| // Sub-functions | |
| /* FUNCTION NAME: FitInTimelineλ | |
| DESCRIPTION:*//**Positions and adjusts an array to fit in a model's timeline*/ | |
| /* REVISIONS: Date Developer Description | |
| Feb 17 2025 Craig Hatmaker Original Development | |
| */ | |
| FitInTimelineλ = LAMBDA( | |
| // Parameter Declarations | |
| [Values], | |
| [ValuesRowTypes], | |
| [ValuesTimeline], | |
| [ModelTimeline], | |
| [EndDates?], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →FitInTimelineλ( Values, ValuesRowTypes, ValuesTimeline, ModelTimeline, [EndDates?])¶" & | |
| "DESCRIPTION: →Positions and adjusts an array to fit in a model's timeline.¶" & | |
| "WEBPAGE: →<Coming Soon>¶" & | |
| "VERSION: →BXL: Feb 17 2025¶" & | |
| "PARAMETERS:→¶" & | |
| " Values →(Required) 2 dimensional array/range containing values to be placed in the¶" & | |
| "→model's timeline.¶" & | |
| " ValuesRowTypes→(Required) A column indicating if a row in Values contains: ¶" & | |
| "→ T=Transactions are SUM'd when several Values periods fit in one model period.¶" & | |
| "→ O=Opening Balances are pulled from first Values period fitting in a model period.¶" & | |
| "→ C=Closing Balances are pulled from last Values period fitting in a model period.¶" & | |
| "→ B=Blank lines.¶" & | |
| " ValuesTimeline→(Required) A row of dates providing the start date for each column in Values¶" & | |
| " ModelTimeline →(Required) The model's timeline¶" & | |
| " EndDates? →(Optional) TRUE (default) = The model's timeline displays period end dates.", | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Values), | |
| ISOMITTED( ValuesRowTypes), | |
| ISOMITTED( ValuesTimeline), | |
| ISOMITTED( ModelTimeline) | |
| ), | |
| // Understand the model's timeline | |
| // Set Constants | |
| MpY, 12, //Months Per Year | |
| DpM, 365/MpY, //Days Per month | |
| QpY, 4, //Quarters Per Year | |
| // Determine how many months are in each of the model's periods | |
| FirstPeriod, INDEX( ModelTimeline, 1), | |
| SecondPeriod, INDEX( ModelTimeline, 2), | |
| MpP, @ROUND(( SecondPeriod - FirstPeriod) / DpM, 0), //Months Per Period | |
| // Set timeline dependent defaults | |
| EndDates?, IF( ISOMITTED( EndDates?), TRUE, EndDates?), | |
| Interval, SWITCH( MpP, 1, "M", 3, "Q", 12, "Y"), | |
| PpY, SWITCH( MpP, 1, MpY, 3, QpY, 12, 1), //Periods Per Year | |
| Starts, IF( EndDates?, EDATE( +ModelTimeline + 1, - MpP), ModelTimeline), | |
| Ends, IF( EndDates?, ModelTimeline, EDATE( +ModelTimeline, MpP) - 1), | |
| // Pre-calcs | |
| Rows, ROWS( Values), | |
| Cols, COLUMNS( Starts), | |
| Window, ( ValuesTimeline >= MIN( Starts)) * ( ValuesTimeline <= MAX( Ends)), | |
| MWindow, (Ends >= MIN( ValuesTimeline)) * (Starts <= MAX( ValuesTimeline)), | |
| PrdNum, IFERROR( MATCH( ValuesTimeline, Starts, 1), 0) * Window, | |
| // Define functions that handle processing each row type. | |
| Functions, FIND(ValuesRowTypes, "OTCB"), | |
| Openings, LAMBDA( r, c, INDEX( INDEX( Values, r, ), MATCH(c, PrdNum, 0))), | |
| Transactions, LAMBDA( r, c, SUM( CHOOSEROWS( Values, r) * (c = PrdNum))), | |
| Closings, LAMBDA( r, c, INDEX( INDEX( Values, r, ), MATCH(c, PrdNum, 1))*INDEX( MWindow, c)), | |
| Blanks, LAMBDA( r, c, ""), | |
| // Procedure | |
| Result, MAKEARRAY( Rows, Cols, | |
| LAMBDA( r, c, | |
| CHOOSE( INDEX( Functions, r), | |
| IFNA( Openings( r, c), 0), | |
| Transactions( r, c), | |
| IFNA( Closings( r, c), 0), | |
| Blanks( r, c) | |
| ) | |
| ) | |
| ), | |
| // Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: TimelineOffsetλ | |
| DESCRIPTION:*//**Determines how many periods one array is offset from the timeline*/ | |
| /* DISCUSSION: The possibility exists requiring calculations before a model's timeline. An example is | |
| depreciation of assets in an existing company wanting to forecast some activity. | |
| In such cases, it can be advantageous to perform the calculations within their own timeline | |
| and then place the results properly within the model's timeline. | |
| This routine determine where such calculation results fit within the model's timeline and | |
| crops those results that do not belong. */ | |
| /* REVISIONS: Date Developer Description | |
| Dec 12 2023 Craig Hatmaker Original Development | |
| Dec 20 2023 Craig Hatmaker Made sure date parameter is numeric, not text. | |
| Dec 21 2023 Craig Hatmaker Added @ to Date | |
| */ | |
| TimelineOffsetλ = LAMBDA( | |
| // Parameter Declarations | |
| [Date], | |
| [Timeline], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →TimelineOffsetλ(ArrayStart, Timeline)¶" & | |
| "DESCRIPTION: →Determines in which period, relative to the timeline's start and interval,¶" & | |
| "→a date falls within.¶" & | |
| "WEBPAGE: →<coming soon>¶" & | |
| "VERSION: →Dec 12 2023¶" & | |
| "PARAMETERS: →¶" & | |
| " Date →(Required) A starting date¶" & | |
| " Timeline →(Required) A model's timline (Row of period start dates).", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Date), | |
| ISOMITTED( Timeline) | |
| ), | |
| // Check inputs - If date is in text, convert to value | |
| Date, @IF( ISTEXT( Date), DATEVALUE( Date), Date), | |
| /* Premise: Create a short timeline that is in sync with the model's timeline that spans | |
| from the timeline start to the date in question plus one period on either side. | |
| No we can use MATCH() to find which timeline period */ | |
| // Determine timeline's interval (length of periods) | |
| Period1, INDEX( Timeline, 1), | |
| Period2, INDEX( Timeline, 2), | |
| MpP, ROUND( (Period2 - Period1)/30.5, 0), //Months Per Period | |
| // Determine if Prior to timeline or after | |
| Direction, IF(Date < Period1, -1, 1), | |
| // Determine the number of months between timeline's start and the date's period start | |
| MonthDiff, @DATEDIF( MIN( Period1, Date), MAX( Period1, Date), "M"), | |
| // Convert to number of periods | |
| PeriodDiff, QUOTIENT( MonthDiff, MpP), | |
| PeriodSeq, SEQUENCE( 1, PeriodDiff + 2, IF( Direction = 1, 0, -( PeriodDiff + 1))), | |
| SearchTimeline, EDATE( Period1, PeriodSeq * MpP), | |
| Result, MATCH( Date, SearchTimeline, 1) - IF( Direction = 1, 1, PeriodDiff + 2), | |
| // Return Result or Help | |
| CHOOSE( Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: TimelinePositionλ | |
| DESCRIPTION:*//**Crops an array based on a model's timeline*/ | |
| /* REVISIONS: Date Developer Description | |
| Dec 12 2023 Craig Hatmaker Original Development | |
| Dec 21 2023 Craig Hatmaker Added @ to Offset | |
| */ | |
| TimelinePositionλ = LAMBDA( | |
| [Array], | |
| [Timeline], | |
| [Offset], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →TimelinePositionλ(Array, Timeline, Offset)¶" & | |
| "DESCRIPTION: →Position an array relative to the model's timeline¶" & | |
| "WEBPAGE: →<coming soon>¶" & | |
| "VERSION: →Dec 12 2023¶" & | |
| "PARAMETERS: →¶" & | |
| " Array →(Required) The array to be positioned relative to a model's timeline¶" & | |
| " Timeline →(Required) A model's timline¶" & | |
| " Offset →(Required) The number of rows or columns to start placing the array within the timeline¶" & | |
| "NOTES! →¶" & | |
| "→Timelines can be vertical or horizontal. Whichever way the timeline goes, the array must match.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Array), | |
| ISOMITTED( Timeline), | |
| ISOMITTED( Offset) | |
| ), | |
| // Check inputs - Make sure scalars are not arrays | |
| Offset, @Offset, | |
| // Precedure | |
| Rows, ROWS(Array), | |
| Cols, COLUMNS(Array), | |
| Result, IF( COLUMNS(TIMELINE) > 1, | |
| // Horizontal Timeline | |
| MAKEARRAY( Rows, COLUMNS( Timeline), | |
| LAMBDA(R, C, | |
| IF( ((C - Offset) >= 1) * (( C - Offset) <= Cols) = 1, | |
| INDEX( Array, R, C - Offset), | |
| 0) | |
| ) | |
| ), | |
| // Vertical Timeline | |
| MAKEARRAY( ROWS( Timeline), Cols, | |
| LAMBDA(R, C, | |
| IF( ((R - Offset) >= 1) * (( R - Offset) <= Rows) = 1, | |
| INDEX( Array, R - Offset, C), | |
| 0) | |
| ) | |
| ) | |
| ), | |
| // Return Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); | |
| /* FUNCTION NAME: PeriodDiffλ | |
| DESCRIPTION:*//**Determine the number of periods between two dates.*/ | |
| /* REVISIONS: Date Developer Description | |
| Apr 15 2024 Craig Hatmaker Copyright | |
| */ | |
| PeriodDiffλ = LAMBDA( | |
| // Parameter Declaration | |
| [StartDate], | |
| [EndDate], | |
| [Interval], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: PeriodDiffλ(StartDate, EndDate, Interval)¶" & | |
| "DESCRIPTION: →Determine the number of periods between two dates.¶" & | |
| "NOTES! →This improves on, and corrects problems w/Excel's deprecated DATEDIF() function.¶" & | |
| "WEBPAGE: →<coming soon>¶" & | |
| "VERSION: →BXL: Apr 15 2024¶" & | |
| "PARAMETERS: →¶" & | |
| " StartDate →(Required) Start Date.¶" & | |
| " EndDate →(Required) End Date¶" & | |
| " Interval →(Required) W=Week, M=Month, Q=Quarter, Y=Year.", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( StartDate), | |
| ISOMITTED( EndDate), | |
| ISOMITTED( Interval) | |
| ), | |
| // Constants | |
| MpP, SWITCH( Interval, "M", 1, "Q", 3, "Y", 12, 0), | |
| // Procedure | |
| Months, DATEDIF( StartDate, EndDate, "M") + 1, | |
| Periods, INT( Months / MpP), | |
| PartialPeriod, N( MOD( Months, MpP) > 0), | |
| EOMCorrection, N( AND( EndDate = EOMONTH( EndDate, 0), DAY( StartDate) > Day( EndDate))), | |
| Result, IF( Interval = "W", | |
| INT((EndDate - StartDate + 1) / 7), | |
| Periods + OR(PartialPeriod, EOMCorrection)), | |
| // Return Result or Help | |
| CHOOSE( Help? + 1, Result, Help) | |
| ) | |
| ); | |
| // Subroutines referenced in this library copied from other libraries | |
| /* FUNCTION NAME: DeriveTimelinesλ | |
| DESCRIPTION:*//**Create internal start and end timelines based on the model's timeline*/ | |
| /* REVISIONS: Date Developer Description | |
| Feb 22 2025 Craig Hatmaker Original Development and copyright | |
| May 16 2025 Craig Hatmaker Added D and W intervals | |
| Jun 17 2025 Craig Hatmaker Efficiency improvements | |
| */ | |
| DeriveTimelinesλ = LAMBDA( | |
| // Parameter Declarations | |
| [ModelTimeline], | |
| [EndDates?], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →DeriveTimelinesλ( ModelTimeline, [EndDates?])¶" & | |
| "DESCRIPTION: →Create internal start and end timelines based on the model's timeline¶" & | |
| "NOTES: →For use by functions that require start and end dates¶" & | |
| "→without requiring models to have both start and end dates¶" & | |
| "WEBPAGE: →<Coming Soon>¶" & | |
| "VERSION: →BXL: May 28 2025¶" & | |
| "PARAMETERS:→¶" & | |
| " ModelTimeline →(Required) Either the model's start or end timeline¶" & | |
| " EndDates? →(Optional) TRUE (default) = The model's timeline displays period end dates.", | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, ISOMITTED( ModelTimeline), | |
| // Understand the model's timeline | |
| // Set Constants | |
| IntervalDays, {1;7;28;88;360}, | |
| IntervalPeriod, {"D";"W";"M";"Q";"Y"}, | |
| // Determine how many days are in model's periods | |
| FirstPeriod, TAKE( ModelTimeline, , 1), | |
| LastPeriod, TAKE( ModelTimeline, , -1), | |
| DpP, INDEX( ModelTimeline, 2) - FirstPeriod, | |
| // Set timeline dependent defaults | |
| EndDates?, IF( ISOMITTED( EndDates?), TRUE, EndDates?), | |
| Interval, XLOOKUP( DpP, IntervalDays, IntervalPeriod, ,-1), | |
| // Select interval formula | |
| DateFunction, IF( OR( AND( NOT( EndDates?), DAY( FirstPeriod = 1)), | |
| AND( EndDates?, DAY( FirstPeriod > 27))), | |
| EOMONTH, EDATE), | |
| // Calculate Start Dates | |
| Starts, IF( EndDates?, | |
| HSTACK( | |
| SWITCH( Interval, | |
| "D", FirstPeriod, | |
| "W", FirstPeriod - 6, | |
| "M", DateFunction( FirstPeriod, -2) + 2, | |
| "Q", DateFunction( FirstPeriod, -4) + 2, | |
| "Y", DateFunction( FirstPeriod, -14) + 2 | |
| ), | |
| DROP( ModelTimeline, , -1) | |
| ), | |
| ModelTimeline | |
| ), | |
| // Calculate End Dates | |
| Ends, IF( EndDates?, | |
| ModelTimeline, | |
| HSTACK( | |
| DROP( ModelTimeline, , 1) - 1, | |
| SWITCH( Interval, | |
| "D", LastPeriod, | |
| "W", LastPeriod + 6, | |
| "M", DateFunction( LastPeriod, 1) - 1, | |
| "Q", DateFunction( LastPeriod, 3) - 1, | |
| "Y", DateFunction( LastPeriod, 12) - 1 | |
| ) | |
| ) | |
| ), | |
| // Procedure | |
| Result, VSTACK(Starts, Ends, IF( Starts, Interval)), | |
| // Result | |
| CHOOSE(Help? + 1, Result, Help) | |
| ) | |
| ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment