Last active
December 7, 2025 00:25
-
-
Save CHatmaker/28031fac3d3e91dc5997c80fd4a180ec to your computer and use it in GitHub Desktop.
BXL 5g Functions LAMBDA Debt
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
| /* Module Contains 5g Compliant functions that deal with debt and loans. */ | |
| /* FUNCTION NAME: Aboutλ | |
| DESCRIPTION:*//**Displays the URL to this module's Gist which includes documentation*/ | |
| /* REVISIONS: Date Developer Description | |
| Nov 07 2025 Craig Hatmaker Copyright | |
| Nov 18 2025 Craig Hatmaker Loan Label Adjustments | |
| Nov 27 2025 Craig Hatmaker Added AdaptToTimelineλ | |
| Nov 29 2025 Craig Hatmaker See DebtLabelsλ | |
| Dec 06 2025 Craig Hatmaker See Installmentλ & AdaptToTimelineλ | |
| */ | |
| Aboutλ = TRIM( | |
| TEXTSPLIT( | |
| "About: →5g Compliant functions that deal with debt and loans. Suggested module name: BXL¶" & | |
| "Version: →BXL: Dec 06 2025¶" & | |
| "Gist URL: →https://gist.github.com/CHatmaker/28031fac3d3e91dc5997c80fd4a180ec ¶" & | |
| "Website: →https://sites.google.com/site/beyondexcel/home/excel-library/ ¶" & | |
| "→¶" & | |
| "Function →Description¶" & | |
| " Aboutλ →Produces this table¶" & | |
| " PMTAλ →Determine monthly payment amount for commercial loans¶" & | |
| " GetBalanceλ →Determine debt with accrued interest when debt payments start¶" & | |
| " TLAλ →Term Loan A Debt Schedule¶" & | |
| " TLAActλ →Term Loan A Debt Schedule with Actuals¶" & | |
| " TLBλ →Term Loan B Debt Schedule (Balance due at maturity)¶" & | |
| " TLBActλ →Term Loan B Debt Schedule (Balance due at maturity) with Actuals¶" & | |
| " Installmentλ →Monthly installment loan schedule where total payment remains constant.¶" & | |
| " Installmentsλ →Process multiple monthly installment loans.¶" & | |
| " DebtLabelsλ →Generate debt schedule line labels¶" & | |
| "→¶" & | |
| "SUB FUNCTIONS →Functions used by other functions or general purpose functions.¶" & | |
| " DeriveTimelineλ →Derive other timeline (starts or ends) from model's timeline", | |
| " AdaptToTimelineλ →Derive other timeline (starts or ends) from model's timeline", | |
| "→","¶" | |
| ) | |
| ); | |
| /* FUNCTION NAME: PMTAλ | |
| DESCRIPTION:*//**Determine monthly payment amount for commercial loans*/ | |
| /* REVISIONS: Date Developer Description | |
| Oct 23 2025 Craig Hatmaker Copyright | |
| */ | |
| PMTAλ = LAMBDA( | |
| [APR], | |
| [TermInMonths], | |
| [DebtAmount], | |
| [StartDate], | |
| [DayCountConv], | |
| [Tolerance], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →PMTAλ( APR, TermInMonths, DebtAmount, StartDate, DayCountConv, [Tolerance], [MaxIterations])¶" & | |
| "DESCRIPTION: →Determine monthly payment amount for Actual/ (360, 365, or Actual)¶" & | |
| "WEBPAGE: →<Coming Soon>¶" & | |
| "VERSION: →BXL: Jul 03 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¶" & | |
| " DayCountConv →(Optional) Select: 1=30/360, 2=Actual/360, 3=Actual/365, or 4=Actual/Actual. Default = 3.¶" & | |
| " 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) | |
| ), | |
| // Defaults | |
| Tolerance, IF( ISOMITTED( Tolerance), .001, Tolerance), | |
| DayCountConv, IF( OR( ISOMITTED( DayCountConv), DayCountConv=0), 3, DayCountConv), | |
| // Pre Calculations | |
| Counter, SEQUENCE( TermInMonths), | |
| DatesArray, EOMONTH( StartDate, Counter - 1), | |
| MonthDays, IF( DayCountConv = 1, IF( DatesArray, 30), DAY( DatesArray)), | |
| LeapYears, MONTH( DATE( YEAR( DatesArray), 2, 29)) = 2, | |
| YearDays, CHOOSE( DayCountConv, | |
| 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 +, guess too low. Increase lower bound | |
| fn( fn, MidPayment, HiGuess), | |
| // If balance 0 or -, guess too high or just right. Decrease upper bound | |
| fn( fn, LoGuess, MidPayment) | |
| ) | |
| ) | |
| ) | |
| ), | |
| // Run solver-like function to find equal monthly payments | |
| Result, fnPMT( fnPMT, LoGuess, HiGuess), | |
| // Result | |
| IF( Help?, Help, Result) | |
| ) | |
| ); | |
| /* FUNCTION NAME: GetBalanceλ | |
| DESCRIPTION:*//**Determine debt with accrued interest when debt payments start*/ | |
| /* REVISIONS: Date Developer Description | |
| Oct 23 2025 Craig Hatmaker Copyright | |
| */ | |
| GetBalanceλ = LAMBDA( | |
| [APR], | |
| [TermInMonths], | |
| [DebtAmount], | |
| [StartDate], | |
| [AccrualMethod], | |
| [Payment], | |
| LET( | |
| // Help | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →GetBalanceλ( APR, TermInMonths, DebtAmount, StartDate, AccrualMethod, [Tolerance], [Payment])¶" & | |
| "DESCRIPTION: →Determine month payment amount for Actual/ (360, 365, or Actual)¶" & | |
| "WEBPAGE: →<Coming Soon>¶" & | |
| "VERSION: →BXL: Jul 08 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 interest is applied¶" & | |
| " AccrualMethod →(Required) Select: 1=30/360, 2=Actual/360, 3=Actual/365, or 4=Actual/Actual¶" & | |
| " Payment →(Required) Amount applied each month to service the debt." , | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( APR), | |
| ISOMITTED( TermInMonths), | |
| ISOMITTED( DebtAmount), | |
| ISOMITTED( StartDate), | |
| ISOMITTED( AccrualMethod), | |
| ISOMITTED( Payment) | |
| ), | |
| Result, IF( Help?, | |
| Help, | |
| LET( | |
| // Pre Calculations | |
| Counter, SEQUENCE( TermInMonths), | |
| DatesArray, EOMONTH( StartDate, Counter - 1), | |
| MonthDays, IF( AccrualMethod = 1, IF( DatesArray, 30), DAY( DatesArray)), | |
| LeapYears, N( MONTH( DATE( YEAR( DatesArray), 2, 29)) = 2), | |
| YearDays, CHOOSE( AccrualMethod, | |
| IF( DatesArray, 360), | |
| IF( DatesArray, 360), | |
| IF( DatesArray, 365), | |
| IF( DatesArray, 365) + LeapYears | |
| ), | |
| Result, REDUCE( DebtAmount, Counter, | |
| LAMBDA( Acc, n, | |
| LET( | |
| MDays, INDEX( MonthDays, n), | |
| YDays, INDEX( YearDays, n), | |
| Acc + Acc * APR / YDays * MDays - Payment | |
| ) | |
| ) | |
| ), | |
| Result | |
| ) | |
| ), | |
| Result | |
| ) | |
| ); | |
| // Term loans - single | |
| /* FUNCTION NAME: TLAλ | |
| DESCRIPTION:*//**Create a Term Loan A debt schedule */ | |
| /* REVISIONS: Date Developer Description | |
| Oct 23 2025 Craig Hatmaker Copyright | |
| */ | |
| TLAλ = LAMBDA( | |
| [DrawAmount], | |
| [DrawDate], | |
| [Payments], | |
| [APRs], | |
| [DayCountConv], | |
| [Timeline], | |
| [AllPositive?], | |
| [Average?], | |
| LET( | |
| // Help | |
| Help, TRIM( TEXTSPLIT( | |
| "FUNCTION: →TLAλ(DrawAmount, [DrawDate], Payments, APRs, [DayCountConv], ¶" & | |
| "→ Timeline, [AllPositive?], [Average?])¶" & | |
| "DESCRIPTION: →Create a debt schedule (No cash sweep)¶" & | |
| "WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/<coming soon>¶" & | |
| "VERSION: →BXL: Oct 20, 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " DrawAmount →(Required) Opening Balance.¶" & | |
| " DrawDate →(Optional) Date of draw. Default is the period before timeline.¶" & | |
| "→ Payments start the period after draw date¶" & | |
| " Payments →(Optional) One or more payment values. Default = 0.¶" & | |
| "→ The last value specified will be applied to all remaining periods.¶" & | |
| "→ To stop applying last rate, set to 0 in the period after last payment.¶" & | |
| " APRs →(Required) One or more annual interest rate percentages.¶" & | |
| "→ The last value specified will be applied to all remaining periods.¶" & | |
| "→ To stop applying last value, set to 0 in the period after last payment.¶" & | |
| " DayCountConv →(Optional) Determine the Day-count convention used for calculating interest.¶" & | |
| "→ 1=30/360, 2=Actual/360, 3=Actual/365, 4=Actual/Actual. Default = 3 (Actual/365).¶" & | |
| " Timeline →(Required) The model's timeline.¶" & | |
| " AllPositive? →(Optional) Set to TRUE to make all values positive. Default = FALSE.¶" & | |
| " Average? →(Optional) If TRUE, interest is applied to each period's open and close balance average.¶" & | |
| "→ If FALSE, interest is applied to each period's open balance. Default = FALSE¶" & | |
| "→ NOTE! Not applicable when timeline is in months.¶" & | |
| "SUGGESTED LINE LABELS: →¶" & | |
| " Use DebtLabelsλ", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( | |
| ISOMITTED(DrawAmount), | |
| ISOMITTED(Payments), | |
| ISOMITTED(APRs), | |
| ISOMITTED(Timeline) | |
| ), | |
| // Set Defaults | |
| DrawDate, IF( ISOMITTED( DrawDate), 0, DrawDate), | |
| Average?, IF( ISOMITTED( Average?), FALSE, Average?), | |
| DayCountConv, IF( ISOMITTED( DayCountConv), 3, DayCountConv), | |
| // Determine Day-Count Convention settings | |
| FirstPrdDays, INDEX( Timeline, 2) - INDEX( Timeline, 1), | |
| MonthInterval, XLOOKUP( FirstPrdDays, { 0, 90, 365}, { 1, 3, 12}, , -1), | |
| Starts?, DAY( INDEX( Timeline, 1)) = 1, | |
| MissingDate, IF( Starts?, | |
| EOMONTH( TAKE( Timeline, 1, -1), MonthInterval -1), | |
| EDATE( TAKE( Timeline, 1, 1), -MonthInterval ) +1 | |
| ), | |
| Starts, IF( Starts?, | |
| Timeline, | |
| HSTACK( MissingDate, DROP( Timeline + 1, 0, -1)) | |
| ), | |
| Ends, IF( Starts?, | |
| HSTACK( DROP( Timeline - 1, 0, 1), MissingDate), | |
| Timeline | |
| ), | |
| PeriodDays, IF( DayCountConv = 1, | |
| IF( Timeline, MonthInterval * 30), | |
| Ends - Starts + 1 | |
| ), | |
| YearDays, CHOOSE( DayCountConv, | |
| IF( Timeline, 360), | |
| IF( Timeline, 360), | |
| IF( Timeline, 365), | |
| DATE( YEAR( Starts), 12, 31) - DATE( YEAR( Starts), 1, 1) + 1 | |
| ), | |
| // Set period counters | |
| Count, COLUMNS( Timeline), | |
| Periods, SEQUENCE( , Count), | |
| // Convert single values to a row | |
| Payments, IF( COLUMNS( Payments) = 1, | |
| IF(Timeline, Payments), | |
| EXPAND(Payments, 1, Count, TAKE( Payments, -1, -1)) | |
| ), | |
| APRs, IF( COLUMNS( APRs) = 1, | |
| IF(Timeline, APRs), | |
| EXPAND( APRs, 1, Count, TAKE( APRs, -1, -1)) | |
| ), | |
| Rates, APRs * PeriodDays / YearDays, | |
| // Configure ColumnStore. Load each period's values into an array of ColumnStores | |
| ColumnStore, LAMBDA( X, LAMBDA( X)), | |
| ArrayStore, BYCOL( VSTACK( Starts, Ends, Periods, Payments), ColumnStore), | |
| // Procedure | |
| OpenBal, IF( DrawDate = 0, DrawAmount, 0), | |
| Close, SCAN( OpenBal, ArrayStore, | |
| LAMBDA( Acc, ArrayColumn, | |
| LET( | |
| Start, INDEX( ArrayColumn(), 1), | |
| End, INDEX( ArrayColumn(), 2), | |
| Period, INDEX( ArrayColumn(), 3), | |
| Payment, INDEX( ArrayColumn(), 4), | |
| Result, IF( AND(DrawDate >= Start, DrawDate <=End), | |
| DrawAmount, | |
| MAX( Acc - Payment, 0) | |
| ), | |
| Result | |
| ) | |
| ) | |
| ), | |
| Open, TAKE( HSTACK( OpenBal, Close), 1, Count), | |
| Flow, Close - Open, | |
| Draws, Flow * (Flow > 0), | |
| Pmts, IF( AllPositive?, -1, 1) * Flow * (Flow < 0), | |
| IntBal, IF( AND(Average?, MonthInterval>1), | |
| BYCOL( VSTACK( Open, Close), AVERAGE) * (Open <> 0), | |
| Open | |
| ), | |
| Interest, IntBal * Rates * (Open <> 0), | |
| BlankLine, IF( Timeline, ""), | |
| Effective, IF( Open, 1), | |
| Result, VSTACK( | |
| Open, Draws, Pmts, Close, | |
| BlankLine, | |
| PeriodDays * Effective, YearDays * Effective, | |
| BlankLine, | |
| IntBal, Rates * (Open > 0), Interest), | |
| // Return Result | |
| IF(Help?, Help, Result) | |
| ) | |
| ); | |
| /* FUNCTION NAME: TLAActλ | |
| DESCRIPTION:*//**Create a Term Loan A with Actuals debt schedule (No cash sweep)*/ | |
| /* REVISIONS: Date Developer Description | |
| Oct 23 2025 Craig Hatmaker Copyright | |
| */ | |
| TLAActλ = LAMBDA( | |
| [Actuals], | |
| [DrawAmount], | |
| [DrawDate], | |
| [Payments], | |
| [APRs], | |
| [DayCountConv], | |
| [Timeline], | |
| [AllPositive?], | |
| [Average?], | |
| LET( | |
| // Help | |
| Help, TRIM( TEXTSPLIT( | |
| "FUNCTION: →TLAActλ(Actuals, [DrawAmount], [DrawDate], [Payments], APRs, ¶" & | |
| "→ [DayCountConv], Timeline, [AllPositive?], [Average?])¶" & | |
| "DESCRIPTION: →Create a Term Loan A with Actuals debt schedule (No cash sweep)¶" & | |
| "WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/<coming soon>¶" & | |
| "VERSION: →BXL: Oct 11, 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " Actuals →(Required) One or more closing period balances. ¶" & | |
| "→ The number of values sent determine actual periods.¶" & | |
| " DrawAmount →(Optional) Opening Balance. Default = 0 (draw comes from Actuals).¶" & | |
| " DrawDate →(Optional) Date of draw. Default is the period before timeline.¶" & | |
| "→ Payments start the period after draw date¶" & | |
| " Payments →(Optional) One or more payment values. Default = 0.¶" & | |
| "→ The last value specified will be applied to all remaining periods.¶" & | |
| "→ To stop applying last rate, set to 0 in the period after last payment.¶" & | |
| " APRs →(Required) One or more annual interest rate percentages.¶" & | |
| "→ The last value specified will be applied to all remaining periods.¶" & | |
| "→ To stop applying last value, set to 0 in the period after last payment.¶" & | |
| " DayCountConv →(Optional) Determine the Day-count convention used for calculating interest.¶" & | |
| "→ 1=30/360, 2=Actual/360, 3=Actual/365, 4=Actual/Actual. Default = 3 (Actual/365).¶" & | |
| " Timeline →(Required) The model's timeline.¶" & | |
| " AllPositive? →(Optional) Set to TRUE to make all values positive. Default = FALSE.¶" & | |
| " Average? →(Optional) If TRUE, interest is applied to each period's open and close balance average.¶" & | |
| "→ If FALSE, interest is applied to each period's open balance. Default = FALSE¶" & | |
| "→ NOTE! Not applicable when timeline is in months.¶" & | |
| "SUGGESTED LINE LABELS: →¶" & | |
| " Use DebtLabelsλ", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( | |
| ISOMITTED(Actuals), | |
| ISOMITTED(APRs), | |
| ISOMITTED(Timeline) | |
| ), | |
| // Set Defaults | |
| Payments, IF( ISOMITTED( Payments), 0, Payments), | |
| DayCountConv, IF( ISOMITTED( DayCountConv), 3, DayCountConv), | |
| Average?, IF( ISOMITTED( Average?), FALSE, Average?), | |
| AllPositive?, IF( ISOMITTED( AllPositive?), FALSE, AllPositive?), | |
| // Determine Day-Count Convention settings | |
| FirstPrdDays, INDEX( Timeline, 2) - INDEX( Timeline, 1), | |
| MonthInterval, XLOOKUP( FirstPrdDays, { 0, 90, 365}, { 1, 3, 12}, , -1), | |
| Starts?, DAY( INDEX( Timeline, 1)) = 1, | |
| MissingDate, IF( Starts?, | |
| EOMONTH( TAKE( Timeline, 1, -1), MonthInterval -1), | |
| EDATE( TAKE( Timeline, 1, 1), -MonthInterval ) +1 | |
| ), | |
| Starts, IF( Starts?, | |
| Timeline, | |
| HSTACK( MissingDate, DROP( Timeline + 1, 0, -1)) | |
| ), | |
| Ends, IF( Starts?, | |
| HSTACK( DROP( Timeline - 1, 0, 1), MissingDate), | |
| Timeline | |
| ), | |
| PeriodDays, IF( DayCountConv = 1, | |
| IF( Timeline, MonthInterval * 30), | |
| Ends - Starts + 1 | |
| ), | |
| YearDays, CHOOSE( DayCountConv, | |
| IF( Timeline, 360), | |
| IF( Timeline, 360), | |
| IF( Timeline, 365), | |
| DATE( YEAR( Starts), 12, 31) - DATE( YEAR( Starts), 1, 1) + 1 | |
| ), | |
| // Set period and Actuals counters | |
| Count, COLUMNS( Timeline), | |
| Periods, SEQUENCE( , Count), | |
| ActualsCount, COLUMNS( Actuals), | |
| // Convert single values or incomplete rows to complete rows | |
| Actuals, EXPAND( Actuals, 1, Count, 0), | |
| PmtRow, IF( COLUMNS( Payments) = 1, | |
| IF(Timeline, Payments), | |
| EXPAND(Payments, 1, Count, TAKE( Payments, -1, -1)) | |
| ), | |
| APRs, IF( COLUMNS( APRs) = 1, | |
| IF(Timeline, APRs), | |
| EXPAND( APRs, 1, Count, TAKE( APRs, -1, -1)) | |
| ), | |
| Rates, APRs * PeriodDays / YearDays, | |
| // Configure ColumnStore. Load each period's values into an array of ColumnStores | |
| ColumnStore, LAMBDA( X, LAMBDA( X)), | |
| ArrayStore, BYCOL( VSTACK( Starts, Ends, Periods, Actuals, PmtRow), ColumnStore), | |
| // Procedure | |
| Close, SCAN( 0, ArrayStore, | |
| LAMBDA( Acc, ArrayColumn, | |
| LET( | |
| Start, INDEX( ArrayColumn(), 1), | |
| End, INDEX( ArrayColumn(), 2), | |
| Period, INDEX( ArrayColumn(), 3), | |
| Actual, INDEX( ArrayColumn(), 4), | |
| Payment, INDEX( ArrayColumn(), 5), | |
| Result, IF( Period <= ActualsCount, | |
| Actual, | |
| if( AND(DrawDate >= Start, DrawDate <=End), | |
| Acc + DrawAmount, | |
| MAX( Acc - Payment, 0) | |
| ) | |
| ), | |
| Result | |
| ) | |
| ) | |
| ), | |
| Open, TAKE( HSTACK( INDEX( Actuals, 1), Close), 1, Count), | |
| Flow, Close - Open, | |
| Draws, Flow * (Flow > 0), | |
| Pmts, IF( AllPositive?, -1, 1) * Flow * (Flow < 0), | |
| IntBal, IF( AND(Average?, MonthInterval>1), | |
| BYCOL( VSTACK( Open, Close), AVERAGE) * (Open <> 0), | |
| Open | |
| ), | |
| Interest, IntBal * Rates * (Open <> 0), | |
| BlankLine, IF( Timeline, ""), | |
| Effective, IF( Open, 1), | |
| Result, VSTACK( | |
| Open, Draws, Pmts, Close, | |
| BlankLine, | |
| PeriodDays * Effective, YearDays * Effective, | |
| BlankLine, | |
| IntBal, Rates * (Open > 0), Interest), | |
| // Return Result | |
| IF(Help?, Help, Result) | |
| ) | |
| ); | |
| /* FUNCTION NAME: TLBλ | |
| DESCRIPTION:*//**Create a Term Loan B debt schedule (balance due at maturity)*/ | |
| /* REVISIONS: Date Developer Description | |
| Oct 23 2025 Craig Hatmaker Copyright | |
| */ | |
| TLBλ = LAMBDA( | |
| [DrawAmount], | |
| [DrawDate], | |
| [MaturityDate], | |
| [Payments], | |
| [APRs], | |
| [DayCountConv], | |
| [Timeline], | |
| [AllPositive?], | |
| [Average?], | |
| LET( | |
| // Help | |
| Help, TRIM( TEXTSPLIT( | |
| "FUNCTION: →TLBλ( DrawAmount, [DrawDate], MaturityDate, [Payments], APRs, )¶" & | |
| "→ [DayCountConv], Timeline, [AllPositive], [Average?])¶" & | |
| "DESCRIPTION: →Create a Term Loan B debt schedule (balance due at maturity)¶" & | |
| "WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/<coming soon>¶" & | |
| "VERSION: →BXL: Oct 20, 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " DrawAmount →(Required) Opening Balance.¶" & | |
| " DrawDate →(Optional) Date of draw. Default is the period before timeline.¶" & | |
| "→ Payments start the period after draw date¶" & | |
| " MaturityDate →(Required) Date when full repayment is required.¶" & | |
| "→ Payments start the period after draw date¶" & | |
| " Payments →(Optional) One or more payment values. Default = 0.¶" & | |
| "→ The last value specified will be applied to all remaining periods.¶" & | |
| "→ To stop applying last rate, set to 0 in the period after last payment.¶" & | |
| " APRs →(Required) One or more annual interest rate percentages.¶" & | |
| "→ The last value specified will be applied to all remaining periods.¶" & | |
| "→ To stop applying last value, set to 0 in the period after last payment.¶" & | |
| " DayCountConv →(Optional) Determine the Day-count convention used for calculating interest.¶" & | |
| "→ 1=30/360, 2=Actual/360, 3=Actual/365, 4=Actual/Actual. Default = 3 (Actual/365).¶" & | |
| " Timeline →(Required) The model's timeline.¶" & | |
| " AllPositive? →(Optional) Set to TRUE to make all values positive. Default = FALSE.¶" & | |
| " Average? →(Optional) If TRUE, interest is applied to each period's open and close balance average.¶" & | |
| "→ If FALSE, interest is applied to each period's open balance. Default = FALSE¶" & | |
| "→ NOTE! Not applicable when timeline is in months.¶" & | |
| "SUGGESTED LINE LABELS: →¶" & | |
| " Use DebtLabelsλ", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( | |
| ISOMITTED(DrawAmount), | |
| ISOMITTED(MaturityDate), | |
| ISOMITTED(APRs), | |
| ISOMITTED(Timeline) | |
| ), | |
| // Set Defaults | |
| DrawDate, IF( ISOMITTED( DrawDate), 0, DrawDate), | |
| Payments, IF( ISOMITTED( Payments), 0, Payments), | |
| DayCountConv, IF( ISOMITTED( DayCountConv), 3, DayCountConv), | |
| Average?, IF( ISOMITTED( Average?), FALSE, Average?), | |
| AllPositive?, IF( ISOMITTED( AllPositive?), FALSE, AllPositive?), | |
| // Determine Day-Count Convention settings | |
| FirstPrdDays, INDEX( Timeline, 2) - INDEX( Timeline, 1), | |
| MonthInterval, XLOOKUP( FirstPrdDays, { 0, 90, 365}, { 1, 3, 12}, , -1), | |
| Starts?, DAY( INDEX( Timeline, 1)) = 1, | |
| MissingDate, IF( Starts?, | |
| EOMONTH( TAKE( Timeline, 1, -1), MonthInterval -1), | |
| EDATE( TAKE( Timeline, 1, 1), -MonthInterval ) +1 | |
| ), | |
| Starts, IF( Starts?, | |
| Timeline, | |
| HSTACK( MissingDate, DROP( Timeline + 1, 0, -1)) | |
| ), | |
| Ends, IF( Starts?, | |
| HSTACK( DROP( Timeline - 1, 0, 1), MissingDate), | |
| Timeline | |
| ), | |
| PeriodDays, IF( DayCountConv = 1, | |
| IF( Timeline, MonthInterval * 30), | |
| Ends - Starts + 1 | |
| ), | |
| YearDays, CHOOSE( DayCountConv, | |
| IF( Timeline, 360), | |
| IF( Timeline, 360), | |
| IF( Timeline, 365), | |
| DATE( YEAR( Starts), 12, 31) - DATE( YEAR( Starts), 1, 1) + 1 | |
| ), | |
| // Set period counters | |
| Count, COLUMNS( Timeline), | |
| Periods, SEQUENCE( , Count), | |
| // Convert single values or incomplete rows to complete rows | |
| PmtRow, IF( COLUMNS( Payments) = 1, | |
| IF(Timeline, Payments), | |
| EXPAND(Payments, 1, Count, TAKE( Payments, -1, -1)) | |
| ), | |
| APRs, IF( COLUMNS( APRs) = 1, | |
| IF(Timeline, APRs), | |
| EXPAND( APRs, 1, Count, TAKE( APRs, -1, -1)) | |
| ), | |
| Rates, APRs * PeriodDays / YearDays, | |
| // Configure ColumnStore then load each period's column into a storage array: ArrayStore | |
| ColumnStore, LAMBDA( X, LAMBDA( X)), | |
| ArrayStore, BYCOL( VSTACK( Starts, Ends, Periods, PmtRow), ColumnStore), | |
| // Procedure | |
| OpenBal, IF( DrawDate = 0, DrawAmount, 0), | |
| Close, SCAN( OpenBal, ArrayStore, | |
| LAMBDA( Acc, ArrayColumn, | |
| LET( | |
| Start, INDEX( ArrayColumn(), 1), | |
| End, INDEX( ArrayColumn(), 2), | |
| Period, INDEX( ArrayColumn(), 3), | |
| Payment, INDEX( ArrayColumn(), 4), | |
| Result, IF( AND(DrawDate >= Start, DrawDate <=End), | |
| DrawAmount, | |
| IF( AND(MaturityDate >= Start, MaturityDate <=End), | |
| 0, | |
| MAX( Acc - Payment, 0) | |
| ) | |
| ), | |
| Result | |
| ) | |
| ) | |
| ), | |
| Open, TAKE( HSTACK( OpenBal, Close), 1, Count), | |
| Flow, Close - Open, | |
| Draws, Flow * (Flow > 0), | |
| Pmts, IF( AllPositive?, -1, 1) * Flow * (Flow < 0), | |
| IntBal, IF( AND(Average?, MonthInterval>1), | |
| BYCOL( VSTACK( Open, Close), AVERAGE) * (Open <> 0), | |
| Open | |
| ), | |
| Interest, IntBal * Rates * (Open <> 0), | |
| BlankLine, IF( Timeline, ""), | |
| Effective, IF( Open, 1), | |
| Result, VSTACK( | |
| Open, Draws, Pmts, Close, | |
| BlankLine, | |
| PeriodDays * Effective, YearDays * Effective, | |
| BlankLine, | |
| IntBal, Rates * (Open > 0), Interest), | |
| // Return Result | |
| IF(Help?, Help, Result) | |
| ) | |
| ); | |
| /* FUNCTION NAME: TLBActλ | |
| DESCRIPTION:*//**Create a Term Loan B debt schedule (balance due at maturity) with Actuals*/ | |
| /* REVISIONS: Date Developer Description | |
| Oct 23 2025 Craig Hatmaker Copyright | |
| */ | |
| TLBActλ = LAMBDA( | |
| [Actuals], | |
| [DrawAmount], | |
| [DrawDate], | |
| [MaturityDate], | |
| [Payments], | |
| [APRs], | |
| [DayCountConv], | |
| [Timeline], | |
| [AllPositive?], | |
| [Average?], | |
| LET( | |
| // Help | |
| Help, TRIM( TEXTSPLIT( | |
| "FUNCTION: →TLBActλ(Actuals, [DrawAmount], [DrawDate], MaturityDate, [Payments], APRs, ¶" & | |
| "→ [DayCountConv], Timeline, [AllPositive?], [Average?])¶" & | |
| "DESCRIPTION: →Create a Term Loan A with Actuals debt schedule (No cash sweep)¶" & | |
| "WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/¶" & | |
| "→5g-component-libraries/<coming soon>¶" & | |
| "VERSION: →BXL: Oct 11, 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " Actuals →(Required) One or more closing period balances. ¶" & | |
| "→ The number of values sent determine actual periods.¶" & | |
| " DrawAmount →(Required) Opening Balance.¶" & | |
| " DrawDate →(Optional) Date of draw. Default is the period before timeline.¶" & | |
| "→ Payments start the period after draw date¶" & | |
| " MaturityDate →(Required) Date when full repayment is required.¶" & | |
| " Payments →(Optional) One or more payment values. Default = 0.¶" & | |
| "→ The last value specified will be applied to all remaining periods.¶" & | |
| "→ To stop applying last rate, set to 0 in the period after last payment.¶" & | |
| " APRs →(Required) One or more annual interest rate percentages.¶" & | |
| "→ The last value specified will be applied to all remaining periods.¶" & | |
| "→ To stop applying last value, set to 0 in the period after last payment.¶" & | |
| " DayCountConv →(Optional) Determine the Day-count convention used for calculating interest.¶" & | |
| "→ 1=30/360, 2=Actual/360, 3=Actual/365, 4=Actual/Actual. Default = 3 (Actual/365).¶" & | |
| " Timeline →(Required) The model's timeline.¶" & | |
| " AllPositive? →(Optional) Set to TRUE to make all values positive. Default = FALSE.¶" & | |
| " Average? →(Optional) If TRUE, interest is applied to each period's open and close balance average.¶" & | |
| "→ If FALSE, interest is applied to each period's open balance. Default = FALSE¶" & | |
| "→ NOTE! Not applicable when timeline is in months.¶" & | |
| "SUGGESTED LINE LABELS: →¶" & | |
| " Use DebtLabelsλ", | |
| "→", "¶" | |
| ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( | |
| ISOMITTED(Actuals), | |
| ISOMITTED(MaturityDate), | |
| ISOMITTED(APRs), | |
| ISOMITTED(Timeline) | |
| ), | |
| Result, IF( Help?, | |
| Help, | |
| LET( | |
| // Set Defaults | |
| Payments, IF( ISOMITTED( Payments), 0, Payments), | |
| DayCountConv, IF( ISOMITTED( DayCountConv), 3, DayCountConv), | |
| Average?, IF( ISOMITTED( Average?), FALSE, Average?), | |
| AllPositive?, IF( ISOMITTED( AllPositive?), FALSE, AllPositive?), | |
| // Determine Day-Count Convention settings | |
| FirstPrdDays, INDEX( Timeline, 2) - INDEX( Timeline, 1), | |
| MonthInterval, XLOOKUP( FirstPrdDays, { 0, 90, 365}, { 1, 3, 12}, , -1), | |
| Starts?, DAY( INDEX( Timeline, 1)) = 1, | |
| MissingDate, IF( Starts?, | |
| EOMONTH( TAKE( Timeline, 1, -1), MonthInterval -1), | |
| EDATE( TAKE( Timeline, 1, 1), -MonthInterval ) +1 | |
| ), | |
| Starts, IF( Starts?, | |
| Timeline, | |
| HSTACK( MissingDate, DROP( Timeline + 1, 0, -1)) | |
| ), | |
| Ends, IF( Starts?, | |
| HSTACK( DROP( Timeline - 1, 0, 1), MissingDate), | |
| Timeline | |
| ), | |
| PeriodDays, IF( DayCountConv = 1, | |
| IF( Timeline, MonthInterval * 30), | |
| Ends - Starts + 1 | |
| ), | |
| YearDays, CHOOSE( DayCountConv, | |
| IF( Timeline, 360), | |
| IF( Timeline, 360), | |
| IF( Timeline, 365), | |
| DATE( YEAR( Starts), 12, 31) - DATE( YEAR( Starts), 1, 1) + 1 | |
| ), | |
| // Set period and Actuals counters | |
| Count, COLUMNS( Timeline), | |
| Periods, SEQUENCE( , Count), | |
| ActualsCount, COLUMNS( Actuals), | |
| // Convert single values or incomplete rows to complete rows | |
| Actuals, EXPAND( Actuals, 1, Count, 0), | |
| PmtRow, IF( COLUMNS( Payments) = 1, | |
| IF(Timeline, Payments), | |
| EXPAND(Payments, 1, Count, TAKE( Payments, -1, -1)) | |
| ), | |
| APRs, IF( COLUMNS( APRs) = 1, | |
| IF(Timeline, APRs), | |
| EXPAND( APRs, 1, Count, TAKE( APRs, -1, -1)) | |
| ), | |
| Rates, APRs * PeriodDays / YearDays, | |
| // Configure ColumnStore. Load each period's values into an array of ColumnStores | |
| ColumnStore, LAMBDA( X, LAMBDA( X)), | |
| ArrayStore, BYCOL( VSTACK( Starts, Ends, Periods, Actuals, PmtRow), ColumnStore), | |
| // Procedure | |
| Closes, SCAN( 0, ArrayStore, | |
| LAMBDA( Acc, ArrayColumn, | |
| LET( | |
| Start, INDEX( ArrayColumn(), 1), | |
| End, INDEX( ArrayColumn(), 2), | |
| Period, INDEX( ArrayColumn(), 3), | |
| Actual, INDEX( ArrayColumn(), 4), | |
| Payment, INDEX( ArrayColumn(), 5), | |
| Result, IF( Period <= ActualsCount, | |
| Actual, | |
| if( AND(DrawDate >= Start, DrawDate <=End), | |
| Acc + DrawAmount, | |
| IF( AND(MaturityDate >= Start, MaturityDate <=End), | |
| 0, | |
| MAX( Acc - Payment, 0) | |
| ) | |
| ) | |
| ), | |
| Result | |
| ) | |
| ) | |
| ), | |
| Opens, TAKE( HSTACK( INDEX( Actuals, 1), Closes), 1, Count), | |
| Flow, Closes - Opens, | |
| Draws, Flow * (Flow > 0), | |
| Pmts, IF( AllPositive?, -1, 1) * Flow * (Flow < 0), | |
| IntBal, IF( AND(Average?, MonthInterval>1), | |
| BYCOL( VSTACK( Opens, Closes), AVERAGE) * (Opens <> 0), | |
| Opens | |
| ), | |
| Interest, IntBal * Rates * (Opens <> 0), | |
| BlankLine, IF( Timeline, ""), | |
| Effective, IF( Opens, 1), | |
| Result, VSTACK( | |
| Opens, Draws, IF( AllPositive?, Pmts, -Pmts) , Closes, | |
| BlankLine, | |
| PeriodDays * Effective, YearDays * Effective, | |
| BlankLine, | |
| IntBal, Rates * Effective, Interest, IF( AllPositive?, Interest, -Interest), | |
| BlankLine, | |
| Pmts + Interest | |
| ), | |
| Result | |
| ) | |
| ), | |
| Result | |
| ) | |
| ); | |
| /* FUNCTION NAME: Installmentλ | |
| DESCRIPTION:*//**Monthly installment loan schedule where total payment remains constant.*/ | |
| /* REVISIONS: Date Developer Description | |
| Oct 23 2025 Craig Hatmaker Copyright | |
| Nov 26 2025 Craig Hatmaker Adapt to varying model timelines | |
| Dec 04 2025 Craig Hatmaker Inline Help changes | |
| */ | |
| Installmentλ = LAMBDA( | |
| [Timeline], | |
| [EndDates?], | |
| [DrawAmount], | |
| [DrawDate], | |
| [APR], | |
| [TermInMonths], | |
| [DayCountConv], | |
| [StartIntChg], | |
| [StartIntPmt], | |
| [StartDebtPmt], | |
| [AllPositive?], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →Installmentλ( Timeline, [EndDates?], DrawAmount, DrawDate, ¶" & | |
| "→ APR, TermInMonths, [DayCountConv], [StartIntChg], [StartIntPmt], , ¶" & | |
| "→ [StartDebtRepmt], [AllPositive?], [Average?]) ¶" & | |
| "DESCRIPTION: →Monthly installment loan schedule where total payment remains constant. ¶" & | |
| "WEBPAGE: →<Coming Soon>¶" & | |
| "VERSION: →BXL: Dec 06 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " Timeline →(Required) The model's timeline. Timeline can be in Months, Quarters, or Years¶" & | |
| " EndDates? →(Optional) TRUE (Default): Timeline displays period end dates. FALSE: Start dates.¶" & | |
| " DrawAmount →(Required) Opening Balance.¶" & | |
| " DrawDate →(Required) Date of draw.¶" & | |
| " APR →(Required) A single annual interest rate percentage.¶" & | |
| " TermInMonths →(Required) How many months to repay the debt.¶" & | |
| " DayCountConv →(Optional) Determine the Day-count convention used for calculating interest.¶" & | |
| "→ 1=30/360, 2=Actual/360, 3=Actual/365, 4=Actual/Actual. Default = 3 (Actual/365).¶" & | |
| " StartIntChg →(Optional) When interest charges start.Default = DrawDate + 1 Month¶" & | |
| " StartIntPmt →(Optional) When current month's interest payments start. Unpaid interest is¶" & | |
| "→accrued and applied to debt balance. Default = Start Interest Charges¶" & | |
| " StartDebtPmt →(Optional) When Payment on draw plus accrued interest starts.¶" & | |
| "→Default = Interst Payment Start¶" & | |
| " AllPositive? →(Optional) Set to TRUE to make all values positive. Default = FALSE.¶" & | |
| "SUGGESTED LINE LABELS: →¶" & | |
| "(Loan label)→¶" & | |
| " Period days→¶" & | |
| " Year days→¶" & | |
| " (APR) x period days/year days→¶" & | |
| " Draw→¶" & | |
| " Debt balance open→¶" & | |
| " Plus draw→¶" & | |
| " Less repayment→¶" & | |
| " Debt balance close→¶" & | |
| " →¶" & | |
| " Interest balance open→¶" & | |
| " Plus interest charge→¶" & | |
| " Less interest payment→¶" & | |
| " Interest balance close→", | |
| "→", "¶") | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Timeline), | |
| ISOMITTED( DrawAmount), | |
| ISOMITTED( DrawDate), | |
| ISOMITTED( APR), | |
| ISOMITTED( TermInMonths) | |
| ), | |
| Result, IF( Help?, | |
| Help, | |
| LET( | |
| // Set Defaults | |
| EndDates?, IF( ISOMITTED( EndDates?), TRUE, EndDates?), | |
| DayCountConv, IF( ISOMITTED( DayCountConv), 3, DayCountConv), | |
| StartIntChg, IF( ISOMITTED( StartIntChg), EDATE( DrawDate, 1), StartIntChg), | |
| StartIntPmt, IF( ISOMITTED( StartIntPmt), StartIntChg, StartIntPmt), | |
| StartDebtPmt, IF( ISOMITTED( StartDebtPmt), StartIntPmt, StartDebtPmt), | |
| AllPositive?, IF( OR( ISOMITTED( AllPositive?), AllPositive?="") , FALSE, AllPositive?), | |
| // Set up Loan timeline starts and ends | |
| LStarts, EDATE( DrawDate, SEQUENCE( , TermInMonths, 0)), | |
| LEnds, EDATE( DrawDate, SEQUENCE( , TermInMonths, 1)) - 1, | |
| // Setup timing Flags | |
| ChgIntFlags, N( StartIntChg <=LStarts), | |
| PayIntFlags, N( StartIntPmt <= LStarts), | |
| PayDebtFlags, N( StartDebtPmt <= LStarts), | |
| // Set up Day Count Convention days | |
| APRs, IF( LStarts, APR), | |
| PeriodDays, IF( DayCountConv = 1, | |
| IF( LStarts, 30), | |
| LEnds - LStarts + 1), | |
| YearDays, CHOOSE( DayCountConv, | |
| IF( LStarts, 360), | |
| IF( LStarts, 360), | |
| IF( LStarts, 365), | |
| DATE( YEAR( LStarts), 12, 31) - DATE( YEAR( LStarts), 1, 1) + 1 | |
| ), | |
| Rates, ChgIntFlags * APR * PeriodDays / YearDays, | |
| // Determine Payment Amount | |
| PmtCount, SUM( PayDebtFlags), | |
| MoDiff, TermInMonths - PmtCount, | |
| StartPmtBal, IF( MoDiff < 1, | |
| DrawAmount, | |
| REDUCE( DrawAmount, SEQUENCE( , MoDiff), | |
| LAMBDA( Acc, n, Acc + Acc * INDEX( Rates, n)))), | |
| Payment, PMTAλ( APR, PmtCount, StartPmtBal, StartDebtPmt, DayCountConv), | |
| Payments, PayDebtFlags * Payment, | |
| // Procedure | |
| Counter, SEQUENCE( ,TermInMonths), | |
| // Create loan values based on loan's monthly timeline | |
| LResult, REDUCE( 0, Counter, | |
| LAMBDA( Acc, Period, | |
| LET( | |
| Draw, IF( Period = 1, DrawAmount, 0), | |
| DebtOpen, IF( Period = 1, 0, INDEX( Acc, 4, Period -1 )), | |
| Rate, INDEX( Rates, Period), | |
| IntChg, DebtOpen * Rate, | |
| TotPmt, MIN( DebtOpen + IntChg, INDEX( Payments, Period)), | |
| Principal, TotPmt - IntChg, | |
| PayIntFlag, INDEX( PayIntFlags, Period), | |
| DebtClose, DebtOpen + Draw - Principal, | |
| IntOpen, IF( Period = 1, 0, TAKE( Acc, -1, -1)), | |
| IntPmt, (IntOpen + IntChg) * PayIntFlag, | |
| IntClose, IntOpen + IntChg - IntPmt, | |
| Block, VSTACK( | |
| DebtOpen, Draw, IF( AllPositive?, Principal, -Principal), DebtClose, "", | |
| IntOpen, IntChg, IF( AllPositive?, IntPmt, -IntPmt), IntClose), | |
| Result, IF( Period = 1, Block, HSTACK( Acc, Block)), | |
| Result | |
| ) | |
| ) | |
| ), | |
| // Adapt to timeline | |
| Timelines, DeriveTimelinesλ( Timeline, EndDates?), | |
| MStarts, CHOOSEROWS( Timelines, 1), | |
| MEnds, CHOOSEROWS( Timelines, 2), | |
| MPeriod, TAKE( Timelines, -1, 1), | |
| BlankLine, IF( PeriodDays, ""), | |
| Values, VSTACK( BlankLine, PeriodDays, YearDays, Rates, BlankLine, lResult, BlankLine), | |
| AggrTypes, {"Spc"; "Sum"; "ANZ"; "Sum"; "Spc"; | |
| "Opn"; "Sum"; "Sum"; "Cls"; "Spc"; | |
| "Opn"; "Sum"; "Sum"; "Cls"; "Spc"}, | |
| Result, AdaptToTimelineλ( Values, AggrTypes, LStarts, LEnds, MStarts, Mends), | |
| Result | |
| ) | |
| ), | |
| Result | |
| ) | |
| ); | |
| // Term loans - multiples | |
| /* FUNCTION NAME: Installmentsλ | |
| DESCRIPTION:*//**Process multiple monthly installment loan schedules.*/ | |
| /* REVISIONS: Date Developer Description | |
| Nov 07 2025 Craig Hatmaker Copyright | |
| Nov 24 2025 Craig Hatmaker Added varying timelines support | |
| Dec 06 2025 Craig Hatmaker Inline Help changes | |
| */ | |
| Installmentsλ = LAMBDA( | |
| [Timeline], | |
| [EndDates?], | |
| [DrawAmounts], | |
| [DrawDates], | |
| [APRs], | |
| [TermsInMonths], | |
| [DayCountConvs], | |
| [StartIntChgs], | |
| [StartIntPmts], | |
| [StartDebtPmts], | |
| [AllPositive?], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →Installmentsλ( Timeline, [EndDates?], DrawAmounts, DrawDates, ¶" & | |
| "→ APRs, TermInMonths, [DayCountConvs], [StartIntChgs], [StartIntPmts], ¶" & | |
| "→ [StartDebtPmts], [AllPositive?]) ¶" & | |
| "DESCRIPTION: →Process multiple monthly installment loan schedules.¶" & | |
| "→ See Installmentλ website or inline help for more information.¶" & | |
| "WEBPAGE: →<Coming Soon>¶" & | |
| "VERSION: →BXL: Dec 06 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " Timeline →(Required) The model's timeline. Timeline can be in Months, Quarters, or Years¶" & | |
| " EndDates? →(Optional) TRUE (Default): Timeline displays period end dates. FALSE: Start dates.¶" & | |
| " DrawAmounts →(Required) Amounts drawn.¶" & | |
| " DrawDate →(Required) Dates drawn.¶" & | |
| " APRs →(Required) Interest rate percentages.¶" & | |
| " TermsInMonths →(Required) How many months to repay the debt.¶" & | |
| " DayCountConvs →(Optional) Conventions used for calculating interest.¶" & | |
| "→ 1=30/360, 2=Actual/360, 3=Actual/365, 4=Actual/Actual. Default = 3 (Actual/365).¶" & | |
| " StartIntChgs →(Optional) Dates interest charges start. Default = DrawDate + 1 Month¶" & | |
| " StartIntPmts →(Optional) Dates interest payments start. Unpaid interest is¶" & | |
| "→accrued and applied to debt balance. Default = Start Interest Charges¶" & | |
| " StartDebtPmts →(Optional) Date Repayment on draw starts.¶" & | |
| "→Default = Interst Payment Start¶" & | |
| " AllPositive? →(Optional) Set to TRUE to make all values positive. Default = FALSE.¶" & | |
| "SUGGESTED LINE LABELS: →¶" & | |
| "(Loan label)→¶" & | |
| " Period days→¶" & | |
| " Year days→¶" & | |
| " (APR) x period days/year days→¶" & | |
| " Draw→¶" & | |
| " Debt balance open→¶" & | |
| " Plus draw→¶" & | |
| " Less repayment→¶" & | |
| " Debt balance close→¶" & | |
| " →¶" & | |
| " Interest balance open→¶" & | |
| " Plus interest charge→¶" & | |
| " Less interest payment→¶" & | |
| " Interest balance close→", | |
| "→", "¶") | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Timeline), | |
| ISOMITTED( DrawAmounts), | |
| ISOMITTED( APRs), | |
| ISOMITTED( TermsInMonths) | |
| ), | |
| Result, IF( Help?, | |
| Help, | |
| LET( | |
| // Set Defaults | |
| EndDates?, IF( OR( ISOMITTED( EndDates?), EndDates? = ""), | |
| IF( DrawAmounts, TRUE), | |
| EndDates?), | |
| DayCountConvs, IF( OR( ISOMITTED( DayCountConvs), DayCountConvs = ""), | |
| IF( DrawAmounts, 3), | |
| DayCountConvs), | |
| StartIntChgs, IF( OR( ISOMITTED( StartIntChgs), StartIntChgs = ""), | |
| IF( DrawAmounts, EDATE( +DrawDates, 1)), | |
| StartIntChgs), | |
| StartIntPmts, IF( OR( ISOMITTED( StartIntPmts), StartIntPmts = ""), | |
| IF( DrawAmounts, StartIntChgs), | |
| StartIntChgs), | |
| StartDebtPmts, IF( OR( ISOMITTED( StartDebtPmts), StartDebtPmts = ""), | |
| IF( DrawAmounts, StartIntPmts), | |
| StartDebtPmts), | |
| AllPositive?, IF( OR( ISOMITTED( AllPositive?), AllPositive? = ""), | |
| IF( DrawAmounts, FALSE), | |
| AllPositive?), | |
| BlankLine, IF( Timeline , ""), | |
| // Setup Counter | |
| Count, ROWS( TOCOL( APRs)), | |
| Counter, SEQUENCE( Count), | |
| // Process each Loan | |
| Result, REDUCE( 0, Counter, | |
| LAMBDA( Acc, n, | |
| LET( | |
| Draw, INDEX( DrawAmounts, n), | |
| Date, INDEX( DrawDates, n), | |
| Apr, INDEX( APRs, n), | |
| Term, INDEX( TermsInMonths, n), | |
| DCC, INDEX( DayCountConvs, n), | |
| Pos, INDEX( AllPositive?, n), | |
| StrIntchg, INDEX( StartIntchgs, n), | |
| StrIntPmt, INDEX( StartIntPmts, n), | |
| StrDebtPmt, INDEX( StartDebtPmts, n), | |
| Loan, Installmentλ( Timeline, EndDates?, Draw, Date, APR, Term, DCC, | |
| StrIntchg, StrIntPmt, StrDebtPmt, Pos), | |
| Result, IF(n = 1, Loan, VSTACK( Acc, Loan)), | |
| Result | |
| ) | |
| ) | |
| ), | |
| Result | |
| ) | |
| ), | |
| Result | |
| ) | |
| ); | |
| /* FUNCTION NAME: DebtLabelsλ | |
| DESCRIPTION:*//**Generate debt schedule line labels.*/ | |
| /* REVISIONS: Date Developer Description | |
| Nov 18 2025 Craig Hatmaker Copyright | |
| Nov 29 2025 Craig Hatmaker Added loan label | |
| */ | |
| DebtLabelsλ = LAMBDA( | |
| [Labels], | |
| [APRs], | |
| [Currency], | |
| // Help | |
| LET(Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →DebtLabelsλ( [Labels], APRs, [Currencies]) ¶" & | |
| "DESCRIPTION: →Creates line labels for 5g Debt functions. ¶" & | |
| "→ See Installmentλ website or inline help for more information.¶" & | |
| "WEBPAGE: →<Coming Soon>¶" & | |
| "VERSION: →BXL: Nov 18 2025¶" & | |
| "PARAMETERS: →¶" & | |
| " Labels →(Optional) A label for each loan. Numbers will be used if omitted.¶" & | |
| " APRs →(Required) Annual interest rate percentages for each loan.¶" & | |
| " Currency →(Optional) Currency label for all loans. Default=USD", | |
| "→", "¶") | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, ISOMITTED( APRs), | |
| Result, IF( Help?, | |
| Help, | |
| LET( | |
| // Set Defaults | |
| Labels, IF( ISOMITTED( Labels), "Loan " & SEQUENCE( ROWS( APRs)), Labels), | |
| Currency, IF( ISOMITTED( Currency), "USD", Currency), | |
| Curs, IF( APRs, Currency), | |
| // Prepare variables | |
| BlankLine, {"", "", ""}, | |
| Count, ROWS( TOCOL( APRs)), | |
| Counter, SEQUENCE( Count), | |
| // Process | |
| Result, REDUCE( "", Counter, | |
| LAMBDA( Acc, n, | |
| LET( | |
| Cur, INDEX( Curs, n), | |
| Label, INDEX( Labels, n), | |
| Apr, INDEX( APRs, n), | |
| Block, VSTACK( | |
| HSTACK( Label, "", ""), | |
| HSTACK( "Period days", "", "days"), | |
| HSTACK( "Year days", "", "days"), | |
| HSTACK( TEXT(APR, "#.00%") & " x period days/year days", "", "rate"), | |
| BlankLine, | |
| HSTACK( "Debt balance open", "", Cur), | |
| HSTACK( " Plus draw", "", Cur), | |
| HSTACK( " Less repayment", "", Cur), | |
| HSTACK( "Debt balance close", "", Cur), | |
| BlankLine, | |
| HSTACK( "Interest balance open", "", Cur), | |
| HSTACK( " Plus interest charge", "", Cur), | |
| HSTACK( " Less interest payments", "", Cur), | |
| HSTACK( "Interest balance close", "", Cur), | |
| BlankLine | |
| ), | |
| Result, IF(n = 1, Block, VSTACK( Acc, Block)), | |
| Result | |
| ) | |
| ) | |
| ), | |
| Result | |
| ) | |
| ), | |
| Result | |
| ) | |
| ); | |
| // 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 | |
| Nov 26 2025 Craig Hatmaker Simplified | |
| */ | |
| 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:→¶" & | |
| " Timeline →(Required) Model's Timeline¶" & | |
| " EndDates? →(Optional) TRUE (Default): Timeline displays period end dates. FALSE: Start dates.", | |
| , | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, ISOMITTED( ModelTimeline), | |
| Result, IF( Help?, | |
| Help, | |
| LET( | |
| // 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), | |
| 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?, | |
| SWITCH( Interval, | |
| "D", ModelTimeline, | |
| "W", ModelTimeline - 6, | |
| "M", DateFunction( +ModelTimeline, -1) + 1, | |
| "Q", DateFunction( +ModelTimeline, -3) + 1, | |
| "Y", DateFunction( +ModelTimeline, -12) + 1 | |
| ), | |
| ModelTimeline | |
| ), | |
| // Calculate End Dates | |
| Ends, IF( EndDates?, | |
| ModelTimeline, | |
| SWITCH( Interval, | |
| "D", ModelTimeline, | |
| "W", ModelTimeline + 6, | |
| "M", DateFunction( +ModelTimeline, 1) - 1, | |
| "Q", DateFunction( +ModelTimeline, 3) - 1, | |
| "Y", DateFunction( +ModelTimeline, 12) - 1 | |
| ) | |
| ), | |
| // Procedure | |
| Result, VSTACK(Starts, Ends, IF( Starts, Interval)), | |
| Result | |
| ) | |
| ), | |
| Result | |
| ) | |
| ); | |
| // Functions from Finance Library | |
| // Functions from Finance Library | |
| /* FUNCTION NAME: AdaptToTimelineλ | |
| DESCRIPTION:*//**Positions and adjusts an array to fit in a model's timeline*/ | |
| /* REVISIONS: Date Developer Description | |
| Nov 27 2025 Craig Hatmaker Copyright | |
| Dec 06 2025 Craig Hatmaker Fixed Opn | |
| */ | |
| AdaptToTimelineλ = LAMBDA( | |
| // Parameter Declarations | |
| [Values], | |
| [ValuesRowTypes], | |
| [ValuesStarts], | |
| [ValuesEnds], | |
| [ModelStarts], | |
| [ModelEnds], | |
| // Help | |
| LET( | |
| Help, TRIM(TEXTSPLIT( | |
| "FUNCTION: →AdaptToTimelineλ( Values, ValuesRowTypes,¶" & | |
| "→ ValuesStarts, ValuesEnds, ModelStarts, ModelEnds, )¶" & | |
| "DESCRIPTION: →Adapts an array based on one timeline, to the model's timeline.¶" & | |
| "WEBPAGE: →<Coming Soon>¶" & | |
| "VERSION: →BXL: Nov 27 2025¶" & | |
| "PARAMETERS:→¶" & | |
| " Values →(Required) 2 dimensional array/range containing values to be placed in the¶" & | |
| "→model's timeline.¶" & | |
| " ValuesRowTypes→(Required) A column of indicators for each row where values row contains: ¶" & | |
| "→ Avg=Average values in period.¶" & | |
| "→ ANZ=Average non-zero values in period.¶" & | |
| "→ Sum=Transactions - SUM'd by model period.¶" & | |
| "→ Opn=Opening Balances - Pulled from first Value fitting in a model period.¶" & | |
| "→ Cls=Closing Balances - Pulled from last Value fitting in a model period.¶" & | |
| "→ Spc=Blank lines.¶" & | |
| " ValuesStarts →(Required) The values timeline in period start dates¶" & | |
| " ValuesEnds →(Required) The values timeline in period End dates¶" & | |
| " ModelStarts →(Required) The model's timeline period start dates¶" & | |
| " ModelEnds →(Required) The model's timeline period End dates", | |
| "→", "¶" ) | |
| ), | |
| // Check inputs - Omitted required arguments | |
| Help?, OR( ISOMITTED( Values), | |
| ISOMITTED( ValuesRowTypes), | |
| ISOMITTED( ValuesStarts), | |
| ISOMITTED( ValuesEnds), | |
| ISOMITTED( ModelStarts), | |
| ISOMITTED( ModelEnds) | |
| ), | |
| Result, IF( Help?, | |
| Help, | |
| LET( | |
| // Pre-calcs | |
| Rows, ROWS( Values), | |
| Cols, COLUMNS( ModelStarts), | |
| // Define functions that handle processing each row type. | |
| Functions, MATCH( ValuesRowTypes, {"Opn", "Cls", "Avg", "ANZ", "Sum", "Spc"}, 0), | |
| // Procedure | |
| Result, REDUCE( 0, SEQUENCE( Rows), | |
| LAMBDA( Acc, r, | |
| LET( | |
| Function, INDEX( Functions, r), | |
| Row, CHOOSEROWS( Values, r), | |
| Effective, N((TAKE( ValuesStarts, , 1) <= ModelStarts) * (TAKE( ValuesEnds, , -1) >= ModelEnds)), | |
| Line, CHOOSE( Function, | |
| // Open | |
| XLOOKUP( ModelStarts, ValuesStarts, Row, 0, 1), // * Effective, | |
| // Close | |
| XLOOKUP( ModelEnds, ValuesStarts, Row, 0, -1), //* Effective, | |
| // Average | |
| MAP( ModelStarts, ModelEnds, | |
| LAMBDA( MStart, MEnd, | |
| AVERAGE(( MStart <= ValuesEnds) * (MEnd >= ValuesEnds) * Row))), | |
| // Average non-zero | |
| MAP( ModelStarts, ModelEnds, | |
| LAMBDA( MStart, MEnd, | |
| LET( | |
| Values, ( MStart <= ValuesEnds) * (MEnd >= ValuesEnds) * Row, | |
| Filter, FILTER( Values, Values <> 0) , | |
| IFERROR(AVERAGE(Filter), 0) | |
| ))), | |
| // Sum | |
| MAP( ModelStarts, ModelEnds, | |
| LAMBDA( MStart, MEnd, | |
| SUM(( MStart <= ValuesEnds) * (MEnd >= ValuesEnds) * Row))) , | |
| // Space | |
| IF( ModelStarts, "") | |
| ), | |
| Result, IF( r = 1, Line, VSTACK( Acc, Line)), | |
| Result | |
| ) | |
| ) | |
| ), | |
| Result | |
| ) | |
| ), | |
| Result | |
| ) | |
| ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment