Skip to content

Instantly share code, notes, and snippets.

@CHatmaker
Last active June 12, 2024 01:24
Show Gist options
  • Save CHatmaker/3e1708888ec2bd1cde2ec9d002dc459b to your computer and use it in GitHub Desktop.
Save CHatmaker/3e1708888ec2bd1cde2ec9d002dc459b to your computer and use it in GitHub Desktop.
5g Functions for Excel: Dates
/* Module Contains 5g Compliant LAMBDAs that deal with dates */
/* FUNCTION NAME: Aboutλ
DESCRIPTION:*//**Displays the URL to this module's Gist which includes documentation*/
/* REVISIONS: Date Developer Description
Mar 17 2023 Craig Hatmaker Original Development
Mar 22 2023 Craig Hatmaker Added About
Apr 06 2023 Craig Hatmaker Added Help to LAMBDAs
Aug 28 2023 Craig Hatmaker Conformed to new template
Jan 02 2024 Craig Hatmaker See CountDOWλ
Jan 05 2024 Craig Hatmaker Add all data validation/diagnostic routines
Jun 06 2024 Craig Hatmaker Code Simplification
*/
Aboutλ = TRIM(
TEXTSPLIT(
"About: →CBSE compliant LAMBDAs dealing with dates. Suggested module name: BXD¶" &
"Version: →Sep 29 2023¶" &
"Gist URL: →https://gist.github.com/CHatmaker/3e1708888ec2bd1cde2ec9d002dc459b ¶" &
"Website: →https://sites.google.com/site/beyondexcel/home/excel-library/ ¶" &
"→¶" &
"Function →Description¶" &
"Aboutλ →Produces this table¶" &
"CountDOWλ →Count instances of a specific day of the week between two dates¶" &
"IsBetweenλ →Determine if a value is between a lower and upper limit¶" &
"IsOccurrenceDateλ →Determine if a date passed is when a potentially repeating event happens¶" &
"OverLapDaysλ →Return how many days overlap two period ranges.¶" &
"Periodsλ →Determine the number of periods from Starts to Ends inclusive¶" &
"PeriodLabelλ →Creates a label for a date based on period interval¶" &
"ScheduleRatesλ →Schedule rates that persist until replaced in a timeline.¶" &
"ScheduleRatesByItemsλ →Schedule rates that persist until replaced in a timeline for each item in a list.¶" &
"ScheduleValuesλ →Schedules values in a timeline. ¶" &
"ScheduleValuesByItemsλ →Schedules values in a timeline for each item in a list.¶" &
"Timelineλ →Creates a horizontal list of start or end dates for a timeline¶" &
"→¶" &
"DIAGNOSTICS →These functions diagnose problems in their associate function's arguments.¶" &
"→Should a function not work, it may be because there is a problem with the arguments.¶" &
"→Argument problems can include text where numbers should be, invalid dates, etc.¶" &
"→To see if you have argument problems, insert 'DV' between 'λ' and '('. ¶" &
"→Here is an example: CountDOWλDV( Start, End, 1). Hit enter to run diagnostics.¶" &
"→When finished, remove 'DV' to return the function to normal operation¶" &
"CountDOWλDV →¶" &
"IsBetweenλDV →¶" &
"IsOccurrenceDateλDV →¶" &
"OverLapDaysλDV →¶" &
"PeriodsλDV →¶" &
"PeriodLabelλDV →¶" &
"ScheduleRatesλDV →¶" &
"ScheduleRatesByItemsλDV→¶" &
"ScheduleValuesλDV →¶" &
"ScheduleValuesByItemsλDV→¶" &
"Timelineλ →",
"→", "¶"
)
);
/* FUNCTION NAME: CountDOWλ
DESCRIPTION:*/ /**Count instances of a specific day of the week between two dates.*/
/* REVISIONS: Date Developer Description
2010 Barry Houdini Original Formula
Aug 28 2023 Craig Hatmaker Original Development
Jan 01 2024 Craig Hatmaker Corrected Help
*/
CountDOWλ = LAMBDA(
// Parameter Declarations
[Starts],
[Ends],
[DayOfWeek],
// Help
LET(Help, TRIM(TEXTSPLIT(
"FUNCTION: →CountDOWλ(Starts, Ends, DayOfWeek)¶" &
"DESCRIPTION: →Count instances of a specific day of the week between two dates.¶" &
"WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-component-libraries/5g-countdow%CE%BB¶" &
"PARAMETERS: →¶" &
"Starts →(Required) One of more start dates (text or numeric) in a row or column¶" &
"Ends →(Required) End dates (text or numeric) for each start date¶" &
"DayOfWeek →(Required) 1=Sunday, 7=Saturday.¶" &
"EXAMPLES: →¶" &
"Result →Formula (BXD is assumed to be the module's name)¶" &
"2 →=BXD.CountDOWλ(""3/22/2010"", ""4/10/2010"", 1)",
"→", "¶"
)
),
// Check inputs - Omitted required arguments
Help?, OR( ISOMITTED( Starts),
ISOMITTED( Ends),
ISOMITTED( DayOfWeek)
),
// Check inputs - Set optional arguments' defaults and apply convenience conversions
CvtStarts, IF( ISOMITTED( Starts),
FALSE,
IF( ISNUMBER( Starts), Starts, DATEVALUE( Starts))
),
CvtEnds, IF( ISOMITTED( Ends),
FALSE,
IF( ISNUMBER( Ends), Ends, DATEVALUE( Ends))
),
// Procedure
Result, INT( ( ( CvtEnds - CvtStarts) + WeekDay( CvtStarts - DayOfWeek)) / 7),
// Return Result
CHOOSE( Help? + 1, Result, Help)
)
);
/* FUNCTION NAME: IsBetweenλ
DESCRIPTION:*//**Determine if a value is between a lower and upper limit*/
/* REVISIONS: Date Developer Description
Jun 01 2022 Craig Hatmaker Copyright
Apr 12 2023 Craig Hatmaker Added Help
*/
IsBetweenλ = LAMBDA(
// Parameter Declarations
[Values],
[Low],
[High],
[Inclusive],
// Help
LET(Help, TRIM(TEXTSPLIT(
"FUNCTION: →IsBetweenλ(Values, Low, High, [Inclusive])¶" &
"DESCRIPTION: →Determine if a value is between a lower and upper limit.¶" &
"WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-sample-components/5g-isbetween%CE%BB ¶" &
"PARAMETERS: →¶" &
"Values →(Required) one or more values to compare.¶" &
"Low →(Required) The lower limit that the value(s) must be greater than¶" &
"High →(Required) The higher limit that the value(s) must be less than¶" &
"Inclusive →(Optional) TRUE: Value(s) can be equal to Low and/or High (default)¶" &
"EXAMPLES: →¶" &
"Result →Formula (BXD is assumed to be the module's name)¶" &
"FALSE →=BXD.IsBetweenλ(1, 2, 4)¶" &
"TRUE →=BXD.IsBetweenλ(Today(), Today(), Today()+1)¶" &
"FALSE →=BXD.IsBetweenλ(""B"", ""B"", ""D"", FALSE)",
"→", "¶"
)
),
// Check inputs - Set optional arguments defaults
Inclusive, IF( ISLOGICAL( Inclusive), Inclusive, TRUE),
// Check inputs - Omitted required arguments
Help?, OR(
ISOMITTED( Values),
ISOMITTED( Low),
ISOMITTED( High)
),
// Procedure
Result, IF(Inclusive, (Values >= Low) * (Values <= High) = 1, (Values > Low) * (Values < High) = 1),
// Return Result
CHOOSE( Help? + 1, Result, Help)
)
);
/* FUNCTION NAME: IsOccurrenceDateλ
DESCRIPTION:*//**Returns TRUE if Date passed is when something happens. This was developed for budgeting where we have expenses that start, and sometimes repeat at regular intervals.*/
/* REVISIONS: Date Developer Description
Jun 01 2022 Craig Hatmaker Copyright
Apr 07 2023 Craig Hatmaker Added Help
*/
IsOccurrenceDateλ = LAMBDA(
// Parameter declarations
[Dates],
[FirstOccurence],
[LastOccurence],
[Repeats],
// Help
LET(
Help, TRIM(TEXTSPLIT(
"FUNCTION: →IsOccurrenceDateλ(Dates, FirstOccurence, [LastOccurence], [Repeats])¶" &
"DESCRIPTION: →Returns TRUE if a Date is when something happens.¶" &
" →This was developed for budgeting where we have expenses that start,¶" &
" →and sometimes repeat at regular intervals.¶" &
"WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-component-libraries/5g-isoccurrencedate%CE%BB¶" &
"PARAMETERS: →¶" &
"Dates →(Required) Date to test. Normally this is horizontal timeline by day¶" &
"FirstOccurence →(Required) When something that may repeat, happens for the first time.¶" &
"LastOccurence →(Optional) When something that may repeat, happens for the last time.¶" &
"→If omitted, it is assumed repeating items repeat forever.¶" &
"Repeats →(Optional) A code that conveys how often this thing repeats. Repeat codes are¶" &
"→O=One time/never repeats (default)¶" &
"→A=Annually¶" &
"→S=Semi-Annually¶" &
"→Q=Quarterly¶" &
"→M=Monthly¶" &
"→B=BiWeekly¶" &
"→W=Weekly¶" &
"→X=Every Workday (USA normal)¶" &
"→D=Daily¶" &
"EXAMPLES: →¶" &
"Result →Formula (BXD is assumed to be the module's name)¶" &
"72 →=SUM(N(BXD.IsOccurrenceDateλ(SEQUENCE(,90,""1/1/2023""), ""1/1/2023"", ""3/13/2023"", ""D"")))¶" &
"01 →=SUM(N(BXD.IsOccurrenceDateλ(SEQUENCE(,90,""1/1/2023""), ""1/1/2023"", ""3/13/2023"", ""O"")))¶" &
"11 →=SUM(N(BXD.IsOccurrenceDateλ(SEQUENCE(,90,""1/1/2023""), ""1/1/2023"", ""3/13/2023"", ""W"")))",
"→", "¶"
)
),
// Named constants
SemiAnnually, 6, //Months
Quarterly, 3, //Months
Biweekly, 14, //Days
Weekly, 7, //Days
Mon2Sun, 2, //WEEKDAY() Type parameter
Friday, 5, //WEEKDAY()'s value for Friday when type= 2
// Check inputs - Omitted required arguments
Help?, OR( ISOMITTED( Dates),
ISOMITTED( FirstOccurence)
),
// Check inputs - Set optional arguments' defaults and apply convenience conversions
CvtDates, IF( ISOMITTED( Dates),
FALSE,
IF( ISNUMBER( Dates), Dates, DATEVALUE( Dates))
),
CvtFirsts, IF( ISOMITTED( FirstOccurence),
FALSE,
IF( ISNUMBER( FirstOccurence), FirstOccurence, DATEVALUE( FirstOccurence))
),
CvtLasts, IF( ISOMITTED( LastOccurence),
FALSE,
IF( OR( ISNUMBER( LastOccurence), TRIM( LastOccurence) = ""), LastOccurence, DATEVALUE( LastOccurence))
),
Repeats, IF( OR( ISOMITTED(Repeats), TRIM( Repeats) = "" ), "O", LEFT(Repeats, 1)),
// Procedure
SameDay, DAY( CvtDates) = DAY( CvtFirsts),
Test, ( CvtFirsts <= CvtDates) *
IF(TRIM( CvtLasts) = "", 1, CvtDates <= CvtLasts) *
SWITCH(
Repeats,
"O", CvtDates = CvtFirsts,
"A", TEXT(CvtDates, "MMDD") = TEXT(CvtFirsts, "MMDD"),
"S", SameDay *
(MOD( MONTH( CvtDates), SemiAnnually) = MOD( MONTH( CvtFirsts), SemiAnnually)),
"Q", SameDay *
(MOD( MONTH( CvtDates), Quarterly) = MOD( MONTH( CvtFirsts), Quarterly)),
"M", SameDay,
"B", MOD( CvtDates, Biweekly) = MOD( CvtFirsts, Biweekly),
"W", MOD( CvtDates, Weekly) = MOD( CvtFirsts, Weekly),
"X", WEEKDAY( CvtDates, Mon2Sun) <= Friday,
"D", TRUE,
0
),
Result, Test = 1,
// Return Result
CHOOSE( Help? + 1, Result, Help)
)
);
/* FUNCTION NAME: OverLapDaysλ
DESCRIPTION:*//**Return how many days overlap two period ranges. */
/* REVISIONS: Date Developer Description
Jun 01 2022 Craig Hatmaker Copyright
Apr 12 2023 Craig Hatmaker Added Help
Jan 04 2024 Craig Hatmaker Fixed data validation bug
*/
OverLapDaysλ = LAMBDA(
// Parameter Declarations
[Period1Start],
[Period1End],
[Period2Start],
[Period2End],
// Help
LET(
Help, TRIM(TEXTSPLIT(
"FUNCTION: →OverLapDaysλ(Period1Start, Period1End, Period2Start, Period2End)¶" &
"DESCRIPTION: →Count days shared between two sets of periods.¶" &
"WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-component-libraries/5g-overlapdays%CE%BB¶" &
"PARAMETERS: →¶" &
"Period1Start →(Required) First period's start date.¶" &
"Period1End →(Required) First period's end date.¶" &
"Period2Start →(Required) Second period's start date.¶" &
"Period2End →(Required) Decond period's end date.¶" &
"EXAMPLES: →¶" &
"Result →Formula (BXD is assumed to be the module's name)¶" &
"5 →=BXD.OverLapDaysλ(08, 14, 10, 17)¶" &
"8 →=BXD.OverLapDaysλ(10, 20, 11, 18)¶" &
"12 →=BXD.OverLapDaysλ(""1/17/23"", ""1/25/23"", ""1/7/23"", ""1/18/23"")",
"→", "¶"
)
),
// Check inputs - Omitted required arguments
Help?, OR( ISOMITTED( Period1Start),
ISOMITTED( Period1End),
ISOMITTED( Period2Start),
ISOMITTED( Period2End)
),
// Check inputs - Set optional arguments' defaults and apply convenience conversions
CvtP1Start, IF( ISOMITTED( Period1Start),
FALSE,
IF( ISNUMBER( Period1Start), Period1Start, DATEVALUE( Period1Start))
),
CvtP1End, IF( ISOMITTED( Period1End),
FALSE,
IF( ISNUMBER( Period1End), Period1End, DATEVALUE( Period1End))
),
CvtP2Start, IF( ISOMITTED( Period2Start),
FALSE,
IF( ISNUMBER( Period2Start), Period2Start, DATEVALUE( Period2Start))
),
CvtP2End, IF( ISOMITTED( Period2End),
FALSE,
IF( ISNUMBER( Period2End), Period2End, DATEVALUE( Period2End))
),
// Procedure
MinOfEnd, IF(Period2End <= Period1End, Period2End, Period1End),
MaxOfStart, IF(Period2Start >= Period1Start, Period2Start, Period1Start),
Days, MinOfEnd - MaxOfStart + 1,
Result, IF(Days > 0, Days, 0),
// Return Result
CHOOSE( Help? + 1, Result, Help)
)
);
/* FUNCTION NAME: PeriodLabelλ
DESCRIPTION:*//**Creates a lable for a date based on period interval*/
/* REVISIONS: Date Developer Description
2023-Feb-25 Craig Hatmaekr Copyright
2023-Apr-14 Craig Hatmaker Added Help
*/
PeriodLabelλ = LAMBDA(
// Parameter Declarations
[Date],
[Interval],
LET(
// Help
Help, TRIM(TEXTSPLIT(
"FUNCTION: →PeriodLabelλ(Date, [Interval])¶" &
"DESCRIPTION: →Creates a label for a date based on period interval¶" &
"WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-component-libraries/5g-periodlabel%CE%BB¶" &
"PARAMETERS: →¶" &
"Date →(Required) A single date or array of date(s) where the first date is the first date of a calendar year.¶" &
"Interval →(Optional) Must be one of: ¶" &
"→D=Daily¶" &
"→W=Weekly¶" &
"→I=Weekly (using ISO week number)¶" &
"→M=Monthly¶" &
"→Q=Quarterly¶" &
"→S=Semi-Annually¶" &
"→A or Y=Annually¶" &
"EXAMPLES: →¶" &
"Result →Formula (BXD is assumed to be the module's name)¶" &
"2023-Feb-26 →=BXD.PeriodLabelλ(DATEVALUE(""02/26/23""), ""D"")¶" &
"2023-Feb →=BXD.PeriodLabelλ(DATEVALUE(""02/26/23""), ""M"")¶" &
"2023:Q01 →=BXD.PeriodLabelλ(DATEVALUE(""02/26/23""), ""Q"")¶" &
"2023 →=BXD.PeriodLabelλ(DATEVALUE(""02/26/23""), ""Y"")",
"→", "¶"
)
),
// Check inputs - Omitted required arguments
Help?, ISOMITTED( Date),
// Check inputs - Set optional arguments' defaults and apply convenience conversions
CvtDate, IF( ISOMITTED( Date),
FALSE,
IF( ISNUMBER( Date), Date, DATEVALUE( Date))
),
Interval, IF( ISOMITTED( Interval), "M", Interval),
// Procedure
Result, SWITCH(Interval,
"D", TEXT(Date, "yyyy-mmm-dd"),
"W", YEAR(Date) & ":W" & TEXT(QUOTIENT(Date - DATE(YEAR(Date), 1, 0) - 1, 7) + 1, "00"),
"I", YEAR(Date) & ":W" & TEXT(ISOWEEKNUM(Date), "00"),
"M", TEXT(Date, "YYYY-MMM"),
"Q", YEAR(Date) & ":Q" & QUOTIENT(MONTH(Date) - 1, 3) + 1,
"S", YEAR(Date) & ":S" & QUOTIENT(MONTH(Date) - 1, 6) + 1,
"A", TEXT(Date, "YYYY"),
"Y", TEXT(Date, "YYYY"),
#Value!
),
// Return Result
CHOOSE( Help? + 1, Result, Help)
)
);
/* FUNCTION NAME: Periodsλ
DESCRIPTION:*//**Determine the number of periods from Starts to Date2 in Days, Weeks, Months, Quarters, or Years.*/
/* REVISIONS: Date Developer Description
Apr 06,2023 Craig Hatmaker Copyright
Aug 17,2023 Craig Hatmaker Work with arrays of dates
*/
Periodsλ = LAMBDA(
// Parameter Declarations
[Starts],
[Ends],
[Intervals],
LET(
// Help
Help, TRIM(TEXTSPLIT(
"FUNCTION: →Periodsλ(Starts, Ends, [Intervals])¶" &
"DESCRIPTION: →Determine the number of periods from date1 to date 2 inclusive.¶" &
"→Differences to Excel4.0's DateDif are:¶" &
"→* Interval includes Weeks & Quarters¶" &
"→* End Date is inclusive¶" &
"→* Returns negative values if Date1 is after Date2¶" &
"WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-component-libraries/5g-periods%CE%BB¶" &
"PARAMETERS: →¶" &
"Starts →(Required) First date (normally older). This can be a single number or array.¶" &
"Ends →(Required) Second date (normally newer). This must be the same shape (# of rows & columns) as Date1.¶" &
"Intervals →(Optional) Days, Weeks, Months (default), Quarters, or Years (D, W, M, Q, Y)¶" &
"EXAMPLES: →¶" &
"Result →Formula (BXD is assumed to be the module's name)¶" &
"2 →=BXD.Periodsλ(""3/31/2023"", ""5/15/2023"")¶" &
"-2 →=BXD.Periodsλ(""5/15/2023"", ""3/31/2023"")¶" &
"-12 →=BXD.Periodsλ(""1/15/2024"", ""1/16/2023"", , FALSE)¶" &
"-53 →=BXD.Periodsλ(""1/15/2024"", ""1/16/2023"", ""W"")",
"→", "¶"
)
),
// Check inputs - Omitted required arguments
Help?, OR( ISOMITTED( Starts),
ISOMITTED( Ends)
),
// Check inputs - Set optional arguments' defaults and apply convenience conversions
CvtStarts, IF( ISOMITTED( Starts),
FALSE,
IF( ISNUMBER( Starts), Starts, DATEVALUE( Starts))
),
CvtEnds, IF( ISOMITTED( Ends),
FALSE,
IF( ISNUMBER( Ends), Ends, DATEVALUE( Ends))
),
Intervals, IF( ISOMITTED( Intervals), "M", Intervals),
// Constant Declarations
MPY, 12, //Months Per Year
MPQ, 3, //Months Per Quarter
DPW, 7, //Days Per Week
// Procedure
Rows, MAX( ROWS( CvtStarts), ROWS( CvtEnds)),
Cols, MAX( COLUMNS( CvtStarts), COLUMNS( CvtEnds)),
Result, MAKEARRAY(Rows, Cols,
LAMBDA(R, C,
LET(DateOne, IF( COUNTA( CvtStarts) = 1, CvtStarts, INDEX( CvtStarts, R, C)),
DateTwo, IF( COUNTA( CvtEnds) = 1, CvtEnds, INDEX( CvtEnds, R, C)),
Interval, IF( COUNTA( Intervals) = 1, Intervals, INDEX( Intervals, R, C)),
Earliest, MIN(DateOne, DateTwo),
Latest, MAX(DateOne, DateTwo) , //+ 1,
Sign, SIGN(Max(DateTwo - DateOne, 1)),
Periods, Switch(Interval,
"D", DATEDIF(Earliest, Latest, "D"),
"W", INT(DATEDIF(Earliest, Latest, "D")/DPW),
"M", DATEDIF(Earliest, Latest, "M"),
"Q", INT(DATEDIF(Earliest, Latest, "M")/MPQ),
"Y", DATEDIF(Earliest, Latest, "Y")
),
Sign * Periods
)
)
),
// Return Result
CHOOSE( Help? + 1, Result, Help)
)
);
/* FUNCTION NAME: ScheduleRatesλ
DESCRIPTION:*//**Schedule rates that persist until replaced in a timeline. */
/* REVISIONS: Date Developer Description
Jun 01 2022 Craig Hatmaker Copyright
Apr 12 2023 Craig Hatmaker Added Help
*/
ScheduleRatesλ = LAMBDA(
// Parameter Declarations
[PeriodEnds],
[RateStarts],
[Rates],
// Help
LET(
Help, TRIM(TEXTSPLIT(
"FUNCTION: →ScheduleRatesλ(PeriodEnds, RateStarts, Rates)¶" &
"DESCRIPTION: →Schedules rates that persist until replaced in a timeline from a schedule in a table.¶" &
"NOTE! →If 2 rates are scheduled within the same period, only the last rate will be selected.¶" &
"WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-component-libraries/5g-schedulerates%CE%BB¶" &
"PARAMETERS: →¶" &
"PeriodEnds →(Required) A row of period end dates (timeline).¶" &
"RateStarts →(Required) A column or row of rate effective dates in any order.¶" &
"Rates →(Required) A column or row of rates associated with RateStarts.¶" &
"EXAMPLES: →¶" &
"Result →Formula (BXD is assumed to be the module's name)¶" &
"_,10,20,30,30 →=BXD.ScheduleRatesλ(Timelineλ(""1/1/2023"",5,""W""),{44931; 44939; 44945}, {10; 20; 30})",
"→", "¶"
)
),
// Check inputs - Omitted required arguments
s, 1,
Help?, OR( ISOMITTED( PeriodEnds),
ISOMITTED( RateStarts),
ISOMITTED( Rates)
),
// Check inputs - Set optional arguments' defaults and apply convenience conversions
CvtEnds, IF( ISOMITTED( PeriodEnds),
FALSE,
IF( ISNUMBER( PeriodEnds), PeriodEnds, DATEVALUE( PeriodEnds))
),
CvtStarts, IF( ISOMITTED( RateStarts),
FALSE,
IF( ISNUMBER( RateStarts), RateStarts, DATEVALUE( RateStarts))
),
// Procedure
Result, XLOOKUP(PeriodEnds, RateStarts, Rates, "", -1),
// Return Result
CHOOSE( Help? + 1, Result, Help)
)
);
/* FUNCTION NAME: ScheduleRatesByItemsλ
DESCRIPTION:*//**Schedule rates that persist until replaced in a timeline for each item in a list. */
/* REVISIONS: Date Developer Description
Aug 26 2022 Craig Hatmaker Copyright
*/
ScheduleRatesByItemsλ = LAMBDA(
// Parameter Declarations
[PeriodEnds],
[ItemFilter],
[ItemList],
[EffectiveList],
[RateList],
[DoNotUse],
// Help
LET(
Help, TRIM(TEXTSPLIT(
"FUNCTION: →ScheduleRatesByItemsλ(PeriodEnds, ItemFilter, ItemList, EffectiveList, RateList)¶" &
"DESCRIPTION: →Schedules each item's rates by period until another rate takes effect.¶" &
"NOTE! →If 2 rates are scheduled within the same period for the same item, .¶" &
"→only the last rate will be selected.¶" &
"WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-component-libraries/5g-scheduleratesbyitems%CE%BB¶" &
"PARAMETERS: →¶" &
"PeriodEnds →(Required) A row or column of period end dates (timeline).¶" &
"ItemFilter →(Required) 1 or more items to filter a rate schedule by. ¶" &
" →A 'rate schedule' is a table/list where each row contains a unique item/effective date pair with a rate.¶" &
"ItemList →(Required) Items in a rate schedule.¶" &
"EffectiveList →(Required) Effective dates in the rate schedule.¶" &
"RateList →(Required) Rates in a rate schedule for each item/effective date pair.¶" &
"DoNotUse →This is a counter used internally. Do not make any entry.¶" &
"EXAMPLES: →¶" &
"Result →Formula (BXD is assumed to be the module's name)¶" &
"→BXD.ScheduleRatesByItemsλ( Timelineλ( ""1/1/2023""), UNIQUE( tblRates(Apartment)),¶" &
"→tblRates[Apartment], tblRates[Rent], tblRates[Effective])",
"→", "¶"
)
),
// Check inputs - Omitted required arguments
Help?, OR( ISOMITTED( PeriodEnds),
ISOMITTED( ItemFilter),
ISOMITTED( ItemList),
ISOMITTED( EffectiveList),
ISOMITTED( RateList)
),
// Check inputs - Set optional arguments' defaults and apply convenience conversions
DoNotUse, IF( ISOMITTED( DoNotUse), 1, DoNotUse),
CvtPrdEnds, IF( ISOMITTED( PeriodEnds),
FALSE,
IF( ISNUMBER( PeriodEnds), PeriodEnds, DATEVALUE( PeriodEnds))
),
CvtEffDates, IF( ISOMITTED( EffectiveList),
FALSE,
IF( ISNUMBER( EffectiveList), EffectiveList, DATEVALUE( EffectiveList))
),
// Procedure
Counts, COUNTA( ItemFilter),
Item, INDEX( ItemFilter, DoNotUse),
ItemDates, FILTER( CvtEffDates, ItemList = Item),
ItemRates, FILTER( RateList, ItemList = Item),
NewRow, XLOOKUP( PeriodEnds, ItemDates, ItemRates, 0, -1),
Result, IF( DoNotUse >= Counts,
NewRow,
VSTACK(
NewRow,
ScheduleRatesByItemsλ( CvtPrdEnds, ItemFilter, ItemList, CvtEffDates, RateList, DoNotUse + 1 )
)
),
// Return Result
CHOOSE( Help? + 1, Result, Help)
)
);
/* FUNCTION NAME: ScheduleValuesλ
DESCRIPTION:*//**Schedules values in a timeline from a schedule in a table. */
/* REVISIONS: Date Developer Description
Apr 12 2023 Craig Hatmaker Copyright
*/
ScheduleValuesλ = LAMBDA(
// Parameter Declarations
[PeriodStarts],
[PeriodEnds],
[Values],
[EffectiveDates],
LET(
// Help
Help, TRIM(TEXTSPLIT(
"FUNCTION: →ScheduleValuesλ(PeriodStarts, PeriodEnds, Values, EffectiveDates)¶" &
"DESCRIPTION: →Place values from a table into a timeline's appropriate period.¶" &
"WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-examples/5g-schedulevalues%CE%BB ¶" &
"PARAMETERS: →¶" &
"PeriodStarts →(Required) A column or row of period start dates (timeline).¶" &
"PeriodEnds →(Required) A column or row of period ends dates (timeline).¶" &
"Values →(Required) A column or row of values to be scheduled.¶" &
"EffectiveDates →(Required) A column or row of when a value is scheduled.¶" &
"EXAMPLES:→¶" &
"Result →Formula (BXD is assumed to be the module's name)¶" &
"→=BXD.ScheduleValuesλ(Timelineλ(""1/1/23""), Timelineλ(""1/1/23"",,,FALSE), {1;3}, {""1/1/23""; ""2/15/23""})",
"→", "¶"
)
),
// Check inputs - Omitted required arguments
Help?, OR( ISOMITTED( PeriodStarts),
ISOMITTED( PeriodEnds),
ISOMITTED( Values),
ISOMITTED( EffectiveDates)
),
// Check inputs - Set optional arguments' defaults and apply convenience conversions
CvtPrdStarts, IF( ISOMITTED( PeriodStarts),
FALSE,
IF( ISNUMBER( PeriodStarts), PeriodStarts, DATEVALUE( PeriodStarts))
),
CvtPrdEnds, IF( ISOMITTED( PeriodEnds),
FALSE,
IF( ISNUMBER( PeriodEnds), PeriodEnds, DATEVALUE( PeriodEnds))
),
CvtEffDates, IF( ISOMITTED( EffectiveDates),
FALSE,
IF( ISNUMBER( EffectiveDates), EffectiveDates, DATEVALUE( EffectiveDates))
),
// Procedure
Result, BYCOL(
SEQUENCE(, COLUMNS(PeriodStarts)),
LAMBDA(Period,
LET(StartDate, INDEX(PeriodStarts, Period),
EndDate, INDEX(PeriodEnds, Period),
SUM(Values * (CvtEffDates >= StartDate) * (CvtEffDates <= EndDate))
)
)
),
// Return Result
CHOOSE( Help? + 1, Result, Help)
)
);
/* FUNCTION NAME: ScheduleValuesByItemsλ
DESCRIPTION:*//**Schedule and sum values in a timeline for each item.*/
/* REVISIONS: Date Developer Description
Aug 28 2022 Craig Hatmaker Copyright
*/
ScheduleValuesByItemsλ = LAMBDA(
// Parameter Declarations
[PeriodStarts],
[PeriodEnds],
[ItemFilter],
[ItemList],
[EffectiveList],
[ValueList],
[DoNotUse],
LET(
// Help
Help, TRIM(TEXTSPLIT(
"FUNCTION: →ScheduleValuesByItemsλ(PeriodStarts, PeriodEnds, ItemFilter, ItemList, EffectiveList, ValueList)¶" &
"DESCRIPTION: →Schedule and sum values in a timeline for each item.¶" &
"WEBPAGE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-component-libraries/5g-schedulevaluesbyitem%CE%BB¶" &
"PARAMETERS: →¶" &
"PeriodStarts →(Required) A row of period start dates (timeline).¶" &
"PeriodEnds →(Required) A row of period end dates (timeline).¶" &
"ItemFilter →(Required) A column of 1 or more items to filter a value schedule by. ¶" &
" →A 'value schedule' is a table/list where each row contains a unique item/effective date pair with a value.¶" &
"ItemList →(Required) Items in a value schedule.¶" &
"EffectiveList →(Required) Effective dates in the value schedule.¶" &
"ValueList →(Required) Values in a value schedule for each item/effective date pair.¶" &
"DoNotUse →This is a counter used internally. Do not make any entry.¶" &
"EXAMPLES: →¶" &
"Result →Formula (BXD is assumed to be the module's name)¶" &
"→BXD.ScheduleRatesByItemsλ( Timelineλ( ""1/1/2023""), Timelineλ( ""1/1/2023"",,,FALSE), ¶" &
"→UNIQUE( tblExp(Items)), tblExp[Items], tblExp[Dates], tblExp[Amount])",
"→", "¶"
)
),
// Check inputs - Omitted required arguments
Help?, OR( ISOMITTED( PeriodStarts),
ISOMITTED( PeriodEnds),
ISOMITTED( ItemFilter),
ISOMITTED( ItemList),
ISOMITTED( EffectiveList),
ISOMITTED( ValueList)
),
// Check inputs - Set optional arguments' defaults and apply convenience conversions
Counter, IF( ISOMITTED( DoNotUse), 1, DoNotUse),
CvtPrdStarts, IF( ISOMITTED( PeriodStarts),
FALSE,
IF( ISNUMBER( PeriodStarts), PeriodStarts, DATEVALUE( PeriodStarts))
),
CvtPrdEnds, IF( ISOMITTED( PeriodEnds),
FALSE,
IF( ISNUMBER( PeriodEnds), PeriodEnds, DATEVALUE( PeriodEnds))
),
CvtEffDates, IF( ISOMITTED( EffectiveList),
FALSE,
IF( ISNUMBER( EffectiveList), EffectiveList, DATEVALUE( EffectiveList))
),
// Procedure
Counts, COUNTA( ItemFilter),
Item, INDEX( ItemFilter, Counter),
ItemDates, FILTER( CvtEffDates, ItemList = Item),
ItemValues, FILTER( ValueList, ItemList = Item),
Array, (ItemDates >= PeriodStarts) * (ItemDates <= PeriodEnds) * ItemValues,
NewRow, BYCOL( Array, LAMBDA( Column, SUM( Column))),
Result, IF( Counter >= Counts,
NewRow,
VSTACK(
NewRow,
ScheduleValuesByItemsλ( CvtPrdStarts, CvtPrdEnds,
ItemFilter, ItemList, CvtEffDates, ValueList, Counter + 1 )
)
),
// Return Result
CHOOSE( Help? + 1, Result, Help)
)
);
/* FUNCTION NAME: Timelineλ
DESCRIPTION:*//**Create a horizontal list of period dates*/
/* REVISIONS: Date Developer Description
Aug 19,2023 Craig Hatmaker Copyright
*/
Timelineλ = LAMBDA(
// Parameter Declarations
[StartDate],
[Periods],
[Interval],
[PeriodStarts?],
LET(
// Help
Help, TRIM(TEXTSPLIT(
"FUNCTION: →Timelineλ(StartDate, Periods, Interval, PeriodStarts?)¶" &
"DESCRIPTION: →Creates a horizontal list of period dates.¶" &
"WEBSITE: →https://sites.google.com/site/beyondexcel/home/5g-modeling/5g-component-libraries/5g-timeline%CE%BB¶" &
"PARAMETERS: →¶" &
"StartDate →(Required) First period's start date. May be entered as text.¶" &
"Periods →(Optional) The number of period in the timeline. Defaults to 12.¶" &
"Interval →(Optional) Period interval: D, W, M, Q or Y (Day, Week, Month <default>, Quarter, or Year)¶" &
"PeriodStarts? →(Optional) TRUE=Display period start dates <default>. FALSE=Period end dates¶" &
"EXAMPLES: →¶" &
"Result →Formula (BXD is assumed to be the module's name)¶" &
"12 mo, starts →=BXD.Timelineλ(TODAY())¶" &
"12 qtr, ends →=BXD.Timelineλ(TODAY(), , ""Q"", FALSE)",
"→", "¶"
)
),
// Named Constants
MPY, 12, //Months Per Year
MPQ, 3, //Months Per Quarter
DPW, 7, //Days Per Week
// Check inputs - Omitted required arguments
Help?, ISOMITTED( StartDate),
// Check inputs - Set optional arguments' defaults and apply convenience conversions
CvtStart, IF( ISOMITTED( StartDate),
FALSE,
IF( ISNUMBER( StartDate), StartDate, DATEVALUE( StartDate))
),
Periods, IF( ISOMITTED( Periods), 12, Periods),
Interval, IF( ISOMITTED( Interval), "M", Interval),
PeriodStarts?, IF( ISLOGICAL( PeriodStarts?), PeriodStarts?, TRUE),
// Procedure
EndDates, N(NOT(PeriodStarts?)),
Result, SWITCH(
Interval,
"Y", EDATE(StartDate, SEQUENCE(1, Periods, EndDates * MPY, MPY)) - EndDates,
"Q", EDATE(StartDate, SEQUENCE(1, Periods, EndDates * MPQ, MPQ)) - EndDates,
"M", EDATE(StartDate, SEQUENCE(1, Periods, EndDates, 1)) - EndDates,
"W", SEQUENCE(1, Periods, StartDate + IF(PeriodStarts?, 0, DPW - 1), DPW),
"D", SEQUENCE(1, Periods, StartDate, 1),
#Value!
),
// Return Result
CHOOSE( Help? + 1, Result, Help)
)
);
@pbartxl
Copy link

pbartxl commented Aug 20, 2022

A set of general business modelling functions presented with such elegance and class!
They inspire confidence.

@ncalm
Copy link

ncalm commented Aug 7, 2023

Hi Craig, this is a very impressive collection. Clearly a lot of care and attention to formatting, error trapping and so on.

I noticed that you define all params as optional, with trapping such as:

    Date,               IF( OR(ISOMITTED(Date), Date = ""), #VALUE!, Date),

I'm unclear of the purpose here. It seems that Date is required, since it returns a #VALUE! error if omitted, but that's the default behavior if a required parameter is omitted, with the benefit that any intervening or subsequent code is not executed.

The same effect could be had with Date not optional, and

    Date,               IF( Date = "", #VALUE!, Date),

What's the purpose of making a required parameter optional? Is it to enable the querying of in-function help?

@CHatmaker
Copy link
Author

What's the purpose of making a required parameter optional? Is it to enable the querying of in-function help?

I'm still working on how to best handle this. Suggestions are invited. If I understand what you mean by "enable the querying of in-function help?" then, yes.

The problem I am trying to address starts with LAMBD'As severe shortcomings compared to native Excel functions:

  1. The ability to evaluate LAMBDA calculations within the formula bar (Select + F9).
  2. The ability to see anything useful for a LAMBDA when clicking Fx.

These two features are extremely helpful in understanding what we did wrong when we enter bad parameters into Excel's native functions. But for for LAMBDAs, the only alternative I see to help users in this regard is to provide good error messages.

In some of my LAMBDAs, which I hope to add to all, I test each parameter, not just for inclusion, but also for content and context. So something may be entered by a user for a required Date parameter but the date may not be a valid date or the date may be in the past when a date in the future is required. So I would like to display a message that tells the user exactly what they did wrong and how to fix it. Many of the LAMBDAs I have out now took an, admittedly, lazy approach to error messages. If anything goes wrong at all, display the inline help which tries to provide sufficient information. My newer approach is in this gist:

https://gist.github.com/CHatmaker/5dcb3de4ad9766d1baf0b95338ebdbf3

In Loanλ() is an array called "ErrorMessages". After testing each parameter, we build "ErrorArray" with each parameters' "ErrorMessages" and test result. We then filter all passed error messages out in the "Messages" step. In the "Error" step we determine which messages (if any) to display by testing if ALL parameter's failed, only some parameter's failed, only the "Result" failed, or nothing failed.

As I said, I'm still trying to figure out what is the best way to handle errors and I am very hopeful people like you will offer assistance. Thank you for your question. I look forward to any suggestions you may have.

@ikellerer
Copy link

ikellerer commented Jan 5, 2024

First of all: this is a great library and together with the EuSpRIG-paper a great source to learn!

Regarding IsOccurrenceDateλ

The Annual check doesn't work e.g. for a German system as it would need to be TEXT(CvtDates, "MMTT") = TEXT(CvtFirsts, "MMTT") (T (Tag) instead of D (Day)) - this doesn't get translated :-(

I updated the check like this:

    //  Procedure
        SameDay,        DAY( CvtDates) = DAY( CvtFirsts),
        SameMonth,      MONTH(CvtDates)=MONTH(CvtFirsts),
        Test,           ( CvtFirsts <= CvtDates) * 
                        IF(TRIM( CvtLasts) = "", 1, CvtDates <= CvtLasts) *
                        SWITCH(
                            Repeats,
                            "O",    CvtDates = CvtFirsts,
                            "A",    SameDay * SameMonth,

@CHatmaker
Copy link
Author

Jkellerer, Thanks for letting me know. Clever fix. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment