-
Notifications
You must be signed in to change notification settings - Fork 354
Add WORKDAY function for calculating working days excluding weekends and holidays #2994
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
6c5397a
cdee6a7
e3fd58a
0cbce62
8707c3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -371,7 +371,25 @@ public EOMonthFunction() | |
|
|
||
| public override IEnumerable<TexlStrings.StringGetter[]> GetSignatures() | ||
| { | ||
| yield return new[] { TexlStrings.EOMonthArg1, TexlStrings.EOMonthArg2 }; | ||
| yield return new[] { TexlStrings.EOMonthArg1, TexlStrings.EOMonthArg2 }; | ||
| } | ||
| } | ||
|
|
||
| // Workday() | ||
| // Equivalent Excel function: WORKDAY | ||
| internal sealed class WorkdayFunction : BuiltinFunction | ||
| { | ||
| public override bool IsSelfContained => true; | ||
|
|
||
| public WorkdayFunction() | ||
| : base("Workday", TexlStrings.AboutWorkday, FunctionCategories.DateTime, DType.Date, 0, 2, 3, DType.DateTime, DType.Number, DType.EmptyTable) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed to use |
||
| { | ||
| } | ||
|
|
||
| public override IEnumerable<TexlStrings.StringGetter[]> GetSignatures() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implemented |
||
| { | ||
| yield return new[] { TexlStrings.WorkdayArg1, TexlStrings.WorkdayArg2 }; | ||
| yield return new[] { TexlStrings.WorkdayArg1, TexlStrings.WorkdayArg2, TexlStrings.WorkdayArg3 }; | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| // Licensed under the MIT license. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics.Contracts; | ||
| using System.Globalization; | ||
| using System.Linq; | ||
|
|
@@ -926,6 +927,90 @@ public static FormulaValue EOMonth(EvalVisitor runner, EvalVisitorContext contex | |
| } | ||
| } | ||
|
|
||
| public static FormulaValue Workday(EvalVisitor runner, EvalVisitorContext context, IRContext irContext, FormulaValue[] args) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot If any arg is an |
||
| { | ||
| var timeZoneInfo = runner.TimeZoneInfo; | ||
|
|
||
| DateTime startDate = runner.GetNormalizedDateTime(args[0]); | ||
|
|
||
| if (args[1] is not NumberValue daysValue) | ||
| { | ||
| throw CommonExceptions.RuntimeMisMatch; | ||
| } | ||
|
|
||
| // Truncate toward zero for Excel compatibility | ||
| int days = (int)Math.Truncate(daysValue.Value); | ||
|
|
||
| // Collect holidays if provided | ||
| var holidays = new HashSet<DateTime>(); | ||
| if (args.Length > 2 && args[2] is not BlankValue) | ||
| { | ||
| if (args[2] is TableValue holidayTable) | ||
| { | ||
| foreach (var row in holidayTable.Rows) | ||
| { | ||
| if (row.IsValue) | ||
| { | ||
| var fields = row.Value.Fields.ToArray(); | ||
| if (fields.Length > 0) | ||
| { | ||
| var field = fields[0]; | ||
| if (field.Value is DateValue dateValue) | ||
| { | ||
| holidays.Add(dateValue.GetConvertedValue(timeZoneInfo).Date); | ||
| } | ||
| else if (field.Value is DateTimeValue dateTimeValue) | ||
| { | ||
| holidays.Add(dateTimeValue.GetConvertedValue(timeZoneInfo).Date); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| try | ||
| { | ||
| DateTime currentDate = startDate.Date; | ||
|
|
||
| // Early return for zero days | ||
| if (days == 0) | ||
| { | ||
| DateTime resultDate = MakeValidDateTime(runner, currentDate, timeZoneInfo); | ||
| return new DateValue(irContext, resultDate); | ||
| } | ||
|
|
||
| int direction = days > 0 ? 1 : -1; | ||
| int remainingDays = Math.Abs(days); | ||
|
|
||
| while (remainingDays > 0) | ||
| { | ||
| currentDate = currentDate.AddDays(direction); | ||
|
|
||
| // Check if it's a weekend (Saturday or Sunday) | ||
| if (currentDate.DayOfWeek == DayOfWeek.Saturday || currentDate.DayOfWeek == DayOfWeek.Sunday) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| // Check if it's a holiday | ||
| if (holidays.Contains(currentDate)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| remainingDays--; | ||
| } | ||
|
|
||
| DateTime newDate = MakeValidDateTime(runner, currentDate, timeZoneInfo); | ||
| return new DateValue(irContext, newDate); | ||
| } | ||
| catch | ||
| { | ||
| return CommonErrors.ArgumentOutOfRange(irContext); | ||
| } | ||
| } | ||
|
|
||
| private static double WeekStartDay(double startOfWeek) | ||
| { | ||
| if (startOfWeek == 1 || startOfWeek == 2) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
|
|
||
| // WORKDAY basic tests | ||
|
|
||
| // Basic forward workday calculations | ||
| >> Workday(Date(2024,8,24),30) | ||
| Date(2024,10,4) | ||
|
|
||
| // Basic backward workday calculations | ||
| >> Workday(Date(2024,8,24),-10) | ||
| Date(2024,8,12) | ||
|
|
||
| // Zero days should return the start date | ||
| >> Workday(Date(2024,8,24),0) | ||
| Date(2024,8,24) | ||
|
|
||
| // Starting on a Saturday (weekend) | ||
| >> Workday(Date(2024,8,24),1) | ||
| Date(2024,8,26) | ||
|
|
||
| // Starting on a Sunday (weekend) | ||
| >> Workday(Date(2024,8,25),1) | ||
| Date(2024,8,26) | ||
|
|
||
| // Starting on a Friday | ||
| >> Workday(Date(2024,8,23),1) | ||
| Date(2024,8,26) | ||
|
|
||
| // Starting on a Monday | ||
| >> Workday(Date(2024,8,26),1) | ||
| Date(2024,8,27) | ||
|
|
||
| // Multiple weeks forward | ||
| >> Workday(Date(2024,1,15),20) | ||
| Date(2024,2,12) | ||
|
|
||
| // Multiple weeks backward | ||
| >> Workday(Date(2024,2,15),-20) | ||
| Date(2024,1,18) | ||
|
|
||
| // Crossing year boundary forward | ||
| >> Workday(Date(2024,12,20),10) | ||
| Date(2025,1,3) | ||
|
|
||
| // Crossing year boundary backward | ||
| >> Workday(Date(2025,1,10),-10) | ||
| Date(2024,12,27) | ||
|
|
||
| // Fractional days should be truncated | ||
| >> Workday(Date(2024,8,24),5.9) | ||
| Date(2024,8,30) | ||
|
|
||
| // Negative fractional days should be truncated | ||
| >> Workday(Date(2024,8,24),-5.9) | ||
| Date(2024,8,19) | ||
|
|
||
| // WORKDAY with holidays | ||
|
|
||
| // Single holiday | ||
| >> Workday(Date(2024,8,24),5,Table({Date:Date(2024,8,28)})) | ||
| Date(2024,9,2) | ||
|
|
||
| // Multiple holidays | ||
| >> Workday(Date(2024,8,24),10,Table({Date:Date(2024,8,28)},{Date:Date(2024,9,2)})) | ||
| Date(2024,9,6) | ||
|
|
||
| // Holiday on weekend should not affect result | ||
| >> Workday(Date(2024,8,24),5,Table({Date:Date(2024,8,25)})) | ||
| Date(2024,8,30) | ||
|
|
||
| // Backward with holidays | ||
| >> Workday(Date(2024,8,30),-5,Table({Date:Date(2024,8,26)})) | ||
| Date(2024,8,21) | ||
|
|
||
| // Holiday that is the start date | ||
| >> Workday(Date(2024,8,26),5,Table({Date:Date(2024,8,26)})) | ||
| Date(2024,9,2) | ||
|
|
||
| // Multiple holidays in range | ||
| >> Workday(Date(2024,12,20),10,Table({Date:Date(2024,12,25)},{Date:Date(2025,1,1)})) | ||
| Date(2025,1,7) | ||
|
|
||
| // WORKDAY with DateTime inputs | ||
|
|
||
| >> Workday(DateTime(2024,8,24,12,30,45),5) | ||
| Date(2024,8,30) | ||
|
|
||
| >> Workday(DateTime(2024,8,24,12,30,45),-5) | ||
| Date(2024,8,19) | ||
|
|
||
| // WORKDAY with Blank values | ||
|
|
||
| >> Workday(Blank(),10) | ||
| Date(1900,1,12) | ||
|
|
||
| >> Workday(Date(2024,8,24),Blank()) | ||
| Date(2024,8,24) | ||
|
|
||
| >> Workday(Blank(),Blank()) | ||
| Date(1899,12,30) | ||
|
|
||
| // Edge cases | ||
|
|
||
| // Large number of workdays forward | ||
| >> Workday(Date(2024,1,1),250) | ||
| Date(2024,12,16) | ||
|
|
||
| // Large number of workdays backward | ||
| >> Workday(Date(2024,12,31),-250) | ||
| Date(2024,1,16) | ||
|
|
||
| // Ensure date values are truncated and do not include time | ||
| >> Text(Workday(DateTime(2024,8,24,12,34,56),5),"yyyy-mm-dd hh:mm:ss") | ||
| "2024-08-30 00:00:00" | ||
|
|
||
| >> Value(Workday(Date(2024,8,24),5)) | ||
| 45535 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function has 2 required args, but have have multiple optional. This should be
int.MaxValueinstead of a fixed 3 value.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually Excel only supports 2 or 3 arguments (and the optional third argument is a list), so that's correct: https://support.microsoft.com/en-us/office/workday-function-f764a5b7-05fc-4494-9486-60d494efbf33