Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ internal static class TexlStrings
public static StringGetter AboutEOMonth = (b) => StringResources.Get("AboutEOMonth", b);
public static StringGetter EOMonthArg1 = (b) => StringResources.Get("EOMonthArg1", b);
public static StringGetter EOMonthArg2 = (b) => StringResources.Get("EOMonthArg2", b);
public static StringGetter AboutWorkday = (b) => StringResources.Get("AboutWorkday", b);
public static StringGetter WorkdayArg1 = (b) => StringResources.Get("WorkdayArg1", b);
public static StringGetter WorkdayArg2 = (b) => StringResources.Get("WorkdayArg2", b);
public static StringGetter WorkdayArg3 = (b) => StringResources.Get("WorkdayArg3", b);

public static StringGetter AboutCalendar__MonthsLong = (b) => StringResources.Get("AboutCalendar__MonthsLong", b);
public static StringGetter AboutCalendar__MonthsShort = (b) => StringResources.Get("AboutCalendar__MonthsShort", b);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ internal class BuiltinFunctionsCore
public static readonly TexlFunction Degrees = _library.Add(new DegreesFunction());
public static readonly TexlFunction DegreesT = _library.Add(new DegreesTableFunction());
public static readonly TexlFunction DropColumns = _library.Add(new DropColumnsFunction());
public static readonly TexlFunction EDate = _library.Add(new EDateFunction());
public static readonly TexlFunction EOMonth = _library.Add(new EOMonthFunction());
public static readonly TexlFunction EDate = _library.Add(new EDateFunction());
public static readonly TexlFunction EOMonth = _library.Add(new EOMonthFunction());
public static readonly TexlFunction Workday = _library.Add(new WorkdayFunction());
public static readonly TexlFunction EncodeHTML = _library.Add(new EncodeHTMLFunction());
public static readonly TexlFunction EncodeUrl = _library.Add(new EncodeUrlFunction());
public static readonly TexlFunction EndsWith = _library.Add(new EndsWithFunction());
Expand Down
32 changes: 31 additions & 1 deletion src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/DateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,37 @@ 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, int.MaxValue, DType.DateTime, DType.Number)
{
}

public override IEnumerable<TexlStrings.StringGetter[]> GetSignatures()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetSignatures

This should handle a N number of arguments.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented GetSignatures(int arity) to handle N number of arguments. The function now accepts up to int.MaxValue holiday dates. Updated in commit 8707c3c.

{
yield return new[] { TexlStrings.WorkdayArg1, TexlStrings.WorkdayArg2 };
yield return new[] { TexlStrings.WorkdayArg1, TexlStrings.WorkdayArg2, TexlStrings.WorkdayArg3 };
yield return new[] { TexlStrings.WorkdayArg1, TexlStrings.WorkdayArg2, TexlStrings.WorkdayArg3, TexlStrings.WorkdayArg3 };
yield return new[] { TexlStrings.WorkdayArg1, TexlStrings.WorkdayArg2, TexlStrings.WorkdayArg3, TexlStrings.WorkdayArg3, TexlStrings.WorkdayArg3 };
}

public override IEnumerable<TexlStrings.StringGetter[]> GetSignatures(int arity)
{
if (arity > 2)
{
return GetGenericSignatures(arity, TexlStrings.WorkdayArg1, TexlStrings.WorkdayArg2, TexlStrings.WorkdayArg3);
}

return base.GetSignatures(arity);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,19 @@ static Library()
returnBehavior: ReturnBehavior.AlwaysEvaluateAndReturnResult,
targetFunction: EOMonth)
},
{
BuiltinFunctionsCore.Workday,
StandardErrorHandling<FormulaValue>(
BuiltinFunctionsCore.Workday.Name,
expandArguments: NoArgExpansion,
replaceBlankValues: ReplaceBlankWith(
new DateTimeValue(IRContext.NotInSource(FormulaType.DateTime), _epoch),
new NumberValue(IRContext.NotInSource(FormulaType.Number), 0)),
checkRuntimeTypes: DeferRuntimeTypeChecking,
checkRuntimeValues: DeferRuntimeValueChecking,
returnBehavior: ReturnBehavior.AlwaysEvaluateAndReturnResult,
targetFunction: Workday)
},
{
BuiltinFunctionsCore.EncodeHTML,
StandardErrorHandling<StringValue>(
Expand Down
106 changes: 106 additions & 0 deletions src/libraries/Microsoft.PowerFx.Interpreter/Functions/LibraryDate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -926,6 +927,111 @@ public static FormulaValue EOMonth(EvalVisitor runner, EvalVisitorContext contex
}
}

public static FormulaValue Workday(EvalVisitor runner, EvalVisitorContext context, IRContext irContext, FormulaValue[] args)
Copy link
Contributor

@anderson-joyle anderson-joyle Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

args

@copilot If any arg is an ErrorValue, return it and thats it.

{
// Check for ErrorValue in any argument and return it immediately
foreach (var arg in args)
{
if (arg is ErrorValue)
{
return arg;
}
}

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 from individual date arguments (args[2] onwards)
var holidays = new HashSet<DateTime>();
for (int i = 2; i < args.Length; i++)
{
if (args[i] is not BlankValue)
{
if (args[i] is DateValue dateValue)
{
holidays.Add(dateValue.GetConvertedValue(timeZoneInfo).Date);
}
else if (args[i] is DateTimeValue dateTimeValue)
{
holidays.Add(dateTimeValue.GetConvertedValue(timeZoneInfo).Date);
}
else if (args[i] is TableValue holidayTable)
{
// Also support table for backward compatibility
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 dateVal)
{
holidays.Add(dateVal.GetConvertedValue(timeZoneInfo).Date);
}
else if (field.Value is DateTimeValue dateTimeVal)
{
holidays.Add(dateTimeVal.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)
Expand Down
28 changes: 28 additions & 0 deletions src/strings/PowerFxResources.en-US.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,34 @@
<value>Number of months by which to change the date. A positive value yields a future date, a negative value a past date, and zero will not change the month.</value>
<comment>Description of the second argument to the 'EOMonth' function.</comment>
</data>
<data name="AboutWorkday" xml:space="preserve">
<value>Returns a date that represents a specified number of working days before or after a starting date. Working days exclude weekends and any specified holidays.</value>
<comment>Description of 'Workday' function.</comment>
</data>
<data name="WorkdayArg1" xml:space="preserve">
<value>start_date</value>
<comment>function_parameter - First parameter for the Workday function - the starting date.</comment>
</data>
<data name="WorkdayArg2" xml:space="preserve">
<value>days</value>
<comment>function_parameter - Second parameter for the Workday function - the number of working days to add or subtract.</comment>
</data>
<data name="WorkdayArg3" xml:space="preserve">
<value>holidays</value>
<comment>function_parameter - Third and subsequent parameters for the Workday function - optional holiday dates to exclude.</comment>
</data>
<data name="AboutWorkday_start_date" xml:space="preserve">
<value>A date value that represents the start date.</value>
<comment>Description of the first argument to the 'Workday' function.</comment>
</data>
<data name="AboutWorkday_days" xml:space="preserve">
<value>The number of nonweekend and nonholiday days before or after start_date. A positive value for days yields a future date; a negative value yields a past date.</value>
<comment>Description of the second argument to the 'Workday' function.</comment>
</data>
<data name="AboutWorkday_holidays" xml:space="preserve">
<value>Optional date values to exclude from the working calendar, such as state and federal holidays and floating holidays. Can also be a table of dates.</value>
<comment>Description of the third and subsequent arguments to the 'Workday' function.</comment>
</data>
<data name="AboutInt" xml:space="preserve">
<value>Truncates 'number' by rounding toward negative infinity.</value>
<comment>Description of 'Int' function.</comment>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@

// 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,Date(2024,8,28))
Date(2024,9,2)

// Single holiday as table (backward compatibility)
>> Workday(Date(2024,8,24),5,Table({Date:Date(2024,8,28)}))
Date(2024,9,2)

// Multiple holidays
>> Workday(Date(2024,8,24),10,Date(2024,8,28),Date(2024,9,2))
Date(2024,9,6)

// Multiple holidays as table (backward compatibility)
>> 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,Date(2024,8,25))
Date(2024,8,30)

// Backward with holidays
>> Workday(Date(2024,8,30),-5,Date(2024,8,26))
Date(2024,8,21)

// Holiday that is the start date
>> Workday(Date(2024,8,26),5,Date(2024,8,26))
Date(2024,9,2)

// Multiple holidays in range
>> Workday(Date(2024,12,20),10,Date(2024,12,25),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