From c9a5650fe97346d5f03dbf7a521773a80c191a01 Mon Sep 17 00:00:00 2001 From: MorjanM Date: Tue, 15 Apr 2025 14:58:40 +0300 Subject: [PATCH] Add AddBusinessDays function and corresponding tests Introduced the AddBusinessDays function to calculate dates adjusted by business days while considering custom weekends. Added comprehensive test cases to confirm correct behavior for various scenarios, including different weekend lengths and start-of-week settings. --- .../Operations/DateOperationTests.cs | 16 ++++ .../Functions/Date/AddBusinessDays.cs | 82 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/Albatross.Expression/Functions/Date/AddBusinessDays.cs 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