diff --git a/src/Albatross.Expression.Test/Operations/DateOperationTests.cs b/src/Albatross.Expression.Test/Operations/DateOperationTests.cs index 2bc5ed1..7f3e3a9 100644 --- a/src/Albatross.Expression.Test/Operations/DateOperationTests.cs +++ b/src/Albatross.Expression.Test/Operations/DateOperationTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using System; namespace Albatross.Expression.Test.Operations { @@ -52,6 +53,21 @@ public class DateOperationTests [TestCase("format(Date(\"2015-2-10\"), \"yyyy-MM-dd\")", ExpectedResult = "2015-02-10")] [TestCase("GetWorkingDays(Date(\"2023-08-29\"), Date(\"2023-09-03\"),1,1)", ExpectedResult = 4d)] + // Add Business Days + [TestCase("AddBusinessDays(CreateDate(2025, 4, 4), 5)", ExpectedResult = "2025-04-10")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 5), 2)", ExpectedResult = "2025-04-08")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 12), 1)", ExpectedResult = "2025-04-14")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 11), 3)", ExpectedResult = "2025-04-15")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 10), -1)", ExpectedResult = "2025-04-10")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 11), -5)", ExpectedResult = "2025-04-07")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 14), -1)", ExpectedResult = "2025-04-14")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 12), 10)", ExpectedResult = "2025-04-25")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 8), 3, 1, 1)", ExpectedResult = "2025-04-10")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 10), 3, 1, 1)", ExpectedResult = "2025-04-13")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 12), 1, 1, 1)", ExpectedResult = "2025-04-13")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 11), 3, 5, 2)", ExpectedResult = "2025-04-13")] + [TestCase("AddBusinessDays(CreateDate(2025, 4, 10), 8, 5, 3)", ExpectedResult = "2025-04-20")] + public object OperationsTesting(string expression) { return Factory.Instance.Create().Compile(expression).EvalValue(null); diff --git a/src/Albatross.Expression/Functions/Date/AddBusinessDays.cs b/src/Albatross.Expression/Functions/Date/AddBusinessDays.cs new file mode 100644 index 0000000..32c72cc --- /dev/null +++ b/src/Albatross.Expression/Functions/Date/AddBusinessDays.cs @@ -0,0 +1,82 @@ +using Albatross.Expression.Documentation; +using Albatross.Expression.Documentation.Attributes; +using Albatross.Expression.Tokens; +using System; +using System.Collections.Generic; + +namespace Albatross.Expression.Functions.Date +{ + [FunctionDoc(Group.Date, "{token}( , )", +@"### Add or Subtract Business Days +#### Inputs: +- `date`: Date (mandatory) - The starting date. +- `count`: Number (mandatory) - Number of business days to add (positive for future, negative for past). +- `startOfWeek`: Start of the week as a value from 1 (Monday) to 7 (Sunday) (Optional, default: 1). +- `weekendLength`: Integer (1 or 2) indicating the length of the weekend (Optional, default: 2). +#### Outputs: +- A calculated `DateTime`, adjusted by the number of business days." + )] + [ParserOperation] + public class AddBusinessDays : PrefixOperationToken + { + public override string Name => "AddBusinessDays"; + + public override bool Symbolic => false; + + public override int MinOperandCount => 2; + + public override int MaxOperandCount => 4; + + public override object EvalValue(Func context) + { + var date = (DateTime)Convert.ChangeType(Operands[0].EvalValue(context), typeof(DateTime)); + var count = (int)Convert.ChangeType(Operands[1].EvalValue(context), typeof(int)); + + var startOfWeek = DayOfWeek.Monday; + if (Operands.Count > 2) + startOfWeek = (DayOfWeek)(((int)Convert.ChangeType(Operands[2].EvalValue(context), typeof(int)) - 1) % 7); + + var weekendLength = 2; + if (Operands.Count > 3) + weekendLength = (int)Convert.ChangeType(Operands[3].EvalValue(context), typeof(int)); + + var adjustedDate = AdjustBusinessDays(date, count, startOfWeek, weekendLength); + return adjustedDate.ToString("yyyy-MM-dd"); + } + + private DateTime AdjustBusinessDays(DateTime date, int count, DayOfWeek startOfWeek, int weekendLength) + { + var weekendDays = CalculateWeekendDays(startOfWeek, weekendLength); + + var step = count > 0 ? 1 : -1; + var daysMoved = 0; + + if (!weekendDays.Contains(date.DayOfWeek)) + daysMoved++; + + while (daysMoved < Math.Abs(count)) + { + date = date.AddDays(step); + + if (!weekendDays.Contains(date.DayOfWeek)) + daysMoved++; + } + + return date; + } + + private HashSet CalculateWeekendDays(DayOfWeek startOfWeek, int weekendLength) + { + var weekendDays = new HashSet(); + + var endOfWeek = ((int)startOfWeek + 7 - 1) % 7; + for (var i = 0; i < weekendLength; i++) + { + var weekendDayIndex = (endOfWeek - i + 7) % 7; + weekendDays.Add((DayOfWeek)weekendDayIndex); + } + + return weekendDays; + } + } +} \ No newline at end of file