From 92e8a08647cd7b7badac1ed24f555d9bdf13694b Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 14 Nov 2023 15:23:21 +0800 Subject: [PATCH 01/52] improvement & target on main branch --- .../FeatureFilters/Recurrence/Recurrence.cs | 21 + .../Recurrence/RecurrenceEvaluator.cs | 1446 ++++++++++ .../Recurrence/RecurrencePattern.cs | 48 + .../Recurrence/RecurrenceRange.cs | 33 + .../FeatureFilters/TimeWindowFilter.cs | 19 +- .../TimeWindowFilterSettings.cs | 10 +- .../FeatureManagement.cs | 17 + .../RecurrenceEvaluator.cs | 2423 +++++++++++++++++ 8 files changed, 4012 insertions(+), 5 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/Recurrence.cs create mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs create mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs create mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs create mode 100644 tests/Tests.FeatureManagement/RecurrenceEvaluator.cs diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/Recurrence.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/Recurrence.cs new file mode 100644 index 00000000..7a63b6bb --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/Recurrence.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement.FeatureFilters +{ + /// + /// A recurrence definition describing how time window recurs + /// + public class Recurrence + { + /// + /// The recurrence pattern specifying how often the time window repeats + /// + public RecurrencePattern Pattern { get; set; } + + /// + /// The recurrence range specifying how long the recurrence pattern repeats + /// + public RecurrenceRange Range { get; set; } + } +} diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs new file mode 100644 index 00000000..530c5fc7 --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -0,0 +1,1446 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.FeatureManagement.FeatureFilters +{ + static class RecurrenceEvaluator + { + // + // Error Message + const string OutOfRange = "The value is out of the accepted range."; + const string UnrecognizableValue = "The value is unrecognizable."; + const string RequiredParameter = "Value cannot be null."; + const string NotMatched = "Start date is not a valid first occurrence."; + + // + // Day of week + const string Sunday = "Sunday"; + const string Monday = "Monday"; + const string Tuesday = "Tuesday"; + const string Wednesday = "Wednesday"; + const string Thursday = "Thursday"; + const string Friday = "Friday"; + const string Saturday = "Saturday"; + + // + // Index + const string First = "First"; + const string Second = "Second"; + const string Third = "Third"; + const string Fourth = "Fourth"; + const string Last = "Last"; + + // + // Recurrence Pattern Type + const string Daily = "Daily"; + const string Weekly = "Weekly"; + const string AbsoluteMonthly = "AbsoluteMonthly"; + const string RelativeMonthly = "RelativeMonthly"; + const string AbsoluteYearly = "AbsoluteYearly"; + const string RelativeYearly = "RelativeYearly"; + + // + // Recurrence Range Type + const string EndDate = "EndDate"; + const string Numbered = "Numbered"; + const string NoEnd = "NoEnd"; + + const int WeekDayNumber = 7; + const int MinMonthDayNumber = 28; + const int MinYearDayNumber = 365; + + /// + /// Checks if a provided timestamp is within any recurring time window specified by the Recurrence section in the time window filter settings. + /// If the time window filter has an invalid recurrence setting, an exception will be thrown. + /// A time stamp. + /// The settings of time window filter. + /// True if the time stamp is within any recurring time window, false otherwise. + /// + public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings settings) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (!TryValidateSettings(settings, out string paramName, out string reason)) + { + throw new ArgumentException(reason, paramName); + } + + if (time < settings.Start.Value) + { + return false; + } + + if (settings.Recurrence == null) + { + return false; + } + + if (!TryGetPreviousOccurrence(time, settings, out DateTimeOffset previousOccurrence)) + { + return false; + } + + if (time <= previousOccurrence + (settings.End.Value - settings.Start.Value)) + { + return true; + } + + return false; + } + + /// + /// Try to find the closest previous recurrence occurrence before the provided time stamp according to the recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// True if the closest previous occurrence is within the recurrence range, false otherwise. + /// + private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence) + { + previousOccurrence = DateTimeOffset.MaxValue; + + DateTimeOffset start = settings.Start.Value; + + if (time < start) + { + return false; + } + + string patternType = settings.Recurrence.Pattern.Type; + + int numberOfOccurrences; + + if (string.Equals(patternType, Daily, StringComparison.OrdinalIgnoreCase)) + { + FindDailyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + } + else if (string.Equals(patternType, Weekly, StringComparison.OrdinalIgnoreCase)) + { + FindWeeklyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + } + else if (string.Equals(patternType, AbsoluteMonthly, StringComparison.OrdinalIgnoreCase)) + { + FindAbsoluteMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + } + else if (string.Equals(patternType, RelativeMonthly, StringComparison.OrdinalIgnoreCase)) + { + FindRelativeMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + } + else if (string.Equals(patternType, AbsoluteYearly, StringComparison.OrdinalIgnoreCase)) + { + FindAbsoluteYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + } + else if (string.Equals(patternType, RelativeYearly, StringComparison.OrdinalIgnoreCase)) + { + FindRelativeYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + } + else + { + throw new ArgumentException(nameof(settings)); + } + + RecurrenceRange range = settings.Recurrence.Range; + + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); + + if (string.Equals(range.Type, EndDate, StringComparison.OrdinalIgnoreCase)) + { + DateTime alignedPreviousOccurrence = previousOccurrence.DateTime + timeZoneOffset - previousOccurrence.Offset; + + if (alignedPreviousOccurrence.Date > range.EndDate.Value.Date) + { + return false; + } + } + + if (string.Equals(range.Type, Numbered, StringComparison.OrdinalIgnoreCase)) + { + if (numberOfOccurrences >= range.NumberOfOccurrences) + { + return false; + } + } + + return true; + } + + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "Daily" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// + private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) + { + RecurrencePattern pattern = settings.Recurrence.Pattern; + + DateTimeOffset start = settings.Start.Value; + + int interval = pattern.Interval; + + TimeSpan timeGap = time - start; + + // + // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. + int numberOfInterval = (int)Math.Floor(timeGap.TotalSeconds / TimeSpan.FromDays(interval).TotalSeconds); + + previousOccurrence = start.AddDays(numberOfInterval * interval); + + numberOfOccurrences = numberOfInterval; + } + + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "Weekly" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of recurring days of week which have occurred between the time and the recurrence start. + /// + private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) + { + previousOccurrence = DateTimeOffset.MaxValue; + + numberOfOccurrences = 0; + + RecurrencePattern pattern = settings.Recurrence.Pattern; + + DateTimeOffset start = settings.Start.Value; + + int interval = pattern.Interval; + + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); + + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + + TimeSpan timeGap = time - start; + + int firstDayOfWeek = pattern.FirstDayOfWeek != null ? DayOfWeekNumber(pattern.FirstDayOfWeek) : 0; // first day of week is Sunday by default + + int remainingDaysOfFirstWeek = RemainingDaysOfWeek((int)alignedStart.DayOfWeek, firstDayOfWeek); + + TimeSpan remainingTimeOfFirstInterval = TimeSpan.FromDays(remainingDaysOfFirstWeek) - alignedStart.TimeOfDay + TimeSpan.FromDays((interval - 1) * 7); + + if (remainingTimeOfFirstInterval <= timeGap) + { + int numberOfInterval = (int)Math.Floor((timeGap - remainingTimeOfFirstInterval).TotalSeconds / TimeSpan.FromDays(interval * 7).TotalSeconds); + + previousOccurrence = start.AddDays(numberOfInterval * interval * 7 + remainingDaysOfFirstWeek + (interval - 1) * 7); + + numberOfOccurrences += numberOfInterval * pattern.DaysOfWeek.Count(); + + // + // Add the occurrences in the first week + numberOfOccurrences += 1; + + DateTime dateTime = alignedStart.AddDays(1); + + while ((int)dateTime.DayOfWeek != firstDayOfWeek) + { + if (pattern.DaysOfWeek.Any(day => + DayOfWeekNumber(day) == (int)dateTime.DayOfWeek)) + { + numberOfOccurrences += 1; + } + + dateTime = dateTime.AddDays(1); + } + } + else // time is still within the first interval + { + previousOccurrence = start; + } + + DateTime alignedPreviousOccurrence = previousOccurrence.DateTime + timeZoneOffset - previousOccurrence.Offset; + + DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + + while (alignedPreviousOccurrence.AddDays(1) <= alignedTime) + { + alignedPreviousOccurrence = alignedPreviousOccurrence.AddDays(1); + + if ((int)alignedPreviousOccurrence.DayOfWeek == firstDayOfWeek) // Come to the next week + { + break; + } + + if (pattern.DaysOfWeek.Any(day => + DayOfWeekNumber(day) == (int)alignedPreviousOccurrence.DayOfWeek)) + { + previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); + + numberOfOccurrences += 1; + } + } + } + + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "AbsoluteMonthly" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// + private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) + { + RecurrencePattern pattern = settings.Recurrence.Pattern; + + DateTimeOffset start = settings.Start.Value; + + int interval = pattern.Interval; + + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); + + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + + DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + + int monthGap = (alignedTime.Year - alignedStart.Year) * 12 + alignedTime.Month - alignedStart.Month; + + if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.Day) < alignedStart.TimeOfDay + TimeSpan.FromDays(alignedStart.Day)) + { + monthGap -= 1; + } + + int numberOfInterval = monthGap / interval; + + previousOccurrence = start.AddMonths(numberOfInterval * interval); + + numberOfOccurrences = numberOfInterval; + } + + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "RelativeMonthly" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// + private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) + { + RecurrencePattern pattern = settings.Recurrence.Pattern; + + DateTimeOffset start = settings.Start.Value; + + int interval = pattern.Interval; + + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); + + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + + DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + + int monthGap = (alignedTime.Year - alignedStart.Year) * 12 + alignedTime.Month - alignedStart.Month; + + if (!pattern.DaysOfWeek.Any(day => + alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + alignedStart.TimeOfDay)) + { + // + // E.g. start: 2023.9.1 (the first Friday in 2023.9) and time: 2023.10.2 (the first Friday in 2023.10 is 2023.10.6) + // Not a complete monthly interval + monthGap -= 1; + } + + int numberOfInterval = monthGap / interval; + + DateTime alignedPreviousOccurrenceMonth = alignedStart.AddMonths(numberOfInterval * interval); + + DateTime alignedPreviousOccurrence = DateTime.MaxValue; + + // + // Find the first occurence date matched the pattern + // Only one day of week in the month will be matched + foreach (string day in pattern.DaysOfWeek) + { + DateTime occurrenceDate = NthDayOfWeekInTheMonth(alignedPreviousOccurrenceMonth, pattern.Index, day); + + if (occurrenceDate + alignedStart.TimeOfDay < alignedPreviousOccurrence) + { + alignedPreviousOccurrence = occurrenceDate + alignedStart.TimeOfDay; + } + } + + previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); + + numberOfOccurrences = numberOfInterval; + } + + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "AbsoluteYearly" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// + private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) + { + RecurrencePattern pattern = settings.Recurrence.Pattern; + + DateTimeOffset start = settings.Start.Value; + + int interval = pattern.Interval; + + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); + + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + + DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + + int yearGap = alignedTime.Year - alignedStart.Year; + + if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.DayOfYear) < alignedStart.TimeOfDay + TimeSpan.FromDays(alignedStart.DayOfYear)) + { + yearGap -= 1; + } + + int numberOfInterval = yearGap / interval; + + previousOccurrence = start.AddYears(numberOfInterval * interval); + + numberOfOccurrences = numberOfInterval; + } + + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "RelativeYearly" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// + private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) + { + RecurrencePattern pattern = settings.Recurrence.Pattern; + + DateTimeOffset start = settings.Start.Value; + + int interval = pattern.Interval; + + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); + + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + + DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + + int yearGap = alignedTime.Year - alignedStart.Year; + + if (alignedTime.Month < alignedStart.Month) + { + // + // E.g. start: 2023.9 and time: 2024.8 + // Not a complete yearly interval + yearGap -= 1; + } + else if (alignedTime.Month == alignedStart.Month && !pattern.DaysOfWeek.Any(day => + alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + alignedStart.TimeOfDay)) + { + // + // E.g. start: 2023.9.1 (the first Friday in 2023.9) and time: 2024.9.2 (the first Friday in 2023.9 is 2024.9.6) + // Not a complete yearly interval + yearGap -= 1; + } + + int numberOfInterval = yearGap / interval; + + DateTime alignedPreviousOccurrenceMonth = alignedStart.AddYears(numberOfInterval * interval); + + DateTime alignedPreviousOccurrence = DateTime.MaxValue; + + // + // Find the first occurence date matched the pattern + // Only one day of week in the month will be matched + foreach (string day in pattern.DaysOfWeek) + { + DateTime occurrenceDate = NthDayOfWeekInTheMonth(alignedPreviousOccurrenceMonth, pattern.Index, day); + + if (occurrenceDate + alignedStart.TimeOfDay < alignedPreviousOccurrence) + { + alignedPreviousOccurrence = occurrenceDate + alignedStart.TimeOfDay; + } + } + + previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); + + numberOfOccurrences = numberOfInterval; + } + + private static bool TryValidateSettings(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + Recurrence recurrence = settings.Recurrence; + + paramName = null; + + reason = null; + + if (recurrence != null) + { + if (!TryValidateGeneralRequiredParameter(settings, out paramName, out reason)) + { + return false; + } + + if (!TryValidateRecurrencePattern(settings, out paramName, out reason)) + { + return false; + } + + if (!TryValidateRecurrenceRange(settings, out paramName, out reason)) + { + return false; + } + } + + return true; + } + + private static bool TryValidateGeneralRequiredParameter(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + Recurrence recurrence = settings.Recurrence; + + paramName = null; + + reason = null; + + if (settings.Start == null) + { + paramName = nameof(settings.Start); + + reason = RequiredParameter; + + return false; + } + + if (settings.End == null) + { + paramName = nameof(settings.End); + + reason = RequiredParameter; + + return false; + } + + if (recurrence.Pattern == null) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(recurrence.Pattern)}"; + + reason = RequiredParameter; + + return false; + } + + if (recurrence.Range == null) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(recurrence.Range)}"; + + reason = RequiredParameter; + + return false; + } + + if (recurrence.Pattern.Type == null) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(recurrence.Pattern)}.{nameof(recurrence.Pattern.Type)}"; + + reason = RequiredParameter; + + return false; + } + + if (recurrence.Range.Type == null) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(recurrence.Range)}.{nameof(recurrence.Range.Type)}"; + + reason = RequiredParameter; + + return false; + } + + if (settings.End.Value - settings.Start.Value <= TimeSpan.Zero) + { + paramName = nameof(settings.End); + + reason = OutOfRange; + + return false; + } + + return true; + } + + private static bool TryValidateRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = null; + + reason = null; + + if (!TryValidateInterval(settings, out paramName, out reason)) + { + return false; + } + + string patternType = settings.Recurrence.Pattern.Type; + + if (string.Equals(patternType, Daily, StringComparison.OrdinalIgnoreCase)) + { + if (!TryValidateDailyRecurrencePattern(settings, out paramName, out reason)) + { + return false; + } + } + else if (string.Equals(patternType, Weekly, StringComparison.OrdinalIgnoreCase)) + { + if (!TryValidateWeeklyRecurrencePattern(settings, out paramName, out reason)) + { + return false; + } + } + else if (string.Equals(patternType, AbsoluteMonthly, StringComparison.OrdinalIgnoreCase)) + { + if (!TryValidateAbsoluteMonthlyRecurrencePattern(settings, out paramName, out reason)) + { + return false; + } + } + else if (string.Equals(patternType, RelativeMonthly, StringComparison.OrdinalIgnoreCase)) + { + if (!TryValidateRelativeMonthlyRecurrencePattern(settings, out paramName, out reason)) + { + return false; + } + } + else if (string.Equals(patternType, AbsoluteYearly, StringComparison.OrdinalIgnoreCase)) + { + if (!TryValidateAbsoluteYearlyRecurrencePattern(settings, out paramName, out reason)) + { + return false; + } + } + else if (string.Equals(patternType, RelativeYearly, StringComparison.OrdinalIgnoreCase)) + { + if (!TryValidateRelativeYearlyRecurrencePattern(settings, out paramName, out reason)) + { + return false; + } + } + else + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Type)}"; + + reason = UnrecognizableValue; + + return false; + } + + return true; + } + + private static bool TryValidateDailyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = null; + + reason = null; + + TimeSpan intervalDuration = TimeSpan.FromDays(settings.Recurrence.Pattern.Interval); + + // + // Time window duration must be shorter than how frequently it occurs + if (settings.End.Value - settings.Start.Value > intervalDuration) + { + paramName = $"{nameof(settings.End)}"; + + reason = OutOfRange; + + return false; + } + + // + // No required parameter for "Daily" pattern + // "Start" is always a valid first occurrence for "Daily" pattern + + return true; + } + + private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = null; + + reason = null; + + RecurrencePattern pattern = settings.Recurrence.Pattern; + + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * WeekDayNumber); + + TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + + // + // Time window duration must be shorter than how frequently it occurs + if (timeWindowDuration > intervalDuration) + { + paramName = $"{nameof(settings.End)}"; + + reason = OutOfRange; + + return false; + } + + // + // Required parameters + if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) + { + return false; + } + + if (!TryValidateFirstDayOfWeek(settings, out paramName, out reason)) + { + return false; + } + + // + // Check whether "Start" is a valid first occurrence + DateTimeOffset start = settings.Start.Value; + + DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; + + if (!pattern.DaysOfWeek.Any(day => + DayOfWeekNumber(day) == (int)alignedStart.DayOfWeek)) + { + paramName = nameof(settings.Start); + + reason = NotMatched; + + return false; + } + + // + // Check whether the time window duration is shorter than the minimum gap between days of week + if (!IsDurationCompliantWithDaysOfWeek(timeWindowDuration, pattern.Interval, pattern.DaysOfWeek, pattern.FirstDayOfWeek)) + { + paramName = nameof(settings.End); + + reason = OutOfRange; + + return false; + } + + return true; + } + + private static bool TryValidateAbsoluteMonthlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = null; + + reason = null; + + RecurrencePattern pattern = settings.Recurrence.Pattern; + + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinMonthDayNumber); + + // + // Time window duration must be shorter than how frequently it occurs + if (settings.End.Value - settings.Start.Value > intervalDuration) + { + paramName = $"{nameof(settings.End)}"; + + reason = OutOfRange; + + return false; + } + + // + // Required parameters + if (!TryValidateDayOfMonth(settings, out paramName, out reason)) + { + return false; + } + + // + // Check whether "Start" is a valid first occurrence + DateTimeOffset start = settings.Start.Value; + + DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; + + if (alignedStart.Day != pattern.DayOfMonth.Value) + { + paramName = nameof(settings.Start); + + reason = NotMatched; + + return false; + } + + return true; + } + + private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = null; + + reason = null; + + RecurrencePattern pattern = settings.Recurrence.Pattern; + + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinMonthDayNumber); + + // + // Time window duration must be shorter than how frequently it occurs + if (settings.End.Value - settings.Start.Value > intervalDuration) + { + paramName = $"{nameof(settings.End)}"; + + reason = OutOfRange; + + return false; + } + + // + // Required parameters + if (!TryValidateIndex(settings, out paramName, out reason)) + { + return false; + } + + if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) + { + return false; + } + + // + // Check whether "Start" is a valid first occurrence + DateTimeOffset start = settings.Start.Value; + + DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; + + if (!pattern.DaysOfWeek.Any(day => + NthDayOfWeekInTheMonth(alignedStart, pattern.Index, day) == alignedStart.Date)) + { + paramName = nameof(settings.Start); + + reason = NotMatched; + + return false; + } + + return true; + } + + private static bool TryValidateAbsoluteYearlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = null; + + reason = null; + + RecurrencePattern pattern = settings.Recurrence.Pattern; + + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinYearDayNumber); + + // + // Time window duration must be shorter than how frequently it occurs + if (settings.End.Value - settings.Start.Value > intervalDuration) + { + paramName = $"{nameof(settings.End)}"; + + reason = OutOfRange; + + return false; + } + + // + // Required parameters + if (!TryValidateMonth(settings, out paramName, out reason)) + { + return false; + } + + if (!TryValidateDayOfMonth(settings, out paramName, out reason)) + { + return false; + } + + // + // Check whether "Start" is a valid first occurrence + DateTimeOffset start = settings.Start.Value; + + DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; + + if (alignedStart.Day != pattern.DayOfMonth.Value || alignedStart.Month != pattern.Month.Value) + { + paramName = nameof(settings.Start); + + reason = NotMatched; + + return false; + } + + return true; + } + + private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = null; + + reason = null; + + RecurrencePattern pattern = settings.Recurrence.Pattern; + + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinYearDayNumber); + + // + // Time window duration must be shorter than how frequently it occurs + if (settings.End.Value - settings.Start.Value > intervalDuration) + { + paramName = $"{nameof(settings.End)}"; + + reason = OutOfRange; + + return false; + } + + // + // Required parameters + if (!TryValidateMonth(settings, out paramName, out reason)) + { + return false; + } + + if (!TryValidateIndex(settings, out paramName, out reason)) + { + return false; + } + + if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) + { + return false; + } + + // + // Check whether "Start" is a valid first occurrence + DateTimeOffset start = settings.Start.Value; + + DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; + + if (alignedStart.Month != pattern.Month.Value || + !pattern.DaysOfWeek.Any(day => + NthDayOfWeekInTheMonth(alignedStart, pattern.Index, day) == alignedStart.Date)) + { + paramName = nameof(settings.Start); + + reason = NotMatched; + + return false; + } + + return true; + } + + private static bool TryValidateRecurrenceRange(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = null; + + reason = null; + + if (!TryValidateRecurrenceTimeZone(settings, out paramName, out reason)) + { + return false; + } + + string rangeType = settings.Recurrence.Range.Type; + + if (string.Equals(rangeType, NoEnd, StringComparison.OrdinalIgnoreCase)) + { + // + // No parameter is required + } + else if (string.Equals(rangeType, EndDate, StringComparison.OrdinalIgnoreCase)) + { + if (!TryValidateEndDate(settings, out paramName, out reason)) + { + return false; + } + } + else if (string.Equals(rangeType, Numbered, StringComparison.OrdinalIgnoreCase)) + { + if (!TryValidateNumberOfOccurrences(settings, out paramName, out reason)) + { + return false; + } + } + else + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.Type)}"; + + reason = UnrecognizableValue; + + return false; + } + + return true; + } + + private static bool TryValidateInterval(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Interval)}"; + + reason = null; + + if (settings.Recurrence.Pattern.Interval <= 0) + { + reason = OutOfRange; + + return false; + } + + return true; + } + + private static bool TryValidateDaysOfWeek(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DaysOfWeek)}"; + + reason = null; + + if (settings.Recurrence.Pattern.DaysOfWeek == null || !settings.Recurrence.Pattern.DaysOfWeek.Any()) + { + reason = RequiredParameter; + + return false; + } + + foreach (string day in settings.Recurrence.Pattern.DaysOfWeek) + { + if (!string.Equals(day, Monday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(day, Tuesday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(day, Wednesday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(day, Thursday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(day, Friday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(day, Saturday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(day, Sunday, StringComparison.OrdinalIgnoreCase)) + { + reason = UnrecognizableValue; + + return false; + } + } + + return true; + } + + private static bool TryValidateFirstDayOfWeek(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.FirstDayOfWeek)}"; + + reason = null; + + string firstDayOfWeek = settings.Recurrence.Pattern.FirstDayOfWeek; + + if (firstDayOfWeek == null) + { + return true; + } + + if (!string.Equals(firstDayOfWeek, Monday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(firstDayOfWeek, Tuesday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(firstDayOfWeek, Wednesday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(firstDayOfWeek, Thursday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(firstDayOfWeek, Friday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(firstDayOfWeek, Saturday, StringComparison.OrdinalIgnoreCase) && + !string.Equals(firstDayOfWeek, Sunday, StringComparison.OrdinalIgnoreCase)) + { + reason = UnrecognizableValue; + + return false; + } + + return true; + } + + private static bool TryValidateIndex(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Index)}"; + + reason = null; + + string index = settings.Recurrence.Pattern.Index; + + if (index == null) + { + reason = RequiredParameter; + + return false; + } + + if (!string.Equals(index, First, StringComparison.Ordinal) && + !string.Equals(index, Second, StringComparison.Ordinal) && + !string.Equals(index, Third, StringComparison.Ordinal) && + !string.Equals(index, Fourth, StringComparison.Ordinal) && + !string.Equals(index, Last, StringComparison.Ordinal)) + { + reason = UnrecognizableValue; + + return false; + } + + return true; + } + + private static bool TryValidateDayOfMonth(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DayOfMonth)}"; + + reason = null; + + if (settings.Recurrence.Pattern.DayOfMonth == null) + { + reason = RequiredParameter; + + return false; + } + + if (settings.Recurrence.Pattern.DayOfMonth < 1 || settings.Recurrence.Pattern.DayOfMonth > 31) + { + reason = OutOfRange; + + return false; + } + + return true; + } + + private static bool TryValidateMonth(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Month)}"; + + reason = null; + + if (settings.Recurrence.Pattern.Month == null) + { + reason = RequiredParameter; + + return false; + } + + if (settings.Recurrence.Pattern.Month < 1 || settings.Recurrence.Pattern.Month > 12) + { + reason = OutOfRange; + + return false; + } + + return true; + } + + private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.EndDate)}"; + + reason = null; + + if (settings.Recurrence.Range.EndDate == null) + { + reason = RequiredParameter; + + return false; + } + + TimeSpan timeZoneOffset; + + if (settings.Start == null) + { + paramName = nameof(settings.Start); + + reason = RequiredParameter; + + return false; + } + + DateTimeOffset start = settings.Start.Value; + + timeZoneOffset = start.Offset; + + if (settings.Recurrence.Range.RecurrenceTimeZone != null && !TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out timeZoneOffset)) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.RecurrenceTimeZone)}"; + + reason = UnrecognizableValue; + + return false; + } + + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + + DateTime endDate = settings.Recurrence.Range.EndDate.Value.DateTime; + + if (endDate.Date < alignedStart.Date) + { + reason = OutOfRange; + + return false; + } + + return true; + } + + private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.NumberOfOccurrences)}"; + + reason = null; + + if (settings.Recurrence.Range.NumberOfOccurrences == null) + { + reason = RequiredParameter; + + return false; + } + + if (settings.Recurrence.Range.NumberOfOccurrences < 1) + { + reason = OutOfRange; + + return false; + } + + return true; + } + + private static bool TryValidateRecurrenceTimeZone(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.RecurrenceTimeZone)}"; + + reason = null; + + if (settings.Recurrence.Range.RecurrenceTimeZone != null && !TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out _)) + { + reason = UnrecognizableValue; + + return false; + } + + return true; + } + + private static bool TryParseTimeZone(string timeZoneStr, out TimeSpan timeZoneOffset) + { + timeZoneOffset = TimeSpan.Zero; + + if (timeZoneStr == null) + { + return false; + } + + if (!timeZoneStr.StartsWith("UTC+") && !timeZoneStr.StartsWith("UTC-")) + { + return false; + } + + if (!TimeSpan.TryParseExact(timeZoneStr.Substring(4), @"hh\:mm", null, out timeZoneOffset)) + { + return false; + } + + if (timeZoneStr[3] == '-') + { + timeZoneOffset = -timeZoneOffset; + } + + return true; + } + + private static TimeSpan GetRecurrenceTimeZone(TimeWindowFilterSettings settings) + { + if (!TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out TimeSpan timeZoneOffset)) + { + timeZoneOffset = settings.Start.Value.Offset; + } + + return timeZoneOffset; + } + + /// + /// Check whether the duration is shorter than the minimum gap between recurrence of days of week. + /// + /// The time span of the duration. + /// The recurrence interval. + /// The days of the week when the recurrence will occur. + /// The first day of the week. + /// True if the duration is compliant with days of week, false otherwise. + private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int interval, IEnumerable daysOfWeek, string firstDayOfWeek) + { + if (daysOfWeek == null || !daysOfWeek.Any()) + { + throw new ArgumentException(nameof(daysOfWeek)); + } + + if (daysOfWeek.Count() == 1) + { + return true; + } + + // + // Shift to the first day of the week + DateTime date = DateTime.Today; + + int offset = RemainingDaysOfWeek((int)date.DayOfWeek, DayOfWeekNumber(firstDayOfWeek)) % 7; + + date = date.AddDays(offset); + + DateTime prevOccurrence = date; + + TimeSpan minGap = TimeSpan.MaxValue; + + for (int i = 0; i < 6; i++) + { + date = date.AddDays(1); + + if (daysOfWeek.Any(day => + DayOfWeekNumber(day) == (int)date.DayOfWeek)) + { + TimeSpan gap = date - prevOccurrence; + + if (gap < minGap) + { + minGap = gap; + } + + prevOccurrence = date; + } + } + + if (interval == 1) + { + // + // It may across weeks. Check the adjacent week + date = date.AddDays(1); + + TimeSpan gap = date - prevOccurrence; + + if (gap < minGap) + { + minGap = gap; + } + } + + return minGap >= duration; + } + + private static int RemainingDaysOfWeek(int day, int firstDayOfWeek) + { + int remainingDays = day - firstDayOfWeek; + + if (remainingDays < 0) + { + return -remainingDays; + } + else + { + return WeekDayNumber - remainingDays; + } + } + + /// + /// Find the nth day of week in the month of the date time. + /// + /// A date time. + /// The index of the day of week in the month. + /// The day of week. + /// The data time of the nth day of week in the month. + private static DateTime NthDayOfWeekInTheMonth(DateTime dateTime, string index, string dayOfWeek) + { + var date = new DateTime(dateTime.Year, dateTime.Month, 1); + + // + // Find the first day of week in the month + while ((int)date.DayOfWeek != DayOfWeekNumber(dayOfWeek)) + { + date = date.AddDays(1); + } + + if (date.AddDays(WeekDayNumber * (IndexNumber(index) - 1)).Month == dateTime.Month) + { + date = date.AddDays(WeekDayNumber * (IndexNumber(index) - 1)); + } + else // There is no the 5th day of week in the month + { + // + // Add 3 weeks to reach the fourth day of week in the month + date = date.AddDays(WeekDayNumber * 3); + } + + return date; + } + + private static int DayOfWeekNumber(string str) + { + if (string.Equals(str, Sunday, StringComparison.OrdinalIgnoreCase)) + { + return 0; + } + else if (string.Equals(str, Monday, StringComparison.OrdinalIgnoreCase)) + { + return 1; + } + else if (string.Equals(str, Tuesday, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + else if (string.Equals(str, Wednesday, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + else if (string.Equals(str, Thursday, StringComparison.OrdinalIgnoreCase)) + { + return 4; + } + else if (string.Equals(str, Friday, StringComparison.OrdinalIgnoreCase)) + { + return 5; + } + else if (string.Equals(str, Saturday, StringComparison.OrdinalIgnoreCase)) + { + return 6; + } + else + { + throw new ArgumentException(nameof(str)); + } + } + + public static int IndexNumber(string str) + { + if (string.Equals(str, First, StringComparison.OrdinalIgnoreCase)) + { + return 1; + } + else if (string.Equals(str, Second, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + else if (string.Equals(str, Third, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + else if (string.Equals(str, Fourth, StringComparison.OrdinalIgnoreCase)) + { + return 4; + } + else if (string.Equals(str, Last, StringComparison.OrdinalIgnoreCase)) + { + return 5; + } + else + { + throw new ArgumentException(nameof(str)); + } + } + } +} diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs new file mode 100644 index 00000000..8d3aaac7 --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System.Collections.Generic; + +namespace Microsoft.FeatureManagement.FeatureFilters +{ + /// + /// The recurrence pattern specifying how often the time window repeats + /// + public class RecurrencePattern + { + /// + /// The recurrence pattern type + /// + public string Type { get; set; } + + /// + /// The number of units between occurrences, where units can be in days, weeks, months, or years, depending on the pattern type + /// + public int Interval { get; set; } = 1; + + /// + /// The days of the week on which the time window occurs + /// + public IEnumerable DaysOfWeek { get; set; } + + /// + /// The first day of the week. + /// + public string FirstDayOfWeek { get; set; } = "Sunday"; + + /// + /// Specifies on which instance of the allowed days specified in DaysOfWeek the time window occurs, counted from the first instance in the month + /// + public string Index { get; set; } = "First"; + + /// + /// The day of the month on which the time window occurs + /// + public int? DayOfMonth { get; set; } + + /// + /// The month on which the time window occurs + /// + public int? Month { get; set; } + } +} diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs new file mode 100644 index 00000000..b176f6f7 --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System; + +namespace Microsoft.FeatureManagement.FeatureFilters +{ + /// + /// The recurrence range specifying how long the recurrence pattern repeats + /// + public class RecurrenceRange + { + /// + /// The recurrence range type + /// + public string Type { get; set; } = "NoEnd"; + + /// + /// The date to stop applying the recurrence pattern + /// + public DateTimeOffset? EndDate { get; set; } + + /// + /// The number of times to repeat the time window + /// + public int? NumberOfOccurrences { get; set; } + + /// + /// Time zone for recurrence settings, e.g. UTC+08:00 + /// + public string RecurrenceTimeZone { get; set; } + } +} diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 884332b3..aec94fec 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -10,6 +10,7 @@ namespace Microsoft.FeatureManagement.FeatureFilters { /// /// A feature filter that can be used to activate a feature based on a time window. + /// The time window filter supports recurrence settings. The time window can occur repeatedly. /// [FilterAlias(Alias)] public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder @@ -23,7 +24,7 @@ public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder /// A logger factory for creating loggers. public TimeWindowFilter(ILoggerFactory loggerFactory) { - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); } /// @@ -37,7 +38,7 @@ public object BindParameters(IConfiguration filterParameters) } /// - /// Evaluates whether a feature is enabled based on a configurable time window. + /// Evaluates whether a feature is enabled based on a configurable fixed time window or recurring time windows. /// /// The feature evaluation context. /// True if the feature is enabled, false otherwise. @@ -56,7 +57,19 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) return Task.FromResult(false); } - return Task.FromResult((!settings.Start.HasValue || now >= settings.Start.Value) && (!settings.End.HasValue || now < settings.End.Value)); + // + // Hit the first occurrence of the time window + if ((!settings.Start.HasValue || now >= settings.Start.Value) && (!settings.End.HasValue || now < settings.End.Value)) + { + return Task.FromResult(true); + } + + if (settings.Recurrence != null) + { + return Task.FromResult(RecurrenceEvaluator.MatchRecurrence(now, settings)); + } + + return Task.FromResult(false); } } } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilterSettings.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilterSettings.cs index 41f87cf3..6a0bb0d4 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilterSettings.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilterSettings.cs @@ -14,12 +14,18 @@ public class TimeWindowFilterSettings /// An optional start time used to determine when a feature configured to use the feature filter should be enabled. /// If no start time is specified the time window is considered to have already started. /// - public DateTimeOffset? Start { get; set; } // E.g. "Wed, 01 May 2019 22:59:30 GMT" + public DateTimeOffset? Start { get; set; } /// /// An optional end time used to determine when a feature configured to use the feature filter should be enabled. /// If no end time is specified the time window is considered to never end. /// - public DateTimeOffset? End { get; set; } // E.g. "Wed, 01 May 2019 23:00:00 GMT" + public DateTimeOffset? End { get; set; } + + /// + /// Add-on recurrence rule allows the time window defined by Start and End to recur. + /// The rule specifies both how often the time window repeats and for how long. + /// + public Recurrence Recurrence { get; set; } } } diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 73816617..72828dcd 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -226,6 +226,8 @@ public async Task TimeWindow() const string feature2 = "feature2"; const string feature3 = "feature3"; const string feature4 = "feature4"; + const string feature5 = "feature5"; + const string feature6 = "feature6"; Environment.SetEnvironmentVariable($"FeatureManagement:{feature1}:EnabledFor:0:Name", "TimeWindow"); Environment.SetEnvironmentVariable($"FeatureManagement:{feature1}:EnabledFor:0:Parameters:End", DateTimeOffset.UtcNow.AddDays(1).ToString("r")); @@ -239,6 +241,19 @@ public async Task TimeWindow() Environment.SetEnvironmentVariable($"FeatureManagement:{feature4}:EnabledFor:0:Name", "TimeWindow"); Environment.SetEnvironmentVariable($"FeatureManagement:{feature4}:EnabledFor:0:Parameters:Start", DateTimeOffset.UtcNow.AddDays(1).ToString("r")); + Environment.SetEnvironmentVariable($"FeatureManagement:{feature5}:EnabledFor:0:Name", "TimeWindow"); + Environment.SetEnvironmentVariable($"FeatureManagement:{feature5}:EnabledFor:0:Parameters:Start", DateTimeOffset.UtcNow.AddDays(-2).ToString("r")); + Environment.SetEnvironmentVariable($"FeatureManagement:{feature5}:EnabledFor:0:Parameters:End", DateTimeOffset.UtcNow.AddDays(-1).ToString("r")); + Environment.SetEnvironmentVariable($"FeatureManagement:{feature5}:EnabledFor:0:Parameters:Recurrence:Pattern:Type", "Daily"); + Environment.SetEnvironmentVariable($"FeatureManagement:{feature5}:EnabledFor:0:Parameters:Recurrence:Range:Type", "NoEnd"); + + Environment.SetEnvironmentVariable($"FeatureManagement:{feature6}:EnabledFor:0:Name", "TimeWindow"); + Environment.SetEnvironmentVariable($"FeatureManagement:{feature6}:EnabledFor:0:Parameters:Start", DateTimeOffset.UtcNow.AddDays(-2).ToString("r")); + Environment.SetEnvironmentVariable($"FeatureManagement:{feature6}:EnabledFor:0:Parameters:End", DateTimeOffset.UtcNow.AddDays(-1).ToString("r")); + Environment.SetEnvironmentVariable($"FeatureManagement:{feature6}:EnabledFor:0:Parameters:Recurrence:Pattern:Type", "Daily"); + Environment.SetEnvironmentVariable($"FeatureManagement:{feature6}:EnabledFor:0:Parameters:Recurrence:Pattern:Interval", "3"); + Environment.SetEnvironmentVariable($"FeatureManagement:{feature6}:EnabledFor:0:Parameters:Recurrence:Range:Type", "NoEnd"); + IConfiguration config = new ConfigurationBuilder().AddEnvironmentVariables().Build(); var serviceCollection = new ServiceCollection(); @@ -254,6 +269,8 @@ public async Task TimeWindow() Assert.False(await featureManager.IsEnabledAsync(feature2)); Assert.True(await featureManager.IsEnabledAsync(feature3)); Assert.False(await featureManager.IsEnabledAsync(feature4)); + Assert.True(await featureManager.IsEnabledAsync(feature5)); + Assert.False(await featureManager.IsEnabledAsync(feature6)); } [Fact] diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs new file mode 100644 index 00000000..14800478 --- /dev/null +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -0,0 +1,2423 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.FeatureManagement.FeatureFilters; +using System; +using System.Collections.Generic; +using Xunit; + +namespace Tests.FeatureManagement +{ + class ErrorMessage + { + public const string OutOfRange = "The value is out of the accepted range."; + public const string UnrecognizableValue = "The value is unrecognizable."; + public const string RequiredParameter = "Value cannot be null."; + public const string NotMatched = "Start date is not a valid first occurrence."; + } + + class ParamName + { + public const string Start = "Start"; + public const string End = "End"; + + public const string Pattern = "Recurrence.Pattern"; + public const string PatternType = "Recurrence.Pattern.Type"; + public const string Interval = "Recurrence.Pattern.Interval"; + public const string Index = "Recurrence.Pattern.Index"; + public const string DaysOfWeek = "Recurrence.Pattern.DaysOfWeek"; + public const string FirstDayOfWeek = "Recurrence.Pattern.FirstDayOfWeek"; + public const string Month = "Recurrence.Pattern.Month"; + public const string DayOfMonth = "Recurrence.Pattern.DayOfMonth"; + + public const string Range = "Recurrence.Range"; + public const string RangeType = "Recurrence.Range.Type"; + public const string NumberOfOccurrences = "Recurrence.Range.NumberOfOccurrences"; + public const string RecurrenceTimeZone = "Recurrence.Range.RecurrenceTimeZone"; + public const string EndDate = "Recurrence.Range.EndDate"; + } + + public class RecurrenceEvaluatorTest + { + private static void ConsumeValidationTestData(List> testData) + { + foreach ((TimeWindowFilterSettings settings, string paramName, string errorMessage) in testData) + { + ArgumentException ex = Assert.Throws( + () => + { + RecurrenceEvaluator.MatchRecurrence(DateTimeOffset.Now, settings); + }); + + Assert.Equal(paramName, ex.ParamName); + Assert.Equal(errorMessage, ex.Message.Substring(0, errorMessage.Length)); + } + } + + private static void ConsumeEvalutationTestData(List> testData) + { + foreach ((DateTimeOffset time, TimeWindowFilterSettings settings, bool expected) in testData) + { + Assert.Equal(RecurrenceEvaluator.MatchRecurrence(time, settings), expected); + } + } + + [Fact] + public void GeneralRequiredParameterTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-25T12:00:00+08:00"), + End = null, + Recurrence = new Recurrence() + }, + ParamName.End, + ErrorMessage.RequiredParameter ), + + ( new TimeWindowFilterSettings() + { + Start = null, + End = DateTimeOffset.Parse("2023-9-25T12:00:00+08:00"), + Recurrence = new Recurrence() + }, + ParamName.Start, + ErrorMessage.RequiredParameter ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-25T02:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = null, + Range = new RecurrenceRange() + } + }, + ParamName.Pattern, + ErrorMessage.RequiredParameter ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-25T02:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern(), + Range = null + } + }, + ParamName.Range, + ErrorMessage.RequiredParameter ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-25T02:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = null + }, + Range = new RecurrenceRange() + } + }, + ParamName.PatternType, + ErrorMessage.RequiredParameter ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-25T02:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily", + }, + Range = new RecurrenceRange() + { + Type = null + } + } + }, + ParamName.RangeType, + ErrorMessage.RequiredParameter ), + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void InvalidValueTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.PatternType, + ErrorMessage.UnrecognizableValue ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily" + }, + Range = new RecurrenceRange() + { + Type = "" + } + } + }, + ParamName.RangeType, + ErrorMessage.UnrecognizableValue ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily", + Interval = 0 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.Interval, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + DaysOfWeek = new List(){ "Monday" }, + FirstDayOfWeek = "" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.FirstDayOfWeek, + ErrorMessage.UnrecognizableValue ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + DaysOfWeek = new List(){ "day" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.DaysOfWeek, + ErrorMessage.UnrecognizableValue ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + Index = "", + DaysOfWeek = new List(){ "Friday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.Index, + ErrorMessage.UnrecognizableValue ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteMonthly", + DayOfMonth = 0, + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.DayOfMonth, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteYearly", + DayOfMonth = 1, + Month = 0 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.Month, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd", + RecurrenceTimeZone = "" + } + } + }, + ParamName.RecurrenceTimeZone, + ErrorMessage.UnrecognizableValue ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily" + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 0 + } + } + }, + ParamName.NumberOfOccurrences, + ErrorMessage.OutOfRange ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void InvalidTimeWindowTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-25T12:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-25T12:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.End, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-27T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily", + Interval = 2 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.End, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 1, + DaysOfWeek = new List(){ "Friday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.End, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-5T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 1, + DaysOfWeek = new List(){ "Monday", "Thursday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.End, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-2-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-3-29T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteMonthly", + Interval = 2, + DayOfMonth = 1 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.End, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + Interval = 1, + DaysOfWeek = new List(){ "Friday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.End, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2024-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteYearly", + Interval = 1, + DayOfMonth = 1, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.End, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2024-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + Interval = 1, + DaysOfWeek = new List(){ "Friday" }, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.End, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily" + }, + Range = new RecurrenceRange() + { + Type = "EndDate", + EndDate = DateTimeOffset.Parse("2023-8-31T00:00:00+08:00") + } + } + }, + ParamName.EndDate, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T23:00:00+00:00"), + End = DateTimeOffset.Parse("2023-9-1T23:00:01+00:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily" + }, + Range = new RecurrenceRange() + { + Type = "EndDate", + EndDate = DateTimeOffset.Parse("2023-9-1"), + RecurrenceTimeZone = "UTC+08:00" + } + } + }, + ParamName.EndDate, + ErrorMessage.OutOfRange ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void WeeklyPatternRequiredParameterTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + DaysOfWeek = null + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.DaysOfWeek, + ErrorMessage.RequiredParameter ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void WeeklyPatternNotMatchTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + DaysOfWeek = new List{ "Monday", "Tuesday", "Wednesday", "Thursday", "Saturday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.Start, + ErrorMessage.NotMatched ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + DaysOfWeek = new List{ "Friday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd", + RecurrenceTimeZone = "UTC+07:00" + } + } + }, + ParamName.Start, + ErrorMessage.NotMatched ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void AbsoluteMonthlyPatternRequiredParameterTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteMonthly", + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.DayOfMonth, + ErrorMessage.RequiredParameter ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void AbsoluteMonthlyPatternNotMatchTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-2T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteMonthly", + DayOfMonth = 1 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd", + } + } + }, + ParamName.Start, + ErrorMessage.NotMatched ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void RelativeMonthlyPatternRequiredParameterTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.DaysOfWeek, + ErrorMessage.RequiredParameter ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List(){ "Friday" }, + Index = null + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.Index, + ErrorMessage.RequiredParameter ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void RelativeMonthlyPatternNotMatchTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List{ "Friday" }, + Index = "Second" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.Start, + ErrorMessage.NotMatched ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void AbsoluteYearlyPatternRequiredParameterTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteYearly", + DayOfMonth = 1 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.Month, + ErrorMessage.RequiredParameter ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteYearly", + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.DayOfMonth, + ErrorMessage.RequiredParameter ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void AbsoluteYearlyPatternNotMatchTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteYearly", + DayOfMonth = 1, + Month = 8 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd", + } + } + }, + ParamName.Start, + ErrorMessage.NotMatched ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void RelativeYearlyPatternRequiredParameterTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = null, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.DaysOfWeek, + ErrorMessage.RequiredParameter ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List{ "Friday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.Month, + ErrorMessage.RequiredParameter ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List{ "Friday" }, + Month = 9, + Index = null + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.Index, + ErrorMessage.RequiredParameter ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void RelativeYearlyPatternNotMatchTest() + { + var testData = new List>() + { + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List{ "Friday" }, + Month = 8 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.Start, + ErrorMessage.NotMatched ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List{ "Friday" }, + Month = 9, + Index = "Second" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + ParamName.Start, + ErrorMessage.NotMatched ) + }; + + ConsumeValidationTestData(testData); + } + + [Fact] + public void MatchDailyRecurrenceTest() + { + var testData = new List>() + { + ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily", + Interval = 2 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-5T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily", + Interval = 4 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-6T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily", + Interval = 4 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-9T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily", + Interval = 4 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily", + Interval = 2 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily" + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 2 + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-2T17:00:00+00:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T17:00:00+00:00"), + End = DateTimeOffset.Parse("2023-9-1T17:30:00+00:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Daily" + }, + Range = new RecurrenceRange() + { + Type = "EndDate", + EndDate = DateTimeOffset.Parse("2023-9-2"), + RecurrenceTimeZone = "UTC+08:00" + } + } + }, + false ) + }; + + ConsumeEvalutationTestData(testData); + } + + [Fact] + public void MatchWeeklyRecurrenceTest() + { + var testData = new List>() + { + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + DaysOfWeek = new List(){ "Monday", "Friday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // Friday in the 2nd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 2, + DaysOfWeek = new List(){ "Friday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // Friday in the 3rd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 2, + DaysOfWeek = new List(){ "Friday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 2, + FirstDayOfWeek = "Monday", + DaysOfWeek = new List(){ "Monday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 1st week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 2, + FirstDayOfWeek = "Sunday", + DaysOfWeek = new List(){ "Monday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + DaysOfWeek = new List(){ "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), // Saturday in the 1st week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + DaysOfWeek = new List(){ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 1 + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 4th week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 2, + FirstDayOfWeek = "Monday", + DaysOfWeek = new List(){ "Monday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 3rd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 2, + FirstDayOfWeek = "Sunday", + DaysOfWeek = new List(){ "Monday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-17T00:00:00+08:00"), // Sunday in the 3rd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 2, + FirstDayOfWeek = "Monday", + DaysOfWeek = new List(){ "Monday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 3 + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-13T00:00:00+08:00"), // Wednesday in the 3rd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 2, + FirstDayOfWeek = "Monday", + DaysOfWeek = new List(){ "Monday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // Tuesday in the 4th week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 2, + FirstDayOfWeek = "Monday", + DaysOfWeek = new List(){ "Monday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 3 + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // Tuesday in the 4th week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "Weekly", + Interval = 2, + FirstDayOfWeek = "Monday", + DaysOfWeek = new List(){ "Monday", "Sunday" } + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 2 + } + } + }, + false ) + }; + + ConsumeEvalutationTestData(testData); + } + + [Fact] + public void MatchAbsoluteMonthlyRecurrenceTest() + { + var testData = new List>() + { + ( DateTimeOffset.Parse("2023-10-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteMonthly", + DayOfMonth = 1 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteMonthly", + DayOfMonth = 1, + Interval = 5 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2024-1-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteMonthly", + DayOfMonth = 1, + Interval = 5 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteMonthly", + DayOfMonth = 1, + Interval = 4 + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 3 + } + } + }, + false ), + + ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-4-29T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-4-29T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteMonthly", + DayOfMonth = 29, + Interval = 2 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-4-29T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-4-29T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteMonthly", + DayOfMonth = 29, + Interval = 2 + }, + Range = new RecurrenceRange() + { + Type = "EndDate", + EndDate = DateTimeOffset.Parse("2024-2-29") + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-10-29T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteMonthly", + DayOfMonth = 29, + }, + Range = new RecurrenceRange() + { + Type = "EndDate", + EndDate = DateTimeOffset.Parse("2023-10-28") + } + } + }, + false ) + }; + + ConsumeEvalutationTestData(testData); + } + + [Fact] + public void MatchRelativeMonthlyRecurrenceTest() + { + var testData = new List>() + { + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 2nd Friday in 2023 Sep + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-10-13T00:00:00+08:00"), // 2nd Friday in 2023 Oct + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday" }, + Index = "Second" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-10-13T00:00:00+08:00"), // 2nd Friday in 2023 Oct + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday" }, + Index = "Second", + Interval = 3 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-12-8T00:00:00+08:00"), // 2nd Friday in 2023 Dec + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday" }, + Index = "Second", + Interval = 3 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // 3rd Friday in 2023 Sep + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday" }, + Index = "Second" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-10-6T00:00:00+08:00"), // 1st Friday in 2023 Oct + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-10-27T00:00:00+08:00"), // 4th Friday in 2023 Oct + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday" }, + Index = "Last" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-11-24T00:00:00+08:00"), // 4th Friday in 2023 Nov + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday" }, + Index = "Last" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-12-29T00:00:00+08:00"), // 5th Friday in 2023 Dec + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday" }, + Index = "Last" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-10-29T00:00:00+08:00"), // 4th Sunday in 2023 Oct + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-25T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Sunday", "Monday" }, + Index = "Last" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-10-30T00:00:00+08:00"), // 5th Monday in 2023 Oct + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-25T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Sunday", "Monday" }, + Index = "Last" + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-10-6T00:00:00+08:00"), // 1st Friday in 2023 Oct + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday" }, + Interval = 3 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // 1st Friday in 2023 Dec + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday" }, + Interval = 3 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // 1st Monday in 2023 Oct + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday", "Monday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-10-6T00:00:00+08:00"), // 1st Friday in 2023 Oct + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday", "Monday" } + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // 1st Monday in 2023 Oct + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday", "Monday" }, + Interval = 2 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-11-3T00:00:00+08:00"), // 1st Friday in 2023 Nov + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday", "Monday" }, + Interval = 2 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-11-6T00:00:00+08:00"), // 1st Monday in 2023 Nov + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Friday", "Monday" }, + Interval = 2 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Interval = 3 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2024-3-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Interval = 3 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 3 + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-10-1T00:00:00+08:00"), // Sunday + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }, + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // Monday + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }, + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-10-3T00:00:00+08:00"), // Tuesday + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeMonthly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Sunday" }, + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ) + }; + + ConsumeEvalutationTestData(testData); + } + + [Fact] + public void MatchAbsoluteYearlyRecurrenceTest() + { + var testData = new List>() + { + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteYearly", + DayOfMonth = 1, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteYearly", + DayOfMonth = 1, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 1 + } + } + }, + false ), + + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteYearly", + DayOfMonth = 1, + Month = 9, + Interval = 3 + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 2 + } + } + }, + true ), + + ( DateTimeOffset.Parse("2029-9-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteYearly", + DayOfMonth = 1, + Month = 9, + Interval = 3 + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 2 + } + } + }, + false ), + + ( DateTimeOffset.Parse("2024-10-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "AbsoluteYearly", + DayOfMonth = 1, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ) + }; + + ConsumeEvalutationTestData(testData); + } + + [Fact] + public void MatchRelativeYearlyRecurrenceTest() + { + var testData = new List>() + { + ( DateTimeOffset.Parse("2024-9-6T00:00:00+08:00"), // 1st Friday + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List() { "Friday" }, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // 1st Sunday + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2024-10-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Month = 9, + Interval = 2 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + false ), + + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Month = 9, + Interval = 3 + }, + Range = new RecurrenceRange() + { + Type = "NoEnd" + } + } + }, + true ), + + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = "RelativeYearly", + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = "Numbered", + NumberOfOccurrences = 3 + } + } + }, + false ), + }; + + ConsumeEvalutationTestData(testData); + } + } +} From a732a2484f4313f36229cad8ff26fa0d5de22e33 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 14 Nov 2023 16:04:29 +0800 Subject: [PATCH 02/52] remove duplicated null check --- .../FeatureFilters/Recurrence/RecurrenceEvaluator.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 530c5fc7..facb8c39 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -77,11 +77,6 @@ public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings return false; } - if (settings.Recurrence == null) - { - return false; - } - if (!TryGetPreviousOccurrence(time, settings, out DateTimeOffset previousOccurrence)) { return false; From 290e96ce1e15c3ef968d911599e4aa017b732aa2 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 14 Nov 2023 23:09:30 +0800 Subject: [PATCH 03/52] remove redundant if --- .../Recurrence/RecurrenceEvaluator.cs | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index facb8c39..4246e185 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -588,45 +588,27 @@ private static bool TryValidateRecurrencePattern(TimeWindowFilterSettings settin if (string.Equals(patternType, Daily, StringComparison.OrdinalIgnoreCase)) { - if (!TryValidateDailyRecurrencePattern(settings, out paramName, out reason)) - { - return false; - } + return TryValidateDailyRecurrencePattern(settings, out paramName, out reason); } else if (string.Equals(patternType, Weekly, StringComparison.OrdinalIgnoreCase)) { - if (!TryValidateWeeklyRecurrencePattern(settings, out paramName, out reason)) - { - return false; - } + return TryValidateWeeklyRecurrencePattern(settings, out paramName, out reason); } else if (string.Equals(patternType, AbsoluteMonthly, StringComparison.OrdinalIgnoreCase)) { - if (!TryValidateAbsoluteMonthlyRecurrencePattern(settings, out paramName, out reason)) - { - return false; - } + return TryValidateAbsoluteMonthlyRecurrencePattern(settings, out paramName, out reason); } else if (string.Equals(patternType, RelativeMonthly, StringComparison.OrdinalIgnoreCase)) { - if (!TryValidateRelativeMonthlyRecurrencePattern(settings, out paramName, out reason)) - { - return false; - } + return TryValidateRelativeMonthlyRecurrencePattern(settings, out paramName, out reason); } else if (string.Equals(patternType, AbsoluteYearly, StringComparison.OrdinalIgnoreCase)) { - if (!TryValidateAbsoluteYearlyRecurrencePattern(settings, out paramName, out reason)) - { - return false; - } + return TryValidateAbsoluteYearlyRecurrencePattern(settings, out paramName, out reason); } else if (string.Equals(patternType, RelativeYearly, StringComparison.OrdinalIgnoreCase)) { - if (!TryValidateRelativeYearlyRecurrencePattern(settings, out paramName, out reason)) - { - return false; - } + return TryValidateRelativeYearlyRecurrencePattern(settings, out paramName, out reason); } else { @@ -636,8 +618,6 @@ private static bool TryValidateRecurrencePattern(TimeWindowFilterSettings settin return false; } - - return true; } private static bool TryValidateDailyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) From 0e3e1e448c11f4a1e8ca1085a99c33b434b6403d Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 15 Nov 2023 12:09:49 +0800 Subject: [PATCH 04/52] improvement --- .../Recurrence/RecurrenceEvaluator.cs | 5 ----- .../RecurrenceEvaluator.cs | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 4246e185..341a4373 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -1251,11 +1251,6 @@ private static TimeSpan GetRecurrenceTimeZone(TimeWindowFilterSettings settings) /// True if the duration is compliant with days of week, false otherwise. private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int interval, IEnumerable daysOfWeek, string firstDayOfWeek) { - if (daysOfWeek == null || !daysOfWeek.Any()) - { - throw new ArgumentException(nameof(daysOfWeek)); - } - if (daysOfWeek.Count() == 1) { return true; diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index 14800478..1ff31109 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -1734,7 +1734,8 @@ public void MatchRelativeMonthlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" } + DaysOfWeek = new List() { "Friday" }, + Index = "First" }, Range = new RecurrenceRange() { @@ -1860,6 +1861,7 @@ public void MatchRelativeMonthlyRecurrenceTest() { Type = "RelativeMonthly", DaysOfWeek = new List() { "Friday" }, + Index = "First", Interval = 3 }, Range = new RecurrenceRange() @@ -1901,7 +1903,8 @@ public void MatchRelativeMonthlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday", "Monday" } + DaysOfWeek = new List() { "Friday", "Monday" }, + Index = "First" }, Range = new RecurrenceRange() { @@ -1942,6 +1945,7 @@ public void MatchRelativeMonthlyRecurrenceTest() { Type = "RelativeMonthly", DaysOfWeek = new List() { "Friday", "Monday" }, + Index = "First", Interval = 2 }, Range = new RecurrenceRange() @@ -1963,6 +1967,7 @@ public void MatchRelativeMonthlyRecurrenceTest() { Type = "RelativeMonthly", DaysOfWeek = new List() { "Friday", "Monday" }, + Index = "First", Interval = 2 }, Range = new RecurrenceRange() @@ -1984,6 +1989,7 @@ public void MatchRelativeMonthlyRecurrenceTest() { Type = "RelativeMonthly", DaysOfWeek = new List() { "Friday", "Monday" }, + Index = "First", Interval = 2 }, Range = new RecurrenceRange() @@ -1994,7 +2000,7 @@ public void MatchRelativeMonthlyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), + ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // the first day of the month new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2015,7 +2021,7 @@ public void MatchRelativeMonthlyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2024-3-1T00:00:00+08:00"), + ( DateTimeOffset.Parse("2024-3-1T00:00:00+08:00"), // the first day of the month new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2308,7 +2314,7 @@ public void MatchRelativeYearlyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // the first day of Sep new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2372,7 +2378,7 @@ public void MatchRelativeYearlyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // the first day of Sep new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), From 3cdcc56edff402f238e9ad46b4a3ff6617a8138a Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Mon, 20 Nov 2023 11:09:35 +0800 Subject: [PATCH 05/52] resolve comments --- .../FeatureFilters/Recurrence/RecurrenceEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 341a4373..64981ce0 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -1260,7 +1260,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int // Shift to the first day of the week DateTime date = DateTime.Today; - int offset = RemainingDaysOfWeek((int)date.DayOfWeek, DayOfWeekNumber(firstDayOfWeek)) % 7; + int offset = RemainingDaysOfWeek((int)date.DayOfWeek, DayOfWeekNumber(firstDayOfWeek)); date = date.AddDays(offset); From 9600e7a8f13caf437bf6087ff4cde4cadbe1b87d Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Mon, 11 Dec 2023 11:21:54 +0800 Subject: [PATCH 06/52] fix typo --- tests/Tests.FeatureManagement/RecurrenceEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index 1ff31109..422afe71 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -1808,7 +1808,7 @@ public void MatchRelativeMonthlyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-10-29T00:00:00+08:00"), // 4th Sunday in 2023 Oct + ( DateTimeOffset.Parse("2023-10-29T00:00:00+08:00"), // 5th Sunday in 2023 Oct new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), From 64904ec5f0f97ad3e7b7809f7caa400e9183320d Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Mon, 11 Dec 2023 13:31:42 +0800 Subject: [PATCH 07/52] update --- tests/Tests.FeatureManagement/RecurrenceEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index 422afe71..04d4d732 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -2103,7 +2103,7 @@ public void MatchRelativeMonthlyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-10-3T00:00:00+08:00"), // Tuesday + ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // Monday new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2113,7 +2113,7 @@ public void MatchRelativeMonthlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = "RelativeMonthly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Sunday" }, + DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, }, Range = new RecurrenceRange() { From bf2e2eb2340e3217e6ecb548a535f011dff6e75a Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 15 Jan 2024 17:02:48 +0800 Subject: [PATCH 08/52] use enum --- .../Recurrence/RecurrenceEvaluator.cs | 457 ++++-------------- .../Recurrence/RecurrencePattern.cs | 23 +- .../Recurrence/RecurrencePatternType.cs | 41 ++ .../Recurrence/RecurrenceRange.cs | 12 +- .../Recurrence/RecurrenceRangeType.cs | 26 + .../FeatureFilters/Recurrence/WeekIndex.cs | 36 ++ 6 files changed, 214 insertions(+), 381 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs create mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRangeType.cs create mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/WeekIndex.cs diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 64981ce0..95544026 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -16,39 +16,6 @@ static class RecurrenceEvaluator const string RequiredParameter = "Value cannot be null."; const string NotMatched = "Start date is not a valid first occurrence."; - // - // Day of week - const string Sunday = "Sunday"; - const string Monday = "Monday"; - const string Tuesday = "Tuesday"; - const string Wednesday = "Wednesday"; - const string Thursday = "Thursday"; - const string Friday = "Friday"; - const string Saturday = "Saturday"; - - // - // Index - const string First = "First"; - const string Second = "Second"; - const string Third = "Third"; - const string Fourth = "Fourth"; - const string Last = "Last"; - - // - // Recurrence Pattern Type - const string Daily = "Daily"; - const string Weekly = "Weekly"; - const string AbsoluteMonthly = "AbsoluteMonthly"; - const string RelativeMonthly = "RelativeMonthly"; - const string AbsoluteYearly = "AbsoluteYearly"; - const string RelativeYearly = "RelativeYearly"; - - // - // Recurrence Range Type - const string EndDate = "EndDate"; - const string Numbered = "Numbered"; - const string NoEnd = "NoEnd"; - const int WeekDayNumber = 7; const int MinMonthDayNumber = 28; const int MinYearDayNumber = 365; @@ -108,59 +75,57 @@ private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilt return false; } - string patternType = settings.Recurrence.Pattern.Type; - int numberOfOccurrences; - if (string.Equals(patternType, Daily, StringComparison.OrdinalIgnoreCase)) - { - FindDailyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - } - else if (string.Equals(patternType, Weekly, StringComparison.OrdinalIgnoreCase)) - { - FindWeeklyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - } - else if (string.Equals(patternType, AbsoluteMonthly, StringComparison.OrdinalIgnoreCase)) - { - FindAbsoluteMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - } - else if (string.Equals(patternType, RelativeMonthly, StringComparison.OrdinalIgnoreCase)) + switch (settings.Recurrence.Pattern.Type) { - FindRelativeMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - } - else if (string.Equals(patternType, AbsoluteYearly, StringComparison.OrdinalIgnoreCase)) - { - FindAbsoluteYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - } - else if (string.Equals(patternType, RelativeYearly, StringComparison.OrdinalIgnoreCase)) - { - FindRelativeYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - } - else - { - throw new ArgumentException(nameof(settings)); + case RecurrencePatternType.Daily: + FindDailyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + + break; + + case RecurrencePatternType.Weekly: + FindWeeklyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + + break; + + case RecurrencePatternType.AbsoluteMonthly: + FindAbsoluteMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + + break; + + case RecurrencePatternType.RelativeMonthly: + FindRelativeMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + + break; + + case RecurrencePatternType.AbsoluteYearly: + FindAbsoluteYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + + break; + + case RecurrencePatternType.RelativeYearly: + FindRelativeYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + + break; + + default: + return false; } RecurrenceRange range = settings.Recurrence.Range; TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - if (string.Equals(range.Type, EndDate, StringComparison.OrdinalIgnoreCase)) + if (range.Type == RecurrenceRangeType.EndDate) { DateTime alignedPreviousOccurrence = previousOccurrence.DateTime + timeZoneOffset - previousOccurrence.Offset; - if (alignedPreviousOccurrence.Date > range.EndDate.Value.Date) - { - return false; - } + return alignedPreviousOccurrence.Date <= range.EndDate.Value.Date; } - - if (string.Equals(range.Type, Numbered, StringComparison.OrdinalIgnoreCase)) - { - if (numberOfOccurrences >= range.NumberOfOccurrences) - { - return false; - } + + if (range.Type == RecurrenceRangeType.Numbered) { + return numberOfOccurrences < range.NumberOfOccurrences; } return true; @@ -217,15 +182,13 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow TimeSpan timeGap = time - start; - int firstDayOfWeek = pattern.FirstDayOfWeek != null ? DayOfWeekNumber(pattern.FirstDayOfWeek) : 0; // first day of week is Sunday by default - - int remainingDaysOfFirstWeek = RemainingDaysOfWeek((int)alignedStart.DayOfWeek, firstDayOfWeek); + int remainingDaysOfFirstWeek = RemainingDaysOfWeek(alignedStart.DayOfWeek, pattern.FirstDayOfWeek); TimeSpan remainingTimeOfFirstInterval = TimeSpan.FromDays(remainingDaysOfFirstWeek) - alignedStart.TimeOfDay + TimeSpan.FromDays((interval - 1) * 7); if (remainingTimeOfFirstInterval <= timeGap) { - int numberOfInterval = (int)Math.Floor((timeGap - remainingTimeOfFirstInterval).TotalSeconds / TimeSpan.FromDays(interval * 7).TotalSeconds); + int numberOfInterval = (int) Math.Floor((timeGap - remainingTimeOfFirstInterval).TotalSeconds / TimeSpan.FromDays(interval * 7).TotalSeconds); previousOccurrence = start.AddDays(numberOfInterval * interval * 7 + remainingDaysOfFirstWeek + (interval - 1) * 7); @@ -237,10 +200,10 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow DateTime dateTime = alignedStart.AddDays(1); - while ((int)dateTime.DayOfWeek != firstDayOfWeek) + while (dateTime.DayOfWeek != pattern.FirstDayOfWeek) { if (pattern.DaysOfWeek.Any(day => - DayOfWeekNumber(day) == (int)dateTime.DayOfWeek)) + day == dateTime.DayOfWeek)) { numberOfOccurrences += 1; } @@ -261,13 +224,13 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow { alignedPreviousOccurrence = alignedPreviousOccurrence.AddDays(1); - if ((int)alignedPreviousOccurrence.DayOfWeek == firstDayOfWeek) // Come to the next week + if (alignedPreviousOccurrence.DayOfWeek == pattern.FirstDayOfWeek) // Come to the next week { break; } if (pattern.DaysOfWeek.Any(day => - DayOfWeekNumber(day) == (int)alignedPreviousOccurrence.DayOfWeek)) + day == alignedPreviousOccurrence.DayOfWeek)) { previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); @@ -338,7 +301,7 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + alignedStart.TimeOfDay)) { // - // E.g. start: 2023.9.1 (the first Friday in 2023.9) and time: 2023.10.2 (the first Friday in 2023.10 is 2023.10.6) + // E.g. start is 2023.9.1 (the first Friday in 2023.9) and current time is 2023.10.2 (the first Friday in next month is 2023.10.6) // Not a complete monthly interval monthGap -= 1; } @@ -352,7 +315,7 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T // // Find the first occurence date matched the pattern // Only one day of week in the month will be matched - foreach (string day in pattern.DaysOfWeek) + foreach (DayOfWeek day in pattern.DaysOfWeek) { DateTime occurrenceDate = NthDayOfWeekInTheMonth(alignedPreviousOccurrenceMonth, pattern.Index, day); @@ -450,7 +413,7 @@ private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, Ti // // Find the first occurence date matched the pattern // Only one day of week in the month will be matched - foreach (string day in pattern.DaysOfWeek) + foreach (DayOfWeek day in pattern.DaysOfWeek) { DateTime occurrenceDate = NthDayOfWeekInTheMonth(alignedPreviousOccurrenceMonth, pattern.Index, day); @@ -543,24 +506,6 @@ private static bool TryValidateGeneralRequiredParameter(TimeWindowFilterSettings return false; } - if (recurrence.Pattern.Type == null) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(recurrence.Pattern)}.{nameof(recurrence.Pattern.Type)}"; - - reason = RequiredParameter; - - return false; - } - - if (recurrence.Range.Type == null) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(recurrence.Range)}.{nameof(recurrence.Range.Type)}"; - - reason = RequiredParameter; - - return false; - } - if (settings.End.Value - settings.Start.Value <= TimeSpan.Zero) { paramName = nameof(settings.End); @@ -575,48 +520,37 @@ private static bool TryValidateGeneralRequiredParameter(TimeWindowFilterSettings private static bool TryValidateRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) { - paramName = null; - - reason = null; - if (!TryValidateInterval(settings, out paramName, out reason)) { return false; } - string patternType = settings.Recurrence.Pattern.Type; - - if (string.Equals(patternType, Daily, StringComparison.OrdinalIgnoreCase)) - { - return TryValidateDailyRecurrencePattern(settings, out paramName, out reason); - } - else if (string.Equals(patternType, Weekly, StringComparison.OrdinalIgnoreCase)) - { - return TryValidateWeeklyRecurrencePattern(settings, out paramName, out reason); - } - else if (string.Equals(patternType, AbsoluteMonthly, StringComparison.OrdinalIgnoreCase)) - { - return TryValidateAbsoluteMonthlyRecurrencePattern(settings, out paramName, out reason); - } - else if (string.Equals(patternType, RelativeMonthly, StringComparison.OrdinalIgnoreCase)) - { - return TryValidateRelativeMonthlyRecurrencePattern(settings, out paramName, out reason); - } - else if (string.Equals(patternType, AbsoluteYearly, StringComparison.OrdinalIgnoreCase)) - { - return TryValidateAbsoluteYearlyRecurrencePattern(settings, out paramName, out reason); - } - else if (string.Equals(patternType, RelativeYearly, StringComparison.OrdinalIgnoreCase)) - { - return TryValidateRelativeYearlyRecurrencePattern(settings, out paramName, out reason); - } - else + switch (settings.Recurrence.Pattern.Type) { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Type)}"; + case RecurrencePatternType.Daily: + return TryValidateDailyRecurrencePattern(settings, out paramName, out reason); - reason = UnrecognizableValue; + case RecurrencePatternType.Weekly: + return TryValidateWeeklyRecurrencePattern(settings, out paramName, out reason); - return false; + case RecurrencePatternType.AbsoluteMonthly: + return TryValidateAbsoluteMonthlyRecurrencePattern(settings, out paramName, out reason); + + case RecurrencePatternType.RelativeMonthly: + return TryValidateRelativeMonthlyRecurrencePattern(settings, out paramName, out reason); + + case RecurrencePatternType.AbsoluteYearly: + return TryValidateAbsoluteYearlyRecurrencePattern(settings, out paramName, out reason); + + case RecurrencePatternType.RelativeYearly: + return TryValidateRelativeYearlyRecurrencePattern(settings, out paramName, out reason); + + default: + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Type)}"; + + reason = UnrecognizableValue; + + return false; } } @@ -669,18 +603,6 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings return false; } - // - // Required parameters - if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) - { - return false; - } - - if (!TryValidateFirstDayOfWeek(settings, out paramName, out reason)) - { - return false; - } - // // Check whether "Start" is a valid first occurrence DateTimeOffset start = settings.Start.Value; @@ -688,7 +610,7 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; if (!pattern.DaysOfWeek.Any(day => - DayOfWeekNumber(day) == (int)alignedStart.DayOfWeek)) + day == alignedStart.DayOfWeek)) { paramName = nameof(settings.Start); @@ -778,18 +700,6 @@ private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilter return false; } - // - // Required parameters - if (!TryValidateIndex(settings, out paramName, out reason)) - { - return false; - } - - if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) - { - return false; - } - // // Check whether "Start" is a valid first occurrence DateTimeOffset start = settings.Start.Value; @@ -888,16 +798,6 @@ private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterS return false; } - if (!TryValidateIndex(settings, out paramName, out reason)) - { - return false; - } - - if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) - { - return false; - } - // // Check whether "Start" is a valid first occurrence DateTimeOffset start = settings.Start.Value; @@ -920,147 +820,40 @@ private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterS private static bool TryValidateRecurrenceRange(TimeWindowFilterSettings settings, out string paramName, out string reason) { - paramName = null; - - reason = null; - if (!TryValidateRecurrenceTimeZone(settings, out paramName, out reason)) { return false; } - string rangeType = settings.Recurrence.Range.Type; - - if (string.Equals(rangeType, NoEnd, StringComparison.OrdinalIgnoreCase)) - { - // - // No parameter is required - } - else if (string.Equals(rangeType, EndDate, StringComparison.OrdinalIgnoreCase)) - { - if (!TryValidateEndDate(settings, out paramName, out reason)) - { - return false; - } - } - else if (string.Equals(rangeType, Numbered, StringComparison.OrdinalIgnoreCase)) - { - if (!TryValidateNumberOfOccurrences(settings, out paramName, out reason)) - { - return false; - } - } - else - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.Type)}"; - - reason = UnrecognizableValue; - - return false; - } - - return true; - } - - private static bool TryValidateInterval(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Interval)}"; - - reason = null; - - if (settings.Recurrence.Pattern.Interval <= 0) + switch(settings.Recurrence.Range.Type) { - reason = OutOfRange; + case RecurrenceRangeType.NoEnd: + return true; - return false; - } - - return true; - } - - private static bool TryValidateDaysOfWeek(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DaysOfWeek)}"; - - reason = null; + case RecurrenceRangeType.EndDate: + return TryValidateEndDate(settings, out paramName, out reason); - if (settings.Recurrence.Pattern.DaysOfWeek == null || !settings.Recurrence.Pattern.DaysOfWeek.Any()) - { - reason = RequiredParameter; + case RecurrenceRangeType.Numbered: + return !TryValidateNumberOfOccurrences(settings, out paramName, out reason); - return false; - } + default: + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.Type)}"; - foreach (string day in settings.Recurrence.Pattern.DaysOfWeek) - { - if (!string.Equals(day, Monday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(day, Tuesday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(day, Wednesday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(day, Thursday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(day, Friday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(day, Saturday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(day, Sunday, StringComparison.OrdinalIgnoreCase)) - { reason = UnrecognizableValue; return false; - } - } - - return true; - } - - private static bool TryValidateFirstDayOfWeek(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.FirstDayOfWeek)}"; - - reason = null; - - string firstDayOfWeek = settings.Recurrence.Pattern.FirstDayOfWeek; - - if (firstDayOfWeek == null) - { - return true; } - - if (!string.Equals(firstDayOfWeek, Monday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(firstDayOfWeek, Tuesday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(firstDayOfWeek, Wednesday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(firstDayOfWeek, Thursday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(firstDayOfWeek, Friday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(firstDayOfWeek, Saturday, StringComparison.OrdinalIgnoreCase) && - !string.Equals(firstDayOfWeek, Sunday, StringComparison.OrdinalIgnoreCase)) - { - reason = UnrecognizableValue; - - return false; - } - - return true; } - private static bool TryValidateIndex(TimeWindowFilterSettings settings, out string paramName, out string reason) + private static bool TryValidateInterval(TimeWindowFilterSettings settings, out string paramName, out string reason) { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Index)}"; + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Interval)}"; reason = null; - string index = settings.Recurrence.Pattern.Index; - - if (index == null) - { - reason = RequiredParameter; - - return false; - } - - if (!string.Equals(index, First, StringComparison.Ordinal) && - !string.Equals(index, Second, StringComparison.Ordinal) && - !string.Equals(index, Third, StringComparison.Ordinal) && - !string.Equals(index, Fourth, StringComparison.Ordinal) && - !string.Equals(index, Last, StringComparison.Ordinal)) + if (settings.Recurrence.Pattern.Interval <= 0) { - reason = UnrecognizableValue; + reason = OutOfRange; return false; } @@ -1249,7 +1042,7 @@ private static TimeSpan GetRecurrenceTimeZone(TimeWindowFilterSettings settings) /// The days of the week when the recurrence will occur. /// The first day of the week. /// True if the duration is compliant with days of week, false otherwise. - private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int interval, IEnumerable daysOfWeek, string firstDayOfWeek) + private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int interval, IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) { if (daysOfWeek.Count() == 1) { @@ -1260,7 +1053,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int // Shift to the first day of the week DateTime date = DateTime.Today; - int offset = RemainingDaysOfWeek((int)date.DayOfWeek, DayOfWeekNumber(firstDayOfWeek)); + int offset = RemainingDaysOfWeek(date.DayOfWeek, firstDayOfWeek); date = date.AddDays(offset); @@ -1273,7 +1066,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int date = date.AddDays(1); if (daysOfWeek.Any(day => - DayOfWeekNumber(day) == (int)date.DayOfWeek)) + day == date.DayOfWeek)) { TimeSpan gap = date - prevOccurrence; @@ -1303,9 +1096,9 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int return minGap >= duration; } - private static int RemainingDaysOfWeek(int day, int firstDayOfWeek) + private static int RemainingDaysOfWeek(DayOfWeek dayOfWeek, DayOfWeek firstDayOfWeek) { - int remainingDays = day - firstDayOfWeek; + int remainingDays = (int) dayOfWeek - (int) firstDayOfWeek; if (remainingDays < 0) { @@ -1324,20 +1117,20 @@ private static int RemainingDaysOfWeek(int day, int firstDayOfWeek) /// The index of the day of week in the month. /// The day of week. /// The data time of the nth day of week in the month. - private static DateTime NthDayOfWeekInTheMonth(DateTime dateTime, string index, string dayOfWeek) + private static DateTime NthDayOfWeekInTheMonth(DateTime dateTime, WeekIndex index, DayOfWeek dayOfWeek) { var date = new DateTime(dateTime.Year, dateTime.Month, 1); // // Find the first day of week in the month - while ((int)date.DayOfWeek != DayOfWeekNumber(dayOfWeek)) + while (date.DayOfWeek != dayOfWeek) { date = date.AddDays(1); } - if (date.AddDays(WeekDayNumber * (IndexNumber(index) - 1)).Month == dateTime.Month) + if (date.AddDays(WeekDayNumber * (int) index).Month == dateTime.Month) { - date = date.AddDays(WeekDayNumber * (IndexNumber(index) - 1)); + date = date.AddDays(WeekDayNumber * (int) index); } else // There is no the 5th day of week in the month { @@ -1348,69 +1141,5 @@ private static DateTime NthDayOfWeekInTheMonth(DateTime dateTime, string index, return date; } - - private static int DayOfWeekNumber(string str) - { - if (string.Equals(str, Sunday, StringComparison.OrdinalIgnoreCase)) - { - return 0; - } - else if (string.Equals(str, Monday, StringComparison.OrdinalIgnoreCase)) - { - return 1; - } - else if (string.Equals(str, Tuesday, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - else if (string.Equals(str, Wednesday, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - else if (string.Equals(str, Thursday, StringComparison.OrdinalIgnoreCase)) - { - return 4; - } - else if (string.Equals(str, Friday, StringComparison.OrdinalIgnoreCase)) - { - return 5; - } - else if (string.Equals(str, Saturday, StringComparison.OrdinalIgnoreCase)) - { - return 6; - } - else - { - throw new ArgumentException(nameof(str)); - } - } - - public static int IndexNumber(string str) - { - if (string.Equals(str, First, StringComparison.OrdinalIgnoreCase)) - { - return 1; - } - else if (string.Equals(str, Second, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - else if (string.Equals(str, Third, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - else if (string.Equals(str, Fourth, StringComparison.OrdinalIgnoreCase)) - { - return 4; - } - else if (string.Equals(str, Last, StringComparison.OrdinalIgnoreCase)) - { - return 5; - } - else - { - throw new ArgumentException(nameof(str)); - } - } } } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs index 8d3aaac7..3fac86b8 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs @@ -1,47 +1,48 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using System; using System.Collections.Generic; namespace Microsoft.FeatureManagement.FeatureFilters { /// - /// The recurrence pattern specifying how often the time window repeats + /// The recurrence pattern describes the frequency by which the time window repeats. /// public class RecurrencePattern { /// - /// The recurrence pattern type + /// The recurrence pattern type. /// - public string Type { get; set; } + public RecurrencePatternType Type { get; set; } /// - /// The number of units between occurrences, where units can be in days, weeks, months, or years, depending on the pattern type + /// The number of units between occurrences, where units can be in days, weeks, months, or years, depending on the pattern type. /// public int Interval { get; set; } = 1; /// - /// The days of the week on which the time window occurs + /// The days of the week on which the time window occurs. /// - public IEnumerable DaysOfWeek { get; set; } + public IEnumerable DaysOfWeek { get; set; } /// /// The first day of the week. /// - public string FirstDayOfWeek { get; set; } = "Sunday"; + public DayOfWeek FirstDayOfWeek { get; set; } /// - /// Specifies on which instance of the allowed days specified in DaysOfWeek the time window occurs, counted from the first instance in the month + /// Specifies on which instance of the allowed days specified in DaysOfWeek the recurrence occurs, counted from the first instance in the month. /// - public string Index { get; set; } = "First"; + public WeekIndex Index { get; set; } /// - /// The day of the month on which the time window occurs + /// The day of the month on which the time window occurs. /// public int? DayOfMonth { get; set; } /// - /// The month on which the time window occurs + /// The month on which the time window occurs. /// public int? Month { get; set; } } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs new file mode 100644 index 00000000..73f863c4 --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement.FeatureFilters +{ + /// + /// The type of specifying the frequency by which the time window repeats. + /// + public enum RecurrencePatternType + { + /// + /// The pattern where the time window will repeat based on the number of days specified by interval between occurrences. + /// + Daily, + + /// + /// The pattern where the time window will repeat on the same day or days of the week, based on the number of weeks between each set of occurrences + /// + Weekly, + + /// + /// The pattern where the time window will repeat on the specified day of the month, based on the number of months between occurrences. + /// + AbsoluteMonthly, + + /// + /// The pattern where the time window will repeat on the specified days of the week, in the same relative position in the month, based on the number of months between occurrences. + /// + RelativeMonthly, + + /// + /// The pattern where the time window will repeat on the specified day and month, based on the number of years between occurrences. + /// + AbsoluteYearly, + + /// + /// The pattern where the time window will repeat on the specified days of the week, in the same relative position in a specific month of the year, based on the number of years between occurrences. + /// + RelativeYearly + } +} diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs index b176f6f7..356eb348 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs @@ -6,27 +6,27 @@ namespace Microsoft.FeatureManagement.FeatureFilters { /// - /// The recurrence range specifying how long the recurrence pattern repeats + /// The recurrence range describes a date range over which the time window repeats. /// public class RecurrenceRange { /// - /// The recurrence range type + /// The recurrence range type. /// - public string Type { get; set; } = "NoEnd"; + public RecurrenceRangeType Type { get; set; } /// - /// The date to stop applying the recurrence pattern + /// The date to stop applying the recurrence pattern. /// public DateTimeOffset? EndDate { get; set; } /// - /// The number of times to repeat the time window + /// The number of times to repeat the time window. /// public int? NumberOfOccurrences { get; set; } /// - /// Time zone for recurrence settings, e.g. UTC+08:00 + /// Time zone for recurrence settings. e.g. UTC+08:00 /// public string RecurrenceTimeZone { get; set; } } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRangeType.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRangeType.cs new file mode 100644 index 00000000..942aa48b --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRangeType.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement.FeatureFilters +{ + /// + /// The type of specifying the date range over which the time window repeats. + /// + public enum RecurrenceRangeType + { + /// + /// The time window repeats on all the days that fit the corresponding . + /// + NoEnd, + + /// + /// The time window repeats on all the days that fit the corresponding before or on the end date specified in EndDate of . + /// + EndDate, + + /// + /// The time window repeats for the number specified in the NumberOfOccurrences of that fit based on the . + /// + Numbered + } +} diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/WeekIndex.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/WeekIndex.cs new file mode 100644 index 00000000..d0ef0947 --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/WeekIndex.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement.FeatureFilters +{ + /// + /// The relative position in the month of the allowed days specified in DaysOfWeek of the recurrence occurs, counted from the first instance in the month. + /// + public enum WeekIndex + { + /// + /// Specifies on the first instance of the allowed day of week, counted from the first instance in the month. + /// + First, + + /// + /// Specifies on the second instance of the allowed day of week, counted from the first instance in the month. + /// + Second, + + /// + /// Specifies on the third instance of the allowed day of week, counted from the first instance in the month. + /// + Third, + + /// + /// Specifies on the fourth instance of the allowed day of week, counted from the first instance in the month. + /// + Fourth, + + /// + /// Specifies on the last instance of the allowed day of week, counted from the first instance in the month. + /// + Last + } +} From 3041b06f9cc3db6f0fc5350adae4bdf1cda65c11 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 16 Jan 2024 14:54:30 +0800 Subject: [PATCH 09/52] Update testcases --- .../Recurrence/RecurrenceEvaluator.cs | 141 +- .../FeatureManagement.cs | 8 +- tests/Tests.FeatureManagement/Features.cs | 1 + .../RecurrenceEvaluator.cs | 1137 ++++++----------- .../Tests.FeatureManagement/appsettings.json | 30 +- 5 files changed, 520 insertions(+), 797 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 95544026..0d32c29e 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -437,10 +437,6 @@ private static bool TryValidateSettings(TimeWindowFilterSettings settings, out s Recurrence recurrence = settings.Recurrence; - paramName = null; - - reason = null; - if (recurrence != null) { if (!TryValidateGeneralRequiredParameter(settings, out paramName, out reason)) @@ -459,17 +455,15 @@ private static bool TryValidateSettings(TimeWindowFilterSettings settings, out s } } + paramName = null; + + reason = null; + return true; } private static bool TryValidateGeneralRequiredParameter(TimeWindowFilterSettings settings, out string paramName, out string reason) { - Recurrence recurrence = settings.Recurrence; - - paramName = null; - - reason = null; - if (settings.Start == null) { paramName = nameof(settings.Start); @@ -488,6 +482,8 @@ private static bool TryValidateGeneralRequiredParameter(TimeWindowFilterSettings return false; } + Recurrence recurrence = settings.Recurrence; + if (recurrence.Pattern == null) { paramName = $"{nameof(settings.Recurrence)}.{nameof(recurrence.Pattern)}"; @@ -515,6 +511,10 @@ private static bool TryValidateGeneralRequiredParameter(TimeWindowFilterSettings return false; } + paramName = null; + + reason = null; + return true; } @@ -556,15 +556,13 @@ private static bool TryValidateRecurrencePattern(TimeWindowFilterSettings settin private static bool TryValidateDailyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) { - paramName = null; - - reason = null; - TimeSpan intervalDuration = TimeSpan.FromDays(settings.Recurrence.Pattern.Interval); + TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + // // Time window duration must be shorter than how frequently it occurs - if (settings.End.Value - settings.Start.Value > intervalDuration) + if (timeWindowDuration > intervalDuration) { paramName = $"{nameof(settings.End)}"; @@ -577,15 +575,15 @@ private static bool TryValidateDailyRecurrencePattern(TimeWindowFilterSettings s // No required parameter for "Daily" pattern // "Start" is always a valid first occurrence for "Daily" pattern + paramName = null; + + reason = null; + return true; } private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) { - paramName = null; - - reason = null; - RecurrencePattern pattern = settings.Recurrence.Pattern; TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * WeekDayNumber); @@ -603,6 +601,13 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings return false; } + // + // Required parameters + if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) + { + return false; + } + // // Check whether "Start" is a valid first occurrence DateTimeOffset start = settings.Start.Value; @@ -635,17 +640,15 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings private static bool TryValidateAbsoluteMonthlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) { - paramName = null; - - reason = null; - RecurrencePattern pattern = settings.Recurrence.Pattern; TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinMonthDayNumber); + TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + // // Time window duration must be shorter than how frequently it occurs - if (settings.End.Value - settings.Start.Value > intervalDuration) + if (timeWindowDuration > intervalDuration) { paramName = $"{nameof(settings.End)}"; @@ -681,17 +684,15 @@ private static bool TryValidateAbsoluteMonthlyRecurrencePattern(TimeWindowFilter private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) { - paramName = null; - - reason = null; - RecurrencePattern pattern = settings.Recurrence.Pattern; TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinMonthDayNumber); + TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + // // Time window duration must be shorter than how frequently it occurs - if (settings.End.Value - settings.Start.Value > intervalDuration) + if (timeWindowDuration > intervalDuration) { paramName = $"{nameof(settings.End)}"; @@ -700,6 +701,13 @@ private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilter return false; } + // + // Required parameters + if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) + { + return false; + } + // // Check whether "Start" is a valid first occurrence DateTimeOffset start = settings.Start.Value; @@ -721,17 +729,15 @@ private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilter private static bool TryValidateAbsoluteYearlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) { - paramName = null; - - reason = null; - RecurrencePattern pattern = settings.Recurrence.Pattern; TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinYearDayNumber); + TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + // // Time window duration must be shorter than how frequently it occurs - if (settings.End.Value - settings.Start.Value > intervalDuration) + if (timeWindowDuration > intervalDuration) { paramName = $"{nameof(settings.End)}"; @@ -772,17 +778,15 @@ private static bool TryValidateAbsoluteYearlyRecurrencePattern(TimeWindowFilterS private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) { - paramName = null; - - reason = null; - RecurrencePattern pattern = settings.Recurrence.Pattern; TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinYearDayNumber); + TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + // // Time window duration must be shorter than how frequently it occurs - if (settings.End.Value - settings.Start.Value > intervalDuration) + if (timeWindowDuration > intervalDuration) { paramName = $"{nameof(settings.End)}"; @@ -793,6 +797,11 @@ private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterS // // Required parameters + if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) + { + return false; + } + if (!TryValidateMonth(settings, out paramName, out reason)) { return false; @@ -834,7 +843,7 @@ private static bool TryValidateRecurrenceRange(TimeWindowFilterSettings settings return TryValidateEndDate(settings, out paramName, out reason); case RecurrenceRangeType.Numbered: - return !TryValidateNumberOfOccurrences(settings, out paramName, out reason); + return TryValidateNumberOfOccurrences(settings, out paramName, out reason); default: paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.Type)}"; @@ -849,8 +858,6 @@ private static bool TryValidateInterval(TimeWindowFilterSettings settings, out s { paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Interval)}"; - reason = null; - if (settings.Recurrence.Pattern.Interval <= 0) { reason = OutOfRange; @@ -858,15 +865,31 @@ private static bool TryValidateInterval(TimeWindowFilterSettings settings, out s return false; } + reason = null; + return true; } - private static bool TryValidateDayOfMonth(TimeWindowFilterSettings settings, out string paramName, out string reason) + private static bool TryValidateDaysOfWeek(TimeWindowFilterSettings settings, out string paramName, out string reason) { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DayOfMonth)}"; + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DaysOfWeek)}"; + + if (settings.Recurrence.Pattern.DaysOfWeek == null || !settings.Recurrence.Pattern.DaysOfWeek.Any()) + { + reason = RequiredParameter; + + return false; + } reason = null; + return true; + } + + private static bool TryValidateDayOfMonth(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DayOfMonth)}"; + if (settings.Recurrence.Pattern.DayOfMonth == null) { reason = RequiredParameter; @@ -881,6 +904,8 @@ private static bool TryValidateDayOfMonth(TimeWindowFilterSettings settings, out return false; } + reason = null; + return true; } @@ -888,8 +913,6 @@ private static bool TryValidateMonth(TimeWindowFilterSettings settings, out stri { paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Month)}"; - reason = null; - if (settings.Recurrence.Pattern.Month == null) { reason = RequiredParameter; @@ -904,6 +927,8 @@ private static bool TryValidateMonth(TimeWindowFilterSettings settings, out stri return false; } + reason = null; + return true; } @@ -911,8 +936,6 @@ private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out st { paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.EndDate)}"; - reason = null; - if (settings.Recurrence.Range.EndDate == null) { reason = RequiredParameter; @@ -920,8 +943,6 @@ private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out st return false; } - TimeSpan timeZoneOffset; - if (settings.Start == null) { paramName = nameof(settings.Start); @@ -933,15 +954,11 @@ private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out st DateTimeOffset start = settings.Start.Value; - timeZoneOffset = start.Offset; + TimeSpan timeZoneOffset = start.Offset; - if (settings.Recurrence.Range.RecurrenceTimeZone != null && !TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out timeZoneOffset)) + if (settings.Recurrence.Range.RecurrenceTimeZone != null) { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.RecurrenceTimeZone)}"; - - reason = UnrecognizableValue; - - return false; + TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out timeZoneOffset); } DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; @@ -955,6 +972,8 @@ private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out st return false; } + reason = null; + return true; } @@ -962,8 +981,6 @@ private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings sett { paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.NumberOfOccurrences)}"; - reason = null; - if (settings.Recurrence.Range.NumberOfOccurrences == null) { reason = RequiredParameter; @@ -978,6 +995,8 @@ private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings sett return false; } + reason = null; + return true; } @@ -985,8 +1004,6 @@ private static bool TryValidateRecurrenceTimeZone(TimeWindowFilterSettings setti { paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.RecurrenceTimeZone)}"; - reason = null; - if (settings.Recurrence.Range.RecurrenceTimeZone != null && !TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out _)) { reason = UnrecognizableValue; @@ -994,6 +1011,8 @@ private static bool TryValidateRecurrenceTimeZone(TimeWindowFilterSettings setti return false; } + reason = null; + return true; } diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index c1139ebc..6c54f925 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -322,7 +322,10 @@ public async Task TimeWindow() Environment.SetEnvironmentVariable($"FeatureManagement:{feature6}:EnabledFor:0:Parameters:Recurrence:Pattern:Interval", "3"); Environment.SetEnvironmentVariable($"FeatureManagement:{feature6}:EnabledFor:0:Parameters:Recurrence:Range:Type", "NoEnd"); - IConfiguration config = new ConfigurationBuilder().AddEnvironmentVariables().Build(); + IConfiguration config = new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddJsonFile("appsettings.json") + .Build(); var serviceCollection = new ServiceCollection(); @@ -339,6 +342,7 @@ public async Task TimeWindow() Assert.False(await featureManager.IsEnabledAsync(feature4)); Assert.True(await featureManager.IsEnabledAsync(feature5)); Assert.False(await featureManager.IsEnabledAsync(feature6)); + Assert.True(await featureManager.IsEnabledAsync(Features.TimeWindowTestFeature)); } [Fact] @@ -349,7 +353,7 @@ public async Task Percentage() Environment.SetEnvironmentVariable($"FeatureManagement:{feature}:EnabledFor:0:Name", "Percentage"); Environment.SetEnvironmentVariable($"FeatureManagement:{feature}:EnabledFor:0:Parameters:Value", "50"); - IConfiguration config = new ConfigurationBuilder().AddEnvironmentVariables().Build(); + IConfiguration config = new ConfigurationBuilder().AddEnvironmentVariables().AddJsonFile("appsettings.json").Build(); var serviceCollection = new ServiceCollection(); diff --git a/tests/Tests.FeatureManagement/Features.cs b/tests/Tests.FeatureManagement/Features.cs index b52ec008..fba454af 100644 --- a/tests/Tests.FeatureManagement/Features.cs +++ b/tests/Tests.FeatureManagement/Features.cs @@ -5,6 +5,7 @@ namespace Tests.FeatureManagement { static class Features { + public const string TimeWindowTestFeature = "TimeWindowTestFeature"; public const string TargetingTestFeature = "TargetingTestFeature"; public const string TargetingTestFeatureWithExclusion = "TargetingTestFeatureWithExclusion"; public const string OnTestFeature = "OnTestFeature"; diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index 04d4d732..a7e1b140 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -4,6 +4,7 @@ using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Generic; +using System.Linq; using Xunit; namespace Tests.FeatureManagement @@ -109,42 +110,7 @@ public void GeneralRequiredParameterTest() } }, ParamName.Range, - ErrorMessage.RequiredParameter ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-25T02:00:00+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = null - }, - Range = new RecurrenceRange() - } - }, - ParamName.PatternType, - ErrorMessage.RequiredParameter ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-25T02:00:00+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = "Daily", - }, - Range = new RecurrenceRange() - { - Type = null - } - } - }, - ParamName.RangeType, - ErrorMessage.RequiredParameter ), + ErrorMessage.RequiredParameter ) }; ConsumeValidationTestData(testData); @@ -163,51 +129,9 @@ public void InvalidValueTest() { Pattern = new RecurrencePattern() { - Type = "" + Interval = 0 // Interval should be larger than 0. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } - } - }, - ParamName.PatternType, - ErrorMessage.UnrecognizableValue ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = "Daily" - }, - Range = new RecurrenceRange() - { - Type = "" - } - } - }, - ParamName.RangeType, - ErrorMessage.UnrecognizableValue ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = "Daily", - Interval = 0 - }, - Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.Interval, @@ -221,38 +145,14 @@ public void InvalidValueTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", - DaysOfWeek = new List(){ "Monday" }, - FirstDayOfWeek = "" + Type = RecurrencePatternType.AbsoluteMonthly, + DayOfMonth = 0, // DayOfMonth should be smaller than 32 and larger than 0. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - ParamName.FirstDayOfWeek, - ErrorMessage.UnrecognizableValue ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = "Weekly", - DaysOfWeek = new List(){ "day" } - }, - Range = new RecurrenceRange() - { - Type = "NoEnd" - } - } - }, - ParamName.DaysOfWeek, - ErrorMessage.UnrecognizableValue ), + ParamName.DayOfMonth, + ErrorMessage.OutOfRange ), ( new TimeWindowFilterSettings() { @@ -262,18 +162,14 @@ public void InvalidValueTest() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - Index = "", - DaysOfWeek = new List(){ "Friday" } + Type = RecurrencePatternType.AbsoluteMonthly, + DayOfMonth = 32, // DayOfMonth should be smaller than 32 and larger than 0. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - ParamName.Index, - ErrorMessage.UnrecognizableValue ), + ParamName.DayOfMonth, + ErrorMessage.OutOfRange ), ( new TimeWindowFilterSettings() { @@ -283,16 +179,14 @@ public void InvalidValueTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteMonthly", - DayOfMonth = 0, + Type = RecurrencePatternType.AbsoluteYearly, + DayOfMonth = 1, + Month = 0 // Month should be larger than 0 and smaller than 13. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - ParamName.DayOfMonth, + ParamName.Month, ErrorMessage.OutOfRange ), ( new TimeWindowFilterSettings() @@ -303,14 +197,11 @@ public void InvalidValueTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteYearly", + Type = RecurrencePatternType.AbsoluteYearly, DayOfMonth = 1, - Month = 0 + Month = 13 // Month should be larger than 0 and smaller than 13. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.Month, @@ -322,13 +213,9 @@ public void InvalidValueTest() End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { - Pattern = new RecurrencePattern() - { - Type = "Daily" - }, + Pattern = new RecurrencePattern(), Range = new RecurrenceRange() { - Type = "NoEnd", RecurrenceTimeZone = "" } } @@ -342,14 +229,11 @@ public void InvalidValueTest() End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { - Pattern = new RecurrencePattern() - { - Type = "Daily" - }, + Pattern = new RecurrencePattern(), Range = new RecurrenceRange() { - Type = "Numbered", - NumberOfOccurrences = 0 + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 0 // NumberOfOccurrences should be larger than 0. } } }, @@ -368,17 +252,11 @@ public void InvalidTimeWindowTest() ( new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-25T12:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-25T12:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-25T12:00:00+08:00"), // End equals to Start. Recurrence = new Recurrence() { - Pattern = new RecurrencePattern() - { - Type = "Daily" - }, + Pattern = new RecurrencePattern(), Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.End, @@ -387,18 +265,15 @@ public void InvalidTimeWindowTest() ( new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-27T00:00:01+08:00"), + End = DateTimeOffset.Parse("2023-9-27T00:00:01+08:00"), // The duration of the time window is longer than how frequently it recurs. Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "Daily", + Type = RecurrencePatternType.Daily, Interval = 2 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.End, @@ -407,19 +282,16 @@ public void InvalidTimeWindowTest() ( new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), + End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), // The duration of the time window is longer than how frequently it recurs. Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "Weekly", + Type = RecurrencePatternType.Weekly, Interval = 1, - DaysOfWeek = new List(){ "Friday" } + DaysOfWeek = new List(){ DayOfWeek.Friday } // 2023.9.1 is Friday. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.End, @@ -428,19 +300,16 @@ public void InvalidTimeWindowTest() ( new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-5T00:00:01+08:00"), + End = DateTimeOffset.Parse("2023-9-5T00:00:01+08:00"), // The duration of the time window is longer than how frequently it recurs. Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "Weekly", + Type = RecurrencePatternType.Weekly, Interval = 1, - DaysOfWeek = new List(){ "Monday", "Thursday", "Sunday" } + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Thursday, DayOfWeek.Sunday } }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.End, @@ -448,20 +317,17 @@ public void InvalidTimeWindowTest() ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-2-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-3-29T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-2-1T00:00:00+08:00"), // The duration of the time window is longer than how frequently it recurs. + End = DateTimeOffset.Parse("2023-3-29T00:00:01+08:00"), // This behavior is the same as the Outlook. Outlook uses 28 days as a monthly interval. Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "AbsoluteMonthly", + Type = RecurrencePatternType.AbsoluteMonthly, Interval = 2, DayOfMonth = 1 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.End, @@ -469,20 +335,17 @@ public void InvalidTimeWindowTest() ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // The duration of the time window is longer than how frequently it recurs. + End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), // This behavior is the same as the Outlook. Outlook uses 28 days as a monthly interval. Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", + Type = RecurrencePatternType.RelativeMonthly, Interval = 1, - DaysOfWeek = new List(){ "Friday" } + DaysOfWeek = new List(){ DayOfWeek.Friday } // 2023.9.1 is Friday. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.End, @@ -490,21 +353,18 @@ public void InvalidTimeWindowTest() ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2024-9-1T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // The duration of the time window is longer than how frequently it recurs. + End = DateTimeOffset.Parse("2024-9-1T00:00:01+08:00"), // This behavior is the same as the Outlook. Outlook uses 365 days as a yearly interval. Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "AbsoluteYearly", + Type = RecurrencePatternType.AbsoluteYearly, Interval = 1, DayOfMonth = 1, Month = 9 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.End, @@ -512,21 +372,18 @@ public void InvalidTimeWindowTest() ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2024-9-1T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // The duration of the time window is longer than how frequently it recurs. + End = DateTimeOffset.Parse("2024-9-1T00:00:01+08:00"), // This behavior is the same as the Outlook. Outlook uses 365 days as a yearly interval. Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", + Type = RecurrencePatternType.RelativeYearly, Interval = 1, - DaysOfWeek = new List(){ "Friday" }, + DaysOfWeek = new List(){ DayOfWeek.Friday }, // 2023.9.1 is Friday. Month = 9 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.End, @@ -538,14 +395,11 @@ public void InvalidTimeWindowTest() End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { - Pattern = new RecurrencePattern() - { - Type = "Daily" - }, + Pattern = new RecurrencePattern(), Range = new RecurrenceRange() { - Type = "EndDate", - EndDate = DateTimeOffset.Parse("2023-8-31T00:00:00+08:00") + Type = RecurrenceRangeType.EndDate, + EndDate = DateTimeOffset.Parse("2023-8-31T00:00:00+08:00") // EndDate is earlier than the Start } } }, @@ -554,19 +408,16 @@ public void InvalidTimeWindowTest() ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T23:00:00+00:00"), + Start = DateTimeOffset.Parse("2023-9-1T23:00:00+00:00"), // 2023-9-2 under the RecurrenceTimeZone End = DateTimeOffset.Parse("2023-9-1T23:00:01+00:00"), Recurrence = new Recurrence() { - Pattern = new RecurrencePattern() - { - Type = "Daily" - }, + Pattern = new RecurrencePattern(), Range = new RecurrenceRange() { - Type = "EndDate", - EndDate = DateTimeOffset.Parse("2023-9-1"), - RecurrenceTimeZone = "UTC+08:00" + Type = RecurrenceRangeType.EndDate, + EndDate = DateTimeOffset.Parse("2023-9-1"), // EndDate is earlier than the Start + RecurrenceTimeZone = "UTC+08:00" // All date time in the recurrence settings will be aligned to the RecurrenceTimeZone } } }, @@ -590,13 +441,10 @@ public void WeeklyPatternRequiredParameterTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", - DaysOfWeek = null + Type = RecurrencePatternType.Weekly, + DaysOfWeek = Enumerable.Empty() }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.DaysOfWeek, @@ -613,19 +461,16 @@ public void WeeklyPatternNotMatchTest() { ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 2023-9-1 is Friday. Start date is not a valid first occurrence. End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "Weekly", - DaysOfWeek = new List{ "Monday", "Tuesday", "Wednesday", "Thursday", "Saturday", "Sunday" } + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List{ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Saturday, DayOfWeek.Sunday } }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.Start, @@ -633,18 +478,17 @@ public void WeeklyPatternNotMatchTest() ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Start (2023-8-31T23:00:00+07:00) is Thursday under the RecurrenceTimeZone. Start date is not a valid first occurrence. End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "Weekly", - DaysOfWeek = new List{ "Friday" } + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List{ DayOfWeek.Friday } }, Range = new RecurrenceRange() { - Type = "NoEnd", RecurrenceTimeZone = "UTC+07:00" } } @@ -669,12 +513,9 @@ public void AbsoluteMonthlyPatternRequiredParameterTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteMonthly", + Type = RecurrencePatternType.AbsoluteMonthly, }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.DayOfMonth, @@ -691,19 +532,16 @@ public void AbsoluteMonthlyPatternNotMatchTest() { ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), // Start date is not a valid first occurrence. End = DateTimeOffset.Parse("2023-9-2T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "AbsoluteMonthly", + Type = RecurrencePatternType.AbsoluteMonthly, DayOfMonth = 1 }, Range = new RecurrenceRange() - { - Type = "NoEnd", - } } }, ParamName.Start, @@ -726,37 +564,13 @@ public void RelativeMonthlyPatternRequiredParameterTest() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = Enumerable.Empty() }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.DaysOfWeek, - ErrorMessage.RequiredParameter ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = "RelativeMonthly", - DaysOfWeek = new List(){ "Friday" }, - Index = null - }, - Range = new RecurrenceRange() - { - Type = "NoEnd" - } - } - }, - ParamName.Index, ErrorMessage.RequiredParameter ) }; @@ -770,20 +584,17 @@ public void RelativeMonthlyPatternNotMatchTest() { ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Start date is the 1st Friday in 2023 Sep, not a valid first occurrence. End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List{ "Friday" }, - Index = "Second" + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List{ DayOfWeek.Friday }, + Index = WeekIndex.Second }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.Start, @@ -806,13 +617,10 @@ public void AbsoluteYearlyPatternRequiredParameterTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteYearly", + Type = RecurrencePatternType.AbsoluteYearly, DayOfMonth = 1 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.Month, @@ -826,13 +634,10 @@ public void AbsoluteYearlyPatternRequiredParameterTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteYearly", + Type = RecurrencePatternType.AbsoluteYearly, Month = 9 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.DayOfMonth, @@ -849,20 +654,17 @@ public void AbsoluteYearlyPatternNotMatchTest() { ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Start date is not a valid first occurrence. End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "AbsoluteYearly", + Type = RecurrencePatternType.AbsoluteYearly, DayOfMonth = 1, Month = 8 }, Range = new RecurrenceRange() - { - Type = "NoEnd", - } } }, ParamName.Start, @@ -885,14 +687,11 @@ public void RelativeYearlyPatternRequiredParameterTest() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = null, + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = Enumerable.Empty(), Month = 9 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.DaysOfWeek, @@ -906,38 +705,13 @@ public void RelativeYearlyPatternRequiredParameterTest() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = new List{ "Friday" } + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List{ DayOfWeek.Friday } }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.Month, - ErrorMessage.RequiredParameter ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = "RelativeYearly", - DaysOfWeek = new List{ "Friday" }, - Month = 9, - Index = null - }, - Range = new RecurrenceRange() - { - Type = "NoEnd" - } - } - }, - ParamName.Index, ErrorMessage.RequiredParameter ) }; @@ -951,20 +725,17 @@ public void RelativeYearlyPatternNotMatchTest() { ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Start date is the 1st Friday in 2023 Sep, not a valid first occurrence. End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = new List{ "Friday" }, + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List{ DayOfWeek.Friday }, Month = 8 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.Start, @@ -972,21 +743,18 @@ public void RelativeYearlyPatternNotMatchTest() ( new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Start date is the 1st Friday in 2023 Sep, not a valid first occurrence. End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = new List{ "Friday" }, + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List{ DayOfWeek.Friday }, Month = 9, - Index = "Second" + Index = WeekIndex.Second }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, ParamName.Start, @@ -1010,12 +778,9 @@ public void MatchDailyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Daily" + Type = RecurrencePatternType.Daily }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), @@ -1029,18 +794,15 @@ public void MatchDailyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Daily", + Type = RecurrencePatternType.Daily, Interval = 2 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, false ), - ( DateTimeOffset.Parse("2023-9-5T00:00:00+08:00"), + ( DateTimeOffset.Parse("2023-9-5T00:00:00+08:00"), // Within the recurring time window 2023-9-5T00:00:00+08:00 ~ 2023-9-7T00:00:00+08:00. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1049,18 +811,15 @@ public void MatchDailyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Daily", + Type = RecurrencePatternType.Daily, Interval = 4 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2023-9-6T00:00:00+08:00"), + ( DateTimeOffset.Parse("2023-9-6T00:00:00+08:00"), // Within the recurring time window 2023-9-5T00:00:00+08:00 ~ 2023-9-7T00:00:00+08:00. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1069,18 +828,15 @@ public void MatchDailyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Daily", + Type = RecurrencePatternType.Daily, Interval = 4 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2023-9-9T00:00:00+08:00"), + ( DateTimeOffset.Parse("2023-9-9T00:00:00+08:00"), // Within the recurring time window 2023-9-9T00:00:00+08:00 ~ 2023-9-11T00:00:00+08:00. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1089,18 +845,15 @@ public void MatchDailyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Daily", + Type = RecurrencePatternType.Daily, Interval = 4 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), + ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Within the recurring time window 2023-9-3T00:00:00+08:00 ~ 2023-9-31T00:00:01+08:00. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1109,18 +862,15 @@ public void MatchDailyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Daily", + Type = RecurrencePatternType.Daily, Interval = 2 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), + ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // The third occurrence. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1129,18 +879,18 @@ public void MatchDailyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Daily" + Type = RecurrencePatternType.Daily }, Range = new RecurrenceRange() { - Type = "Numbered", + Type = RecurrenceRangeType.Numbered, NumberOfOccurrences = 2 } } }, false ), - ( DateTimeOffset.Parse("2023-9-2T17:00:00+00:00"), + ( DateTimeOffset.Parse("2023-9-2T17:00:00+00:00"), // 2023-9-3T01:00:00+08:00 under the RecurrenceTimeZone, which is beyond the EndDate new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T17:00:00+00:00"), @@ -1149,11 +899,11 @@ public void MatchDailyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Daily" + Type = RecurrencePatternType.Daily }, Range = new RecurrenceRange() { - Type = "EndDate", + Type = RecurrenceRangeType.EndDate, EndDate = DateTimeOffset.Parse("2023-9-2"), RecurrenceTimeZone = "UTC+08:00" } @@ -1170,7 +920,7 @@ public void MatchWeeklyRecurrenceTest() { var testData = new List>() { - ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday @@ -1179,18 +929,16 @@ public void MatchWeeklyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", - DaysOfWeek = new List(){ "Monday", "Friday" } + Type = RecurrencePatternType.Weekly, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // Friday in the 2nd week + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // Friday in the 2nd week after the Start date new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday @@ -1199,19 +947,16 @@ public void MatchWeeklyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", + Type = RecurrencePatternType.Weekly, Interval = 2, - DaysOfWeek = new List(){ "Friday" } + DaysOfWeek = new List(){ DayOfWeek.Friday } }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, false ), - ( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // Friday in the 3rd week + ( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // Friday in the 3rd week after the Start date new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday @@ -1220,63 +965,54 @@ public void MatchWeeklyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", + Type = RecurrencePatternType.Weekly, Interval = 2, - DaysOfWeek = new List(){ "Friday" } + DaysOfWeek = new List(){ DayOfWeek.Friday } }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday is not included in DaysOfWeek new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "Weekly", - Interval = 2, - FirstDayOfWeek = "Monday", - DaysOfWeek = new List(){ "Monday", "Sunday" } + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, false ), - ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 1st week + ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), // The 2nd occurrence. new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "Weekly", - Interval = 2, - FirstDayOfWeek = "Sunday", - DaysOfWeek = new List(){ "Monday", "Sunday" } + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } }, Range = new RecurrenceRange() { - Type = "NoEnd" + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 1 } } }, - true ), + false ), - ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday @@ -1285,18 +1021,19 @@ public void MatchWeeklyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", - DaysOfWeek = new List(){ "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" } + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } }, Range = new RecurrenceRange() { - Type = "NoEnd" + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 7 } } }, false ), - ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), // Saturday in the 1st week + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday @@ -1305,19 +1042,38 @@ public void MatchWeeklyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", - DaysOfWeek = new List(){ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" } + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } }, Range = new RecurrenceRange() { - Type = "Numbered", - NumberOfOccurrences = 1 + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 8 } } }, + true ), + + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 is the last day of the 1st week after the Start date + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + } + }, false ), - ( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 4th week + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 1st week after the Start date new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday @@ -1326,20 +1082,36 @@ public void MatchWeeklyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", + Type = RecurrencePatternType.Weekly, Interval = 2, - FirstDayOfWeek = "Monday", - DaysOfWeek = new List(){ "Monday", "Sunday" } + // FirstDayOfWeek is Sunday by default, 2023-9-3 ~ 2023-9-9 is the 1st week after the Start date + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } }, Range = new RecurrenceRange() + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 4th week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() { - Type = "NoEnd" - } + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 1st week, 9-4 ~ 9-10 2nd week (Skipped), 9-11 ~ 9-17 3rd week, 9-18 ~ 9-24 4th week (Skipped) + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() } }, false ), - ( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 3rd week + ( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 3rd week after the Start date new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday @@ -1348,20 +1120,17 @@ public void MatchWeeklyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", + Type = RecurrencePatternType.Weekly, Interval = 2, - FirstDayOfWeek = "Sunday", - DaysOfWeek = new List(){ "Monday", "Sunday" } + // FirstDayOfWeek is Sunday by default, 2023-9-3 ~ 9-9 1st week, 9-17 ~ 9-23 3rd week + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2023-9-17T00:00:00+08:00"), // Sunday in the 3rd week + ( DateTimeOffset.Parse("2023-9-17T00:00:00+08:00"), // Sunday in the 3rd week after the Start date new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday @@ -1370,21 +1139,21 @@ public void MatchWeeklyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", + Type = RecurrencePatternType.Weekly, Interval = 2, - FirstDayOfWeek = "Monday", - DaysOfWeek = new List(){ "Monday", "Sunday" } + FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 1st week, 9-11 ~ 9-17 3rd week + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // 2023-9-3, 9-11. 9-17 }, Range = new RecurrenceRange() { - Type = "Numbered", + Type = RecurrenceRangeType.Numbered, NumberOfOccurrences = 3 } } }, true ), - ( DateTimeOffset.Parse("2023-9-13T00:00:00+08:00"), // Wednesday in the 3rd week + ( DateTimeOffset.Parse("2023-9-13T00:00:00+08:00"), // Within the recurring time window 2023-9-11T:00:00:00+08:00 ~ 2023-9-15T:00:00:00+08:00. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday @@ -1393,20 +1162,17 @@ public void MatchWeeklyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", + Type = RecurrencePatternType.Weekly, Interval = 2, - FirstDayOfWeek = "Monday", - DaysOfWeek = new List(){ "Monday", "Sunday" } + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // Tuesday in the 4th week + ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrence: 2023-9-17T:00:00:00+08:00 ~ 2023-9-21T:00:00:00+08:00. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday @@ -1415,21 +1181,21 @@ public void MatchWeeklyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", + Type = RecurrencePatternType.Weekly, Interval = 2, - FirstDayOfWeek = "Monday", - DaysOfWeek = new List(){ "Monday", "Sunday" } + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) }, Range = new RecurrenceRange() { - Type = "Numbered", + Type = RecurrenceRangeType.Numbered, NumberOfOccurrences = 3 } } }, true ), - ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // Tuesday in the 4th week + ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrences new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday @@ -1438,14 +1204,14 @@ public void MatchWeeklyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "Weekly", + Type = RecurrencePatternType.Weekly, Interval = 2, - FirstDayOfWeek = "Monday", - DaysOfWeek = new List(){ "Monday", "Sunday" } + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) }, Range = new RecurrenceRange() { - Type = "Numbered", + Type = RecurrenceRangeType.Numbered, NumberOfOccurrences = 2 } } @@ -1470,13 +1236,10 @@ public void MatchAbsoluteMonthlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteMonthly", + Type = RecurrencePatternType.AbsoluteMonthly, DayOfMonth = 1 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), @@ -1490,14 +1253,29 @@ public void MatchAbsoluteMonthlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteMonthly", + Type = RecurrencePatternType.AbsoluteMonthly, DayOfMonth = 1, - Interval = 5 + Interval = 5 // 2023-9-1, 2024-2-1, 2024-7-1 ... }, Range = new RecurrenceRange() + } + }, + true ), + + ( DateTimeOffset.Parse("2024-7-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() { - Type = "NoEnd" - } + Type = RecurrencePatternType.AbsoluteMonthly, + DayOfMonth = 1, + Interval = 5 // 2023-9-1, 2024-2-1, 2024-7-1 ... + }, + Range = new RecurrenceRange() } }, true ), @@ -1511,19 +1289,16 @@ public void MatchAbsoluteMonthlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteMonthly", + Type = RecurrencePatternType.AbsoluteMonthly, DayOfMonth = 1, - Interval = 5 + Interval = 5 // 2023-9-1, 2024-2-1, 2024-7-1 ... }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, false ), - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), + ( DateTimeOffset.Parse("2024-8-1T00:00:00+08:00"), new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1532,39 +1307,36 @@ public void MatchAbsoluteMonthlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteMonthly", + Type = RecurrencePatternType.AbsoluteMonthly, DayOfMonth = 1, - Interval = 4 + Interval = 5 // 2023-9-1, 2024-2-1, 2024-7-1 ... }, Range = new RecurrenceRange() - { - Type = "Numbered", - NumberOfOccurrences = 3 - } } }, false ), - ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 4th occurrence. new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-4-29T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-4-29T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "AbsoluteMonthly", - DayOfMonth = 29, - Interval = 2 + Type = RecurrencePatternType.AbsoluteMonthly, + DayOfMonth = 1, + Interval = 4 // 2023-9-1, 2024-1-1, 2024-5-1, 2024-9-1 ... }, Range = new RecurrenceRange() { - Type = "NoEnd" + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 3 } } }, - true ), + false ), ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1575,39 +1347,40 @@ public void MatchAbsoluteMonthlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteMonthly", + Type = RecurrencePatternType.AbsoluteMonthly, DayOfMonth = 29, Interval = 2 }, Range = new RecurrenceRange() { - Type = "EndDate", + Type = RecurrenceRangeType.EndDate, EndDate = DateTimeOffset.Parse("2024-2-29") } } }, true ), - ( DateTimeOffset.Parse("2023-10-29T00:00:00+08:00"), + ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-4-29T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-4-29T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "AbsoluteMonthly", + Type = RecurrencePatternType.AbsoluteMonthly, DayOfMonth = 29, + Interval = 2 }, Range = new RecurrenceRange() { - Type = "EndDate", - EndDate = DateTimeOffset.Parse("2023-10-28") + Type = RecurrenceRangeType.EndDate, + EndDate = DateTimeOffset.Parse("2024-2-28") } } }, - false ) + false ), }; ConsumeEvalutationTestData(testData); @@ -1618,150 +1391,130 @@ public void MatchRelativeMonthlyRecurrenceTest() { var testData = new List>() { - ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 2nd Friday in 2023 Sep + ( DateTimeOffset.Parse("2023-10-6T00:00:00+08:00"), // 1st Friday in 2023 Oct new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" } + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday }, + // Index is First by default. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - false ), + true ), - ( DateTimeOffset.Parse("2023-10-13T00:00:00+08:00"), // 2nd Friday in 2023 Oct + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 2nd Friday in 2023 Sep new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" }, - Index = "Second" + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday } + // Index is First by default. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - true ), + false ), ( DateTimeOffset.Parse("2023-10-13T00:00:00+08:00"), // 2nd Friday in 2023 Oct new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 1st Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" }, - Index = "Second", - Interval = 3 + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday }, + Index = WeekIndex.Second }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - false ), + true ), - ( DateTimeOffset.Parse("2023-12-8T00:00:00+08:00"), // 2nd Friday in 2023 Dec + ( DateTimeOffset.Parse("2023-10-13T00:00:00+08:00"), // 2nd Friday in 2023 Oct new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 1st Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" }, - Index = "Second", - Interval = 3 + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday }, + Index = WeekIndex.Second, + Interval = 3 // 2023-9, 2023-12 ... }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - true ), + false ), - ( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // 3rd Friday in 2023 Sep + ( DateTimeOffset.Parse("2023-12-8T00:00:00+08:00"), // 2nd Friday in 2023 Dec new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 2nd Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" }, - Index = "Second" + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday }, + Index = WeekIndex.Second, + Interval = 3 // 2023-9, 2023-12 ... }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - false ), + true ), - ( DateTimeOffset.Parse("2023-10-6T00:00:00+08:00"), // 1st Friday in 2023 Oct + ( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // 3rd Friday in 2023 Sep new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 2nd Friday in 2023 Sep + End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" }, - Index = "First" + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday }, + Index = WeekIndex.Second }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - true ), + false ), ( DateTimeOffset.Parse("2023-10-27T00:00:00+08:00"), // 4th Friday in 2023 Oct new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), // 2nd Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" }, - Index = "Last" + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday }, + Index = WeekIndex.Last }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), @@ -1769,20 +1522,17 @@ public void MatchRelativeMonthlyRecurrenceTest() ( DateTimeOffset.Parse("2023-11-24T00:00:00+08:00"), // 4th Friday in 2023 Nov new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), // 5th Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" }, - Index = "Last" + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday }, + Index = WeekIndex.Last }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), @@ -1790,20 +1540,17 @@ public void MatchRelativeMonthlyRecurrenceTest() ( DateTimeOffset.Parse("2023-12-29T00:00:00+08:00"), // 5th Friday in 2023 Dec new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), // 5th Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" }, - Index = "Last" + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday }, + Index = WeekIndex.Last }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), @@ -1811,20 +1558,17 @@ public void MatchRelativeMonthlyRecurrenceTest() ( DateTimeOffset.Parse("2023-10-29T00:00:00+08:00"), // 5th Sunday in 2023 Oct new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), // 4th Monday in 2023 Sep End = DateTimeOffset.Parse("2023-9-25T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Sunday", "Monday" }, - Index = "Last" + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Sunday, DayOfWeek.Monday }, + Index = WeekIndex.Last }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), @@ -1832,20 +1576,17 @@ public void MatchRelativeMonthlyRecurrenceTest() ( DateTimeOffset.Parse("2023-10-30T00:00:00+08:00"), // 5th Monday in 2023 Oct new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), // 4th Monday in 2023 Sep End = DateTimeOffset.Parse("2023-9-25T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Sunday", "Monday" }, - Index = "Last" + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Sunday, DayOfWeek.Monday }, + Index = WeekIndex.Last }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, false ), @@ -1853,21 +1594,18 @@ public void MatchRelativeMonthlyRecurrenceTest() ( DateTimeOffset.Parse("2023-10-6T00:00:00+08:00"), // 1st Friday in 2023 Oct new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" }, - Index = "First", + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday }, + // Index is First by default. Interval = 3 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, false ), @@ -1875,20 +1613,18 @@ public void MatchRelativeMonthlyRecurrenceTest() ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // 1st Friday in 2023 Dec new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday" }, + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday }, + // Index is First by default. Interval = 3 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), @@ -1896,20 +1632,17 @@ public void MatchRelativeMonthlyRecurrenceTest() ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // 1st Monday in 2023 Oct new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday", "Monday" }, - Index = "First" + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday }, + // Index is First by default. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), @@ -1917,63 +1650,35 @@ public void MatchRelativeMonthlyRecurrenceTest() ( DateTimeOffset.Parse("2023-10-6T00:00:00+08:00"), // 1st Friday in 2023 Oct new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday", "Monday" } + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday } }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - false ), - - ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // 1st Monday in 2023 Oct - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday", "Monday" }, - Index = "First", - Interval = 2 - }, - Range = new RecurrenceRange() - { - Type = "NoEnd" - } - } - }, - false ), + false ), // The time window will only occur on either 1st Monday or 1st Friday, the 1st Monday of 2023 Oct is 10.2 ( DateTimeOffset.Parse("2023-11-3T00:00:00+08:00"), // 1st Friday in 2023 Nov new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday", "Monday" }, - Index = "First", + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday }, + // Index is First by default. Interval = 2 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), @@ -1981,24 +1686,21 @@ public void MatchRelativeMonthlyRecurrenceTest() ( DateTimeOffset.Parse("2023-11-6T00:00:00+08:00"), // 1st Monday in 2023 Nov new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Friday", "Monday" }, - Index = "First", + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday }, + // Index is First by default. Interval = 2 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - false ), + false ), // The time window will only occur on either 1st Monday or 1st Friday, the 1st Monday of 2023 Nov is 11.3 ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // the first day of the month new TimeWindowFilterSettings() @@ -2009,14 +1711,12 @@ public void MatchRelativeMonthlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, + // Index is First by default. Interval = 3 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), @@ -2030,19 +1730,17 @@ public void MatchRelativeMonthlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, - Interval = 3 + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, + // Index is First by default. + Interval = 3 // 2023-9, 2023-12, 2024-3 ... }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), + ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // The 4th occurrence. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2051,19 +1749,20 @@ public void MatchRelativeMonthlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + // Index is First by default. }, Range = new RecurrenceRange() { - Type = "Numbered", + Type = RecurrenceRangeType.Numbered, NumberOfOccurrences = 3 } } }, false ), - ( DateTimeOffset.Parse("2023-10-1T00:00:00+08:00"), // Sunday + ( DateTimeOffset.Parse("2023-10-1T00:00:00+08:00"), // Sunday is not included in the DaysOfWeek new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2072,18 +1771,16 @@ public void MatchRelativeMonthlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }, + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday} + // Index is First by default. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, false ), - ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // Monday + ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // 1st Monday new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2092,18 +1789,16 @@ public void MatchRelativeMonthlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }, + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday} + // Index is First by default. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - true ), + true ), // 2023-10-1 is Sunday which is not included in the DaysOfWeek. - ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // Monday + ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // 1st Monday, new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2112,16 +1807,14 @@ public void MatchRelativeMonthlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeMonthly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + // Index is First by default. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - false ) + false ) // The time window will occur on 2023-10-1 }; ConsumeEvalutationTestData(testData); @@ -2141,19 +1834,16 @@ public void MatchAbsoluteYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteYearly", + Type = RecurrencePatternType.AbsoluteYearly, DayOfMonth = 1, Month = 9 }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), + ( DateTimeOffset.Parse("2024-10-1T00:00:00+08:00"), new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2162,20 +1852,16 @@ public void MatchAbsoluteYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteYearly", + Type = RecurrencePatternType.AbsoluteYearly, DayOfMonth = 1, Month = 9 }, Range = new RecurrenceRange() - { - Type = "Numbered", - NumberOfOccurrences = 1 - } } }, false ), - ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 2nd occurrence. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2184,21 +1870,20 @@ public void MatchAbsoluteYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteYearly", + Type = RecurrencePatternType.AbsoluteYearly, DayOfMonth = 1, - Month = 9, - Interval = 3 + Month = 9 }, Range = new RecurrenceRange() { - Type = "Numbered", - NumberOfOccurrences = 2 + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 1 } } }, - true ), + false ), - ( DateTimeOffset.Parse("2029-9-1T00:00:00+08:00"), + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 2nd occurrence. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2207,21 +1892,21 @@ public void MatchAbsoluteYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteYearly", + Type = RecurrencePatternType.AbsoluteYearly, DayOfMonth = 1, Month = 9, - Interval = 3 + Interval = 3 // 2023, 2026, ... }, Range = new RecurrenceRange() { - Type = "Numbered", + Type = RecurrenceRangeType.Numbered, NumberOfOccurrences = 2 } } }, - false ), + true ), - ( DateTimeOffset.Parse("2024-10-1T00:00:00+08:00"), + ( DateTimeOffset.Parse("2029-9-1T00:00:00+08:00"), // The 3rd occurrence. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2230,13 +1915,15 @@ public void MatchAbsoluteYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "AbsoluteYearly", + Type = RecurrencePatternType.AbsoluteYearly, DayOfMonth = 1, - Month = 9 + Month = 9, + Interval = 3 // 2023, 2026, 2029 ... }, Range = new RecurrenceRange() { - Type = "NoEnd" + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 2 } } }, @@ -2251,7 +1938,7 @@ public void MatchRelativeYearlyRecurrenceTest() { var testData = new List>() { - ( DateTimeOffset.Parse("2024-9-6T00:00:00+08:00"), // 1st Friday + ( DateTimeOffset.Parse("2024-9-6T00:00:00+08:00"), // 1st Friday in 2024 Sep new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2260,19 +1947,17 @@ public void MatchRelativeYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = new List() { "Friday" }, + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List() { DayOfWeek.Friday }, Month = 9 + // Index is First by default. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // 1st Sunday + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // 1st Sunday in 2024 Sep, Sunday is not included in the DaysOfWeek. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2281,19 +1966,17 @@ public void MatchRelativeYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }, + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday }, Month = 9 + // Index is First by default. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, false ), - - ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), + + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2302,19 +1985,17 @@ public void MatchRelativeYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, Month = 9 + // Index is First by default. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - false ), + true ), - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // the first day of Sep + ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2323,17 +2004,15 @@ public void MatchRelativeYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, Month = 9 + // Index is First by default. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, - true ), + false ), ( DateTimeOffset.Parse("2024-10-1T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -2344,14 +2023,12 @@ public void MatchRelativeYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, Month = 9 + // Index is First by default. }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, false ), @@ -2365,15 +2042,12 @@ public void MatchRelativeYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, Month = 9, - Interval = 2 + Interval = 2 // 2023, 2025 ... }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, false ), @@ -2387,20 +2061,17 @@ public void MatchRelativeYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, Month = 9, - Interval = 3 + Interval = 3 // 2023, 2026 ... }, Range = new RecurrenceRange() - { - Type = "NoEnd" - } } }, true ), - ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 4th occurrence. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2409,13 +2080,13 @@ public void MatchRelativeYearlyRecurrenceTest() { Pattern = new RecurrencePattern() { - Type = "RelativeYearly", - DaysOfWeek = new List() { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, Month = 9 }, Range = new RecurrenceRange() { - Type = "Numbered", + Type = RecurrenceRangeType.Numbered, NumberOfOccurrences = 3 } } diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index 77aa687e..f84e8ad0 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -18,7 +18,35 @@ { "Name": "Percentage", "Parameters": { - "Value": 100 + "Value": 100 + } + } + ] + }, + "TimeWindowTestFeature": { + "EnabledFor": [ + { + "Name": "TimeWindow", + "Parameters": { + "Start": "Sun, 14 Jan 2024 00:00:00 GMT", + "End": "Mon, 15 Jan 2024 00:00:00 GMT", + "Recurrence": { + "Pattern": { + "Type": "Weekly", + "DaysOfWeek": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ] + }, + "Range": { + "Type": "NoEnd" + } + } } } ] From ba89364819b6f25ed8e89c99b910934f55f9c91f Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 16 Jan 2024 16:46:15 +0800 Subject: [PATCH 10/52] fix bug --- .../Recurrence/RecurrenceEvaluator.cs | 1248 +++++++++-------- .../RecurrenceEvaluator.cs | 106 +- 2 files changed, 716 insertions(+), 638 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 0d32c29e..41417c0f 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -3,6 +3,7 @@ // using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Microsoft.FeatureManagement.FeatureFilters @@ -13,12 +14,12 @@ static class RecurrenceEvaluator // Error Message const string OutOfRange = "The value is out of the accepted range."; const string UnrecognizableValue = "The value is unrecognizable."; - const string RequiredParameter = "Value cannot be null."; + const string RequiredParameter = "Value cannot be null or empty."; const string NotMatched = "Start date is not a valid first occurrence."; - const int WeekDayNumber = 7; - const int MinMonthDayNumber = 28; - const int MinYearDayNumber = 365; + const int DayNumberOfWeek = 7; + const int MinDayNumberOfMonth = 28; + const int MinDayNumberOfYear = 365; /// /// Checks if a provided timestamp is within any recurring time window specified by the Recurrence section in the time window filter settings. @@ -34,7 +35,7 @@ public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings throw new ArgumentNullException(nameof(settings)); } - if (!TryValidateSettings(settings, out string paramName, out string reason)) + if (!TryValidateRecurrenceSettings(settings, out string paramName, out string reason)) { throw new ArgumentException(reason, paramName); } @@ -44,6 +45,7 @@ public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings return false; } + // time is before the first occurrence or time is beyond the end of the recurrence range if (!TryGetPreviousOccurrence(time, settings, out DateTimeOffset previousOccurrence)) { return false; @@ -57,404 +59,20 @@ public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings return false; } - /// - /// Try to find the closest previous recurrence occurrence before the provided time stamp according to the recurrence pattern. - /// A time stamp. - /// The settings of time window filter. - /// The closest previous occurrence. - /// True if the closest previous occurrence is within the recurrence range, false otherwise. - /// - private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence) - { - previousOccurrence = DateTimeOffset.MaxValue; - - DateTimeOffset start = settings.Start.Value; - - if (time < start) - { - return false; - } - - int numberOfOccurrences; - - switch (settings.Recurrence.Pattern.Type) - { - case RecurrencePatternType.Daily: - FindDailyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - - break; - - case RecurrencePatternType.Weekly: - FindWeeklyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - - break; - - case RecurrencePatternType.AbsoluteMonthly: - FindAbsoluteMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - - break; - - case RecurrencePatternType.RelativeMonthly: - FindRelativeMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - - break; - - case RecurrencePatternType.AbsoluteYearly: - FindAbsoluteYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - - break; - - case RecurrencePatternType.RelativeYearly: - FindRelativeYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - - break; - - default: - return false; - } - - RecurrenceRange range = settings.Recurrence.Range; - - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - - if (range.Type == RecurrenceRangeType.EndDate) - { - DateTime alignedPreviousOccurrence = previousOccurrence.DateTime + timeZoneOffset - previousOccurrence.Offset; - - return alignedPreviousOccurrence.Date <= range.EndDate.Value.Date; - } - - if (range.Type == RecurrenceRangeType.Numbered) { - return numberOfOccurrences < range.NumberOfOccurrences; - } - - return true; - } - - /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "Daily" recurrence pattern. - /// A time stamp. - /// The settings of time window filter. - /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. - /// - private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - DateTimeOffset start = settings.Start.Value; - - int interval = pattern.Interval; - - TimeSpan timeGap = time - start; - - // - // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. - int numberOfInterval = (int)Math.Floor(timeGap.TotalSeconds / TimeSpan.FromDays(interval).TotalSeconds); - - previousOccurrence = start.AddDays(numberOfInterval * interval); - - numberOfOccurrences = numberOfInterval; - } - - /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "Weekly" recurrence pattern. - /// A time stamp. - /// The settings of time window filter. - /// The closest previous occurrence. - /// The number of recurring days of week which have occurred between the time and the recurrence start. - /// - private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) - { - previousOccurrence = DateTimeOffset.MaxValue; - - numberOfOccurrences = 0; - - RecurrencePattern pattern = settings.Recurrence.Pattern; - - DateTimeOffset start = settings.Start.Value; - - int interval = pattern.Interval; - - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; - - TimeSpan timeGap = time - start; - - int remainingDaysOfFirstWeek = RemainingDaysOfWeek(alignedStart.DayOfWeek, pattern.FirstDayOfWeek); - - TimeSpan remainingTimeOfFirstInterval = TimeSpan.FromDays(remainingDaysOfFirstWeek) - alignedStart.TimeOfDay + TimeSpan.FromDays((interval - 1) * 7); - - if (remainingTimeOfFirstInterval <= timeGap) - { - int numberOfInterval = (int) Math.Floor((timeGap - remainingTimeOfFirstInterval).TotalSeconds / TimeSpan.FromDays(interval * 7).TotalSeconds); - - previousOccurrence = start.AddDays(numberOfInterval * interval * 7 + remainingDaysOfFirstWeek + (interval - 1) * 7); - - numberOfOccurrences += numberOfInterval * pattern.DaysOfWeek.Count(); - - // - // Add the occurrences in the first week - numberOfOccurrences += 1; - - DateTime dateTime = alignedStart.AddDays(1); - - while (dateTime.DayOfWeek != pattern.FirstDayOfWeek) - { - if (pattern.DaysOfWeek.Any(day => - day == dateTime.DayOfWeek)) - { - numberOfOccurrences += 1; - } - - dateTime = dateTime.AddDays(1); - } - } - else // time is still within the first interval - { - previousOccurrence = start; - } - - DateTime alignedPreviousOccurrence = previousOccurrence.DateTime + timeZoneOffset - previousOccurrence.Offset; - - DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - - while (alignedPreviousOccurrence.AddDays(1) <= alignedTime) - { - alignedPreviousOccurrence = alignedPreviousOccurrence.AddDays(1); - - if (alignedPreviousOccurrence.DayOfWeek == pattern.FirstDayOfWeek) // Come to the next week - { - break; - } - - if (pattern.DaysOfWeek.Any(day => - day == alignedPreviousOccurrence.DayOfWeek)) - { - previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); - - numberOfOccurrences += 1; - } - } - } - - /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "AbsoluteMonthly" recurrence pattern. - /// A time stamp. - /// The settings of time window filter. - /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. - /// - private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - DateTimeOffset start = settings.Start.Value; - - int interval = pattern.Interval; - - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; - - DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - - int monthGap = (alignedTime.Year - alignedStart.Year) * 12 + alignedTime.Month - alignedStart.Month; - - if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.Day) < alignedStart.TimeOfDay + TimeSpan.FromDays(alignedStart.Day)) - { - monthGap -= 1; - } - - int numberOfInterval = monthGap / interval; - - previousOccurrence = start.AddMonths(numberOfInterval * interval); - - numberOfOccurrences = numberOfInterval; - } - - /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "RelativeMonthly" recurrence pattern. - /// A time stamp. - /// The settings of time window filter. - /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. - /// - private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - DateTimeOffset start = settings.Start.Value; - - int interval = pattern.Interval; - - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; - - DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - - int monthGap = (alignedTime.Year - alignedStart.Year) * 12 + alignedTime.Month - alignedStart.Month; - - if (!pattern.DaysOfWeek.Any(day => - alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + alignedStart.TimeOfDay)) - { - // - // E.g. start is 2023.9.1 (the first Friday in 2023.9) and current time is 2023.10.2 (the first Friday in next month is 2023.10.6) - // Not a complete monthly interval - monthGap -= 1; - } - - int numberOfInterval = monthGap / interval; - - DateTime alignedPreviousOccurrenceMonth = alignedStart.AddMonths(numberOfInterval * interval); - - DateTime alignedPreviousOccurrence = DateTime.MaxValue; - - // - // Find the first occurence date matched the pattern - // Only one day of week in the month will be matched - foreach (DayOfWeek day in pattern.DaysOfWeek) - { - DateTime occurrenceDate = NthDayOfWeekInTheMonth(alignedPreviousOccurrenceMonth, pattern.Index, day); - - if (occurrenceDate + alignedStart.TimeOfDay < alignedPreviousOccurrence) - { - alignedPreviousOccurrence = occurrenceDate + alignedStart.TimeOfDay; - } - } - - previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); - - numberOfOccurrences = numberOfInterval; - } - - /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "AbsoluteYearly" recurrence pattern. - /// A time stamp. - /// The settings of time window filter. - /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. - /// - private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - DateTimeOffset start = settings.Start.Value; - - int interval = pattern.Interval; - - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; - - DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - - int yearGap = alignedTime.Year - alignedStart.Year; - - if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.DayOfYear) < alignedStart.TimeOfDay + TimeSpan.FromDays(alignedStart.DayOfYear)) - { - yearGap -= 1; - } - - int numberOfInterval = yearGap / interval; - - previousOccurrence = start.AddYears(numberOfInterval * interval); - - numberOfOccurrences = numberOfInterval; - } - - /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "RelativeYearly" recurrence pattern. - /// A time stamp. - /// The settings of time window filter. - /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. - /// - private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - DateTimeOffset start = settings.Start.Value; - - int interval = pattern.Interval; - - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; - - DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - - int yearGap = alignedTime.Year - alignedStart.Year; - - if (alignedTime.Month < alignedStart.Month) - { - // - // E.g. start: 2023.9 and time: 2024.8 - // Not a complete yearly interval - yearGap -= 1; - } - else if (alignedTime.Month == alignedStart.Month && !pattern.DaysOfWeek.Any(day => - alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + alignedStart.TimeOfDay)) - { - // - // E.g. start: 2023.9.1 (the first Friday in 2023.9) and time: 2024.9.2 (the first Friday in 2023.9 is 2024.9.6) - // Not a complete yearly interval - yearGap -= 1; - } - - int numberOfInterval = yearGap / interval; - - DateTime alignedPreviousOccurrenceMonth = alignedStart.AddYears(numberOfInterval * interval); - - DateTime alignedPreviousOccurrence = DateTime.MaxValue; - - // - // Find the first occurence date matched the pattern - // Only one day of week in the month will be matched - foreach (DayOfWeek day in pattern.DaysOfWeek) - { - DateTime occurrenceDate = NthDayOfWeekInTheMonth(alignedPreviousOccurrenceMonth, pattern.Index, day); - - if (occurrenceDate + alignedStart.TimeOfDay < alignedPreviousOccurrence) - { - alignedPreviousOccurrence = occurrenceDate + alignedStart.TimeOfDay; - } - } - - previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); - - numberOfOccurrences = numberOfInterval; - } - - private static bool TryValidateSettings(TimeWindowFilterSettings settings, out string paramName, out string reason) + private static bool TryValidateRecurrenceSettings(TimeWindowFilterSettings settings, out string paramName, out string reason) { if (settings == null) { throw new ArgumentNullException(nameof(settings)); } - Recurrence recurrence = settings.Recurrence; - - if (recurrence != null) - { - if (!TryValidateGeneralRequiredParameter(settings, out paramName, out reason)) - { - return false; - } - - if (!TryValidateRecurrencePattern(settings, out paramName, out reason)) - { - return false; - } - - if (!TryValidateRecurrenceRange(settings, out paramName, out reason)) - { - return false; - } - } - + if (settings.Recurrence != null) + { + return TryValidateRecurrenceRequiredParameter(settings, out paramName, out reason) && + TryValidateRecurrencePattern(settings, out paramName, out reason) && + TryValidateRecurrenceRange(settings, out paramName, out reason); + } + paramName = null; reason = null; @@ -462,8 +80,11 @@ private static bool TryValidateSettings(TimeWindowFilterSettings settings, out s return true; } - private static bool TryValidateGeneralRequiredParameter(TimeWindowFilterSettings settings, out string paramName, out string reason) + private static bool TryValidateRecurrenceRequiredParameter(TimeWindowFilterSettings settings, out string paramName, out string reason) { + Debug.Assert(settings != null); + Debug.Assert(settings.Recurrence != null); + if (settings.Start == null) { paramName = nameof(settings.Start); @@ -520,6 +141,10 @@ private static bool TryValidateGeneralRequiredParameter(TimeWindowFilterSettings private static bool TryValidateRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) { + Debug.Assert(settings != null); + Debug.Assert(settings.Recurrence != null); + Debug.Assert(settings.Recurrence.Pattern != null); + if (!TryValidateInterval(settings, out paramName, out reason)) { return false; @@ -586,7 +211,7 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings { RecurrencePattern pattern = settings.Recurrence.Pattern; - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * WeekDayNumber); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * DayNumberOfWeek); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -642,7 +267,7 @@ private static bool TryValidateAbsoluteMonthlyRecurrencePattern(TimeWindowFilter { RecurrencePattern pattern = settings.Recurrence.Pattern; - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinMonthDayNumber); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfMonth); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -686,7 +311,101 @@ private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilter { RecurrencePattern pattern = settings.Recurrence.Pattern; - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinMonthDayNumber); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfMonth); + + TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + + // + // Time window duration must be shorter than how frequently it occurs + if (timeWindowDuration > intervalDuration) + { + paramName = $"{nameof(settings.End)}"; + + reason = OutOfRange; + + return false; + } + + // + // Required parameters + if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) + { + return false; + } + + // + // Check whether "Start" is a valid first occurrence + DateTimeOffset start = settings.Start.Value; + + DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; + + if (!pattern.DaysOfWeek.Any(day => + NthDayOfWeekInTheMonth(alignedStart, pattern.Index, day) == alignedStart.Date)) + { + paramName = nameof(settings.Start); + + reason = NotMatched; + + return false; + } + + return true; + } + + private static bool TryValidateAbsoluteYearlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + RecurrencePattern pattern = settings.Recurrence.Pattern; + + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfYear); + + TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + + // + // Time window duration must be shorter than how frequently it occurs + if (timeWindowDuration > intervalDuration) + { + paramName = $"{nameof(settings.End)}"; + + reason = OutOfRange; + + return false; + } + + // + // Required parameters + if (!TryValidateMonth(settings, out paramName, out reason)) + { + return false; + } + + if (!TryValidateDayOfMonth(settings, out paramName, out reason)) + { + return false; + } + + // + // Check whether "Start" is a valid first occurrence + DateTimeOffset start = settings.Start.Value; + + DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; + + if (alignedStart.Day != pattern.DayOfMonth.Value || alignedStart.Month != pattern.Month.Value) + { + paramName = nameof(settings.Start); + + reason = NotMatched; + + return false; + } + + return true; + } + + private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + RecurrencePattern pattern = settings.Recurrence.Pattern; + + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfYear); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -708,339 +427,621 @@ private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilter return false; } - // - // Check whether "Start" is a valid first occurrence - DateTimeOffset start = settings.Start.Value; + if (!TryValidateMonth(settings, out paramName, out reason)) + { + return false; + } + + // + // Check whether "Start" is a valid first occurrence + DateTimeOffset start = settings.Start.Value; + + DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; + + if (alignedStart.Month != pattern.Month.Value || + !pattern.DaysOfWeek.Any(day => + NthDayOfWeekInTheMonth(alignedStart, pattern.Index, day) == alignedStart.Date)) + { + paramName = nameof(settings.Start); + + reason = NotMatched; + + return false; + } + + return true; + } + + private static bool TryValidateRecurrenceRange(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + Debug.Assert(settings != null); + Debug.Assert(settings.Recurrence != null); + Debug.Assert(settings.Recurrence.Range != null); + + if (!TryValidateRecurrenceTimeZone(settings, out paramName, out reason)) + { + return false; + } + + switch(settings.Recurrence.Range.Type) + { + case RecurrenceRangeType.NoEnd: + return true; + + case RecurrenceRangeType.EndDate: + return TryValidateEndDate(settings, out paramName, out reason); + + case RecurrenceRangeType.Numbered: + return TryValidateNumberOfOccurrences(settings, out paramName, out reason); + + default: + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.Type)}"; + + reason = UnrecognizableValue; + + return false; + } + } + + private static bool TryValidateInterval(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Interval)}"; + + if (settings.Recurrence.Pattern.Interval <= 0) + { + reason = OutOfRange; + + return false; + } + + reason = null; + + return true; + } + + private static bool TryValidateDaysOfWeek(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DaysOfWeek)}"; + + if (settings.Recurrence.Pattern.DaysOfWeek == null || !settings.Recurrence.Pattern.DaysOfWeek.Any()) + { + reason = RequiredParameter; + + return false; + } + + reason = null; + + return true; + } + + private static bool TryValidateDayOfMonth(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DayOfMonth)}"; + + if (settings.Recurrence.Pattern.DayOfMonth == null) + { + reason = RequiredParameter; + + return false; + } + + if (settings.Recurrence.Pattern.DayOfMonth < 1 || settings.Recurrence.Pattern.DayOfMonth > 31) + { + reason = OutOfRange; + + return false; + } + + reason = null; + + return true; + } + + private static bool TryValidateMonth(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Month)}"; + + if (settings.Recurrence.Pattern.Month == null) + { + reason = RequiredParameter; + + return false; + } + + if (settings.Recurrence.Pattern.Month < 1 || settings.Recurrence.Pattern.Month > 12) + { + reason = OutOfRange; + + return false; + } + + reason = null; + + return true; + } + + private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.EndDate)}"; + + if (settings.Recurrence.Range.EndDate == null) + { + reason = RequiredParameter; + + return false; + } + + if (settings.Start == null) + { + paramName = nameof(settings.Start); + + reason = RequiredParameter; + + return false; + } + + DateTimeOffset start = settings.Start.Value; + + TimeSpan timeZoneOffset = start.Offset; + + if (settings.Recurrence.Range.RecurrenceTimeZone != null) + { + TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out timeZoneOffset); + } + + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; - DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; + DateTime endDate = settings.Recurrence.Range.EndDate.Value.DateTime; - if (!pattern.DaysOfWeek.Any(day => - NthDayOfWeekInTheMonth(alignedStart, pattern.Index, day) == alignedStart.Date)) + if (endDate.Date < alignedStart.Date) { - paramName = nameof(settings.Start); - - reason = NotMatched; + reason = OutOfRange; return false; } + reason = null; + return true; } - private static bool TryValidateAbsoluteYearlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings settings, out string paramName, out string reason) { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinYearDayNumber); - - TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.NumberOfOccurrences)}"; - // - // Time window duration must be shorter than how frequently it occurs - if (timeWindowDuration > intervalDuration) + if (settings.Recurrence.Range.NumberOfOccurrences == null) { - paramName = $"{nameof(settings.End)}"; - - reason = OutOfRange; + reason = RequiredParameter; return false; } - // - // Required parameters - if (!TryValidateMonth(settings, out paramName, out reason)) + if (settings.Recurrence.Range.NumberOfOccurrences < 1) { - return false; - } + reason = OutOfRange; - if (!TryValidateDayOfMonth(settings, out paramName, out reason)) - { return false; } - // - // Check whether "Start" is a valid first occurrence - DateTimeOffset start = settings.Start.Value; + reason = null; - DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; + return true; + } - if (alignedStart.Day != pattern.DayOfMonth.Value || alignedStart.Month != pattern.Month.Value) - { - paramName = nameof(settings.Start); + private static bool TryValidateRecurrenceTimeZone(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.RecurrenceTimeZone)}"; - reason = NotMatched; + if (settings.Recurrence.Range.RecurrenceTimeZone != null && !TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out _)) + { + reason = UnrecognizableValue; return false; } + reason = null; + return true; } - private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + private static bool TryParseTimeZone(string timeZoneStr, out TimeSpan timeZoneOffset) { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinYearDayNumber); - - TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + timeZoneOffset = TimeSpan.Zero; - // - // Time window duration must be shorter than how frequently it occurs - if (timeWindowDuration > intervalDuration) + if (timeZoneStr == null) { - paramName = $"{nameof(settings.End)}"; - - reason = OutOfRange; - return false; } - // - // Required parameters - if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) + if (!timeZoneStr.StartsWith("UTC+") && !timeZoneStr.StartsWith("UTC-")) { return false; } - if (!TryValidateMonth(settings, out paramName, out reason)) + if (!TimeSpan.TryParseExact(timeZoneStr.Substring(4), @"hh\:mm", null, out timeZoneOffset)) { return false; } - // - // Check whether "Start" is a valid first occurrence - DateTimeOffset start = settings.Start.Value; - - DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; - - if (alignedStart.Month != pattern.Month.Value || - !pattern.DaysOfWeek.Any(day => - NthDayOfWeekInTheMonth(alignedStart, pattern.Index, day) == alignedStart.Date)) + if (timeZoneStr[3] == '-') { - paramName = nameof(settings.Start); - - reason = NotMatched; - - return false; + timeZoneOffset = -timeZoneOffset; } return true; } - private static bool TryValidateRecurrenceRange(TimeWindowFilterSettings settings, out string paramName, out string reason) + /// + /// Try to find the closest previous recurrence occurrence before the provided time stamp according to the recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// True if the closest previous occurrence is within the recurrence range, false otherwise. + /// + private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence) { - if (!TryValidateRecurrenceTimeZone(settings, out paramName, out reason)) + previousOccurrence = DateTimeOffset.MaxValue; + + DateTimeOffset start = settings.Start.Value; + + if (time < start) { return false; } - switch(settings.Recurrence.Range.Type) + int numberOfOccurrences; + + switch (settings.Recurrence.Pattern.Type) { - case RecurrenceRangeType.NoEnd: - return true; + case RecurrencePatternType.Daily: + FindDailyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - case RecurrenceRangeType.EndDate: - return TryValidateEndDate(settings, out paramName, out reason); + break; - case RecurrenceRangeType.Numbered: - return TryValidateNumberOfOccurrences(settings, out paramName, out reason); + case RecurrencePatternType.Weekly: + FindWeeklyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - default: - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.Type)}"; + break; - reason = UnrecognizableValue; + case RecurrencePatternType.AbsoluteMonthly: + FindAbsoluteMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + + break; + + case RecurrencePatternType.RelativeMonthly: + FindRelativeMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + + break; + + case RecurrencePatternType.AbsoluteYearly: + FindAbsoluteYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + + break; + case RecurrencePatternType.RelativeYearly: + FindRelativeYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + + break; + + default: return false; } - } - private static bool TryValidateInterval(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Interval)}"; + RecurrenceRange range = settings.Recurrence.Range; - if (settings.Recurrence.Pattern.Interval <= 0) + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); + + if (range.Type == RecurrenceRangeType.EndDate) { - reason = OutOfRange; + DateTime alignedPreviousOccurrence = previousOccurrence.DateTime + timeZoneOffset - previousOccurrence.Offset; - return false; + return alignedPreviousOccurrence.Date <= range.EndDate.Value.Date; } - reason = null; + if (range.Type == RecurrenceRangeType.Numbered) + { + return numberOfOccurrences < range.NumberOfOccurrences; + } return true; } - private static bool TryValidateDaysOfWeek(TimeWindowFilterSettings settings, out string paramName, out string reason) + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "Daily" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// + private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DaysOfWeek)}"; + RecurrencePattern pattern = settings.Recurrence.Pattern; - if (settings.Recurrence.Pattern.DaysOfWeek == null || !settings.Recurrence.Pattern.DaysOfWeek.Any()) + DateTimeOffset start = settings.Start.Value; + + int interval = pattern.Interval; + + TimeSpan timeGap = time - start; + + // + // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. + int numberOfInterval = (int)Math.Floor(timeGap.TotalSeconds / TimeSpan.FromDays(interval).TotalSeconds); + + previousOccurrence = start.AddDays(numberOfInterval * interval); + + numberOfOccurrences = numberOfInterval; + } + + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "Weekly" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of recurring days of week which have occurred between the time and the recurrence start. + /// + private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) + { + previousOccurrence = DateTimeOffset.MaxValue; + + numberOfOccurrences = 0; + + RecurrencePattern pattern = settings.Recurrence.Pattern; + + DateTimeOffset start = settings.Start.Value; + + int interval = pattern.Interval; + + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); + + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + + TimeSpan timeGap = time - start; + + int remainingDaysOfFirstWeek = RemainingDaysOfTheWeek(alignedStart.DayOfWeek, pattern.FirstDayOfWeek); + + TimeSpan remainingTimeOfFirstInterval = TimeSpan.FromDays(remainingDaysOfFirstWeek) - alignedStart.TimeOfDay + TimeSpan.FromDays((interval - 1) * 7); + + if (remainingTimeOfFirstInterval <= timeGap) + { + int numberOfInterval = (int)Math.Floor((timeGap - remainingTimeOfFirstInterval).TotalSeconds / TimeSpan.FromDays(interval * 7).TotalSeconds); + + previousOccurrence = start.AddDays(numberOfInterval * interval * 7 + remainingDaysOfFirstWeek + (interval - 1) * 7); + + numberOfOccurrences += numberOfInterval * pattern.DaysOfWeek.Count(); + + // + // Add the occurrences in the first week + numberOfOccurrences += 1; + + DateTime dateTime = alignedStart.AddDays(1); + + while (dateTime.DayOfWeek != pattern.FirstDayOfWeek) + { + if (pattern.DaysOfWeek.Any(day => + day == dateTime.DayOfWeek)) + { + numberOfOccurrences += 1; + } + + dateTime = dateTime.AddDays(1); + } + } + else // time is still within the first interval + { + previousOccurrence = start; + } + + DateTime alignedPreviousOccurrence = previousOccurrence.DateTime + timeZoneOffset - previousOccurrence.Offset; + + DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + + while (alignedPreviousOccurrence.AddDays(1) <= alignedTime) { - reason = RequiredParameter; + alignedPreviousOccurrence = alignedPreviousOccurrence.AddDays(1); - return false; - } + if (alignedPreviousOccurrence.DayOfWeek == pattern.FirstDayOfWeek) // Come to the next week + { + break; + } - reason = null; + if (pattern.DaysOfWeek.Any(day => + day == alignedPreviousOccurrence.DayOfWeek)) + { + previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); - return true; + numberOfOccurrences += 1; + } + } } - private static bool TryValidateDayOfMonth(TimeWindowFilterSettings settings, out string paramName, out string reason) + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "AbsoluteMonthly" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// + private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DayOfMonth)}"; - - if (settings.Recurrence.Pattern.DayOfMonth == null) - { - reason = RequiredParameter; + RecurrencePattern pattern = settings.Recurrence.Pattern; - return false; - } + DateTimeOffset start = settings.Start.Value; - if (settings.Recurrence.Pattern.DayOfMonth < 1 || settings.Recurrence.Pattern.DayOfMonth > 31) - { - reason = OutOfRange; + int interval = pattern.Interval; - return false; - } + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - reason = null; + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; - return true; - } + DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - private static bool TryValidateMonth(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Month)}"; + int monthGap = (alignedTime.Year - alignedStart.Year) * 12 + alignedTime.Month - alignedStart.Month; - if (settings.Recurrence.Pattern.Month == null) + if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.Day) < alignedStart.TimeOfDay + TimeSpan.FromDays(alignedStart.Day)) { - reason = RequiredParameter; - - return false; + monthGap -= 1; } - if (settings.Recurrence.Pattern.Month < 1 || settings.Recurrence.Pattern.Month > 12) - { - reason = OutOfRange; - - return false; - } + int numberOfInterval = monthGap / interval; - reason = null; + previousOccurrence = start.AddMonths(numberOfInterval * interval); - return true; + numberOfOccurrences = numberOfInterval; } - private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out string paramName, out string reason) + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "RelativeMonthly" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// + private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.EndDate)}"; - - if (settings.Recurrence.Range.EndDate == null) - { - reason = RequiredParameter; + RecurrencePattern pattern = settings.Recurrence.Pattern; - return false; - } + DateTimeOffset start = settings.Start.Value; - if (settings.Start == null) - { - paramName = nameof(settings.Start); + int interval = pattern.Interval; - reason = RequiredParameter; + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - return false; - } + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; - DateTimeOffset start = settings.Start.Value; + DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - TimeSpan timeZoneOffset = start.Offset; + int monthGap = (alignedTime.Year - alignedStart.Year) * 12 + alignedTime.Month - alignedStart.Month; - if (settings.Recurrence.Range.RecurrenceTimeZone != null) + if (!pattern.DaysOfWeek.Any(day => + alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + alignedStart.TimeOfDay)) { - TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out timeZoneOffset); + // + // E.g. start is 2023.9.1 (the first Friday in 2023.9) and current time is 2023.10.2 (the first Friday in next month is 2023.10.6) + // Not a complete monthly interval + monthGap -= 1; } - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + int numberOfInterval = monthGap / interval; - DateTime endDate = settings.Recurrence.Range.EndDate.Value.DateTime; + DateTime alignedPreviousOccurrenceMonth = alignedStart.AddMonths(numberOfInterval * interval); - if (endDate.Date < alignedStart.Date) + DateTime alignedPreviousOccurrence = DateTime.MaxValue; + + // + // Find the first occurence date matched the pattern + // Only one day of week in the month will be matched + foreach (DayOfWeek day in pattern.DaysOfWeek) { - reason = OutOfRange; + DateTime occurrenceDate = NthDayOfWeekInTheMonth(alignedPreviousOccurrenceMonth, pattern.Index, day); - return false; + if (occurrenceDate + alignedStart.TimeOfDay < alignedPreviousOccurrence) + { + alignedPreviousOccurrence = occurrenceDate + alignedStart.TimeOfDay; + } } - reason = null; + previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); - return true; + numberOfOccurrences = numberOfInterval; } - private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings settings, out string paramName, out string reason) + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "AbsoluteYearly" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// + private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.NumberOfOccurrences)}"; + RecurrencePattern pattern = settings.Recurrence.Pattern; - if (settings.Recurrence.Range.NumberOfOccurrences == null) - { - reason = RequiredParameter; + DateTimeOffset start = settings.Start.Value; - return false; - } + int interval = pattern.Interval; - if (settings.Recurrence.Range.NumberOfOccurrences < 1) - { - reason = OutOfRange; + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - return false; + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + + DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + + int yearGap = alignedTime.Year - alignedStart.Year; + + if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.DayOfYear) < alignedStart.TimeOfDay + TimeSpan.FromDays(alignedStart.DayOfYear)) + { + yearGap -= 1; } - reason = null; + int numberOfInterval = yearGap / interval; - return true; + previousOccurrence = start.AddYears(numberOfInterval * interval); + + numberOfOccurrences = numberOfInterval; } - private static bool TryValidateRecurrenceTimeZone(TimeWindowFilterSettings settings, out string paramName, out string reason) + /// + /// Find the closest previous recurrence occurrence before the provided time stamp according to the "RelativeYearly" recurrence pattern. + /// A time stamp. + /// The settings of time window filter. + /// The closest previous occurrence. + /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// + private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.RecurrenceTimeZone)}"; + RecurrencePattern pattern = settings.Recurrence.Pattern; - if (settings.Recurrence.Range.RecurrenceTimeZone != null && !TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out _)) - { - reason = UnrecognizableValue; + DateTimeOffset start = settings.Start.Value; - return false; - } + int interval = pattern.Interval; - reason = null; + TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - return true; - } + DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; - private static bool TryParseTimeZone(string timeZoneStr, out TimeSpan timeZoneOffset) - { - timeZoneOffset = TimeSpan.Zero; + DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - if (timeZoneStr == null) - { - return false; - } + int yearGap = alignedTime.Year - alignedStart.Year; - if (!timeZoneStr.StartsWith("UTC+") && !timeZoneStr.StartsWith("UTC-")) + if (alignedTime.Month < alignedStart.Month) { - return false; + // + // E.g. start: 2023.9 and time: 2024.8 + // Not a complete yearly interval + yearGap -= 1; } - - if (!TimeSpan.TryParseExact(timeZoneStr.Substring(4), @"hh\:mm", null, out timeZoneOffset)) + else if (alignedTime.Month == alignedStart.Month && !pattern.DaysOfWeek.Any(day => + alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + alignedStart.TimeOfDay)) { - return false; + // + // E.g. start: 2023.9.1 (the first Friday in 2023.9) and time: 2024.9.2 (the first Friday in 2023.9 is 2024.9.6) + // Not a complete yearly interval + yearGap -= 1; } - if (timeZoneStr[3] == '-') + int numberOfInterval = yearGap / interval; + + DateTime alignedPreviousOccurrenceMonth = alignedStart.AddYears(numberOfInterval * interval); + + DateTime alignedPreviousOccurrence = DateTime.MaxValue; + + // + // Find the first occurence date matched the pattern + // Only one day of week in the month will be matched + foreach (DayOfWeek day in pattern.DaysOfWeek) { - timeZoneOffset = -timeZoneOffset; + DateTime occurrenceDate = NthDayOfWeekInTheMonth(alignedPreviousOccurrenceMonth, pattern.Index, day); + + if (occurrenceDate + alignedStart.TimeOfDay < alignedPreviousOccurrence) + { + alignedPreviousOccurrence = occurrenceDate + alignedStart.TimeOfDay; + } } - return true; + previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); + + numberOfOccurrences = numberOfInterval; } private static TimeSpan GetRecurrenceTimeZone(TimeWindowFilterSettings settings) @@ -1068,54 +1069,71 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int return true; } - // - // Shift to the first day of the week DateTime date = DateTime.Today; - int offset = RemainingDaysOfWeek(date.DayOfWeek, firstDayOfWeek); + int offset = RemainingDaysOfTheWeek(date.DayOfWeek, firstDayOfWeek); + // Shift to the first day of the week date = date.AddDays(offset); - DateTime prevOccurrence = date; + DateTime prevOccurrence = DateTime.MinValue; TimeSpan minGap = TimeSpan.MaxValue; - for (int i = 0; i < 6; i++) + for (int i = 0; i < DayNumberOfWeek; i++) { - date = date.AddDays(1); - if (daysOfWeek.Any(day => day == date.DayOfWeek)) { - TimeSpan gap = date - prevOccurrence; - - if (gap < minGap) + if (prevOccurrence == DateTime.MinValue) { - minGap = gap; + prevOccurrence = date; } + else + { + TimeSpan gap = date - prevOccurrence; - prevOccurrence = date; + if (gap < minGap) + { + minGap = gap; + } + + prevOccurrence = date; + } } + + date = date.AddDays(1); } + // + // It may across weeks. Check the adjacent week if the interval is one week. if (interval == 1) { - // - // It may across weeks. Check the adjacent week - date = date.AddDays(1); + for (int i = 1; i <= DayNumberOfWeek; i++) + { + // + // If there are multiple day in DaysOfWeek, it will eventually enter the following if branch + if (daysOfWeek.Any(day => + day == date.DayOfWeek)) + { + TimeSpan gap = date - prevOccurrence; - TimeSpan gap = date - prevOccurrence; + if (gap < minGap) + { + minGap = gap; + } - if (gap < minGap) - { - minGap = gap; + break; + } + + date = date.AddDays(1); } } return minGap >= duration; } - private static int RemainingDaysOfWeek(DayOfWeek dayOfWeek, DayOfWeek firstDayOfWeek) + private static int RemainingDaysOfTheWeek(DayOfWeek dayOfWeek, DayOfWeek firstDayOfWeek) { int remainingDays = (int) dayOfWeek - (int) firstDayOfWeek; @@ -1125,7 +1143,7 @@ private static int RemainingDaysOfWeek(DayOfWeek dayOfWeek, DayOfWeek firstDayOf } else { - return WeekDayNumber - remainingDays; + return DayNumberOfWeek - remainingDays; } } @@ -1141,21 +1159,21 @@ private static DateTime NthDayOfWeekInTheMonth(DateTime dateTime, WeekIndex inde var date = new DateTime(dateTime.Year, dateTime.Month, 1); // - // Find the first day of week in the month + // Find the first provided day of week in the month while (date.DayOfWeek != dayOfWeek) { date = date.AddDays(1); } - if (date.AddDays(WeekDayNumber * (int) index).Month == dateTime.Month) + if (date.AddDays(DayNumberOfWeek * (int) index).Month == dateTime.Month) { - date = date.AddDays(WeekDayNumber * (int) index); + date = date.AddDays(DayNumberOfWeek * (int) index); } else // There is no the 5th day of week in the month { // // Add 3 weeks to reach the fourth day of week in the month - date = date.AddDays(WeekDayNumber * 3); + date = date.AddDays(DayNumberOfWeek * 3); } return date; diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index a7e1b140..f61c854f 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime; using Xunit; +using Xunit.Sdk; namespace Tests.FeatureManagement { @@ -13,7 +15,7 @@ class ErrorMessage { public const string OutOfRange = "The value is out of the accepted range."; public const string UnrecognizableValue = "The value is unrecognizable."; - public const string RequiredParameter = "Value cannot be null."; + public const string RequiredParameter = "Value cannot be null or empty."; public const string NotMatched = "Start date is not a valid first occurrence."; } @@ -25,9 +27,7 @@ class ParamName public const string Pattern = "Recurrence.Pattern"; public const string PatternType = "Recurrence.Pattern.Type"; public const string Interval = "Recurrence.Pattern.Interval"; - public const string Index = "Recurrence.Pattern.Index"; public const string DaysOfWeek = "Recurrence.Pattern.DaysOfWeek"; - public const string FirstDayOfWeek = "Recurrence.Pattern.FirstDayOfWeek"; public const string Month = "Recurrence.Pattern.Month"; public const string DayOfMonth = "Recurrence.Pattern.DayOfMonth"; @@ -315,6 +315,42 @@ public void InvalidTimeWindowTest() ParamName.End, ErrorMessage.OutOfRange ), + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-5T00:00:01+08:00"), // The duration of the time window is longer than how frequently it recurs. + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 1, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Saturday } // The time window duration should be shorter than 2 days because the gap between Saturday in the previous week and Monday in this week is 2 days. + }, + Range = new RecurrenceRange() + } + }, + ParamName.End, + ErrorMessage.OutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-1-16T00:00:00+08:00"), + End = DateTimeOffset.Parse("2024-1-19T00:00:01+08:00"), // The duration of the time window is longer than how frequently it recurs. + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 1, + DaysOfWeek = new List(){ DayOfWeek.Tuesday, DayOfWeek.Saturday } // The time window duration should be shorter than 3 days because the gap between Saturday in the previous week and Tuesday in this week is 3 days. + }, + Range = new RecurrenceRange() + } + }, + ParamName.End, + ErrorMessage.OutOfRange ), + ( new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-2-1T00:00:00+08:00"), // The duration of the time window is longer than how frequently it recurs. @@ -428,6 +464,30 @@ public void InvalidTimeWindowTest() ConsumeValidationTestData(testData); } + [Fact] + public void ValidTimeWindowAcrossWeeks() + { + var settings = new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-1-16T00:00:00+08:00"), // Tuesday + End = DateTimeOffset.Parse("2024-1-19T00:00:00+08:00"), // Time window duration is 3 days + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 1, + DaysOfWeek = new List() { DayOfWeek.Tuesday, DayOfWeek.Saturday } // The time window duration should be shorter than 3 days because the gap between Saturday in the previous week and Tuesday in this week is 3 days. + }, + Range = new RecurrenceRange() + } + }; + + // + // The settings is valid. No exception should be thrown. + RecurrenceEvaluator.MatchRecurrence(DateTimeOffset.Now, settings); + } + [Fact] public void WeeklyPatternRequiredParameterTest() { @@ -1348,8 +1408,8 @@ public void MatchAbsoluteMonthlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 29, - Interval = 2 + Interval = 2, + DayOfMonth = 29 }, Range = new RecurrenceRange() { @@ -1370,8 +1430,8 @@ public void MatchAbsoluteMonthlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 29, - Interval = 2 + Interval = 2, + DayOfMonth = 29 }, Range = new RecurrenceRange() { @@ -1674,9 +1734,9 @@ public void MatchRelativeMonthlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday }, + Interval = 2, + DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday } // Index is First by default. - Interval = 2 }, Range = new RecurrenceRange() } @@ -1693,9 +1753,9 @@ public void MatchRelativeMonthlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday }, + Interval = 2, + DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday } // Index is First by default. - Interval = 2 }, Range = new RecurrenceRange() } @@ -1712,9 +1772,9 @@ public void MatchRelativeMonthlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, + Interval = 3, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } // Index is First by default. - Interval = 3 }, Range = new RecurrenceRange() } @@ -1731,9 +1791,9 @@ public void MatchRelativeMonthlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, + Interval = 3, // 2023-9, 2023-12, 2024-3 ... + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } // Index is First by default. - Interval = 3 // 2023-9, 2023-12, 2024-3 ... }, Range = new RecurrenceRange() } @@ -1893,9 +1953,9 @@ public void MatchAbsoluteYearlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = RecurrencePatternType.AbsoluteYearly, + Interval = 3, // 2023, 2026, ... DayOfMonth = 1, - Month = 9, - Interval = 3 // 2023, 2026, ... + Month = 9 }, Range = new RecurrenceRange() { @@ -1916,9 +1976,9 @@ public void MatchAbsoluteYearlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = RecurrencePatternType.AbsoluteYearly, + Interval = 3, // 2023, 2026, 2029 ... DayOfMonth = 1, - Month = 9, - Interval = 3 // 2023, 2026, 2029 ... + Month = 9 }, Range = new RecurrenceRange() { @@ -2043,9 +2103,9 @@ public void MatchRelativeYearlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = RecurrencePatternType.RelativeYearly, + Interval = 2, // 2023, 2025 ... DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, - Month = 9, - Interval = 2 // 2023, 2025 ... + Month = 9 }, Range = new RecurrenceRange() } @@ -2062,9 +2122,9 @@ public void MatchRelativeYearlyRecurrenceTest() Pattern = new RecurrencePattern() { Type = RecurrencePatternType.RelativeYearly, + Interval = 3, // 2023, 2026 ... DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, - Month = 9, - Interval = 3 // 2023, 2026 ... + Month = 9 }, Range = new RecurrenceRange() } From d8c1790cfb09229d5d078968f223c4bf9d05ca1d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 18 Jan 2024 13:09:11 +0800 Subject: [PATCH 11/52] update --- .../Recurrence/RecurrenceEvaluator.cs | 241 ++++++------------ .../Recurrence/RecurrenceRange.cs | 5 - .../RecurrenceEvaluator.cs | 81 +----- 3 files changed, 87 insertions(+), 240 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 41417c0f..075b309e 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -237,10 +237,8 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings // Check whether "Start" is a valid first occurrence DateTimeOffset start = settings.Start.Value; - DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; - if (!pattern.DaysOfWeek.Any(day => - day == alignedStart.DayOfWeek)) + day == start.DayOfWeek)) { paramName = nameof(settings.Start); @@ -293,9 +291,7 @@ private static bool TryValidateAbsoluteMonthlyRecurrencePattern(TimeWindowFilter // Check whether "Start" is a valid first occurrence DateTimeOffset start = settings.Start.Value; - DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; - - if (alignedStart.Day != pattern.DayOfMonth.Value) + if (start.Day != pattern.DayOfMonth.Value) { paramName = nameof(settings.Start); @@ -337,10 +333,8 @@ private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilter // Check whether "Start" is a valid first occurrence DateTimeOffset start = settings.Start.Value; - DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; - if (!pattern.DaysOfWeek.Any(day => - NthDayOfWeekInTheMonth(alignedStart, pattern.Index, day) == alignedStart.Date)) + NthDayOfWeekInTheMonth(start.DateTime, pattern.Index, day) == start.Date)) { paramName = nameof(settings.Start); @@ -387,9 +381,7 @@ private static bool TryValidateAbsoluteYearlyRecurrencePattern(TimeWindowFilterS // Check whether "Start" is a valid first occurrence DateTimeOffset start = settings.Start.Value; - DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; - - if (alignedStart.Day != pattern.DayOfMonth.Value || alignedStart.Month != pattern.Month.Value) + if (start.Day != pattern.DayOfMonth.Value || start.Month != pattern.Month.Value) { paramName = nameof(settings.Start); @@ -436,11 +428,9 @@ private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterS // Check whether "Start" is a valid first occurrence DateTimeOffset start = settings.Start.Value; - DateTime alignedStart = start.DateTime + GetRecurrenceTimeZone(settings) - start.Offset; - - if (alignedStart.Month != pattern.Month.Value || + if (start.Month != pattern.Month.Value || !pattern.DaysOfWeek.Any(day => - NthDayOfWeekInTheMonth(alignedStart, pattern.Index, day) == alignedStart.Date)) + NthDayOfWeekInTheMonth(start.DateTime, pattern.Index, day) == start.Date)) { paramName = nameof(settings.Start); @@ -458,14 +448,13 @@ private static bool TryValidateRecurrenceRange(TimeWindowFilterSettings settings Debug.Assert(settings.Recurrence != null); Debug.Assert(settings.Recurrence.Range != null); - if (!TryValidateRecurrenceTimeZone(settings, out paramName, out reason)) - { - return false; - } - switch(settings.Recurrence.Range.Type) { case RecurrenceRangeType.NoEnd: + paramName = null; + + reason = null; + return true; case RecurrenceRangeType.EndDate: @@ -583,18 +572,9 @@ private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out st DateTimeOffset start = settings.Start.Value; - TimeSpan timeZoneOffset = start.Offset; + DateTimeOffset endDate = settings.Recurrence.Range.EndDate.Value; - if (settings.Recurrence.Range.RecurrenceTimeZone != null) - { - TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out timeZoneOffset); - } - - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; - - DateTime endDate = settings.Recurrence.Range.EndDate.Value.DateTime; - - if (endDate.Date < alignedStart.Date) + if (endDate < start) { reason = OutOfRange; @@ -629,49 +609,6 @@ private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings sett return true; } - private static bool TryValidateRecurrenceTimeZone(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.RecurrenceTimeZone)}"; - - if (settings.Recurrence.Range.RecurrenceTimeZone != null && !TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out _)) - { - reason = UnrecognizableValue; - - return false; - } - - reason = null; - - return true; - } - - private static bool TryParseTimeZone(string timeZoneStr, out TimeSpan timeZoneOffset) - { - timeZoneOffset = TimeSpan.Zero; - - if (timeZoneStr == null) - { - return false; - } - - if (!timeZoneStr.StartsWith("UTC+") && !timeZoneStr.StartsWith("UTC-")) - { - return false; - } - - if (!TimeSpan.TryParseExact(timeZoneStr.Substring(4), @"hh\:mm", null, out timeZoneOffset)) - { - return false; - } - - if (timeZoneStr[3] == '-') - { - timeZoneOffset = -timeZoneOffset; - } - - return true; - } - /// /// Try to find the closest previous recurrence occurrence before the provided time stamp according to the recurrence pattern. /// A time stamp. @@ -681,6 +618,11 @@ private static bool TryParseTimeZone(string timeZoneStr, out TimeSpan timeZoneOf /// private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence) { + Debug.Assert(settings.Start != null); + Debug.Assert(settings.Recurrence != null); + Debug.Assert(settings.Recurrence.Pattern != null); + Debug.Assert(settings.Recurrence.Range != null); + previousOccurrence = DateTimeOffset.MaxValue; DateTimeOffset start = settings.Start.Value; @@ -730,13 +672,9 @@ private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilt RecurrenceRange range = settings.Recurrence.Range; - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - if (range.Type == RecurrenceRangeType.EndDate) { - DateTime alignedPreviousOccurrence = previousOccurrence.DateTime + timeZoneOffset - previousOccurrence.Offset; - - return alignedPreviousOccurrence.Date <= range.EndDate.Value.Date; + return previousOccurrence <= range.EndDate.Value; } if (range.Type == RecurrenceRangeType.Numbered) @@ -777,13 +715,11 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF /// Find the closest previous recurrence occurrence before the provided time stamp according to the "Weekly" recurrence pattern. /// A time stamp. /// The settings of time window filter. - /// The closest previous occurrence. + /// The closest previous occurrence. /// The number of recurring days of week which have occurred between the time and the recurrence start. /// private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { - previousOccurrence = DateTimeOffset.MaxValue; - numberOfOccurrences = 0; RecurrencePattern pattern = settings.Recurrence.Pattern; @@ -792,66 +728,79 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow int interval = pattern.Interval; - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); + TimeSpan timeGap = time - start; - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + int remainingDaysOfFirstWeek = RemainingDaysOfTheWeek(start.DayOfWeek, pattern.FirstDayOfWeek); - TimeSpan timeGap = time - start; + TimeSpan remainingTimeOfFirstWeek = TimeSpan.FromDays(remainingDaysOfFirstWeek) - start.TimeOfDay; - int remainingDaysOfFirstWeek = RemainingDaysOfTheWeek(alignedStart.DayOfWeek, pattern.FirstDayOfWeek); + TimeSpan remainingTimeOfFirstInterval = remainingTimeOfFirstWeek + TimeSpan.FromDays((interval - 1) * DayNumberOfWeek); - TimeSpan remainingTimeOfFirstInterval = TimeSpan.FromDays(remainingDaysOfFirstWeek) - alignedStart.TimeOfDay + TimeSpan.FromDays((interval - 1) * 7); + DateTimeOffset tentativePreviousOccurrence = start; + // + // Time is not within the first interval if (remainingTimeOfFirstInterval <= timeGap) { - int numberOfInterval = (int)Math.Floor((timeGap - remainingTimeOfFirstInterval).TotalSeconds / TimeSpan.FromDays(interval * 7).TotalSeconds); - - previousOccurrence = start.AddDays(numberOfInterval * interval * 7 + remainingDaysOfFirstWeek + (interval - 1) * 7); - - numberOfOccurrences += numberOfInterval * pattern.DaysOfWeek.Count(); - // - // Add the occurrences in the first week + // Add the occurrences in the first occurrence (i.e. start) numberOfOccurrences += 1; - DateTime dateTime = alignedStart.AddDays(1); + // + // Add the occurrence in the first week + DateTime date = start.AddDays(1).DateTime; - while (dateTime.DayOfWeek != pattern.FirstDayOfWeek) + while (date.DayOfWeek != pattern.FirstDayOfWeek) { if (pattern.DaysOfWeek.Any(day => - day == dateTime.DayOfWeek)) + day == date.DayOfWeek)) { numberOfOccurrences += 1; } - dateTime = dateTime.AddDays(1); + date = date.AddDays(1); } - } - else // time is still within the first interval - { - previousOccurrence = start; + + // + // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. + int numberOfInterval = (int) Math.Floor((timeGap - remainingTimeOfFirstInterval).TotalSeconds / TimeSpan.FromDays(interval * DayNumberOfWeek).TotalSeconds); + + int remainingDaysOfFirstInterval = remainingDaysOfFirstWeek + (interval - 1) * DayNumberOfWeek; + + // + // Shift the tentative previous occurrence to the first day of the first week of the latest interval + tentativePreviousOccurrence = start.AddDays(remainingDaysOfFirstInterval + numberOfInterval * interval * DayNumberOfWeek); + + numberOfOccurrences += numberOfInterval * pattern.DaysOfWeek.Count(); } - DateTime alignedPreviousOccurrence = previousOccurrence.DateTime + timeZoneOffset - previousOccurrence.Offset; + // + // Tentative previous occurrence should either be the start or the first day of the first week of the latest interval. + previousOccurrence = tentativePreviousOccurrence; - DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + // + // Check the following days of the first week if time is still within the first interval + // Otherwise, check the first week of the latest interval + tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays(1); - while (alignedPreviousOccurrence.AddDays(1) <= alignedTime) + while (tentativePreviousOccurrence <= time) { - alignedPreviousOccurrence = alignedPreviousOccurrence.AddDays(1); - - if (alignedPreviousOccurrence.DayOfWeek == pattern.FirstDayOfWeek) // Come to the next week + if (tentativePreviousOccurrence.DayOfWeek == pattern.FirstDayOfWeek) { + // + // It comes to the next week, so break. break; } if (pattern.DaysOfWeek.Any(day => - day == alignedPreviousOccurrence.DayOfWeek)) + day == tentativePreviousOccurrence.DayOfWeek)) { - previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); + previousOccurrence = tentativePreviousOccurrence; numberOfOccurrences += 1; } + + tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays(1); } } @@ -870,15 +819,13 @@ private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, T int interval = pattern.Interval; - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + TimeSpan timeZoneOffset = start.Offset; DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - int monthGap = (alignedTime.Year - alignedStart.Year) * 12 + alignedTime.Month - alignedStart.Month; + int monthGap = (alignedTime.Year - start.Year) * 12 + alignedTime.Month - start.Month; - if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.Day) < alignedStart.TimeOfDay + TimeSpan.FromDays(alignedStart.Day)) + if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.Day) < start.TimeOfDay + TimeSpan.FromDays(start.Day)) { monthGap -= 1; } @@ -905,16 +852,14 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T int interval = pattern.Interval; - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + TimeSpan timeZoneOffset = start.Offset; DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - int monthGap = (alignedTime.Year - alignedStart.Year) * 12 + alignedTime.Month - alignedStart.Month; + int monthGap = (alignedTime.Year - start.Year) * 12 + alignedTime.Month - start.Month; if (!pattern.DaysOfWeek.Any(day => - alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + alignedStart.TimeOfDay)) + alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + start.TimeOfDay)) { // // E.g. start is 2023.9.1 (the first Friday in 2023.9) and current time is 2023.10.2 (the first Friday in next month is 2023.10.6) @@ -924,25 +869,23 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T int numberOfInterval = monthGap / interval; - DateTime alignedPreviousOccurrenceMonth = alignedStart.AddMonths(numberOfInterval * interval); + DateTime previousOccurrenceMonth = start.AddMonths(numberOfInterval * interval).DateTime; - DateTime alignedPreviousOccurrence = DateTime.MaxValue; + previousOccurrence = DateTimeOffset.MaxValue; // // Find the first occurence date matched the pattern // Only one day of week in the month will be matched foreach (DayOfWeek day in pattern.DaysOfWeek) { - DateTime occurrenceDate = NthDayOfWeekInTheMonth(alignedPreviousOccurrenceMonth, pattern.Index, day); + DateTime occurrenceDate = NthDayOfWeekInTheMonth(previousOccurrenceMonth, pattern.Index, day); - if (occurrenceDate + alignedStart.TimeOfDay < alignedPreviousOccurrence) + if (occurrenceDate + start.TimeOfDay < previousOccurrence) { - alignedPreviousOccurrence = occurrenceDate + alignedStart.TimeOfDay; + previousOccurrence = occurrenceDate + start.TimeOfDay; } } - previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); - numberOfOccurrences = numberOfInterval; } @@ -961,15 +904,13 @@ private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, Ti int interval = pattern.Interval; - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + TimeSpan timeZoneOffset = start.Offset; DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - int yearGap = alignedTime.Year - alignedStart.Year; + int yearGap = alignedTime.Year - start.Year; - if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.DayOfYear) < alignedStart.TimeOfDay + TimeSpan.FromDays(alignedStart.DayOfYear)) + if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.DayOfYear) < start.TimeOfDay + TimeSpan.FromDays(start.DayOfYear)) { yearGap -= 1; } @@ -996,23 +937,21 @@ private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, Ti int interval = pattern.Interval; - TimeSpan timeZoneOffset = GetRecurrenceTimeZone(settings); - - DateTime alignedStart = start.DateTime + timeZoneOffset - start.Offset; + TimeSpan timeZoneOffset = start.Offset; DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; - int yearGap = alignedTime.Year - alignedStart.Year; + int yearGap = alignedTime.Year - start.Year; - if (alignedTime.Month < alignedStart.Month) + if (alignedTime.Month < start.Month) { // // E.g. start: 2023.9 and time: 2024.8 // Not a complete yearly interval yearGap -= 1; } - else if (alignedTime.Month == alignedStart.Month && !pattern.DaysOfWeek.Any(day => - alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + alignedStart.TimeOfDay)) + else if (alignedTime.Month == start.Month && !pattern.DaysOfWeek.Any(day => + alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + start.TimeOfDay)) { // // E.g. start: 2023.9.1 (the first Friday in 2023.9) and time: 2024.9.2 (the first Friday in 2023.9 is 2024.9.6) @@ -1022,38 +961,26 @@ private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, Ti int numberOfInterval = yearGap / interval; - DateTime alignedPreviousOccurrenceMonth = alignedStart.AddYears(numberOfInterval * interval); + DateTime previousOccurrenceMonth = start.AddYears(numberOfInterval * interval).DateTime; - DateTime alignedPreviousOccurrence = DateTime.MaxValue; + previousOccurrence = DateTime.MaxValue; // // Find the first occurence date matched the pattern // Only one day of week in the month will be matched foreach (DayOfWeek day in pattern.DaysOfWeek) { - DateTime occurrenceDate = NthDayOfWeekInTheMonth(alignedPreviousOccurrenceMonth, pattern.Index, day); + DateTime occurrenceDate = NthDayOfWeekInTheMonth(previousOccurrenceMonth, pattern.Index, day); - if (occurrenceDate + alignedStart.TimeOfDay < alignedPreviousOccurrence) + if (occurrenceDate + start.TimeOfDay < previousOccurrence) { - alignedPreviousOccurrence = occurrenceDate + alignedStart.TimeOfDay; + previousOccurrence = occurrenceDate + start.TimeOfDay; } } - previousOccurrence = new DateTimeOffset(alignedPreviousOccurrence, timeZoneOffset); - numberOfOccurrences = numberOfInterval; } - private static TimeSpan GetRecurrenceTimeZone(TimeWindowFilterSettings settings) - { - if (!TryParseTimeZone(settings.Recurrence.Range.RecurrenceTimeZone, out TimeSpan timeZoneOffset)) - { - timeZoneOffset = settings.Start.Value.Offset; - } - - return timeZoneOffset; - } - /// /// Check whether the duration is shorter than the minimum gap between recurrence of days of week. /// diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs index 356eb348..cb412a2c 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs @@ -24,10 +24,5 @@ public class RecurrenceRange /// The number of times to repeat the time window. /// public int? NumberOfOccurrences { get; set; } - - /// - /// Time zone for recurrence settings. e.g. UTC+08:00 - /// - public string RecurrenceTimeZone { get; set; } } } diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index f61c854f..102c440f 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -207,22 +207,6 @@ public void InvalidValueTest() ParamName.Month, ErrorMessage.OutOfRange ), - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern(), - Range = new RecurrenceRange() - { - RecurrenceTimeZone = "" - } - } - }, - ParamName.RecurrenceTimeZone, - ErrorMessage.UnrecognizableValue ), - ( new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -435,25 +419,7 @@ public void InvalidTimeWindowTest() Range = new RecurrenceRange() { Type = RecurrenceRangeType.EndDate, - EndDate = DateTimeOffset.Parse("2023-8-31T00:00:00+08:00") // EndDate is earlier than the Start - } - } - }, - ParamName.EndDate, - ErrorMessage.OutOfRange ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T23:00:00+00:00"), // 2023-9-2 under the RecurrenceTimeZone - End = DateTimeOffset.Parse("2023-9-1T23:00:01+00:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern(), - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.EndDate, - EndDate = DateTimeOffset.Parse("2023-9-1"), // EndDate is earlier than the Start - RecurrenceTimeZone = "UTC+08:00" // All date time in the recurrence settings will be aligned to the RecurrenceTimeZone + EndDate = DateTimeOffset.Parse("2023-8-31T23:59:59+08:00") // EndDate is earlier than the Start } } }, @@ -534,26 +500,6 @@ public void WeeklyPatternNotMatchTest() } }, ParamName.Start, - ErrorMessage.NotMatched ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Start (2023-8-31T23:00:00+07:00) is Thursday under the RecurrenceTimeZone. Start date is not a valid first occurrence. - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - DaysOfWeek = new List{ DayOfWeek.Friday } - }, - Range = new RecurrenceRange() - { - RecurrenceTimeZone = "UTC+07:00" - } - } - }, - ParamName.Start, ErrorMessage.NotMatched ) }; @@ -948,27 +894,6 @@ public void MatchDailyRecurrenceTest() } } }, - false ), - - ( DateTimeOffset.Parse("2023-9-2T17:00:00+00:00"), // 2023-9-3T01:00:00+08:00 under the RecurrenceTimeZone, which is beyond the EndDate - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T17:00:00+00:00"), - End = DateTimeOffset.Parse("2023-9-1T17:30:00+00:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Daily - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.EndDate, - EndDate = DateTimeOffset.Parse("2023-9-2"), - RecurrenceTimeZone = "UTC+08:00" - } - } - }, false ) }; @@ -1414,7 +1339,7 @@ public void MatchAbsoluteMonthlyRecurrenceTest() Range = new RecurrenceRange() { Type = RecurrenceRangeType.EndDate, - EndDate = DateTimeOffset.Parse("2024-2-29") + EndDate = DateTimeOffset.Parse("2024-2-29T00:00:00+08:00") } } }, @@ -1436,7 +1361,7 @@ public void MatchAbsoluteMonthlyRecurrenceTest() Range = new RecurrenceRange() { Type = RecurrenceRangeType.EndDate, - EndDate = DateTimeOffset.Parse("2024-2-28") + EndDate = DateTimeOffset.Parse("2024-2-28T23:59:59+08:00") } } }, From e2b3d8484818468dd84fab09957b6625a36fa103 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 18 Jan 2024 13:22:47 +0800 Subject: [PATCH 12/52] update the logic of FindWeeklyPreviousOccurrence --- .../Recurrence/RecurrenceEvaluator.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 075b309e..f2caa35d 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -715,7 +715,7 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF /// Find the closest previous recurrence occurrence before the provided time stamp according to the "Weekly" recurrence pattern. /// A time stamp. /// The settings of time window filter. - /// The closest previous occurrence. + /// The closest previous occurrence. /// The number of recurring days of week which have occurred between the time and the recurrence start. /// private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) @@ -746,31 +746,36 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow // Add the occurrences in the first occurrence (i.e. start) numberOfOccurrences += 1; - // - // Add the occurrence in the first week - DateTime date = start.AddDays(1).DateTime; + tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays(1); - while (date.DayOfWeek != pattern.FirstDayOfWeek) + while (tentativePreviousOccurrence.DayOfWeek != pattern.FirstDayOfWeek) { if (pattern.DaysOfWeek.Any(day => - day == date.DayOfWeek)) + day == tentativePreviousOccurrence.DayOfWeek)) { + // + // Add the occurrence in the first week numberOfOccurrences += 1; } - date = date.AddDays(1); + tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays(1); } // + // Shift the tentative previous occurrence to the first day of the first week of the second interval + tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays((interval - 1) * DayNumberOfWeek); + + // + // The number of intervals between the first and the latest intervals (not inclusive) // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. int numberOfInterval = (int) Math.Floor((timeGap - remainingTimeOfFirstInterval).TotalSeconds / TimeSpan.FromDays(interval * DayNumberOfWeek).TotalSeconds); - int remainingDaysOfFirstInterval = remainingDaysOfFirstWeek + (interval - 1) * DayNumberOfWeek; - // // Shift the tentative previous occurrence to the first day of the first week of the latest interval - tentativePreviousOccurrence = start.AddDays(remainingDaysOfFirstInterval + numberOfInterval * interval * DayNumberOfWeek); + tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays(numberOfInterval * interval * DayNumberOfWeek); + // + // Add the occurrence in the intervals between the first and the latest intervals (not inclusive) numberOfOccurrences += numberOfInterval * pattern.DaysOfWeek.Count(); } From e2fccc2ec2ce403783f2dbe2f153f0505634d53c Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 18 Jan 2024 14:36:56 +0800 Subject: [PATCH 13/52] fix bug --- .../Recurrence/RecurrenceEvaluator.cs | 72 +- .../RecurrenceEvaluator.cs | 682 ++++++++++-------- 2 files changed, 435 insertions(+), 319 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index f2caa35d..8b2c07ff 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -679,7 +679,7 @@ private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilt if (range.Type == RecurrenceRangeType.Numbered) { - return numberOfOccurrences < range.NumberOfOccurrences; + return numberOfOccurrences <= range.NumberOfOccurrences; } return true; @@ -698,6 +698,8 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF DateTimeOffset start = settings.Start.Value; + Debug.Assert(time >= start); + int interval = pattern.Interval; TimeSpan timeGap = time - start; @@ -708,7 +710,7 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF previousOccurrence = start.AddDays(numberOfInterval * interval); - numberOfOccurrences = numberOfInterval; + numberOfOccurrences = numberOfInterval + 1; } /// @@ -726,6 +728,8 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow DateTimeOffset start = settings.Start.Value; + Debug.Assert(time >= start); + int interval = pattern.Interval; TimeSpan timeGap = time - start; @@ -734,7 +738,9 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow TimeSpan remainingTimeOfFirstWeek = TimeSpan.FromDays(remainingDaysOfFirstWeek) - start.TimeOfDay; - TimeSpan remainingTimeOfFirstInterval = remainingTimeOfFirstWeek + TimeSpan.FromDays((interval - 1) * DayNumberOfWeek); + TimeSpan remaingTimeOfFirstIntervalAfterFirstWeek = TimeSpan.FromDays((interval - 1) * DayNumberOfWeek); + + TimeSpan remainingTimeOfFirstInterval = remainingTimeOfFirstWeek + remaingTimeOfFirstIntervalAfterFirstWeek; DateTimeOffset tentativePreviousOccurrence = start; @@ -743,27 +749,22 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow if (remainingTimeOfFirstInterval <= timeGap) { // - // Add the occurrences in the first occurrence (i.e. start) - numberOfOccurrences += 1; - - tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays(1); - - while (tentativePreviousOccurrence.DayOfWeek != pattern.FirstDayOfWeek) + // Add the occurrence in the first week and shift the tentative previous occurrence to the next week + while (tentativePreviousOccurrence.DayOfWeek != pattern.FirstDayOfWeek || + tentativePreviousOccurrence == start) { if (pattern.DaysOfWeek.Any(day => day == tentativePreviousOccurrence.DayOfWeek)) { - // - // Add the occurrence in the first week numberOfOccurrences += 1; } - tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays(1); + tentativePreviousOccurrence += TimeSpan.FromDays(1); } // // Shift the tentative previous occurrence to the first day of the first week of the second interval - tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays((interval - 1) * DayNumberOfWeek); + tentativePreviousOccurrence += remaingTimeOfFirstIntervalAfterFirstWeek; // // The number of intervals between the first and the latest intervals (not inclusive) @@ -772,7 +773,7 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow // // Shift the tentative previous occurrence to the first day of the first week of the latest interval - tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays(numberOfInterval * interval * DayNumberOfWeek); + tentativePreviousOccurrence += TimeSpan.FromDays(numberOfInterval * interval * DayNumberOfWeek); // // Add the occurrence in the intervals between the first and the latest intervals (not inclusive) @@ -786,7 +787,7 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow // // Check the following days of the first week if time is still within the first interval // Otherwise, check the first week of the latest interval - tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays(1); + tentativePreviousOccurrence += TimeSpan.FromDays(1); while (tentativePreviousOccurrence <= time) { @@ -805,7 +806,7 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow numberOfOccurrences += 1; } - tentativePreviousOccurrence = tentativePreviousOccurrence.AddDays(1); + tentativePreviousOccurrence += TimeSpan.FromDays(1); } } @@ -822,6 +823,8 @@ private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, T DateTimeOffset start = settings.Start.Value; + Debug.Assert(time >= start); + int interval = pattern.Interval; TimeSpan timeZoneOffset = start.Offset; @@ -839,7 +842,7 @@ private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, T previousOccurrence = start.AddMonths(numberOfInterval * interval); - numberOfOccurrences = numberOfInterval; + numberOfOccurrences = numberOfInterval + 1; } /// @@ -855,6 +858,8 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T DateTimeOffset start = settings.Start.Value; + Debug.Assert(time >= start); + int interval = pattern.Interval; TimeSpan timeZoneOffset = start.Offset; @@ -876,11 +881,11 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T DateTime previousOccurrenceMonth = start.AddMonths(numberOfInterval * interval).DateTime; - previousOccurrence = DateTimeOffset.MaxValue; - // // Find the first occurence date matched the pattern // Only one day of week in the month will be matched + previousOccurrence = DateTimeOffset.MaxValue; + foreach (DayOfWeek day in pattern.DaysOfWeek) { DateTime occurrenceDate = NthDayOfWeekInTheMonth(previousOccurrenceMonth, pattern.Index, day); @@ -891,7 +896,7 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T } } - numberOfOccurrences = numberOfInterval; + numberOfOccurrences = numberOfInterval + 1; } /// @@ -924,7 +929,7 @@ private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, Ti previousOccurrence = start.AddYears(numberOfInterval * interval); - numberOfOccurrences = numberOfInterval; + numberOfOccurrences = numberOfInterval + 1; } /// @@ -968,11 +973,11 @@ private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, Ti DateTime previousOccurrenceMonth = start.AddYears(numberOfInterval * interval).DateTime; - previousOccurrence = DateTime.MaxValue; - // // Find the first occurence date matched the pattern // Only one day of week in the month will be matched + previousOccurrence = DateTime.MaxValue; + foreach (DayOfWeek day in pattern.DaysOfWeek) { DateTime occurrenceDate = NthDayOfWeekInTheMonth(previousOccurrenceMonth, pattern.Index, day); @@ -983,7 +988,7 @@ private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, Ti } } - numberOfOccurrences = numberOfInterval; + numberOfOccurrences = numberOfInterval + 1; } /// @@ -1001,12 +1006,9 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int return true; } - DateTime date = DateTime.Today; - - int offset = RemainingDaysOfTheWeek(date.DayOfWeek, firstDayOfWeek); - // Shift to the first day of the week - date = date.AddDays(offset); + DateTime date = DateTime.Today.AddDays( + RemainingDaysOfTheWeek(DateTime.Today.DayOfWeek, firstDayOfWeek)); DateTime prevOccurrence = DateTime.MinValue; @@ -1019,6 +1021,8 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int { if (prevOccurrence == DateTime.MinValue) { + // + // init prevOccurrence = date; } else @@ -1034,7 +1038,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int } } - date = date.AddDays(1); + date += TimeSpan.FromDays(1); } // @@ -1058,7 +1062,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int break; } - date = date.AddDays(1); + date += TimeSpan.FromDays(1); } } @@ -1094,18 +1098,18 @@ private static DateTime NthDayOfWeekInTheMonth(DateTime dateTime, WeekIndex inde // Find the first provided day of week in the month while (date.DayOfWeek != dayOfWeek) { - date = date.AddDays(1); + date += TimeSpan.FromDays(1); } if (date.AddDays(DayNumberOfWeek * (int) index).Month == dateTime.Month) { - date = date.AddDays(DayNumberOfWeek * (int) index); + date += TimeSpan.FromDays(DayNumberOfWeek * (int) index); } else // There is no the 5th day of week in the month { // // Add 3 weeks to reach the fourth day of week in the month - date = date.AddDays(DayNumberOfWeek * 3); + date += TimeSpan.FromDays(DayNumberOfWeek * 3); } return date; diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index 102c440f..45053930 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -905,303 +905,349 @@ public void MatchWeeklyRecurrenceTest() { var testData = new List>() { - ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + //( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // // FirstDayOfWeek is Sunday by default. + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } + // }, + // Range = new RecurrenceRange() + // } + //}, + //true ), + + //( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // Friday in the 2nd week after the Start date + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // Interval = 2, + // DaysOfWeek = new List(){ DayOfWeek.Friday } + // }, + // Range = new RecurrenceRange() + // } + //}, + //false ), + + //( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // Friday in the 3rd week after the Start date + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // Interval = 2, + // DaysOfWeek = new List(){ DayOfWeek.Friday } + // }, + // Range = new RecurrenceRange() + // } + //}, + //true ), + + //( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday is not included in DaysOfWeek + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // DaysOfWeek = new List(){ DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + // }, + // Range = new RecurrenceRange() + // } + //}, + //false ), + + //( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), // The 2nd occurrence. + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + // }, + // Range = new RecurrenceRange() + // { + // Type = RecurrenceRangeType.Numbered, + // NumberOfOccurrences = 1 + // } + // } + //}, + //false ), + + //( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + // }, + // Range = new RecurrenceRange() + // { + // Type = RecurrenceRangeType.Numbered, + // NumberOfOccurrences = 7 + // } + // } + //}, + //false ), + + //( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + // }, + // Range = new RecurrenceRange() + // { + // Type = RecurrenceRangeType.Numbered, + // NumberOfOccurrences = 8 + // } + // } + //}, + //true ), + + //( DateTimeOffset.Parse("2024-1-18T00:30:00+08:00"), // The 4th occurence. + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2024-1-4T00:00:00+08:00"), // Thursday + // End = DateTimeOffset.Parse("2024-1-4T01:00:00+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // // FirstDayOfWeek is Sunday by default. + // DaysOfWeek = new List(){ DayOfWeek.Tuesday, DayOfWeek.Thursday, DayOfWeek.Friday}, + // Interval = 2 + // }, + // Range = new RecurrenceRange() + // { + // Type = RecurrenceRangeType.Numbered, + // NumberOfOccurrences = 3 + // } + // } + //}, + //false ), + + ( DateTimeOffset.Parse("2024-1-18T00:30:00+08:00"), // The 4th occurence. + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-1-4T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-1-4T01:00:00+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { Type = RecurrencePatternType.Weekly, // FirstDayOfWeek is Sunday by default. - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // Friday in the 2nd week after the Start date - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - Interval = 2, - DaysOfWeek = new List(){ DayOfWeek.Friday } - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // Friday in the 3rd week after the Start date - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - Interval = 2, - DaysOfWeek = new List(){ DayOfWeek.Friday } - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday is not included in DaysOfWeek - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - DaysOfWeek = new List(){ DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), // The 2nd occurrence. - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 1 - } - } - }, - false ), - - ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 7 - } - } - }, - false ), - - ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 8 - } - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - Interval = 2, - FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 is the last day of the 1st week after the Start date - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 1st week after the Start date - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - Interval = 2, - // FirstDayOfWeek is Sunday by default, 2023-9-3 ~ 2023-9-9 is the 1st week after the Start date - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 4th week after the Start date - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - Interval = 2, - FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 1st week, 9-4 ~ 9-10 2nd week (Skipped), 9-11 ~ 9-17 3rd week, 9-18 ~ 9-24 4th week (Skipped) - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 3rd week after the Start date - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - Interval = 2, - // FirstDayOfWeek is Sunday by default, 2023-9-3 ~ 9-9 1st week, 9-17 ~ 9-23 3rd week - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-17T00:00:00+08:00"), // Sunday in the 3rd week after the Start date - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - Interval = 2, - FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 1st week, 9-11 ~ 9-17 3rd week - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // 2023-9-3, 9-11. 9-17 - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 3 - } - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-13T00:00:00+08:00"), // Within the recurring time window 2023-9-11T:00:00:00+08:00 ~ 2023-9-15T:00:00:00+08:00. - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - Interval = 2, - FirstDayOfWeek = DayOfWeek.Monday, - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrence: 2023-9-17T:00:00:00+08:00 ~ 2023-9-21T:00:00:00+08:00. - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - Interval = 2, - FirstDayOfWeek = DayOfWeek.Monday, - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) + DaysOfWeek = new List(){ DayOfWeek.Tuesday, DayOfWeek.Thursday, DayOfWeek.Friday}, + Interval = 2 }, Range = new RecurrenceRange() { Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 3 + NumberOfOccurrences = 4 } } }, true ), - ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrences - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - Interval = 2, - FirstDayOfWeek = DayOfWeek.Monday, - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 2 - } - } - }, - false ) + //( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + // End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // Interval = 2, + // FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 is the last day of the 1st week after the Start date + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } + // }, + // Range = new RecurrenceRange() + // } + //}, + //false ), + + //( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 1st week after the Start date + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + // End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // Interval = 2, + // // FirstDayOfWeek is Sunday by default, 2023-9-3 ~ 2023-9-9 is the 1st week after the Start date + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } + // }, + // Range = new RecurrenceRange() + // } + //}, + //true ), + + //( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 4th week after the Start date + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), + // End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // Interval = 2, + // FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 1st week, 9-4 ~ 9-10 2nd week (Skipped), 9-11 ~ 9-17 3rd week, 9-18 ~ 9-24 4th week (Skipped) + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } + // }, + // Range = new RecurrenceRange() + // } + //}, + //false ), + + //( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 3rd week after the Start date + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + // End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // Interval = 2, + // // FirstDayOfWeek is Sunday by default, 2023-9-3 ~ 9-9 1st week, 9-17 ~ 9-23 3rd week + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } + // }, + // Range = new RecurrenceRange() + // } + //}, + //true ), + + //( DateTimeOffset.Parse("2023-9-17T00:00:00+08:00"), // Sunday in the 3rd week after the Start date + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + // End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // Interval = 2, + // FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 1st week, 9-11 ~ 9-17 3rd week + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // 2023-9-3, 9-11. 9-17 + // }, + // Range = new RecurrenceRange() + // { + // Type = RecurrenceRangeType.Numbered, + // NumberOfOccurrences = 3 + // } + // } + //}, + //true ), + + //( DateTimeOffset.Parse("2023-9-13T00:00:00+08:00"), // Within the recurring time window 2023-9-11T:00:00:00+08:00 ~ 2023-9-15T:00:00:00+08:00. + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + // End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // Interval = 2, + // FirstDayOfWeek = DayOfWeek.Monday, + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) + // }, + // Range = new RecurrenceRange() + // } + //}, + //true ), + + //( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrence: 2023-9-17T:00:00:00+08:00 ~ 2023-9-21T:00:00:00+08:00. + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + // End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // Interval = 2, + // FirstDayOfWeek = DayOfWeek.Monday, + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) + // }, + // Range = new RecurrenceRange() + // { + // Type = RecurrenceRangeType.Numbered, + // NumberOfOccurrences = 3 + // } + // } + //}, + //true ), + + //( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrences + //new TimeWindowFilterSettings() + //{ + // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + // End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), + // Recurrence = new Recurrence() + // { + // Pattern = new RecurrencePattern() + // { + // Type = RecurrencePatternType.Weekly, + // Interval = 2, + // FirstDayOfWeek = DayOfWeek.Monday, + // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) + // }, + // Range = new RecurrenceRange() + // { + // Type = RecurrenceRangeType.Numbered, + // NumberOfOccurrences = 2 + // } + // } + //}, + //false ) }; ConsumeEvalutationTestData(testData); @@ -1323,6 +1369,28 @@ public void MatchAbsoluteMonthlyRecurrenceTest() }, false ), + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 4th occurrence. + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.AbsoluteMonthly, + DayOfMonth = 1, + Interval = 4 // 2023-9-1, 2024-1-1, 2024-5-1, 2024-9-1 ... + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 4 + } + } + }, + true ), + ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), new TimeWindowFilterSettings() { @@ -1365,7 +1433,7 @@ public void MatchAbsoluteMonthlyRecurrenceTest() } } }, - false ), + false ) }; ConsumeEvalutationTestData(testData); @@ -1741,12 +1809,34 @@ public void MatchRelativeMonthlyRecurrenceTest() Range = new RecurrenceRange() { Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 3 + NumberOfOccurrences = 3 // 2023-9-1, 2023-10-1, 2023-11-1, 2023-12-1 ... } } }, false ), + ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // The 4th occurrence. + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.RelativeMonthly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + // Index is First by default. + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 4 // 2023-9-1, 2023-10-1, 2023-11-1, 2023-12-1 ... + } + } + }, + true ), + ( DateTimeOffset.Parse("2023-10-1T00:00:00+08:00"), // Sunday is not included in the DaysOfWeek new TimeWindowFilterSettings() { @@ -2072,11 +2162,33 @@ public void MatchRelativeYearlyRecurrenceTest() Range = new RecurrenceRange() { Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 3 + NumberOfOccurrences = 3 // 2023-9-1, 2024-9-1, 2025-9-1, 2026-9-1 ... } } }, false ), + + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 4th occurrence. + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.RelativeYearly, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, + Month = 9 + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 4 // 2023-9-1, 2024-9-1, 2025-9-1, 2026-9-1 ... + } + } + }, + true ) }; ConsumeEvalutationTestData(testData); From 25ff14f5eae386612bcdc252a0d03d197239693d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 18 Jan 2024 14:59:34 +0800 Subject: [PATCH 14/52] add comments --- .../Recurrence/RecurrenceEvaluator.cs | 53 +++++++++---------- .../RecurrenceEvaluator.cs | 2 - 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 8b2c07ff..2b57e8ca 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -334,7 +334,7 @@ private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilter DateTimeOffset start = settings.Start.Value; if (!pattern.DaysOfWeek.Any(day => - NthDayOfWeekInTheMonth(start.DateTime, pattern.Index, day) == start.Date)) + NthDayOfWeekInTheMonth(start.DateTime, pattern.Index, day) == start.Date)) { paramName = nameof(settings.Start); @@ -706,7 +706,7 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF // // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. - int numberOfInterval = (int)Math.Floor(timeGap.TotalSeconds / TimeSpan.FromDays(interval).TotalSeconds); + int numberOfInterval = (int) Math.Floor(timeGap.TotalSeconds / TimeSpan.FromDays(interval).TotalSeconds); previousOccurrence = start.AddDays(numberOfInterval * interval); @@ -827,14 +827,15 @@ private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, T int interval = pattern.Interval; - TimeSpan timeZoneOffset = start.Offset; - - DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + DateTime alignedTime = time.DateTime + start.Offset - time.Offset; int monthGap = (alignedTime.Year - start.Year) * 12 + alignedTime.Month - start.Month; if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.Day) < start.TimeOfDay + TimeSpan.FromDays(start.Day)) { + // + // E.g. start: 2023-9-1T12:00:00 and time: 2023-10-1T11:00:00 + // Not a complete month monthGap -= 1; } @@ -862,9 +863,7 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T int interval = pattern.Interval; - TimeSpan timeZoneOffset = start.Offset; - - DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + DateTime alignedTime = time.DateTime + start.Offset - time.Offset; int monthGap = (alignedTime.Year - start.Year) * 12 + alignedTime.Month - start.Month; @@ -873,7 +872,7 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T { // // E.g. start is 2023.9.1 (the first Friday in 2023.9) and current time is 2023.10.2 (the first Friday in next month is 2023.10.6) - // Not a complete monthly interval + // Not a complete month monthGap -= 1; } @@ -914,14 +913,15 @@ private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, Ti int interval = pattern.Interval; - TimeSpan timeZoneOffset = start.Offset; - - DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + DateTime alignedTime = time.DateTime + start.Offset - time.Offset; int yearGap = alignedTime.Year - start.Year; if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.DayOfYear) < start.TimeOfDay + TimeSpan.FromDays(start.DayOfYear)) { + // + // E.g. start: 2023-9-1T12:00:00 and time: 2024-9-1T11:00:00 + // Not a complete year yearGap -= 1; } @@ -947,9 +947,7 @@ private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, Ti int interval = pattern.Interval; - TimeSpan timeZoneOffset = start.Offset; - - DateTime alignedTime = time.DateTime + timeZoneOffset - time.Offset; + DateTime alignedTime = time.DateTime + start.Offset - time.Offset; int yearGap = alignedTime.Year - start.Year; @@ -957,15 +955,16 @@ private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, Ti { // // E.g. start: 2023.9 and time: 2024.8 - // Not a complete yearly interval + // Not a complete year yearGap -= 1; } - else if (alignedTime.Month == start.Month && !pattern.DaysOfWeek.Any(day => - alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + start.TimeOfDay)) + else if (alignedTime.Month == start.Month && + !pattern.DaysOfWeek.Any(day => + alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + start.TimeOfDay)) { // // E.g. start: 2023.9.1 (the first Friday in 2023.9) and time: 2024.9.2 (the first Friday in 2023.9 is 2024.9.6) - // Not a complete yearly interval + // Not a complete year yearGap -= 1; } @@ -1010,31 +1009,31 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int DateTime date = DateTime.Today.AddDays( RemainingDaysOfTheWeek(DateTime.Today.DayOfWeek, firstDayOfWeek)); - DateTime prevOccurrence = DateTime.MinValue; + DateTime prev = DateTime.MinValue; - TimeSpan minGap = TimeSpan.MaxValue; + TimeSpan minGap = TimeSpan.FromDays(DayNumberOfWeek); for (int i = 0; i < DayNumberOfWeek; i++) { if (daysOfWeek.Any(day => day == date.DayOfWeek)) { - if (prevOccurrence == DateTime.MinValue) + if (prev == DateTime.MinValue) { // - // init - prevOccurrence = date; + // Find a occurrence for the first time + prev = date; } else { - TimeSpan gap = date - prevOccurrence; + TimeSpan gap = date - prev; if (gap < minGap) { minGap = gap; } - prevOccurrence = date; + prev = date; } } @@ -1052,7 +1051,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int if (daysOfWeek.Any(day => day == date.DayOfWeek)) { - TimeSpan gap = date - prevOccurrence; + TimeSpan gap = date - prev; if (gap < minGap) { diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index 45053930..77465f0b 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -5,9 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime; using Xunit; -using Xunit.Sdk; namespace Tests.FeatureManagement { From bee871854de416f9b5c9f6c21c2815e25a896bf6 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 18 Jan 2024 15:23:51 +0800 Subject: [PATCH 15/52] update --- .../Recurrence/RecurrenceEvaluator.cs | 23 ++++++++----------- .../Recurrence/RecurrenceRange.cs | 2 +- .../Tests.FeatureManagement/appsettings.json | 4 +++- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 2b57e8ca..a630af56 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -590,13 +590,6 @@ private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings sett { paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.NumberOfOccurrences)}"; - if (settings.Recurrence.Range.NumberOfOccurrences == null) - { - reason = RequiredParameter; - - return false; - } - if (settings.Recurrence.Range.NumberOfOccurrences < 1) { reason = OutOfRange; @@ -722,8 +715,6 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF /// private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { - numberOfOccurrences = 0; - RecurrencePattern pattern = settings.Recurrence.Pattern; DateTimeOffset start = settings.Start.Value; @@ -744,6 +735,8 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow DateTimeOffset tentativePreviousOccurrence = start; + numberOfOccurrences = 0; + // // Time is not within the first interval if (remainingTimeOfFirstInterval <= timeGap) @@ -911,6 +904,8 @@ private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, Ti DateTimeOffset start = settings.Start.Value; + Debug.Assert(time >= start); + int interval = pattern.Interval; DateTime alignedTime = time.DateTime + start.Offset - time.Offset; @@ -945,6 +940,8 @@ private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, Ti DateTimeOffset start = settings.Start.Value; + Debug.Assert(time >= start); + int interval = pattern.Interval; DateTime alignedTime = time.DateTime + start.Offset - time.Offset; @@ -1041,13 +1038,11 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int } // - // It may across weeks. Check the adjacent week if the interval is one week. + // It may across weeks. Check the next week if the interval is one week. if (interval == 1) { - for (int i = 1; i <= DayNumberOfWeek; i++) + for (int i = 0; i < DayNumberOfWeek; i++) { - // - // If there are multiple day in DaysOfWeek, it will eventually enter the following if branch if (daysOfWeek.Any(day => day == date.DayOfWeek)) { @@ -1058,6 +1053,8 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int minGap = gap; } + // + // Only check the first occurrence in the next week break; } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs index cb412a2c..c0403d8d 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs @@ -23,6 +23,6 @@ public class RecurrenceRange /// /// The number of times to repeat the time window. /// - public int? NumberOfOccurrences { get; set; } + public int NumberOfOccurrences { get; set; } = 1; } } diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index f84e8ad0..9bfdfee9 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -41,7 +41,9 @@ "Thursday", "Friday", "Saturday" - ] + ], + "FirstDayOfWeek": "Monday", + "Index": "Last" }, "Range": { "Type": "NoEnd" From ce5dfae1595e6b48b50b032f220db6d9aaf76ba4 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 18 Jan 2024 16:13:01 +0800 Subject: [PATCH 16/52] fix bug & add testcases --- .../Recurrence/RecurrenceEvaluator.cs | 85 +- .../RecurrenceEvaluator.cs | 769 ++++++++++-------- 2 files changed, 454 insertions(+), 400 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index a630af56..13e4e8f5 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -12,10 +12,11 @@ static class RecurrenceEvaluator { // // Error Message - const string OutOfRange = "The value is out of the accepted range."; + const string ValueOutOfRange = "The value is out of the accepted range."; const string UnrecognizableValue = "The value is unrecognizable."; const string RequiredParameter = "Value cannot be null or empty."; - const string NotMatched = "Start date is not a valid first occurrence."; + const string StartNotMatched = "Start date is not a valid first occurrence."; + const string TimeWindowDurationOutOfRange = "Time window duration cannot be longer than how frequently it occurs"; const int DayNumberOfWeek = 7; const int MinDayNumberOfMonth = 28; @@ -127,7 +128,7 @@ private static bool TryValidateRecurrenceRequiredParameter(TimeWindowFilterSetti { paramName = nameof(settings.End); - reason = OutOfRange; + reason = ValueOutOfRange; return false; } @@ -181,6 +182,8 @@ private static bool TryValidateRecurrencePattern(TimeWindowFilterSettings settin private static bool TryValidateDailyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) { + Debug.Assert(settings.Recurrence.Pattern.Interval > 0); + TimeSpan intervalDuration = TimeSpan.FromDays(settings.Recurrence.Pattern.Interval); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -191,7 +194,7 @@ private static bool TryValidateDailyRecurrencePattern(TimeWindowFilterSettings s { paramName = $"{nameof(settings.End)}"; - reason = OutOfRange; + reason = TimeWindowDurationOutOfRange; return false; } @@ -211,17 +214,20 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings { RecurrencePattern pattern = settings.Recurrence.Pattern; + Debug.Assert(pattern.Interval > 0); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * DayNumberOfWeek); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; // // Time window duration must be shorter than how frequently it occurs - if (timeWindowDuration > intervalDuration) + if (timeWindowDuration > intervalDuration || + !IsDurationCompliantWithDaysOfWeek(timeWindowDuration, pattern.Interval, pattern.DaysOfWeek, pattern.FirstDayOfWeek)) { paramName = $"{nameof(settings.End)}"; - reason = OutOfRange; + reason = TimeWindowDurationOutOfRange; return false; } @@ -242,18 +248,7 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings { paramName = nameof(settings.Start); - reason = NotMatched; - - return false; - } - - // - // Check whether the time window duration is shorter than the minimum gap between days of week - if (!IsDurationCompliantWithDaysOfWeek(timeWindowDuration, pattern.Interval, pattern.DaysOfWeek, pattern.FirstDayOfWeek)) - { - paramName = nameof(settings.End); - - reason = OutOfRange; + reason = StartNotMatched; return false; } @@ -265,6 +260,8 @@ private static bool TryValidateAbsoluteMonthlyRecurrencePattern(TimeWindowFilter { RecurrencePattern pattern = settings.Recurrence.Pattern; + Debug.Assert(pattern.Interval > 0); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfMonth); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -275,7 +272,7 @@ private static bool TryValidateAbsoluteMonthlyRecurrencePattern(TimeWindowFilter { paramName = $"{nameof(settings.End)}"; - reason = OutOfRange; + reason = TimeWindowDurationOutOfRange; return false; } @@ -295,7 +292,7 @@ private static bool TryValidateAbsoluteMonthlyRecurrencePattern(TimeWindowFilter { paramName = nameof(settings.Start); - reason = NotMatched; + reason = StartNotMatched; return false; } @@ -307,6 +304,8 @@ private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilter { RecurrencePattern pattern = settings.Recurrence.Pattern; + Debug.Assert(pattern.Interval > 0); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfMonth); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -317,7 +316,7 @@ private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilter { paramName = $"{nameof(settings.End)}"; - reason = OutOfRange; + reason = TimeWindowDurationOutOfRange; return false; } @@ -338,7 +337,7 @@ private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilter { paramName = nameof(settings.Start); - reason = NotMatched; + reason = StartNotMatched; return false; } @@ -350,6 +349,8 @@ private static bool TryValidateAbsoluteYearlyRecurrencePattern(TimeWindowFilterS { RecurrencePattern pattern = settings.Recurrence.Pattern; + Debug.Assert(pattern.Interval > 0); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfYear); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -360,7 +361,7 @@ private static bool TryValidateAbsoluteYearlyRecurrencePattern(TimeWindowFilterS { paramName = $"{nameof(settings.End)}"; - reason = OutOfRange; + reason = TimeWindowDurationOutOfRange; return false; } @@ -385,7 +386,7 @@ private static bool TryValidateAbsoluteYearlyRecurrencePattern(TimeWindowFilterS { paramName = nameof(settings.Start); - reason = NotMatched; + reason = StartNotMatched; return false; } @@ -397,6 +398,8 @@ private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterS { RecurrencePattern pattern = settings.Recurrence.Pattern; + Debug.Assert(pattern.Interval > 0); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfYear); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -407,7 +410,7 @@ private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterS { paramName = $"{nameof(settings.End)}"; - reason = OutOfRange; + reason = TimeWindowDurationOutOfRange; return false; } @@ -434,7 +437,7 @@ private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterS { paramName = nameof(settings.Start); - reason = NotMatched; + reason = StartNotMatched; return false; } @@ -478,7 +481,7 @@ private static bool TryValidateInterval(TimeWindowFilterSettings settings, out s if (settings.Recurrence.Pattern.Interval <= 0) { - reason = OutOfRange; + reason = ValueOutOfRange; return false; } @@ -517,7 +520,7 @@ private static bool TryValidateDayOfMonth(TimeWindowFilterSettings settings, out if (settings.Recurrence.Pattern.DayOfMonth < 1 || settings.Recurrence.Pattern.DayOfMonth > 31) { - reason = OutOfRange; + reason = ValueOutOfRange; return false; } @@ -540,7 +543,7 @@ private static bool TryValidateMonth(TimeWindowFilterSettings settings, out stri if (settings.Recurrence.Pattern.Month < 1 || settings.Recurrence.Pattern.Month > 12) { - reason = OutOfRange; + reason = ValueOutOfRange; return false; } @@ -576,7 +579,7 @@ private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out st if (endDate < start) { - reason = OutOfRange; + reason = ValueOutOfRange; return false; } @@ -592,7 +595,7 @@ private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings sett if (settings.Recurrence.Range.NumberOfOccurrences < 1) { - reason = OutOfRange; + reason = ValueOutOfRange; return false; } @@ -780,17 +783,8 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow // // Check the following days of the first week if time is still within the first interval // Otherwise, check the first week of the latest interval - tentativePreviousOccurrence += TimeSpan.FromDays(1); - while (tentativePreviousOccurrence <= time) { - if (tentativePreviousOccurrence.DayOfWeek == pattern.FirstDayOfWeek) - { - // - // It comes to the next week, so break. - break; - } - if (pattern.DaysOfWeek.Any(day => day == tentativePreviousOccurrence.DayOfWeek)) { @@ -800,7 +794,16 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow } tentativePreviousOccurrence += TimeSpan.FromDays(1); + + if (tentativePreviousOccurrence.DayOfWeek == pattern.FirstDayOfWeek) + { + // + // It comes to the next week, so break. + break; + } } + + Console.WriteLine("YES"); } /// @@ -997,6 +1000,8 @@ private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, Ti /// True if the duration is compliant with days of week, false otherwise. private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int interval, IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) { + Debug.Assert(interval > 0); + if (daysOfWeek.Count() == 1) { return true; diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index 77465f0b..8068e66c 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -11,10 +11,11 @@ namespace Tests.FeatureManagement { class ErrorMessage { - public const string OutOfRange = "The value is out of the accepted range."; + public const string ValueOutOfRange = "The value is out of the accepted range."; public const string UnrecognizableValue = "The value is unrecognizable."; public const string RequiredParameter = "Value cannot be null or empty."; - public const string NotMatched = "Start date is not a valid first occurrence."; + public const string StartNotMatched = "Start date is not a valid first occurrence."; + public const string TimeWindowDurationOutOfRange = "Time window duration cannot be longer than how frequently it occurs"; } class ParamName @@ -133,7 +134,7 @@ public void InvalidValueTest() } }, ParamName.Interval, - ErrorMessage.OutOfRange ), + ErrorMessage.ValueOutOfRange ), ( new TimeWindowFilterSettings() { @@ -150,7 +151,7 @@ public void InvalidValueTest() } }, ParamName.DayOfMonth, - ErrorMessage.OutOfRange ), + ErrorMessage.ValueOutOfRange ), ( new TimeWindowFilterSettings() { @@ -167,7 +168,7 @@ public void InvalidValueTest() } }, ParamName.DayOfMonth, - ErrorMessage.OutOfRange ), + ErrorMessage.ValueOutOfRange ), ( new TimeWindowFilterSettings() { @@ -185,7 +186,7 @@ public void InvalidValueTest() } }, ParamName.Month, - ErrorMessage.OutOfRange ), + ErrorMessage.ValueOutOfRange ), ( new TimeWindowFilterSettings() { @@ -203,7 +204,7 @@ public void InvalidValueTest() } }, ParamName.Month, - ErrorMessage.OutOfRange ), + ErrorMessage.ValueOutOfRange ), ( new TimeWindowFilterSettings() { @@ -220,7 +221,24 @@ public void InvalidValueTest() } }, ParamName.NumberOfOccurrences, - ErrorMessage.OutOfRange ) + ErrorMessage.ValueOutOfRange ), + + ( new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern(), + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.EndDate, + EndDate = DateTimeOffset.Parse("2023-8-31T23:59:59+08:00") // EndDate is earlier than the Start + } + } + }, + ParamName.EndDate, + ErrorMessage.ValueOutOfRange ) }; ConsumeValidationTestData(testData); @@ -242,7 +260,7 @@ public void InvalidTimeWindowTest() } }, ParamName.End, - ErrorMessage.OutOfRange ), + ErrorMessage.ValueOutOfRange ), ( new TimeWindowFilterSettings() { @@ -259,7 +277,7 @@ public void InvalidTimeWindowTest() } }, ParamName.End, - ErrorMessage.OutOfRange ), + ErrorMessage.TimeWindowDurationOutOfRange ), ( new TimeWindowFilterSettings() { @@ -277,7 +295,7 @@ public void InvalidTimeWindowTest() } }, ParamName.End, - ErrorMessage.OutOfRange ), + ErrorMessage.TimeWindowDurationOutOfRange ), ( new TimeWindowFilterSettings() { @@ -289,13 +307,14 @@ public void InvalidTimeWindowTest() { Type = RecurrencePatternType.Weekly, Interval = 1, + // FirstDayOfWeek is Sunday by default DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Thursday, DayOfWeek.Sunday } }, Range = new RecurrenceRange() } }, ParamName.End, - ErrorMessage.OutOfRange ), + ErrorMessage.TimeWindowDurationOutOfRange ), ( new TimeWindowFilterSettings() { @@ -307,13 +326,14 @@ public void InvalidTimeWindowTest() { Type = RecurrencePatternType.Weekly, Interval = 1, + // FirstDayOfWeek is Sunday by default DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Saturday } // The time window duration should be shorter than 2 days because the gap between Saturday in the previous week and Monday in this week is 2 days. }, Range = new RecurrenceRange() } }, ParamName.End, - ErrorMessage.OutOfRange ), + ErrorMessage.TimeWindowDurationOutOfRange ), ( new TimeWindowFilterSettings() { @@ -331,7 +351,7 @@ public void InvalidTimeWindowTest() } }, ParamName.End, - ErrorMessage.OutOfRange ), + ErrorMessage.TimeWindowDurationOutOfRange ), ( new TimeWindowFilterSettings() { @@ -349,7 +369,7 @@ public void InvalidTimeWindowTest() } }, ParamName.End, - ErrorMessage.OutOfRange ), + ErrorMessage.TimeWindowDurationOutOfRange ), ( new TimeWindowFilterSettings() { @@ -367,7 +387,7 @@ public void InvalidTimeWindowTest() } }, ParamName.End, - ErrorMessage.OutOfRange ), + ErrorMessage.TimeWindowDurationOutOfRange ), ( new TimeWindowFilterSettings() { @@ -386,7 +406,7 @@ public void InvalidTimeWindowTest() } }, ParamName.End, - ErrorMessage.OutOfRange ), + ErrorMessage.TimeWindowDurationOutOfRange ), ( new TimeWindowFilterSettings() { @@ -405,24 +425,7 @@ public void InvalidTimeWindowTest() } }, ParamName.End, - ErrorMessage.OutOfRange ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern(), - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.EndDate, - EndDate = DateTimeOffset.Parse("2023-8-31T23:59:59+08:00") // EndDate is earlier than the Start - } - } - }, - ParamName.EndDate, - ErrorMessage.OutOfRange ) + ErrorMessage.TimeWindowDurationOutOfRange ) }; ConsumeValidationTestData(testData); @@ -450,6 +453,52 @@ public void ValidTimeWindowAcrossWeeks() // // The settings is valid. No exception should be thrown. RecurrenceEvaluator.MatchRecurrence(DateTimeOffset.Now, settings); + + settings = new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-1-15T00:00:00+08:00"), // Monday + End = DateTimeOffset.Parse("2024-1-19T00:00:00+08:00"), // Time window duration is 4 days + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, // The interval is larger than one week, there is no across-week issue + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + } + }; + + // + // The settings is valid. No exception should be thrown. + RecurrenceEvaluator.MatchRecurrence(DateTimeOffset.Now, settings); + + settings = new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-1-15T00:00:00+08:00"), // Monday + End = DateTimeOffset.Parse("2024-1-19T00:00:00+08:00"), // Time window duration is 4 days + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 1, + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + } + }; + + // + // The settings is invalid, since we change the interval to 1. + Assert.Throws( + () => + { + RecurrenceEvaluator.MatchRecurrence(DateTimeOffset.Now, settings); + }); } [Fact] @@ -498,7 +547,7 @@ public void WeeklyPatternNotMatchTest() } }, ParamName.Start, - ErrorMessage.NotMatched ) + ErrorMessage.StartNotMatched ) }; ConsumeValidationTestData(testData); @@ -549,7 +598,7 @@ public void AbsoluteMonthlyPatternNotMatchTest() } }, ParamName.Start, - ErrorMessage.NotMatched ) + ErrorMessage.StartNotMatched ) }; ConsumeValidationTestData(testData); @@ -602,7 +651,7 @@ public void RelativeMonthlyPatternNotMatchTest() } }, ParamName.Start, - ErrorMessage.NotMatched ) + ErrorMessage.StartNotMatched ) }; ConsumeValidationTestData(testData); @@ -672,7 +721,7 @@ public void AbsoluteYearlyPatternNotMatchTest() } }, ParamName.Start, - ErrorMessage.NotMatched ) + ErrorMessage.StartNotMatched ) }; ConsumeValidationTestData(testData); @@ -743,7 +792,7 @@ public void RelativeYearlyPatternNotMatchTest() } }, ParamName.Start, - ErrorMessage.NotMatched ), + ErrorMessage.StartNotMatched ), ( new TimeWindowFilterSettings() { @@ -762,7 +811,7 @@ public void RelativeYearlyPatternNotMatchTest() } }, ParamName.Start, - ErrorMessage.NotMatched ) + ErrorMessage.StartNotMatched ) }; ConsumeValidationTestData(testData); @@ -903,162 +952,162 @@ public void MatchWeeklyRecurrenceTest() { var testData = new List>() { - //( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // // FirstDayOfWeek is Sunday by default. - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } - // }, - // Range = new RecurrenceRange() - // } - //}, - //true ), - - //( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // Friday in the 2nd week after the Start date - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // Interval = 2, - // DaysOfWeek = new List(){ DayOfWeek.Friday } - // }, - // Range = new RecurrenceRange() - // } - //}, - //false ), - - //( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // Friday in the 3rd week after the Start date - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // Interval = 2, - // DaysOfWeek = new List(){ DayOfWeek.Friday } - // }, - // Range = new RecurrenceRange() - // } - //}, - //true ), - - //( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday is not included in DaysOfWeek - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // DaysOfWeek = new List(){ DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - // }, - // Range = new RecurrenceRange() - // } - //}, - //false ), - - //( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), // The 2nd occurrence. - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - // }, - // Range = new RecurrenceRange() - // { - // Type = RecurrenceRangeType.Numbered, - // NumberOfOccurrences = 1 - // } - // } - //}, - //false ), - - //( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - // }, - // Range = new RecurrenceRange() - // { - // Type = RecurrenceRangeType.Numbered, - // NumberOfOccurrences = 7 - // } - // } - //}, - //false ), - - //( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - // End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - // }, - // Range = new RecurrenceRange() - // { - // Type = RecurrenceRangeType.Numbered, - // NumberOfOccurrences = 8 - // } - // } - //}, - //true ), - - //( DateTimeOffset.Parse("2024-1-18T00:30:00+08:00"), // The 4th occurence. - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2024-1-4T00:00:00+08:00"), // Thursday - // End = DateTimeOffset.Parse("2024-1-4T01:00:00+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // // FirstDayOfWeek is Sunday by default. - // DaysOfWeek = new List(){ DayOfWeek.Tuesday, DayOfWeek.Thursday, DayOfWeek.Friday}, - // Interval = 2 - // }, - // Range = new RecurrenceRange() - // { - // Type = RecurrenceRangeType.Numbered, - // NumberOfOccurrences = 3 - // } - // } - //}, - //false ), + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } + }, + Range = new RecurrenceRange() + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // Friday in the 2nd week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + DaysOfWeek = new List(){ DayOfWeek.Friday } + }, + Range = new RecurrenceRange() + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // Friday in the 3rd week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + DaysOfWeek = new List(){ DayOfWeek.Friday } + }, + Range = new RecurrenceRange() + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday is not included in DaysOfWeek + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), // The 2nd occurrence. + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 1 + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 7 + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 8 + } + } + }, + true ), + + ( DateTimeOffset.Parse("2024-1-18T00:30:00+08:00"), // The 4th occurence. + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-1-4T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-1-4T01:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Tuesday, DayOfWeek.Thursday, DayOfWeek.Friday}, + Interval = 2 + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 3 + } + } + }, + false ), ( DateTimeOffset.Parse("2024-1-18T00:30:00+08:00"), // The 4th occurence. new TimeWindowFilterSettings() @@ -1083,169 +1132,169 @@ public void MatchWeeklyRecurrenceTest() }, true ), - //( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - // End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // Interval = 2, - // FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 is the last day of the 1st week after the Start date - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } - // }, - // Range = new RecurrenceRange() - // } - //}, - //false ), - - //( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 1st week after the Start date - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - // End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // Interval = 2, - // // FirstDayOfWeek is Sunday by default, 2023-9-3 ~ 2023-9-9 is the 1st week after the Start date - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } - // }, - // Range = new RecurrenceRange() - // } - //}, - //true ), - - //( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 4th week after the Start date - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), - // End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // Interval = 2, - // FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 1st week, 9-4 ~ 9-10 2nd week (Skipped), 9-11 ~ 9-17 3rd week, 9-18 ~ 9-24 4th week (Skipped) - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } - // }, - // Range = new RecurrenceRange() - // } - //}, - //false ), - - //( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 3rd week after the Start date - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - // End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // Interval = 2, - // // FirstDayOfWeek is Sunday by default, 2023-9-3 ~ 9-9 1st week, 9-17 ~ 9-23 3rd week - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } - // }, - // Range = new RecurrenceRange() - // } - //}, - //true ), - - //( DateTimeOffset.Parse("2023-9-17T00:00:00+08:00"), // Sunday in the 3rd week after the Start date - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - // End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // Interval = 2, - // FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 1st week, 9-11 ~ 9-17 3rd week - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // 2023-9-3, 9-11. 9-17 - // }, - // Range = new RecurrenceRange() - // { - // Type = RecurrenceRangeType.Numbered, - // NumberOfOccurrences = 3 - // } - // } - //}, - //true ), - - //( DateTimeOffset.Parse("2023-9-13T00:00:00+08:00"), // Within the recurring time window 2023-9-11T:00:00:00+08:00 ~ 2023-9-15T:00:00:00+08:00. - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - // End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // Interval = 2, - // FirstDayOfWeek = DayOfWeek.Monday, - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) - // }, - // Range = new RecurrenceRange() - // } - //}, - //true ), - - //( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrence: 2023-9-17T:00:00:00+08:00 ~ 2023-9-21T:00:00:00+08:00. - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - // End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // Interval = 2, - // FirstDayOfWeek = DayOfWeek.Monday, - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) - // }, - // Range = new RecurrenceRange() - // { - // Type = RecurrenceRangeType.Numbered, - // NumberOfOccurrences = 3 - // } - // } - //}, - //true ), - - //( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrences - //new TimeWindowFilterSettings() - //{ - // Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday - // End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), - // Recurrence = new Recurrence() - // { - // Pattern = new RecurrencePattern() - // { - // Type = RecurrencePatternType.Weekly, - // Interval = 2, - // FirstDayOfWeek = DayOfWeek.Monday, - // DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) - // }, - // Range = new RecurrenceRange() - // { - // Type = RecurrenceRangeType.Numbered, - // NumberOfOccurrences = 2 - // } - // } - //}, - //false ) + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 is the last day of the 1st week after the Start date + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 1st week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + // FirstDayOfWeek is Sunday by default, 2023-9-3 ~ 2023-9-9 is the 1st week after the Start date + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 4th week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 1st week, 9-4 ~ 9-10 2nd week (Skipped), 9-11 ~ 9-17 3rd week, 9-18 ~ 9-24 4th week (Skipped) + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-18T00:00:00+08:00"), // Monday in the 3rd week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + // FirstDayOfWeek is Sunday by default, 2023-9-3 ~ 9-9 1st week, 9-17 ~ 9-23 3rd week + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-17T00:00:00+08:00"), // Sunday in the 3rd week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-3T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, // 2023-9-3 1st week, 9-11 ~ 9-17 3rd week + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // 2023-9-3, 9-11. 9-17 + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 3 + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-13T00:00:00+08:00"), // Within the recurring time window 2023-9-11T:00:00:00+08:00 ~ 2023-9-15T:00:00:00+08:00. + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) + }, + Range = new RecurrenceRange() + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrence: 2023-9-17T:00:00:00+08:00 ~ 2023-9-21T:00:00:00+08:00. + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 3 + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrences + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 2 + } + } + }, + false ) }; ConsumeEvalutationTestData(testData); From 724292b5a7a378ab49947daa67ab9f2fded209e6 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 18 Jan 2024 16:32:17 +0800 Subject: [PATCH 17/52] update --- .../Recurrence/RecurrenceEvaluator.cs | 2 - .../RecurrenceEvaluator.cs | 110 ++++++++++++------ 2 files changed, 76 insertions(+), 36 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 13e4e8f5..d277a020 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -802,8 +802,6 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow break; } } - - Console.WriteLine("YES"); } /// diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index 8068e66c..fa1b938f 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -233,7 +233,7 @@ public void InvalidValueTest() Range = new RecurrenceRange() { Type = RecurrenceRangeType.EndDate, - EndDate = DateTimeOffset.Parse("2023-8-31T23:59:59+08:00") // EndDate is earlier than the Start + EndDate = DateTimeOffset.Parse("2023-8-31T23:59:59+08:00") // EndDate is earlier than the Start. } } }, @@ -437,7 +437,7 @@ public void ValidTimeWindowAcrossWeeks() var settings = new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2024-1-16T00:00:00+08:00"), // Tuesday - End = DateTimeOffset.Parse("2024-1-19T00:00:00+08:00"), // Time window duration is 3 days + End = DateTimeOffset.Parse("2024-1-19T00:00:00+08:00"), // Time window duration is 3 days. Recurrence = new Recurrence() { Pattern = new RecurrencePattern() @@ -457,13 +457,13 @@ public void ValidTimeWindowAcrossWeeks() settings = new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2024-1-15T00:00:00+08:00"), // Monday - End = DateTimeOffset.Parse("2024-1-19T00:00:00+08:00"), // Time window duration is 4 days + End = DateTimeOffset.Parse("2024-1-19T00:00:00+08:00"), // Time window duration is 4 days. Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { Type = RecurrencePatternType.Weekly, - Interval = 2, // The interval is larger than one week, there is no across-week issue + Interval = 2, // The interval is larger than one week, there is no across-week issue. FirstDayOfWeek = DayOfWeek.Monday, DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Sunday } }, @@ -478,7 +478,7 @@ public void ValidTimeWindowAcrossWeeks() settings = new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2024-1-15T00:00:00+08:00"), // Monday - End = DateTimeOffset.Parse("2024-1-19T00:00:00+08:00"), // Time window duration is 4 days + End = DateTimeOffset.Parse("2024-1-19T00:00:00+08:00"), // Time window duration is 4 days. Recurrence = new Recurrence() { Pattern = new RecurrencePattern() @@ -855,7 +855,7 @@ public void MatchDailyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2023-9-5T00:00:00+08:00"), // Within the recurring time window 2023-9-5T00:00:00+08:00 ~ 2023-9-7T00:00:00+08:00. + ( DateTimeOffset.Parse("2023-9-5T00:00:00+08:00"), // Within the recurring time window 2023-9-5T00:00:00+08:00 ~ 2023-9-7T00:00:00+08:00 new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -872,7 +872,7 @@ public void MatchDailyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-9-6T00:00:00+08:00"), // Within the recurring time window 2023-9-5T00:00:00+08:00 ~ 2023-9-7T00:00:00+08:00. + ( DateTimeOffset.Parse("2023-9-6T00:00:00+08:00"), // Within the recurring time window 2023-9-5T00:00:00+08:00 ~ 2023-9-7T00:00:00+08:00 new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -889,7 +889,7 @@ public void MatchDailyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-9-9T00:00:00+08:00"), // Within the recurring time window 2023-9-9T00:00:00+08:00 ~ 2023-9-11T00:00:00+08:00. + ( DateTimeOffset.Parse("2023-9-9T00:00:00+08:00"), // Within the recurring time window 2023-9-9T00:00:00+08:00 ~ 2023-9-11T00:00:00+08:00 new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -906,7 +906,7 @@ public void MatchDailyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Within the recurring time window 2023-9-3T00:00:00+08:00 ~ 2023-9-31T00:00:01+08:00. + ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Within the recurring time window 2023-9-3T00:00:00+08:00 ~ 2023-9-31T00:00:01+08:00 new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -923,7 +923,7 @@ public void MatchDailyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // The third occurrence. + ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // The third occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1006,7 +1006,7 @@ public void MatchWeeklyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday is not included in DaysOfWeek + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday is not included in DaysOfWeek. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday @@ -1023,7 +1023,7 @@ public void MatchWeeklyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), // The 2nd occurrence. + ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), // The 2nd occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1044,7 +1044,49 @@ public void MatchWeeklyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. + ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // The 3rd occurrence + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 2 + } + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // The 3rd occurrence + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 3 + } + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday @@ -1065,7 +1107,7 @@ public void MatchWeeklyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence. + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // The 8th occurence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday @@ -1086,7 +1128,7 @@ public void MatchWeeklyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2024-1-18T00:30:00+08:00"), // The 4th occurence. + ( DateTimeOffset.Parse("2024-1-18T00:30:00+08:00"), // The 4th occurence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2024-1-4T00:00:00+08:00"), // Thursday @@ -1109,7 +1151,7 @@ public void MatchWeeklyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2024-1-18T00:30:00+08:00"), // The 4th occurence. + ( DateTimeOffset.Parse("2024-1-18T00:30:00+08:00"), // The 4th occurence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2024-1-4T00:00:00+08:00"), // Thursday @@ -1231,7 +1273,7 @@ public void MatchWeeklyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-9-13T00:00:00+08:00"), // Within the recurring time window 2023-9-11T:00:00:00+08:00 ~ 2023-9-15T:00:00:00+08:00. + ( DateTimeOffset.Parse("2023-9-13T00:00:00+08:00"), // Within the recurring time window 2023-9-11T:00:00:00+08:00 ~ 2023-9-15T:00:00:00+08:00 new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday @@ -1250,7 +1292,7 @@ public void MatchWeeklyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrence: 2023-9-17T:00:00:00+08:00 ~ 2023-9-21T:00:00:00+08:00. + ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrence: 2023-9-17T:00:00:00+08:00 ~ 2023-9-21T:00:00:00+08:00 new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday @@ -1273,7 +1315,7 @@ public void MatchWeeklyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrences + ( DateTimeOffset.Parse("2023-9-19T00:00:00+08:00"), // The 3rd occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday @@ -1394,7 +1436,7 @@ public void MatchAbsoluteMonthlyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 4th occurrence. + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 4th occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1416,7 +1458,7 @@ public void MatchAbsoluteMonthlyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 4th occurrence. + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 4th occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1509,7 +1551,7 @@ public void MatchRelativeMonthlyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 2nd Friday in 2023 Sep + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 2nd Friday in 2023 Sep new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep @@ -1762,7 +1804,7 @@ public void MatchRelativeMonthlyRecurrenceTest() Range = new RecurrenceRange() } }, - false ), // The time window will only occur on either 1st Monday or 1st Friday, the 1st Monday of 2023 Oct is 10.2 + false ), // The time window will only occur on either 1st Monday or 1st Friday, the 1st Monday of 2023 Oct is 10.2 . ( DateTimeOffset.Parse("2023-11-3T00:00:00+08:00"), // 1st Friday in 2023 Nov new TimeWindowFilterSettings() @@ -1800,7 +1842,7 @@ public void MatchRelativeMonthlyRecurrenceTest() Range = new RecurrenceRange() } }, - false ), // The time window will only occur on either 1st Monday or 1st Friday, the 1st Monday of 2023 Nov is 11.3 + false ), // The time window will only occur on either 1st Monday or 1st Friday, the 1st Monday of 2023 Nov is 11.3. ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // the first day of the month new TimeWindowFilterSettings() @@ -1840,7 +1882,7 @@ public void MatchRelativeMonthlyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // The 4th occurrence. + ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // The 4th occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1862,7 +1904,7 @@ public void MatchRelativeMonthlyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // The 4th occurrence. + ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // The 4th occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1884,7 +1926,7 @@ public void MatchRelativeMonthlyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2023-10-1T00:00:00+08:00"), // Sunday is not included in the DaysOfWeek + ( DateTimeOffset.Parse("2023-10-1T00:00:00+08:00"), // Sunday is not included in the DaysOfWeek. new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1920,7 +1962,7 @@ public void MatchRelativeMonthlyRecurrenceTest() }, true ), // 2023-10-1 is Sunday which is not included in the DaysOfWeek. - ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // 1st Monday, + ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // 1st Monday new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -1983,7 +2025,7 @@ public void MatchAbsoluteYearlyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 2nd occurrence. + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 2nd occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2005,7 +2047,7 @@ public void MatchAbsoluteYearlyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 2nd occurrence. + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 2nd occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2028,7 +2070,7 @@ public void MatchAbsoluteYearlyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2029-9-1T00:00:00+08:00"), // The 3rd occurrence. + ( DateTimeOffset.Parse("2029-9-1T00:00:00+08:00"), // The 3rd occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2174,7 +2216,7 @@ public void MatchRelativeYearlyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // the first day of Sep + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The first day of Sep new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2193,7 +2235,7 @@ public void MatchRelativeYearlyRecurrenceTest() }, true ), - ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 4th occurrence. + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 4th occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -2215,7 +2257,7 @@ public void MatchRelativeYearlyRecurrenceTest() }, false ), - ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 4th occurrence. + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 4th occurrence new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), From 63c7343ba16851a4e4471cad1b473a77538267b1 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 18 Jan 2024 17:14:59 +0800 Subject: [PATCH 18/52] update comment --- .../FeatureFilters/Recurrence/RecurrenceEvaluator.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index d277a020..441aaca8 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -686,7 +686,7 @@ private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilt /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { @@ -714,7 +714,7 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of recurring days of week which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { @@ -809,7 +809,7 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { @@ -845,7 +845,7 @@ private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, T /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { @@ -897,7 +897,7 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { @@ -933,7 +933,7 @@ private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, Ti /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { From d78d844b84220f3bc59038184528af8779c774a2 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 18 Jan 2024 23:43:46 +0800 Subject: [PATCH 19/52] test --- .../FeatureFilters/Recurrence/RecurrenceEvaluator.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index d277a020..441aaca8 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -686,7 +686,7 @@ private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilt /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { @@ -714,7 +714,7 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of recurring days of week which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { @@ -809,7 +809,7 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { @@ -845,7 +845,7 @@ private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, T /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { @@ -897,7 +897,7 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { @@ -933,7 +933,7 @@ private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, Ti /// A time stamp. /// The settings of time window filter. /// The closest previous occurrence. - /// The number of complete recurrence intervals which have occurred between the time and the recurrence start. + /// The number of occurrences between the time and the recurrence start. /// private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { From 2136562a01cd120b41f396bbf677f0b851ee9aa4 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 22 Jan 2024 11:03:43 +0800 Subject: [PATCH 20/52] update comments --- .../Recurrence/RecurrenceEvaluator.cs | 36 +++++++++---------- .../FeatureFilters/TimeWindowFilter.cs | 4 +-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 441aaca8..c4f2c41b 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -18,9 +18,9 @@ static class RecurrenceEvaluator const string StartNotMatched = "Start date is not a valid first occurrence."; const string TimeWindowDurationOutOfRange = "Time window duration cannot be longer than how frequently it occurs"; - const int DayNumberOfWeek = 7; - const int MinDayNumberOfMonth = 28; - const int MinDayNumberOfYear = 365; + const int DaysPerWeek = 7; + const int MinDaysPerMonth = 28; + const int MinDaysPerYear = 365; /// /// Checks if a provided timestamp is within any recurring time window specified by the Recurrence section in the time window filter settings. @@ -216,7 +216,7 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings Debug.Assert(pattern.Interval > 0); - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * DayNumberOfWeek); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * DaysPerWeek); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -262,7 +262,7 @@ private static bool TryValidateAbsoluteMonthlyRecurrencePattern(TimeWindowFilter Debug.Assert(pattern.Interval > 0); - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfMonth); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDaysPerMonth); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -306,7 +306,7 @@ private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilter Debug.Assert(pattern.Interval > 0); - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfMonth); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDaysPerMonth); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -351,7 +351,7 @@ private static bool TryValidateAbsoluteYearlyRecurrencePattern(TimeWindowFilterS Debug.Assert(pattern.Interval > 0); - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfYear); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDaysPerYear); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -400,7 +400,7 @@ private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterS Debug.Assert(pattern.Interval > 0); - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDayNumberOfYear); + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDaysPerYear); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -732,7 +732,7 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow TimeSpan remainingTimeOfFirstWeek = TimeSpan.FromDays(remainingDaysOfFirstWeek) - start.TimeOfDay; - TimeSpan remaingTimeOfFirstIntervalAfterFirstWeek = TimeSpan.FromDays((interval - 1) * DayNumberOfWeek); + TimeSpan remaingTimeOfFirstIntervalAfterFirstWeek = TimeSpan.FromDays((interval - 1) * DaysPerWeek); TimeSpan remainingTimeOfFirstInterval = remainingTimeOfFirstWeek + remaingTimeOfFirstIntervalAfterFirstWeek; @@ -765,11 +765,11 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow // // The number of intervals between the first and the latest intervals (not inclusive) // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. - int numberOfInterval = (int) Math.Floor((timeGap - remainingTimeOfFirstInterval).TotalSeconds / TimeSpan.FromDays(interval * DayNumberOfWeek).TotalSeconds); + int numberOfInterval = (int) Math.Floor((timeGap - remainingTimeOfFirstInterval).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds); // // Shift the tentative previous occurrence to the first day of the first week of the latest interval - tentativePreviousOccurrence += TimeSpan.FromDays(numberOfInterval * interval * DayNumberOfWeek); + tentativePreviousOccurrence += TimeSpan.FromDays(numberOfInterval * interval * DaysPerWeek); // // Add the occurrence in the intervals between the first and the latest intervals (not inclusive) @@ -1011,9 +1011,9 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int DateTime prev = DateTime.MinValue; - TimeSpan minGap = TimeSpan.FromDays(DayNumberOfWeek); + TimeSpan minGap = TimeSpan.FromDays(DaysPerWeek); - for (int i = 0; i < DayNumberOfWeek; i++) + for (int i = 0; i < DaysPerWeek; i++) { if (daysOfWeek.Any(day => day == date.DayOfWeek)) @@ -1044,7 +1044,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int // It may across weeks. Check the next week if the interval is one week. if (interval == 1) { - for (int i = 0; i < DayNumberOfWeek; i++) + for (int i = 0; i < DaysPerWeek; i++) { if (daysOfWeek.Any(day => day == date.DayOfWeek)) @@ -1078,7 +1078,7 @@ private static int RemainingDaysOfTheWeek(DayOfWeek dayOfWeek, DayOfWeek firstDa } else { - return DayNumberOfWeek - remainingDays; + return DaysPerWeek - remainingDays; } } @@ -1100,15 +1100,15 @@ private static DateTime NthDayOfWeekInTheMonth(DateTime dateTime, WeekIndex inde date += TimeSpan.FromDays(1); } - if (date.AddDays(DayNumberOfWeek * (int) index).Month == dateTime.Month) + if (date.AddDays(DaysPerWeek * (int) index).Month == dateTime.Month) { - date += TimeSpan.FromDays(DayNumberOfWeek * (int) index); + date += TimeSpan.FromDays(DaysPerWeek * (int) index); } else // There is no the 5th day of week in the month { // // Add 3 weeks to reach the fourth day of week in the month - date += TimeSpan.FromDays(DayNumberOfWeek * 3); + date += TimeSpan.FromDays(DaysPerWeek * 3); } return date; diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index aec94fec..8c3272a9 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -10,7 +10,7 @@ namespace Microsoft.FeatureManagement.FeatureFilters { /// /// A feature filter that can be used to activate a feature based on a time window. - /// The time window filter supports recurrence settings. The time window can occur repeatedly. + /// The time window can be configured to recur periodically. /// [FilterAlias(Alias)] public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder @@ -38,7 +38,7 @@ public object BindParameters(IConfiguration filterParameters) } /// - /// Evaluates whether a feature is enabled based on a configurable fixed time window or recurring time windows. + /// Evaluates whether a feature is enabled based on the specifed in the configuration. /// /// The feature evaluation context. /// True if the feature is enabled, false otherwise. From ab9961cdc2df6effd2fb453b1767b617b8eb65ef Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 23 Jan 2024 12:52:35 +0800 Subject: [PATCH 21/52] update --- .../Recurrence/RecurrenceEvaluator.cs | 38 ++++++++++--------- .../Recurrence/RecurrencePatternType.cs | 2 +- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index c4f2c41b..0779b064 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -25,9 +25,9 @@ static class RecurrenceEvaluator /// /// Checks if a provided timestamp is within any recurring time window specified by the Recurrence section in the time window filter settings. /// If the time window filter has an invalid recurrence setting, an exception will be thrown. - /// A time stamp. + /// A timestamp. /// The settings of time window filter. - /// True if the time stamp is within any recurring time window, false otherwise. + /// True if the timestamp is within any recurring time window, false otherwise. /// public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings settings) { @@ -606,8 +606,8 @@ private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings sett } /// - /// Try to find the closest previous recurrence occurrence before the provided time stamp according to the recurrence pattern. - /// A time stamp. + /// Try to find the closest previous recurrence occurrence before the provided timestamp according to the recurrence pattern. + /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. /// True if the closest previous occurrence is within the recurrence range, false otherwise. @@ -682,8 +682,8 @@ private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilt } /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "Daily" recurrence pattern. - /// A time stamp. + /// Find the closest previous recurrence occurrence before the provided timestamp according to the "Daily" recurrence pattern. + /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. /// The number of occurrences between the time and the recurrence start. @@ -710,8 +710,8 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF } /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "Weekly" recurrence pattern. - /// A time stamp. + /// Find the closest previous recurrence occurrence before the provided timestamp according to the "Weekly" recurrence pattern. + /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. /// The number of occurrences between the time and the recurrence start. @@ -805,8 +805,8 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow } /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "AbsoluteMonthly" recurrence pattern. - /// A time stamp. + /// Find the closest previous recurrence occurrence before the provided timestamp according to the "AbsoluteMonthly" recurrence pattern. + /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. /// The number of occurrences between the time and the recurrence start. @@ -841,8 +841,8 @@ private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, T } /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "RelativeMonthly" recurrence pattern. - /// A time stamp. + /// Find the closest previous recurrence occurrence before the provided timestamp according to the "RelativeMonthly" recurrence pattern. + /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. /// The number of occurrences between the time and the recurrence start. @@ -893,8 +893,8 @@ private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, T } /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "AbsoluteYearly" recurrence pattern. - /// A time stamp. + /// Find the closest previous recurrence occurrence before the provided timestamp according to the "AbsoluteYearly" recurrence pattern. + /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. /// The number of occurrences between the time and the recurrence start. @@ -929,8 +929,8 @@ private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, Ti } /// - /// Find the closest previous recurrence occurrence before the provided time stamp according to the "RelativeYearly" recurrence pattern. - /// A time stamp. + /// Find the closest previous recurrence occurrence before the provided timestamp according to the "RelativeYearly" recurrence pattern. + /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. /// The number of occurrences between the time and the recurrence start. @@ -1078,6 +1078,8 @@ private static int RemainingDaysOfTheWeek(DayOfWeek dayOfWeek, DayOfWeek firstDa } else { + // + // If the dayOfWeek is the firstDayOfWeek, there will be 7 days remaining in this week. return DaysPerWeek - remainingDays; } } @@ -1095,9 +1097,9 @@ private static DateTime NthDayOfWeekInTheMonth(DateTime dateTime, WeekIndex inde // // Find the first provided day of week in the month - while (date.DayOfWeek != dayOfWeek) + if (date.DayOfWeek != dayOfWeek) { - date += TimeSpan.FromDays(1); + date += TimeSpan.FromDays(RemainingDaysOfTheWeek(date.DayOfWeek, dayOfWeek)); } if (date.AddDays(DaysPerWeek * (int) index).Month == dateTime.Month) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs index 73f863c4..a45274e5 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs @@ -14,7 +14,7 @@ public enum RecurrencePatternType Daily, /// - /// The pattern where the time window will repeat on the same day or days of the week, based on the number of weeks between each set of occurrences + /// The pattern where the time window will repeat on the same day or days of the week, based on the number of weeks between each set of occurrences. /// Weekly, From 252b5c32e09b54a3874f1787afd5b549678b4c0f Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 23 Jan 2024 13:23:08 +0800 Subject: [PATCH 22/52] update --- .../Recurrence/RecurrenceEvaluator.cs | 14 +++++--------- .../FeatureFilters/Recurrence/RecurrenceRange.cs | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 0779b064..c46f5d58 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -143,6 +143,8 @@ private static bool TryValidateRecurrenceRequiredParameter(TimeWindowFilterSetti private static bool TryValidateRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) { Debug.Assert(settings != null); + Debug.Assert(settings.Start != null); + Debug.Assert(settings.End != null); Debug.Assert(settings.Recurrence != null); Debug.Assert(settings.Recurrence.Pattern != null); @@ -448,6 +450,7 @@ private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterS private static bool TryValidateRecurrenceRange(TimeWindowFilterSettings settings, out string paramName, out string reason) { Debug.Assert(settings != null); + Debug.Assert(settings.Start != null); Debug.Assert(settings.Recurrence != null); Debug.Assert(settings.Recurrence.Range != null); @@ -557,13 +560,6 @@ private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out st { paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.EndDate)}"; - if (settings.Recurrence.Range.EndDate == null) - { - reason = RequiredParameter; - - return false; - } - if (settings.Start == null) { paramName = nameof(settings.Start); @@ -575,7 +571,7 @@ private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out st DateTimeOffset start = settings.Start.Value; - DateTimeOffset endDate = settings.Recurrence.Range.EndDate.Value; + DateTimeOffset endDate = settings.Recurrence.Range.EndDate; if (endDate < start) { @@ -670,7 +666,7 @@ private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilt if (range.Type == RecurrenceRangeType.EndDate) { - return previousOccurrence <= range.EndDate.Value; + return previousOccurrence <= range.EndDate; } if (range.Type == RecurrenceRangeType.Numbered) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs index c0403d8d..8c83b320 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs @@ -18,7 +18,7 @@ public class RecurrenceRange /// /// The date to stop applying the recurrence pattern. /// - public DateTimeOffset? EndDate { get; set; } + public DateTimeOffset EndDate { get; set; } = DateTimeOffset.MaxValue; /// /// The number of times to repeat the time window. From 9fc5bcce2c34a2b0523db33d3979980b700cfe9e Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 24 Jan 2024 11:25:12 +0800 Subject: [PATCH 23/52] add testcase --- .../RecurrenceEvaluator.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index fa1b938f..f6063bc4 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -2047,6 +2047,25 @@ public void MatchAbsoluteYearlyRecurrenceTest() }, false ), + ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.AbsoluteYearly, + Interval = 2, // 2023, 2025 ... + DayOfMonth = 1, + Month = 9 + }, + Range = new RecurrenceRange() + } + }, + false ), + ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 2nd occurrence new TimeWindowFilterSettings() { @@ -2210,6 +2229,7 @@ public void MatchRelativeYearlyRecurrenceTest() Interval = 2, // 2023, 2025 ... DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, Month = 9 + // Index is First by default. }, Range = new RecurrenceRange() } From 29efb5d1e95928c1319fb794be601575d9ea3d09 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 29 Jan 2024 15:43:09 +0800 Subject: [PATCH 24/52] remove monthly/yearly recurrence pattern --- .../Recurrence/RecurrenceEvaluator.cs | 485 ------ .../Recurrence/RecurrencePattern.cs | 15 - .../Recurrence/RecurrencePatternType.cs | 22 +- .../FeatureFilters/Recurrence/WeekIndex.cs | 36 - .../RecurrenceEvaluator.cs | 1497 +---------------- 5 files changed, 65 insertions(+), 1990 deletions(-) delete mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/WeekIndex.cs diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index c46f5d58..e71015d4 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -19,8 +19,6 @@ static class RecurrenceEvaluator const string TimeWindowDurationOutOfRange = "Time window duration cannot be longer than how frequently it occurs"; const int DaysPerWeek = 7; - const int MinDaysPerMonth = 28; - const int MinDaysPerYear = 365; /// /// Checks if a provided timestamp is within any recurring time window specified by the Recurrence section in the time window filter settings. @@ -161,18 +159,6 @@ private static bool TryValidateRecurrencePattern(TimeWindowFilterSettings settin case RecurrencePatternType.Weekly: return TryValidateWeeklyRecurrencePattern(settings, out paramName, out reason); - case RecurrencePatternType.AbsoluteMonthly: - return TryValidateAbsoluteMonthlyRecurrencePattern(settings, out paramName, out reason); - - case RecurrencePatternType.RelativeMonthly: - return TryValidateRelativeMonthlyRecurrencePattern(settings, out paramName, out reason); - - case RecurrencePatternType.AbsoluteYearly: - return TryValidateAbsoluteYearlyRecurrencePattern(settings, out paramName, out reason); - - case RecurrencePatternType.RelativeYearly: - return TryValidateRelativeYearlyRecurrencePattern(settings, out paramName, out reason); - default: paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Type)}"; @@ -258,195 +244,6 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings return true; } - private static bool TryValidateAbsoluteMonthlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - Debug.Assert(pattern.Interval > 0); - - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDaysPerMonth); - - TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; - - // - // Time window duration must be shorter than how frequently it occurs - if (timeWindowDuration > intervalDuration) - { - paramName = $"{nameof(settings.End)}"; - - reason = TimeWindowDurationOutOfRange; - - return false; - } - - // - // Required parameters - if (!TryValidateDayOfMonth(settings, out paramName, out reason)) - { - return false; - } - - // - // Check whether "Start" is a valid first occurrence - DateTimeOffset start = settings.Start.Value; - - if (start.Day != pattern.DayOfMonth.Value) - { - paramName = nameof(settings.Start); - - reason = StartNotMatched; - - return false; - } - - return true; - } - - private static bool TryValidateRelativeMonthlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - Debug.Assert(pattern.Interval > 0); - - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDaysPerMonth); - - TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; - - // - // Time window duration must be shorter than how frequently it occurs - if (timeWindowDuration > intervalDuration) - { - paramName = $"{nameof(settings.End)}"; - - reason = TimeWindowDurationOutOfRange; - - return false; - } - - // - // Required parameters - if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) - { - return false; - } - - // - // Check whether "Start" is a valid first occurrence - DateTimeOffset start = settings.Start.Value; - - if (!pattern.DaysOfWeek.Any(day => - NthDayOfWeekInTheMonth(start.DateTime, pattern.Index, day) == start.Date)) - { - paramName = nameof(settings.Start); - - reason = StartNotMatched; - - return false; - } - - return true; - } - - private static bool TryValidateAbsoluteYearlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - Debug.Assert(pattern.Interval > 0); - - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDaysPerYear); - - TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; - - // - // Time window duration must be shorter than how frequently it occurs - if (timeWindowDuration > intervalDuration) - { - paramName = $"{nameof(settings.End)}"; - - reason = TimeWindowDurationOutOfRange; - - return false; - } - - // - // Required parameters - if (!TryValidateMonth(settings, out paramName, out reason)) - { - return false; - } - - if (!TryValidateDayOfMonth(settings, out paramName, out reason)) - { - return false; - } - - // - // Check whether "Start" is a valid first occurrence - DateTimeOffset start = settings.Start.Value; - - if (start.Day != pattern.DayOfMonth.Value || start.Month != pattern.Month.Value) - { - paramName = nameof(settings.Start); - - reason = StartNotMatched; - - return false; - } - - return true; - } - - private static bool TryValidateRelativeYearlyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - Debug.Assert(pattern.Interval > 0); - - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * MinDaysPerYear); - - TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; - - // - // Time window duration must be shorter than how frequently it occurs - if (timeWindowDuration > intervalDuration) - { - paramName = $"{nameof(settings.End)}"; - - reason = TimeWindowDurationOutOfRange; - - return false; - } - - // - // Required parameters - if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) - { - return false; - } - - if (!TryValidateMonth(settings, out paramName, out reason)) - { - return false; - } - - // - // Check whether "Start" is a valid first occurrence - DateTimeOffset start = settings.Start.Value; - - if (start.Month != pattern.Month.Value || - !pattern.DaysOfWeek.Any(day => - NthDayOfWeekInTheMonth(start.DateTime, pattern.Index, day) == start.Date)) - { - paramName = nameof(settings.Start); - - reason = StartNotMatched; - - return false; - } - - return true; - } - private static bool TryValidateRecurrenceRange(TimeWindowFilterSettings settings, out string paramName, out string reason) { Debug.Assert(settings != null); @@ -510,52 +307,6 @@ private static bool TryValidateDaysOfWeek(TimeWindowFilterSettings settings, out return true; } - private static bool TryValidateDayOfMonth(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DayOfMonth)}"; - - if (settings.Recurrence.Pattern.DayOfMonth == null) - { - reason = RequiredParameter; - - return false; - } - - if (settings.Recurrence.Pattern.DayOfMonth < 1 || settings.Recurrence.Pattern.DayOfMonth > 31) - { - reason = ValueOutOfRange; - - return false; - } - - reason = null; - - return true; - } - - private static bool TryValidateMonth(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Month)}"; - - if (settings.Recurrence.Pattern.Month == null) - { - reason = RequiredParameter; - - return false; - } - - if (settings.Recurrence.Pattern.Month < 1 || settings.Recurrence.Pattern.Month > 12) - { - reason = ValueOutOfRange; - - return false; - } - - reason = null; - - return true; - } - private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out string paramName, out string reason) { paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.EndDate)}"; @@ -638,26 +389,6 @@ private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilt break; - case RecurrencePatternType.AbsoluteMonthly: - FindAbsoluteMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - - break; - - case RecurrencePatternType.RelativeMonthly: - FindRelativeMonthlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - - break; - - case RecurrencePatternType.AbsoluteYearly: - FindAbsoluteYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - - break; - - case RecurrencePatternType.RelativeYearly: - FindRelativeYearlyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - - break; - default: return false; } @@ -800,190 +531,6 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow } } - /// - /// Find the closest previous recurrence occurrence before the provided timestamp according to the "AbsoluteMonthly" recurrence pattern. - /// A timestamp. - /// The settings of time window filter. - /// The closest previous occurrence. - /// The number of occurrences between the time and the recurrence start. - /// - private static void FindAbsoluteMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - DateTimeOffset start = settings.Start.Value; - - Debug.Assert(time >= start); - - int interval = pattern.Interval; - - DateTime alignedTime = time.DateTime + start.Offset - time.Offset; - - int monthGap = (alignedTime.Year - start.Year) * 12 + alignedTime.Month - start.Month; - - if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.Day) < start.TimeOfDay + TimeSpan.FromDays(start.Day)) - { - // - // E.g. start: 2023-9-1T12:00:00 and time: 2023-10-1T11:00:00 - // Not a complete month - monthGap -= 1; - } - - int numberOfInterval = monthGap / interval; - - previousOccurrence = start.AddMonths(numberOfInterval * interval); - - numberOfOccurrences = numberOfInterval + 1; - } - - /// - /// Find the closest previous recurrence occurrence before the provided timestamp according to the "RelativeMonthly" recurrence pattern. - /// A timestamp. - /// The settings of time window filter. - /// The closest previous occurrence. - /// The number of occurrences between the time and the recurrence start. - /// - private static void FindRelativeMonthlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - DateTimeOffset start = settings.Start.Value; - - Debug.Assert(time >= start); - - int interval = pattern.Interval; - - DateTime alignedTime = time.DateTime + start.Offset - time.Offset; - - int monthGap = (alignedTime.Year - start.Year) * 12 + alignedTime.Month - start.Month; - - if (!pattern.DaysOfWeek.Any(day => - alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + start.TimeOfDay)) - { - // - // E.g. start is 2023.9.1 (the first Friday in 2023.9) and current time is 2023.10.2 (the first Friday in next month is 2023.10.6) - // Not a complete month - monthGap -= 1; - } - - int numberOfInterval = monthGap / interval; - - DateTime previousOccurrenceMonth = start.AddMonths(numberOfInterval * interval).DateTime; - - // - // Find the first occurence date matched the pattern - // Only one day of week in the month will be matched - previousOccurrence = DateTimeOffset.MaxValue; - - foreach (DayOfWeek day in pattern.DaysOfWeek) - { - DateTime occurrenceDate = NthDayOfWeekInTheMonth(previousOccurrenceMonth, pattern.Index, day); - - if (occurrenceDate + start.TimeOfDay < previousOccurrence) - { - previousOccurrence = occurrenceDate + start.TimeOfDay; - } - } - - numberOfOccurrences = numberOfInterval + 1; - } - - /// - /// Find the closest previous recurrence occurrence before the provided timestamp according to the "AbsoluteYearly" recurrence pattern. - /// A timestamp. - /// The settings of time window filter. - /// The closest previous occurrence. - /// The number of occurrences between the time and the recurrence start. - /// - private static void FindAbsoluteYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - DateTimeOffset start = settings.Start.Value; - - Debug.Assert(time >= start); - - int interval = pattern.Interval; - - DateTime alignedTime = time.DateTime + start.Offset - time.Offset; - - int yearGap = alignedTime.Year - start.Year; - - if (alignedTime.TimeOfDay + TimeSpan.FromDays(alignedTime.DayOfYear) < start.TimeOfDay + TimeSpan.FromDays(start.DayOfYear)) - { - // - // E.g. start: 2023-9-1T12:00:00 and time: 2024-9-1T11:00:00 - // Not a complete year - yearGap -= 1; - } - - int numberOfInterval = yearGap / interval; - - previousOccurrence = start.AddYears(numberOfInterval * interval); - - numberOfOccurrences = numberOfInterval + 1; - } - - /// - /// Find the closest previous recurrence occurrence before the provided timestamp according to the "RelativeYearly" recurrence pattern. - /// A timestamp. - /// The settings of time window filter. - /// The closest previous occurrence. - /// The number of occurrences between the time and the recurrence start. - /// - private static void FindRelativeYearlyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - DateTimeOffset start = settings.Start.Value; - - Debug.Assert(time >= start); - - int interval = pattern.Interval; - - DateTime alignedTime = time.DateTime + start.Offset - time.Offset; - - int yearGap = alignedTime.Year - start.Year; - - if (alignedTime.Month < start.Month) - { - // - // E.g. start: 2023.9 and time: 2024.8 - // Not a complete year - yearGap -= 1; - } - else if (alignedTime.Month == start.Month && - !pattern.DaysOfWeek.Any(day => - alignedTime >= NthDayOfWeekInTheMonth(alignedTime, pattern.Index, day) + start.TimeOfDay)) - { - // - // E.g. start: 2023.9.1 (the first Friday in 2023.9) and time: 2024.9.2 (the first Friday in 2023.9 is 2024.9.6) - // Not a complete year - yearGap -= 1; - } - - int numberOfInterval = yearGap / interval; - - DateTime previousOccurrenceMonth = start.AddYears(numberOfInterval * interval).DateTime; - - // - // Find the first occurence date matched the pattern - // Only one day of week in the month will be matched - previousOccurrence = DateTime.MaxValue; - - foreach (DayOfWeek day in pattern.DaysOfWeek) - { - DateTime occurrenceDate = NthDayOfWeekInTheMonth(previousOccurrenceMonth, pattern.Index, day); - - if (occurrenceDate + start.TimeOfDay < previousOccurrence) - { - previousOccurrence = occurrenceDate + start.TimeOfDay; - } - } - - numberOfOccurrences = numberOfInterval + 1; - } - /// /// Check whether the duration is shorter than the minimum gap between recurrence of days of week. /// @@ -1079,37 +626,5 @@ private static int RemainingDaysOfTheWeek(DayOfWeek dayOfWeek, DayOfWeek firstDa return DaysPerWeek - remainingDays; } } - - /// - /// Find the nth day of week in the month of the date time. - /// - /// A date time. - /// The index of the day of week in the month. - /// The day of week. - /// The data time of the nth day of week in the month. - private static DateTime NthDayOfWeekInTheMonth(DateTime dateTime, WeekIndex index, DayOfWeek dayOfWeek) - { - var date = new DateTime(dateTime.Year, dateTime.Month, 1); - - // - // Find the first provided day of week in the month - if (date.DayOfWeek != dayOfWeek) - { - date += TimeSpan.FromDays(RemainingDaysOfTheWeek(date.DayOfWeek, dayOfWeek)); - } - - if (date.AddDays(DaysPerWeek * (int) index).Month == dateTime.Month) - { - date += TimeSpan.FromDays(DaysPerWeek * (int) index); - } - else // There is no the 5th day of week in the month - { - // - // Add 3 weeks to reach the fourth day of week in the month - date += TimeSpan.FromDays(DaysPerWeek * 3); - } - - return date; - } } } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs index 3fac86b8..5bea6b09 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs @@ -30,20 +30,5 @@ public class RecurrencePattern /// The first day of the week. /// public DayOfWeek FirstDayOfWeek { get; set; } - - /// - /// Specifies on which instance of the allowed days specified in DaysOfWeek the recurrence occurs, counted from the first instance in the month. - /// - public WeekIndex Index { get; set; } - - /// - /// The day of the month on which the time window occurs. - /// - public int? DayOfMonth { get; set; } - - /// - /// The month on which the time window occurs. - /// - public int? Month { get; set; } } } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs index a45274e5..89142f37 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePatternType.cs @@ -16,26 +16,6 @@ public enum RecurrencePatternType /// /// The pattern where the time window will repeat on the same day or days of the week, based on the number of weeks between each set of occurrences. /// - Weekly, - - /// - /// The pattern where the time window will repeat on the specified day of the month, based on the number of months between occurrences. - /// - AbsoluteMonthly, - - /// - /// The pattern where the time window will repeat on the specified days of the week, in the same relative position in the month, based on the number of months between occurrences. - /// - RelativeMonthly, - - /// - /// The pattern where the time window will repeat on the specified day and month, based on the number of years between occurrences. - /// - AbsoluteYearly, - - /// - /// The pattern where the time window will repeat on the specified days of the week, in the same relative position in a specific month of the year, based on the number of years between occurrences. - /// - RelativeYearly + Weekly } } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/WeekIndex.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/WeekIndex.cs deleted file mode 100644 index d0ef0947..00000000 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/WeekIndex.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -namespace Microsoft.FeatureManagement.FeatureFilters -{ - /// - /// The relative position in the month of the allowed days specified in DaysOfWeek of the recurrence occurs, counted from the first instance in the month. - /// - public enum WeekIndex - { - /// - /// Specifies on the first instance of the allowed day of week, counted from the first instance in the month. - /// - First, - - /// - /// Specifies on the second instance of the allowed day of week, counted from the first instance in the month. - /// - Second, - - /// - /// Specifies on the third instance of the allowed day of week, counted from the first instance in the month. - /// - Third, - - /// - /// Specifies on the fourth instance of the allowed day of week, counted from the first instance in the month. - /// - Fourth, - - /// - /// Specifies on the last instance of the allowed day of week, counted from the first instance in the month. - /// - Last - } -} diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index f6063bc4..c273ba30 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -136,76 +136,6 @@ public void InvalidValueTest() ParamName.Interval, ErrorMessage.ValueOutOfRange ), - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 0, // DayOfMonth should be smaller than 32 and larger than 0. - }, - Range = new RecurrenceRange() - } - }, - ParamName.DayOfMonth, - ErrorMessage.ValueOutOfRange ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 32, // DayOfMonth should be smaller than 32 and larger than 0. - }, - Range = new RecurrenceRange() - } - }, - ParamName.DayOfMonth, - ErrorMessage.ValueOutOfRange ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteYearly, - DayOfMonth = 1, - Month = 0 // Month should be larger than 0 and smaller than 13. - }, - Range = new RecurrenceRange() - } - }, - ParamName.Month, - ErrorMessage.ValueOutOfRange ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteYearly, - DayOfMonth = 1, - Month = 13 // Month should be larger than 0 and smaller than 13. - }, - Range = new RecurrenceRange() - } - }, - ParamName.Month, - ErrorMessage.ValueOutOfRange ), - ( new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), @@ -352,80 +282,6 @@ public void InvalidTimeWindowTest() }, ParamName.End, ErrorMessage.TimeWindowDurationOutOfRange ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-2-1T00:00:00+08:00"), // The duration of the time window is longer than how frequently it recurs. - End = DateTimeOffset.Parse("2023-3-29T00:00:01+08:00"), // This behavior is the same as the Outlook. Outlook uses 28 days as a monthly interval. - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - Interval = 2, - DayOfMonth = 1 - }, - Range = new RecurrenceRange() - } - }, - ParamName.End, - ErrorMessage.TimeWindowDurationOutOfRange ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // The duration of the time window is longer than how frequently it recurs. - End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), // This behavior is the same as the Outlook. Outlook uses 28 days as a monthly interval. - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - Interval = 1, - DaysOfWeek = new List(){ DayOfWeek.Friday } // 2023.9.1 is Friday. - }, - Range = new RecurrenceRange() - } - }, - ParamName.End, - ErrorMessage.TimeWindowDurationOutOfRange ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // The duration of the time window is longer than how frequently it recurs. - End = DateTimeOffset.Parse("2024-9-1T00:00:01+08:00"), // This behavior is the same as the Outlook. Outlook uses 365 days as a yearly interval. - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteYearly, - Interval = 1, - DayOfMonth = 1, - Month = 9 - }, - Range = new RecurrenceRange() - } - }, - ParamName.End, - ErrorMessage.TimeWindowDurationOutOfRange ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // The duration of the time window is longer than how frequently it recurs. - End = DateTimeOffset.Parse("2024-9-1T00:00:01+08:00"), // This behavior is the same as the Outlook. Outlook uses 365 days as a yearly interval. - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - Interval = 1, - DaysOfWeek = new List(){ DayOfWeek.Friday }, // 2023.9.1 is Friday. - Month = 9 - }, - Range = new RecurrenceRange() - } - }, - ParamName.End, - ErrorMessage.TimeWindowDurationOutOfRange ) }; ConsumeValidationTestData(testData); @@ -528,7 +384,7 @@ public void WeeklyPatternRequiredParameterTest() } [Fact] - public void WeeklyPatternNotMatchTest() + public void WeeklyPatternStartNotMatchedTest() { var testData = new List>() { @@ -554,11 +410,12 @@ public void WeeklyPatternNotMatchTest() } [Fact] - public void AbsoluteMonthlyPatternRequiredParameterTest() + public void MatchDailyRecurrenceTest() { - var testData = new List>() + var testData = new List>() { - ( new TimeWindowFilterSettings() + ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), + new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), @@ -566,120 +423,83 @@ public void AbsoluteMonthlyPatternRequiredParameterTest() { Pattern = new RecurrencePattern() { - Type = RecurrencePatternType.AbsoluteMonthly, + Type = RecurrencePatternType.Daily }, Range = new RecurrenceRange() } }, - ParamName.DayOfMonth, - ErrorMessage.RequiredParameter ) - }; - - ConsumeValidationTestData(testData); - } + true ), - [Fact] - public void AbsoluteMonthlyPatternNotMatchTest() - { - var testData = new List>() - { - ( new TimeWindowFilterSettings() + ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), + new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), // Start date is not a valid first occurrence. - End = DateTimeOffset.Parse("2023-9-2T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 1 + Type = RecurrencePatternType.Daily, + Interval = 2 }, Range = new RecurrenceRange() } }, - ParamName.Start, - ErrorMessage.StartNotMatched ) - }; - - ConsumeValidationTestData(testData); - } + false ), - [Fact] - public void RelativeMonthlyPatternRequiredParameterTest() - { - var testData = new List>() - { - ( new TimeWindowFilterSettings() + ( DateTimeOffset.Parse("2023-9-5T00:00:00+08:00"), // Within the recurring time window 2023-9-5T00:00:00+08:00 ~ 2023-9-7T00:00:00+08:00 + new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + End = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = Enumerable.Empty() + Type = RecurrencePatternType.Daily, + Interval = 4 }, Range = new RecurrenceRange() } }, - ParamName.DaysOfWeek, - ErrorMessage.RequiredParameter ) - }; - - ConsumeValidationTestData(testData); - } + true ), - [Fact] - public void RelativeMonthlyPatternNotMatchTest() - { - var testData = new List>() - { - ( new TimeWindowFilterSettings() + ( DateTimeOffset.Parse("2023-9-6T00:00:00+08:00"), // Within the recurring time window 2023-9-5T00:00:00+08:00 ~ 2023-9-7T00:00:00+08:00 + new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Start date is the 1st Friday in 2023 Sep, not a valid first occurrence. - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List{ DayOfWeek.Friday }, - Index = WeekIndex.Second + Type = RecurrencePatternType.Daily, + Interval = 4 }, Range = new RecurrenceRange() } }, - ParamName.Start, - ErrorMessage.StartNotMatched ) - }; - - ConsumeValidationTestData(testData); - } + true ), - [Fact] - public void AbsoluteYearlyPatternRequiredParameterTest() - { - var testData = new List>() - { - ( new TimeWindowFilterSettings() + ( DateTimeOffset.Parse("2023-9-9T00:00:00+08:00"), // Within the recurring time window 2023-9-9T00:00:00+08:00 ~ 2023-9-11T00:00:00+08:00 + new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + End = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = RecurrencePatternType.AbsoluteYearly, - DayOfMonth = 1 + Type = RecurrencePatternType.Daily, + Interval = 4 }, Range = new RecurrenceRange() } }, - ParamName.Month, - ErrorMessage.RequiredParameter ), + true ), - ( new TimeWindowFilterSettings() + ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Within the recurring time window 2023-9-3T00:00:00+08:00 ~ 2023-9-31T00:00:01+08:00 + new TimeWindowFilterSettings() { Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), @@ -687,306 +507,78 @@ public void AbsoluteYearlyPatternRequiredParameterTest() { Pattern = new RecurrencePattern() { - Type = RecurrencePatternType.AbsoluteYearly, - Month = 9 + Type = RecurrencePatternType.Daily, + Interval = 2 }, Range = new RecurrenceRange() } }, - ParamName.DayOfMonth, - ErrorMessage.RequiredParameter ) - }; - - ConsumeValidationTestData(testData); - } + true ), - [Fact] - public void AbsoluteYearlyPatternNotMatchTest() - { - var testData = new List>() - { - ( new TimeWindowFilterSettings() + ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // The third occurrence + new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Start date is not a valid first occurrence. + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = RecurrencePatternType.AbsoluteYearly, - DayOfMonth = 1, - Month = 8 + Type = RecurrencePatternType.Daily }, Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 2 + } } }, - ParamName.Start, - ErrorMessage.StartNotMatched ) + false ) }; - ConsumeValidationTestData(testData); + ConsumeEvalutationTestData(testData); } [Fact] - public void RelativeYearlyPatternRequiredParameterTest() + public void MatchWeeklyRecurrenceTest() { - var testData = new List>() + var testData = new List>() { - ( new TimeWindowFilterSettings() + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date + new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = RecurrencePatternType.RelativeYearly, - DaysOfWeek = Enumerable.Empty(), - Month = 9 + Type = RecurrencePatternType.Weekly, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } }, Range = new RecurrenceRange() } }, - ParamName.DaysOfWeek, - ErrorMessage.RequiredParameter ), + true ), - ( new TimeWindowFilterSettings() + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // Friday in the 2nd week after the Start date + new TimeWindowFilterSettings() { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), Recurrence = new Recurrence() { Pattern = new RecurrencePattern() { - Type = RecurrencePatternType.RelativeYearly, - DaysOfWeek = new List{ DayOfWeek.Friday } + Type = RecurrencePatternType.Weekly, + Interval = 2, + DaysOfWeek = new List(){ DayOfWeek.Friday } }, Range = new RecurrenceRange() } }, - ParamName.Month, - ErrorMessage.RequiredParameter ) - }; - - ConsumeValidationTestData(testData); - } - - [Fact] - public void RelativeYearlyPatternNotMatchTest() - { - var testData = new List>() - { - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Start date is the 1st Friday in 2023 Sep, not a valid first occurrence. - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - DaysOfWeek = new List{ DayOfWeek.Friday }, - Month = 8 - }, - Range = new RecurrenceRange() - } - }, - ParamName.Start, - ErrorMessage.StartNotMatched ), - - ( new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Start date is the 1st Friday in 2023 Sep, not a valid first occurrence. - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - DaysOfWeek = new List{ DayOfWeek.Friday }, - Month = 9, - Index = WeekIndex.Second - }, - Range = new RecurrenceRange() - } - }, - ParamName.Start, - ErrorMessage.StartNotMatched ) - }; - - ConsumeValidationTestData(testData); - } - - [Fact] - public void MatchDailyRecurrenceTest() - { - var testData = new List>() - { - ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Daily - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Daily, - Interval = 2 - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2023-9-5T00:00:00+08:00"), // Within the recurring time window 2023-9-5T00:00:00+08:00 ~ 2023-9-7T00:00:00+08:00 - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Daily, - Interval = 4 - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-6T00:00:00+08:00"), // Within the recurring time window 2023-9-5T00:00:00+08:00 ~ 2023-9-7T00:00:00+08:00 - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Daily, - Interval = 4 - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-9T00:00:00+08:00"), // Within the recurring time window 2023-9-9T00:00:00+08:00 ~ 2023-9-11T00:00:00+08:00 - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Daily, - Interval = 4 - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Within the recurring time window 2023-9-3T00:00:00+08:00 ~ 2023-9-31T00:00:01+08:00 - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Daily, - Interval = 2 - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // The third occurrence - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Daily - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 2 - } - } - }, - false ) - }; - - ConsumeEvalutationTestData(testData); - } - - [Fact] - public void MatchWeeklyRecurrenceTest() - { - var testData = new List>() - { - ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Monday in the 2nd week after the Start date - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - // FirstDayOfWeek is Sunday by default. - DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // Friday in the 2nd week after the Start date - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.Weekly, - Interval = 2, - DaysOfWeek = new List(){ DayOfWeek.Friday } - }, - Range = new RecurrenceRange() - } - }, - false ), + false ), ( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // Friday in the 3rd week after the Start date new TimeWindowFilterSettings() @@ -1341,966 +933,5 @@ public void MatchWeeklyRecurrenceTest() ConsumeEvalutationTestData(testData); } - - [Fact] - public void MatchAbsoluteMonthlyRecurrenceTest() - { - var testData = new List>() - { - ( DateTimeOffset.Parse("2023-10-1T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 1 - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 1, - Interval = 5 // 2023-9-1, 2024-2-1, 2024-7-1 ... - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2024-7-1T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 1, - Interval = 5 // 2023-9-1, 2024-2-1, 2024-7-1 ... - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2024-1-1T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 1, - Interval = 5 // 2023-9-1, 2024-2-1, 2024-7-1 ... - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2024-8-1T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 1, - Interval = 5 // 2023-9-1, 2024-2-1, 2024-7-1 ... - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 4th occurrence - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 1, - Interval = 4 // 2023-9-1, 2024-1-1, 2024-5-1, 2024-9-1 ... - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 3 - } - } - }, - false ), - - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 4th occurrence - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - DayOfMonth = 1, - Interval = 4 // 2023-9-1, 2024-1-1, 2024-5-1, 2024-9-1 ... - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 4 - } - } - }, - true ), - - ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-4-29T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-4-29T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - Interval = 2, - DayOfMonth = 29 - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.EndDate, - EndDate = DateTimeOffset.Parse("2024-2-29T00:00:00+08:00") - } - } - }, - true ), - - ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-4-29T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-4-29T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteMonthly, - Interval = 2, - DayOfMonth = 29 - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.EndDate, - EndDate = DateTimeOffset.Parse("2024-2-28T23:59:59+08:00") - } - } - }, - false ) - }; - - ConsumeEvalutationTestData(testData); - } - - [Fact] - public void MatchRelativeMonthlyRecurrenceTest() - { - var testData = new List>() - { - ( DateTimeOffset.Parse("2023-10-6T00:00:00+08:00"), // 1st Friday in 2023 Oct - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday }, - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 2nd Friday in 2023 Sep - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday } - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2023-10-13T00:00:00+08:00"), // 2nd Friday in 2023 Oct - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 1st Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday }, - Index = WeekIndex.Second - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-10-13T00:00:00+08:00"), // 2nd Friday in 2023 Oct - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 1st Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday }, - Index = WeekIndex.Second, - Interval = 3 // 2023-9, 2023-12 ... - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2023-12-8T00:00:00+08:00"), // 2nd Friday in 2023 Dec - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 2nd Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday }, - Index = WeekIndex.Second, - Interval = 3 // 2023-9, 2023-12 ... - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-15T00:00:00+08:00"), // 3rd Friday in 2023 Sep - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // 2nd Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-8T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday }, - Index = WeekIndex.Second - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2023-10-27T00:00:00+08:00"), // 4th Friday in 2023 Oct - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), // 2nd Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday }, - Index = WeekIndex.Last - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-11-24T00:00:00+08:00"), // 4th Friday in 2023 Nov - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), // 5th Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday }, - Index = WeekIndex.Last - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-12-29T00:00:00+08:00"), // 5th Friday in 2023 Dec - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-29T00:00:00+08:00"), // 5th Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-29T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday }, - Index = WeekIndex.Last - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-10-29T00:00:00+08:00"), // 5th Sunday in 2023 Oct - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), // 4th Monday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-25T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Sunday, DayOfWeek.Monday }, - Index = WeekIndex.Last - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-10-30T00:00:00+08:00"), // 5th Monday in 2023 Oct - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-25T00:00:00+08:00"), // 4th Monday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-25T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Sunday, DayOfWeek.Monday }, - Index = WeekIndex.Last - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2023-10-6T00:00:00+08:00"), // 1st Friday in 2023 Oct - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday }, - // Index is First by default. - Interval = 3 - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // 1st Friday in 2023 Dec - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday }, - // Index is First by default. - Interval = 3 - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // 1st Monday in 2023 Oct - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday }, - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-10-6T00:00:00+08:00"), // 1st Friday in 2023 Oct - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday } - }, - Range = new RecurrenceRange() - } - }, - false ), // The time window will only occur on either 1st Monday or 1st Friday, the 1st Monday of 2023 Oct is 10.2 . - - ( DateTimeOffset.Parse("2023-11-3T00:00:00+08:00"), // 1st Friday in 2023 Nov - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - Interval = 2, - DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday } - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-11-6T00:00:00+08:00"), // 1st Monday in 2023 Nov - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // 1st Friday in 2023 Sep - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - Interval = 2, - DaysOfWeek = new List() { DayOfWeek.Friday, DayOfWeek.Monday } - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - false ), // The time window will only occur on either 1st Monday or 1st Friday, the 1st Monday of 2023 Nov is 11.3. - - ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // the first day of the month - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - Interval = 3, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2024-3-1T00:00:00+08:00"), // the first day of the month - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - Interval = 3, // 2023-9, 2023-12, 2024-3 ... - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // The 4th occurrence - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - // Index is First by default. - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 3 // 2023-9-1, 2023-10-1, 2023-11-1, 2023-12-1 ... - } - } - }, - false ), - - ( DateTimeOffset.Parse("2023-12-1T00:00:00+08:00"), // The 4th occurrence - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - // Index is First by default. - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 4 // 2023-9-1, 2023-10-1, 2023-11-1, 2023-12-1 ... - } - } - }, - true ), - - ( DateTimeOffset.Parse("2023-10-1T00:00:00+08:00"), // Sunday is not included in the DaysOfWeek. - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday} - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // 1st Monday - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday} - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - true ), // 2023-10-1 is Sunday which is not included in the DaysOfWeek. - - ( DateTimeOffset.Parse("2023-10-2T00:00:00+08:00"), // 1st Monday - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeMonthly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - false ) // The time window will occur on 2023-10-1 - }; - - ConsumeEvalutationTestData(testData); - } - - [Fact] - public void MatchAbsoluteYearlyRecurrenceTest() - { - var testData = new List>() - { - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteYearly, - DayOfMonth = 1, - Month = 9 - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2024-10-1T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteYearly, - DayOfMonth = 1, - Month = 9 - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // The 2nd occurrence - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteYearly, - DayOfMonth = 1, - Month = 9 - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 1 - } - } - }, - false ), - - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteYearly, - Interval = 2, // 2023, 2025 ... - DayOfMonth = 1, - Month = 9 - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 2nd occurrence - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteYearly, - Interval = 3, // 2023, 2026, ... - DayOfMonth = 1, - Month = 9 - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 2 - } - } - }, - true ), - - ( DateTimeOffset.Parse("2029-9-1T00:00:00+08:00"), // The 3rd occurrence - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.AbsoluteYearly, - Interval = 3, // 2023, 2026, 2029 ... - DayOfMonth = 1, - Month = 9 - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 2 - } - } - }, - false ) - }; - - ConsumeEvalutationTestData(testData); - } - - [Fact] - public void MatchRelativeYearlyRecurrenceTest() - { - var testData = new List>() - { - ( DateTimeOffset.Parse("2024-9-6T00:00:00+08:00"), // 1st Friday in 2024 Sep - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - DaysOfWeek = new List() { DayOfWeek.Friday }, - Month = 9 - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), // 1st Sunday in 2024 Sep, Sunday is not included in the DaysOfWeek. - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday }, - Month = 9 - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, - Month = 9 - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2023-9-2T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, - Month = 9 - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2024-10-1T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, - Month = 9 - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2024-9-1T00:00:00+08:00"), - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - Interval = 2, // 2023, 2025 ... - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, - Month = 9 - // Index is First by default. - }, - Range = new RecurrenceRange() - } - }, - false ), - - ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The first day of Sep - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - Interval = 3, // 2023, 2026 ... - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, - Month = 9 - }, - Range = new RecurrenceRange() - } - }, - true ), - - ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 4th occurrence - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, - Month = 9 - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 3 // 2023-9-1, 2024-9-1, 2025-9-1, 2026-9-1 ... - } - } - }, - false ), - - ( DateTimeOffset.Parse("2026-9-1T00:00:00+08:00"), // The 4th occurrence - new TimeWindowFilterSettings() - { - Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), - End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), - Recurrence = new Recurrence() - { - Pattern = new RecurrencePattern() - { - Type = RecurrencePatternType.RelativeYearly, - DaysOfWeek = new List() { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }, - Month = 9 - }, - Range = new RecurrenceRange() - { - Type = RecurrenceRangeType.Numbered, - NumberOfOccurrences = 4 // 2023-9-1, 2024-9-1, 2025-9-1, 2026-9-1 ... - } - } - }, - true ) - }; - - ConsumeEvalutationTestData(testData); - } } } From 421ec48134b38a919e4a2d1374f67adf4ef446ce Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sat, 10 Feb 2024 20:34:07 +0800 Subject: [PATCH 25/52] do not mention monthly and yearly pattern --- .../FeatureFilters/Recurrence/RecurrencePattern.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs index 5bea6b09..0db6637e 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs @@ -17,7 +17,7 @@ public class RecurrencePattern public RecurrencePatternType Type { get; set; } /// - /// The number of units between occurrences, where units can be in days, weeks, months, or years, depending on the pattern type. + /// The number of units between occurrences, where units can be in days or weeks, depending on the pattern type. /// public int Interval { get; set; } = 1; From e78a0f652921f63283f3cc5c0b9f6253044ef767 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 20 Feb 2024 14:04:08 +0800 Subject: [PATCH 26/52] add more comments --- .../FeatureFilters/Recurrence/RecurrencePattern.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs index 0db6637e..b750a99a 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrencePattern.cs @@ -22,12 +22,12 @@ public class RecurrencePattern public int Interval { get; set; } = 1; /// - /// The days of the week on which the time window occurs. + /// The days of the week on which the time window occurs. This property is only applicable for weekly pattern. /// public IEnumerable DaysOfWeek { get; set; } /// - /// The first day of the week. + /// The first day of the week. This property is only applicable for weekly pattern. /// public DayOfWeek FirstDayOfWeek { get; set; } } From b7eb58695b196545a6e0550dd20f82b3fa8141a5 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 20 Feb 2024 19:46:39 +0800 Subject: [PATCH 27/52] update the algorithm to find weekly previous occurrence --- .../Recurrence/RecurrenceEvaluator.cs | 111 +++++++++--------- .../RecurrenceEvaluator.cs | 19 +++ 2 files changed, 74 insertions(+), 56 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index e71015d4..aacc57f0 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -453,82 +453,64 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow int interval = pattern.Interval; - TimeSpan timeGap = time - start; - - int remainingDaysOfFirstWeek = RemainingDaysOfTheWeek(start.DayOfWeek, pattern.FirstDayOfWeek); - - TimeSpan remainingTimeOfFirstWeek = TimeSpan.FromDays(remainingDaysOfFirstWeek) - start.TimeOfDay; + DateTimeOffset firstDayOfStartWeek = start - TimeSpan.FromDays(DaysPerWeek - RemainingDaysOfTheWeek(start.DayOfWeek, pattern.FirstDayOfWeek)); - TimeSpan remaingTimeOfFirstIntervalAfterFirstWeek = TimeSpan.FromDays((interval - 1) * DaysPerWeek); + DateTimeOffset alignedTime = start + (time - start); - TimeSpan remainingTimeOfFirstInterval = remainingTimeOfFirstWeek + remaingTimeOfFirstIntervalAfterFirstWeek; + // + // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. + DateTimeOffset firstDayOfMostRecentOccurringWeek = firstDayOfStartWeek + + TimeSpan.FromDays( + Math.Floor((alignedTime.Date - firstDayOfStartWeek.Date).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds) * (interval * DaysPerWeek)); - DateTimeOffset tentativePreviousOccurrence = start; + List daysOfWeek = SortDayOfWeek(pattern.DaysOfWeek, pattern.FirstDayOfWeek); - numberOfOccurrences = 0; + numberOfOccurrences = (int)Math.Floor((alignedTime.Date - firstDayOfStartWeek.Date).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds) * daysOfWeek.Count - daysOfWeek.IndexOf(start.DayOfWeek); - // - // Time is not within the first interval - if (remainingTimeOfFirstInterval <= timeGap) + if (time - firstDayOfMostRecentOccurringWeek > TimeSpan.FromDays(DaysPerWeek)) { - // - // Add the occurrence in the first week and shift the tentative previous occurrence to the next week - while (tentativePreviousOccurrence.DayOfWeek != pattern.FirstDayOfWeek || - tentativePreviousOccurrence == start) - { - if (pattern.DaysOfWeek.Any(day => - day == tentativePreviousOccurrence.DayOfWeek)) - { - numberOfOccurrences += 1; - } + numberOfOccurrences += daysOfWeek.Count; - tentativePreviousOccurrence += TimeSpan.FromDays(1); - } + previousOccurrence = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(daysOfWeek.Last(), pattern.FirstDayOfWeek)); - // - // Shift the tentative previous occurrence to the first day of the first week of the second interval - tentativePreviousOccurrence += remaingTimeOfFirstIntervalAfterFirstWeek; + return; + } - // - // The number of intervals between the first and the latest intervals (not inclusive) - // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. - int numberOfInterval = (int) Math.Floor((timeGap - remainingTimeOfFirstInterval).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds); + DateTimeOffset minOffset = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(daysOfWeek.First(), pattern.FirstDayOfWeek)); - // - // Shift the tentative previous occurrence to the first day of the first week of the latest interval - tentativePreviousOccurrence += TimeSpan.FromDays(numberOfInterval * interval * DaysPerWeek); + if (minOffset < start) + { + numberOfOccurrences = 0; - // - // Add the occurrence in the intervals between the first and the latest intervals (not inclusive) - numberOfOccurrences += numberOfInterval * pattern.DaysOfWeek.Count(); + minOffset = start; } - // - // Tentative previous occurrence should either be the start or the first day of the first week of the latest interval. - previousOccurrence = tentativePreviousOccurrence; - - // - // Check the following days of the first week if time is still within the first interval - // Otherwise, check the first week of the latest interval - while (tentativePreviousOccurrence <= time) + if (time >= minOffset) { - if (pattern.DaysOfWeek.Any(day => - day == tentativePreviousOccurrence.DayOfWeek)) + previousOccurrence = minOffset; + + numberOfOccurrences++; + + for (int i = daysOfWeek.IndexOf(minOffset.DayOfWeek) + 1; i < daysOfWeek.Count; i++) { - previousOccurrence = tentativePreviousOccurrence; + DateTimeOffset offset = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(daysOfWeek[i], pattern.FirstDayOfWeek)); - numberOfOccurrences += 1; - } + if (time < offset) + { + break; + } - tentativePreviousOccurrence += TimeSpan.FromDays(1); + previousOccurrence = offset; - if (tentativePreviousOccurrence.DayOfWeek == pattern.FirstDayOfWeek) - { - // - // It comes to the next week, so break. - break; + numberOfOccurrences++; } } + else + { + DateTimeOffset firstDayOfLastOccurringWeek = firstDayOfMostRecentOccurringWeek - TimeSpan.FromDays(interval * DaysPerWeek); + + previousOccurrence = firstDayOfLastOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(daysOfWeek.Last(), pattern.FirstDayOfWeek)); + } } /// @@ -626,5 +608,22 @@ private static int RemainingDaysOfTheWeek(DayOfWeek dayOfWeek, DayOfWeek firstDa return DaysPerWeek - remainingDays; } } + + private static int DayOfWeekOffset(DayOfWeek dayOfWeek, DayOfWeek firstDayOfWeek) + { + return ((int)dayOfWeek - (int)firstDayOfWeek + DaysPerWeek) % DaysPerWeek; + } + + private static List SortDayOfWeek(IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) + { + List result = daysOfWeek.ToList(); + + result.Sort((x, y) => + DayOfWeekOffset(x, firstDayOfWeek) + .CompareTo( + DayOfWeekOffset(y, firstDayOfWeek))); + + return result; + } } } diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index c273ba30..c67aee2b 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -865,6 +865,25 @@ public void MatchWeeklyRecurrenceTest() }, true ), + ( DateTimeOffset.Parse("2024-2-12T08:00:00+08:00"), // Monday in the 3rd week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-2T12:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2024-2-3T12:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Sunday, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } + }, + Range = new RecurrenceRange() + } + }, + false ), + ( DateTimeOffset.Parse("2023-9-13T00:00:00+08:00"), // Within the recurring time window 2023-9-11T:00:00:00+08:00 ~ 2023-9-15T:00:00:00+08:00 new TimeWindowFilterSettings() { From c6531984781e3fcb0fa19e1d9b6e526810a0a2d2 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 20 Feb 2024 20:10:57 +0800 Subject: [PATCH 28/52] update --- .../Recurrence/RecurrenceEvaluator.cs | 96 ++++++++----------- 1 file changed, 41 insertions(+), 55 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index aacc57f0..9e4ac95b 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -172,6 +172,10 @@ private static bool TryValidateDailyRecurrencePattern(TimeWindowFilterSettings s { Debug.Assert(settings.Recurrence.Pattern.Interval > 0); + // + // No required parameter for "Daily" pattern + // "Start" is always a valid first occurrence for "Daily" pattern + TimeSpan intervalDuration = TimeSpan.FromDays(settings.Recurrence.Pattern.Interval); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -187,10 +191,6 @@ private static bool TryValidateDailyRecurrencePattern(TimeWindowFilterSettings s return false; } - // - // No required parameter for "Daily" pattern - // "Start" is always a valid first occurrence for "Daily" pattern - paramName = null; reason = null; @@ -204,6 +204,13 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings Debug.Assert(pattern.Interval > 0); + // + // Required parameters + if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) + { + return false; + } + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * DaysPerWeek); TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; @@ -220,13 +227,6 @@ private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings return false; } - // - // Required parameters - if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) - { - return false; - } - // // Check whether "Start" is a valid first occurrence DateTimeOffset start = settings.Start.Value; @@ -463,20 +463,20 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow TimeSpan.FromDays( Math.Floor((alignedTime.Date - firstDayOfStartWeek.Date).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds) * (interval * DaysPerWeek)); - List daysOfWeek = SortDayOfWeek(pattern.DaysOfWeek, pattern.FirstDayOfWeek); + List sortedDaysOfWeek = SortDayOfWeek(pattern.DaysOfWeek, pattern.FirstDayOfWeek); - numberOfOccurrences = (int)Math.Floor((alignedTime.Date - firstDayOfStartWeek.Date).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds) * daysOfWeek.Count - daysOfWeek.IndexOf(start.DayOfWeek); + numberOfOccurrences = (int)Math.Floor((alignedTime.Date - firstDayOfStartWeek.Date).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds) * sortedDaysOfWeek.Count - sortedDaysOfWeek.IndexOf(start.DayOfWeek); if (time - firstDayOfMostRecentOccurringWeek > TimeSpan.FromDays(DaysPerWeek)) { - numberOfOccurrences += daysOfWeek.Count; + numberOfOccurrences += sortedDaysOfWeek.Count; - previousOccurrence = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(daysOfWeek.Last(), pattern.FirstDayOfWeek)); + previousOccurrence = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); return; } - DateTimeOffset minOffset = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(daysOfWeek.First(), pattern.FirstDayOfWeek)); + DateTimeOffset minOffset = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(sortedDaysOfWeek.First(), pattern.FirstDayOfWeek)); if (minOffset < start) { @@ -491,9 +491,9 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow numberOfOccurrences++; - for (int i = daysOfWeek.IndexOf(minOffset.DayOfWeek) + 1; i < daysOfWeek.Count; i++) + for (int i = sortedDaysOfWeek.IndexOf(minOffset.DayOfWeek) + 1; i < sortedDaysOfWeek.Count; i++) { - DateTimeOffset offset = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(daysOfWeek[i], pattern.FirstDayOfWeek)); + DateTimeOffset offset = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(sortedDaysOfWeek[i], pattern.FirstDayOfWeek)); if (time < offset) { @@ -509,7 +509,7 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow { DateTimeOffset firstDayOfLastOccurringWeek = firstDayOfMostRecentOccurringWeek - TimeSpan.FromDays(interval * DaysPerWeek); - previousOccurrence = firstDayOfLastOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(daysOfWeek.Last(), pattern.FirstDayOfWeek)); + previousOccurrence = firstDayOfLastOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); } } @@ -530,63 +530,49 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int return true; } - // Shift to the first day of the week - DateTime date = DateTime.Today.AddDays( + DateTime firstDayOfThisWeek = DateTime.Today.AddDays( RemainingDaysOfTheWeek(DateTime.Today.DayOfWeek, firstDayOfWeek)); + List sortedDaysOfWeek = SortDayOfWeek(daysOfWeek, firstDayOfWeek); + DateTime prev = DateTime.MinValue; TimeSpan minGap = TimeSpan.FromDays(DaysPerWeek); - for (int i = 0; i < DaysPerWeek; i++) + foreach(DayOfWeek dayOfWeek in sortedDaysOfWeek) { - if (daysOfWeek.Any(day => - day == date.DayOfWeek)) + if (prev == DateTime.MinValue) { - if (prev == DateTime.MinValue) - { - // - // Find a occurrence for the first time - prev = date; - } - else - { - TimeSpan gap = date - prev; + prev = firstDayOfThisWeek + TimeSpan.FromDays(DayOfWeekOffset(dayOfWeek, firstDayOfWeek)); + } + else + { + DateTime date = firstDayOfThisWeek + TimeSpan.FromDays(DayOfWeekOffset(dayOfWeek, firstDayOfWeek)); - if (gap < minGap) - { - minGap = gap; - } + TimeSpan gap = date - prev; - prev = date; + if (gap < minGap) + { + minGap = gap; } - } - date += TimeSpan.FromDays(1); + prev = date; + } } // // It may across weeks. Check the next week if the interval is one week. if (interval == 1) { - for (int i = 0; i < DaysPerWeek; i++) - { - if (daysOfWeek.Any(day => - day == date.DayOfWeek)) - { - TimeSpan gap = date - prev; + DateTime firstDayOfNextWeek = firstDayOfThisWeek + TimeSpan.FromDays(7); - if (gap < minGap) - { - minGap = gap; - } + DateTime firstOccurrenceInNextWeek = firstDayOfNextWeek + TimeSpan.FromDays(DayOfWeekOffset(sortedDaysOfWeek.First(), firstDayOfWeek)); - // - // Only check the first occurrence in the next week - break; - } + TimeSpan gap = firstOccurrenceInNextWeek - prev; - date += TimeSpan.FromDays(1); + if (gap < minGap) + { + minGap = gap; } } From f9537d49a1281390414ab58e4ecf5d604c4edd2f Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 20 Feb 2024 20:14:46 +0800 Subject: [PATCH 29/52] fix typo --- .../FeatureFilters/Recurrence/RecurrenceEvaluator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 9e4ac95b..197fc2f2 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -463,7 +463,7 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow TimeSpan.FromDays( Math.Floor((alignedTime.Date - firstDayOfStartWeek.Date).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds) * (interval * DaysPerWeek)); - List sortedDaysOfWeek = SortDayOfWeek(pattern.DaysOfWeek, pattern.FirstDayOfWeek); + List sortedDaysOfWeek = SortDaysOfWeek(pattern.DaysOfWeek, pattern.FirstDayOfWeek); numberOfOccurrences = (int)Math.Floor((alignedTime.Date - firstDayOfStartWeek.Date).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds) * sortedDaysOfWeek.Count - sortedDaysOfWeek.IndexOf(start.DayOfWeek); @@ -533,7 +533,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int DateTime firstDayOfThisWeek = DateTime.Today.AddDays( RemainingDaysOfTheWeek(DateTime.Today.DayOfWeek, firstDayOfWeek)); - List sortedDaysOfWeek = SortDayOfWeek(daysOfWeek, firstDayOfWeek); + List sortedDaysOfWeek = SortDaysOfWeek(daysOfWeek, firstDayOfWeek); DateTime prev = DateTime.MinValue; @@ -600,7 +600,7 @@ private static int DayOfWeekOffset(DayOfWeek dayOfWeek, DayOfWeek firstDayOfWeek return ((int)dayOfWeek - (int)firstDayOfWeek + DaysPerWeek) % DaysPerWeek; } - private static List SortDayOfWeek(IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) + private static List SortDaysOfWeek(IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) { List result = daysOfWeek.ToList(); From 9c7ec7d2b5b1a11bd4561d3b59d14eef6f5f2e6d Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 20 Feb 2024 21:03:51 +0800 Subject: [PATCH 30/52] update --- .../Recurrence/RecurrenceEvaluator.cs | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 197fc2f2..8f837deb 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -429,7 +429,7 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF // // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. - int numberOfInterval = (int) Math.Floor(timeGap.TotalSeconds / TimeSpan.FromDays(interval).TotalSeconds); + int numberOfInterval = (int)Math.Floor(timeGap.TotalSeconds / TimeSpan.FromDays(interval).TotalSeconds); previousOccurrence = start.AddDays(numberOfInterval * interval); @@ -453,63 +453,75 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow int interval = pattern.Interval; - DateTimeOffset firstDayOfStartWeek = start - TimeSpan.FromDays(DaysPerWeek - RemainingDaysOfTheWeek(start.DayOfWeek, pattern.FirstDayOfWeek)); - - DateTimeOffset alignedTime = start + (time - start); + DateTimeOffset firstDayOfStartWeek = start.AddDays(RemainingDaysOfTheWeek(start.DayOfWeek, pattern.FirstDayOfWeek) - DaysPerWeek); // // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. - DateTimeOffset firstDayOfMostRecentOccurringWeek = firstDayOfStartWeek + - TimeSpan.FromDays( - Math.Floor((alignedTime.Date - firstDayOfStartWeek.Date).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds) * (interval * DaysPerWeek)); + int numberOfInterval = (int)Math.Floor((time - firstDayOfStartWeek).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds); + + DateTimeOffset firstDayOfMostRecentOccurringWeek = firstDayOfStartWeek.AddDays(numberOfInterval * (interval * DaysPerWeek)); List sortedDaysOfWeek = SortDaysOfWeek(pattern.DaysOfWeek, pattern.FirstDayOfWeek); - numberOfOccurrences = (int)Math.Floor((alignedTime.Date - firstDayOfStartWeek.Date).TotalSeconds / TimeSpan.FromDays(interval * DaysPerWeek).TotalSeconds) * sortedDaysOfWeek.Count - sortedDaysOfWeek.IndexOf(start.DayOfWeek); + // + // substract the day before the start in the first week + numberOfOccurrences = numberOfInterval * sortedDaysOfWeek.Count - sortedDaysOfWeek.IndexOf(start.DayOfWeek); + // + // The current time is not within the most recent occurring week. if (time - firstDayOfMostRecentOccurringWeek > TimeSpan.FromDays(DaysPerWeek)) { numberOfOccurrences += sortedDaysOfWeek.Count; - previousOccurrence = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); + // + // day with max offset in the most recent occurring week + previousOccurrence = firstDayOfMostRecentOccurringWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); return; } - DateTimeOffset minOffset = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(sortedDaysOfWeek.First(), pattern.FirstDayOfWeek)); + // + // day with the min offset in the most recent occurring week + DateTimeOffset dayWithMinOffset = firstDayOfMostRecentOccurringWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek.First(), pattern.FirstDayOfWeek)); - if (minOffset < start) + if (dayWithMinOffset < start) { numberOfOccurrences = 0; - minOffset = start; + dayWithMinOffset = start; } - if (time >= minOffset) + if (time >= dayWithMinOffset) { - previousOccurrence = minOffset; + previousOccurrence = dayWithMinOffset; numberOfOccurrences++; - for (int i = sortedDaysOfWeek.IndexOf(minOffset.DayOfWeek) + 1; i < sortedDaysOfWeek.Count; i++) + // + // Find the day with the max offset that is less than the current time. + for (int i = sortedDaysOfWeek.IndexOf(dayWithMinOffset.DayOfWeek) + 1; i < sortedDaysOfWeek.Count; i++) { - DateTimeOffset offset = firstDayOfMostRecentOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(sortedDaysOfWeek[i], pattern.FirstDayOfWeek)); + DateTimeOffset dayOfWeek = firstDayOfMostRecentOccurringWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek[i], pattern.FirstDayOfWeek)); - if (time < offset) + if (time < dayOfWeek) { break; } - previousOccurrence = offset; + previousOccurrence = dayOfWeek; numberOfOccurrences++; } } else { - DateTimeOffset firstDayOfLastOccurringWeek = firstDayOfMostRecentOccurringWeek - TimeSpan.FromDays(interval * DaysPerWeek); + // + // the last occurring week + DateTimeOffset firstDayOfLastOccurringWeek = firstDayOfMostRecentOccurringWeek.AddDays(-interval * DaysPerWeek); - previousOccurrence = firstDayOfLastOccurringWeek + TimeSpan.FromDays(DayOfWeekOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); + // + // day with max offset in the last occurring week + previousOccurrence = firstDayOfLastOccurringWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); } } @@ -543,11 +555,11 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int { if (prev == DateTime.MinValue) { - prev = firstDayOfThisWeek + TimeSpan.FromDays(DayOfWeekOffset(dayOfWeek, firstDayOfWeek)); + prev = firstDayOfThisWeek.AddDays(DayOfWeekOffset(dayOfWeek, firstDayOfWeek)); } else { - DateTime date = firstDayOfThisWeek + TimeSpan.FromDays(DayOfWeekOffset(dayOfWeek, firstDayOfWeek)); + DateTime date = firstDayOfThisWeek.AddDays(DayOfWeekOffset(dayOfWeek, firstDayOfWeek)); TimeSpan gap = date - prev; @@ -564,9 +576,9 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int // It may across weeks. Check the next week if the interval is one week. if (interval == 1) { - DateTime firstDayOfNextWeek = firstDayOfThisWeek + TimeSpan.FromDays(7); + DateTime firstDayOfNextWeek = firstDayOfThisWeek.AddDays(DaysPerWeek); - DateTime firstOccurrenceInNextWeek = firstDayOfNextWeek + TimeSpan.FromDays(DayOfWeekOffset(sortedDaysOfWeek.First(), firstDayOfWeek)); + DateTime firstOccurrenceInNextWeek = firstDayOfNextWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek.First(), firstDayOfWeek)); TimeSpan gap = firstOccurrenceInNextWeek - prev; @@ -581,7 +593,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int private static int RemainingDaysOfTheWeek(DayOfWeek dayOfWeek, DayOfWeek firstDayOfWeek) { - int remainingDays = (int) dayOfWeek - (int) firstDayOfWeek; + int remainingDays = (int)dayOfWeek - (int)firstDayOfWeek; if (remainingDays < 0) { From 2187121ad0ff6ba896dc87661262d07a2ccfae14 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 20 Feb 2024 21:06:31 +0800 Subject: [PATCH 31/52] rename variable --- .../FeatureFilters/Recurrence/RecurrenceEvaluator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 8f837deb..128f5611 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -516,12 +516,12 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow else { // - // the last occurring week - DateTimeOffset firstDayOfLastOccurringWeek = firstDayOfMostRecentOccurringWeek.AddDays(-interval * DaysPerWeek); + // the previous occurring week + DateTimeOffset firstDayOfPreviousOccurringWeek = firstDayOfMostRecentOccurringWeek.AddDays(-interval * DaysPerWeek); // // day with max offset in the last occurring week - previousOccurrence = firstDayOfLastOccurringWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); + previousOccurrence = firstDayOfPreviousOccurringWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); } } From 934784cca44e5029345c864c3291f6da3d027fbc Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 28 Feb 2024 20:27:46 +0800 Subject: [PATCH 32/52] cache added & do validation for only once --- .../Recurrence/RecurrenceEvaluator.cs | 172 ++++++++++++------ .../FeatureFilters/TimeWindowFilter.cs | 66 ++++++- .../RecurrenceEvaluator.cs | 24 +-- 3 files changed, 192 insertions(+), 70 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 128f5611..bffb5e54 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -29,36 +29,94 @@ static class RecurrenceEvaluator /// public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings settings) { - if (settings == null) + if (time < settings.Start.Value) { - throw new ArgumentNullException(nameof(settings)); + return false; } - if (!TryValidateRecurrenceSettings(settings, out string paramName, out string reason)) + if (TryFindPreviousOccurrence(time, settings, out DateTimeOffset previousOccurrence, out int _)) { - throw new ArgumentException(reason, paramName); + return time <= previousOccurrence + (settings.End.Value - settings.Start.Value); } + return false; + } + + /// + /// Try to find the closest previous recurrence occurrence (if any) before the provided timestamp and the next occurrence behind it. + /// A timestamp. + /// The settings of time window filter. + /// The closest previous occurrence. If there is no previous occurrence, it will be set to . + /// The next occurrence. + /// True if the closest previous occurrence is within the recurrence range or the time is before the first occurrence, false otherwise. + /// + public static bool TryFindPrevAndNextOccurrences(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset prevOccurrence, out DateTimeOffset nextOccurrence) + { + prevOccurrence = DateTimeOffset.MinValue; + + nextOccurrence = DateTimeOffset.MaxValue; + if (time < settings.Start.Value) { - return false; - } + // + // The time is before the first occurrence. + nextOccurrence = settings.Start.Value; - // time is before the first occurrence or time is beyond the end of the recurrence range - if (!TryGetPreviousOccurrence(time, settings, out DateTimeOffset previousOccurrence)) - { - return false; + return true; } - if (time <= previousOccurrence + (settings.End.Value - settings.Start.Value)) + if (TryFindPreviousOccurrence(time, settings, out prevOccurrence, out int numberOfOccurrences)) { + RecurrencePattern pattern = settings.Recurrence.Pattern; + + switch (pattern.Type) + { + case RecurrencePatternType.Daily: + nextOccurrence = prevOccurrence.AddDays(pattern.Interval); + + break; + + case RecurrencePatternType.Weekly: + nextOccurrence = GetWeeklyNextOccurrence(prevOccurrence, settings); + + break; + + default: + return false; + } + + RecurrenceRange range = settings.Recurrence.Range; + + if (range.Type == RecurrenceRangeType.EndDate) + { + if (nextOccurrence > range.EndDate) + { + nextOccurrence = DateTimeOffset.MaxValue; + } + } + + if (range.Type == RecurrenceRangeType.Numbered) + { + if (numberOfOccurrences >= range.NumberOfOccurrences) + { + nextOccurrence = DateTimeOffset.MaxValue; + } + } + return true; } return false; } - private static bool TryValidateRecurrenceSettings(TimeWindowFilterSettings settings, out string paramName, out string reason) + /// + /// Perform validation of time window settings. + /// The settings of time window filter. + /// The name of the invalid setting, if any. + /// The reason that the setting is invalid. + /// True if the provided settings are valid. False if the provided settings are invalid. + /// + public static bool TryValidateSettings(TimeWindowFilterSettings settings, out string paramName, out string reason) { if (settings == null) { @@ -122,7 +180,7 @@ private static bool TryValidateRecurrenceRequiredParameter(TimeWindowFilterSetti return false; } - if (settings.End.Value - settings.Start.Value <= TimeSpan.Zero) + if (settings.End.Value <= settings.Start.Value) { paramName = nameof(settings.End); @@ -357,25 +415,20 @@ private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings sett /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. + /// The number of occurrences between the time and the recurrence start. /// True if the closest previous occurrence is within the recurrence range, false otherwise. /// - private static bool TryGetPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence) + private static bool TryFindPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { Debug.Assert(settings.Start != null); Debug.Assert(settings.Recurrence != null); Debug.Assert(settings.Recurrence.Pattern != null); Debug.Assert(settings.Recurrence.Range != null); + Debug.Assert(settings.Start.Value <= time); - previousOccurrence = DateTimeOffset.MaxValue; + previousOccurrence = DateTimeOffset.MinValue; - DateTimeOffset start = settings.Start.Value; - - if (time < start) - { - return false; - } - - int numberOfOccurrences; + numberOfOccurrences = 0; switch (settings.Recurrence.Pattern.Type) { @@ -453,7 +506,8 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow int interval = pattern.Interval; - DateTimeOffset firstDayOfStartWeek = start.AddDays(RemainingDaysOfTheWeek(start.DayOfWeek, pattern.FirstDayOfWeek) - DaysPerWeek); + DateTimeOffset firstDayOfStartWeek = start.AddDays( + -CalculateWeeklyDayOffset(start.DayOfWeek, pattern.FirstDayOfWeek)); // // netstandard2.0 does not support '/' operator for TimeSpan. After we stop supporting netstandard2.0, we can remove .TotalSeconds. @@ -475,14 +529,16 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow // // day with max offset in the most recent occurring week - previousOccurrence = firstDayOfMostRecentOccurringWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); + previousOccurrence = firstDayOfMostRecentOccurringWeek.AddDays( + CalculateWeeklyDayOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); return; } // // day with the min offset in the most recent occurring week - DateTimeOffset dayWithMinOffset = firstDayOfMostRecentOccurringWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek.First(), pattern.FirstDayOfWeek)); + DateTimeOffset dayWithMinOffset = firstDayOfMostRecentOccurringWeek.AddDays( + CalculateWeeklyDayOffset(sortedDaysOfWeek.First(), pattern.FirstDayOfWeek)); if (dayWithMinOffset < start) { @@ -501,7 +557,8 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow // Find the day with the max offset that is less than the current time. for (int i = sortedDaysOfWeek.IndexOf(dayWithMinOffset.DayOfWeek) + 1; i < sortedDaysOfWeek.Count; i++) { - DateTimeOffset dayOfWeek = firstDayOfMostRecentOccurringWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek[i], pattern.FirstDayOfWeek)); + DateTimeOffset dayOfWeek = firstDayOfMostRecentOccurringWeek.AddDays( + CalculateWeeklyDayOffset(sortedDaysOfWeek[i], pattern.FirstDayOfWeek)); if (time < dayOfWeek) { @@ -521,10 +578,34 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow // // day with max offset in the last occurring week - previousOccurrence = firstDayOfPreviousOccurringWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); + previousOccurrence = firstDayOfPreviousOccurringWeek.AddDays( + CalculateWeeklyDayOffset(sortedDaysOfWeek.Last(), pattern.FirstDayOfWeek)); } } + /// + /// Find the next recurrence occurrence after the provided previous occurrence according to the "Weekly" recurrence pattern. + /// The previous occurrence. + /// The settings of time window filter. + /// + private static DateTimeOffset GetWeeklyNextOccurrence(DateTimeOffset previousOccurrence, TimeWindowFilterSettings settings) + { + RecurrencePattern pattern = settings.Recurrence.Pattern; + + List sortedDaysOfWeek = SortDaysOfWeek(pattern.DaysOfWeek, pattern.FirstDayOfWeek); + + int i = sortedDaysOfWeek.IndexOf(previousOccurrence.DayOfWeek) + 1; + + if (i < sortedDaysOfWeek.Count()) + { + return previousOccurrence.AddDays( + CalculateWeeklyDayOffset(sortedDaysOfWeek[i], previousOccurrence.DayOfWeek)); + } + + return previousOccurrence.AddDays( + pattern.Interval * DaysPerWeek - CalculateWeeklyDayOffset(previousOccurrence.DayOfWeek, sortedDaysOfWeek.First())); + } + /// /// Check whether the duration is shorter than the minimum gap between recurrence of days of week. /// @@ -543,7 +624,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int } DateTime firstDayOfThisWeek = DateTime.Today.AddDays( - RemainingDaysOfTheWeek(DateTime.Today.DayOfWeek, firstDayOfWeek)); + DaysPerWeek - CalculateWeeklyDayOffset(DateTime.Today.DayOfWeek, firstDayOfWeek)); List sortedDaysOfWeek = SortDaysOfWeek(daysOfWeek, firstDayOfWeek); @@ -555,11 +636,13 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int { if (prev == DateTime.MinValue) { - prev = firstDayOfThisWeek.AddDays(DayOfWeekOffset(dayOfWeek, firstDayOfWeek)); + prev = firstDayOfThisWeek.AddDays( + CalculateWeeklyDayOffset(dayOfWeek, firstDayOfWeek)); } else { - DateTime date = firstDayOfThisWeek.AddDays(DayOfWeekOffset(dayOfWeek, firstDayOfWeek)); + DateTime date = firstDayOfThisWeek.AddDays( + CalculateWeeklyDayOffset(dayOfWeek, firstDayOfWeek)); TimeSpan gap = date - prev; @@ -578,7 +661,8 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int { DateTime firstDayOfNextWeek = firstDayOfThisWeek.AddDays(DaysPerWeek); - DateTime firstOccurrenceInNextWeek = firstDayOfNextWeek.AddDays(DayOfWeekOffset(sortedDaysOfWeek.First(), firstDayOfWeek)); + DateTime firstOccurrenceInNextWeek = firstDayOfNextWeek.AddDays( + CalculateWeeklyDayOffset(sortedDaysOfWeek.First(), firstDayOfWeek)); TimeSpan gap = firstOccurrenceInNextWeek - prev; @@ -591,25 +675,9 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int return minGap >= duration; } - private static int RemainingDaysOfTheWeek(DayOfWeek dayOfWeek, DayOfWeek firstDayOfWeek) - { - int remainingDays = (int)dayOfWeek - (int)firstDayOfWeek; - - if (remainingDays < 0) - { - return -remainingDays; - } - else - { - // - // If the dayOfWeek is the firstDayOfWeek, there will be 7 days remaining in this week. - return DaysPerWeek - remainingDays; - } - } - - private static int DayOfWeekOffset(DayOfWeek dayOfWeek, DayOfWeek firstDayOfWeek) + private static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) { - return ((int)dayOfWeek - (int)firstDayOfWeek + DaysPerWeek) % DaysPerWeek; + return ((int)day1 - (int)day2 + DaysPerWeek) % DaysPerWeek; } private static List SortDaysOfWeek(IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) @@ -617,9 +685,9 @@ private static List SortDaysOfWeek(IEnumerable daysOfWeek, List result = daysOfWeek.ToList(); result.Sort((x, y) => - DayOfWeekOffset(x, firstDayOfWeek) + CalculateWeeklyDayOffset(x, firstDayOfWeek) .CompareTo( - DayOfWeekOffset(y, firstDayOfWeek))); + CalculateWeeklyDayOffset(y, firstDayOfWeek))); return result; } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 8c3272a9..cc075bbf 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -4,6 +4,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -17,6 +19,7 @@ public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder { private const string Alias = "Microsoft.TimeWindow"; private readonly ILogger _logger; + private readonly ConcurrentDictionary _recurrenceCache; /// /// Creates a time window based feature filter. @@ -25,6 +28,7 @@ public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder public TimeWindowFilter(ILoggerFactory loggerFactory) { _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); + _recurrenceCache = new ConcurrentDictionary(); } /// @@ -34,11 +38,18 @@ public TimeWindowFilter(ILoggerFactory loggerFactory) /// that can later be used in feature evaluation. public object BindParameters(IConfiguration filterParameters) { - return filterParameters.Get() ?? new TimeWindowFilterSettings(); + var settings = filterParameters.Get() ?? new TimeWindowFilterSettings(); + + if (!RecurrenceEvaluator.TryValidateSettings(settings, out string paramName, out string reason)) + { + throw new ArgumentException(reason, paramName); + } + + return settings; } /// - /// Evaluates whether a feature is enabled based on the specifed in the configuration. + /// Evaluates whether a feature is enabled based on the specified in the configuration. /// /// The feature evaluation context. /// True if the feature is enabled, false otherwise. @@ -66,6 +77,57 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) if (settings.Recurrence != null) { + if (context.Settings != null) + { + DateTimeOffset cachedTime = _recurrenceCache.GetOrAdd( + settings, + (_) => + { + if (RecurrenceEvaluator.TryFindPrevAndNextOccurrences(now, settings, out DateTimeOffset prevOccurrence, out DateTimeOffset _)) + { + return prevOccurrence; + } + + // + // There is no previous occurrence within the reccurrence range. + return DateTimeOffset.MaxValue; + }); + + if (now < cachedTime) + { + return Task.FromResult(false); + } + + if (now <= cachedTime + (settings.End.Value - settings.Start.Value)) + { + return Task.FromResult(true); + } + + if (RecurrenceEvaluator.TryFindPrevAndNextOccurrences(now, settings, out DateTimeOffset prevOccurrence, out DateTimeOffset nextOccurrrence)) + { + bool isWithinPreviousTimeWindow = + now <= prevOccurrence + (settings.End.Value - settings.Start.Value); + + _recurrenceCache.AddOrUpdate( + settings, + (_) => throw new KeyNotFoundException(), + (_, _) => isWithinPreviousTimeWindow ? + prevOccurrence : + nextOccurrrence); + + return Task.FromResult(isWithinPreviousTimeWindow); + } + + // + // There is no previous occurrence within the reccurrence range. + _recurrenceCache.AddOrUpdate( + settings, + (_) => throw new KeyNotFoundException(), + (_, _) => DateTimeOffset.MaxValue); + + return Task.FromResult(false); + } + return Task.FromResult(RecurrenceEvaluator.MatchRecurrence(now, settings)); } diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index c67aee2b..54d5109d 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -41,16 +41,12 @@ public class RecurrenceEvaluatorTest { private static void ConsumeValidationTestData(List> testData) { - foreach ((TimeWindowFilterSettings settings, string paramName, string errorMessage) in testData) + foreach ((TimeWindowFilterSettings settings, string paramNameRef, string errorMessageRef) in testData) { - ArgumentException ex = Assert.Throws( - () => - { - RecurrenceEvaluator.MatchRecurrence(DateTimeOffset.Now, settings); - }); + RecurrenceEvaluator.TryValidateSettings(settings, out string paramName, out string errorMessage); - Assert.Equal(paramName, ex.ParamName); - Assert.Equal(errorMessage, ex.Message.Substring(0, errorMessage.Length)); + Assert.Equal(paramNameRef, paramName); + Assert.Equal(errorMessageRef, errorMessage); } } @@ -281,7 +277,7 @@ public void InvalidTimeWindowTest() } }, ParamName.End, - ErrorMessage.TimeWindowDurationOutOfRange ), + ErrorMessage.TimeWindowDurationOutOfRange ) }; ConsumeValidationTestData(testData); @@ -348,13 +344,9 @@ public void ValidTimeWindowAcrossWeeks() } }; - // - // The settings is invalid, since we change the interval to 1. - Assert.Throws( - () => - { - RecurrenceEvaluator.MatchRecurrence(DateTimeOffset.Now, settings); - }); + Assert.False(RecurrenceEvaluator.TryValidateSettings(settings, out string paramName, out string errorMessage)); + Assert.Equal(ParamName.End, paramName); + Assert.Equal(ErrorMessage.TimeWindowDurationOutOfRange, errorMessage); } [Fact] From 77b24b179edef93e57e411c08ecd419b01f03fdc Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 28 Feb 2024 20:32:10 +0800 Subject: [PATCH 33/52] add comments --- .../FeatureFilters/TimeWindowFilter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index cc075bbf..d79f56a6 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -77,6 +77,10 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) if (settings.Recurrence != null) { + // + // The reference of the object will be used for hash key. + // If there is no pre-bounded settings attached to the context, there will be no cached filter settings and each call will have a unique settings object. + // In this case, the cache for recurrence settings won't work. if (context.Settings != null) { DateTimeOffset cachedTime = _recurrenceCache.GetOrAdd( From f1eefe9294687e0ba4a075254d270698ad5cf484 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 29 Feb 2024 15:05:15 +0800 Subject: [PATCH 34/52] add more testcases --- .../FeatureManagement.cs | 7 +- tests/Tests.FeatureManagement/Features.cs | 2 +- .../MockedTimeWindowFilter.cs | 114 +++++ .../RecurrenceEvaluator.cs | 463 +++++++++++++++++- .../Tests.FeatureManagement/appsettings.json | 5 +- 5 files changed, 584 insertions(+), 7 deletions(-) create mode 100644 tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index b6f55dcb..342662ce 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -343,7 +343,12 @@ public async Task TimeWindow() Assert.False(await featureManager.IsEnabledAsync(feature4)); Assert.True(await featureManager.IsEnabledAsync(feature5)); Assert.False(await featureManager.IsEnabledAsync(feature6)); - Assert.True(await featureManager.IsEnabledAsync(Features.TimeWindowTestFeature)); + + for (int i = 0; i < 10; i++) + { + Assert.True(await featureManager.IsEnabledAsync(Features.RecurringTimeWindowTestFeature)); + } + } [Fact] diff --git a/tests/Tests.FeatureManagement/Features.cs b/tests/Tests.FeatureManagement/Features.cs index fba454af..6ad57472 100644 --- a/tests/Tests.FeatureManagement/Features.cs +++ b/tests/Tests.FeatureManagement/Features.cs @@ -5,7 +5,7 @@ namespace Tests.FeatureManagement { static class Features { - public const string TimeWindowTestFeature = "TimeWindowTestFeature"; + public const string RecurringTimeWindowTestFeature = "TimeWindowTestFeature"; public const string TargetingTestFeature = "TargetingTestFeature"; public const string TargetingTestFeatureWithExclusion = "TargetingTestFeatureWithExclusion"; public const string OnTestFeature = "OnTestFeature"; diff --git a/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs b/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs new file mode 100644 index 00000000..31078713 --- /dev/null +++ b/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs @@ -0,0 +1,114 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.FeatureManagement; +using Microsoft.FeatureManagement.FeatureFilters; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Tests.FeatureManagement +{ + public class MockedTimeWindowFilter + { + private readonly ConcurrentDictionary _recurrenceCache; + + public MockedTimeWindowFilter() + { + _recurrenceCache = new ConcurrentDictionary(); + } + + public object BindParameters(IConfiguration filterParameters) + { + var settings = filterParameters.Get() ?? new TimeWindowFilterSettings(); + + if (!RecurrenceEvaluator.TryValidateSettings(settings, out string paramName, out string reason)) + { + throw new ArgumentException(reason, paramName); + } + + return settings; + } + + public bool Evaluate(DateTimeOffset now, FeatureFilterEvaluationContext context) + { + // + // Check if prebound settings available, otherwise bind from parameters. + TimeWindowFilterSettings settings = (TimeWindowFilterSettings)context.Settings ?? (TimeWindowFilterSettings)BindParameters(context.Parameters); + + if (!settings.Start.HasValue && !settings.End.HasValue) + { + return false; + } + + // + // Hit the first occurrence of the time window + if ((!settings.Start.HasValue || now >= settings.Start.Value) && (!settings.End.HasValue || now < settings.End.Value)) + { + return true; + } + + if (settings.Recurrence != null) + { + // + // The reference of the object will be used for hash key. + // If there is no pre-bounded settings attached to the context, there will be no cached filter settings and each call will have a unique settings object. + // In this case, the cache for recurrence settings won't work. + if (context.Settings != null) + { + DateTimeOffset cachedTime = _recurrenceCache.GetOrAdd( + settings, + (_) => + { + if (RecurrenceEvaluator.TryFindPrevAndNextOccurrences(now, settings, out DateTimeOffset prevOccurrence, out DateTimeOffset _)) + { + return prevOccurrence; + } + + // + // There is no previous occurrence within the reccurrence range. + return DateTimeOffset.MaxValue; + }); + + if (now < cachedTime) + { + return false; + } + + if (now <= cachedTime + (settings.End.Value - settings.Start.Value)) + { + return true; + } + + if (RecurrenceEvaluator.TryFindPrevAndNextOccurrences(now, settings, out DateTimeOffset prevOccurrence, out DateTimeOffset nextOccurrrence)) + { + bool isWithinPreviousTimeWindow = + now <= prevOccurrence + (settings.End.Value - settings.Start.Value); + + _recurrenceCache.AddOrUpdate( + settings, + (_) => throw new KeyNotFoundException(), + (_, _) => isWithinPreviousTimeWindow ? + prevOccurrence : + nextOccurrrence); + + return isWithinPreviousTimeWindow; + } + + // + // There is no previous occurrence within the reccurrence range. + _recurrenceCache.AddOrUpdate( + settings, + (_) => throw new KeyNotFoundException(), + (_, _) => DateTimeOffset.MaxValue); + + return false; + } + + return RecurrenceEvaluator.MatchRecurrence(now, settings); + } + + return false; + } + } +} diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index 54d5109d..03ce7689 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -50,7 +50,7 @@ private static void ConsumeValidationTestData(List> testData) + private static void ConsumeEvaluationTestData(List> testData) { foreach ((DateTimeOffset time, TimeWindowFilterSettings settings, bool expected) in testData) { @@ -58,6 +58,20 @@ private static void ConsumeEvalutationTestData(List> testData) + { + foreach ((DateTimeOffset time, TimeWindowFilterSettings settings, bool expectedRes, DateTimeOffset expectedPrev, DateTimeOffset expectedNext) in testData) + { + Assert.Equal(expectedRes, RecurrenceEvaluator.TryFindPrevAndNextOccurrences(time, settings, out DateTimeOffset prev, out DateTimeOffset next)); + + if (expectedRes) + { + Assert.Equal(expectedPrev, prev); + Assert.Equal(expectedNext, next); + } + } + } + [Fact] public void GeneralRequiredParameterTest() { @@ -525,10 +539,30 @@ public void MatchDailyRecurrenceTest() } } }, + false ), + + ( DateTimeOffset.Parse("2023-9-4T00:00:00+08:00"), // Behind end date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.EndDate, + EndDate = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00") + } + } + }, false ) }; - ConsumeEvalutationTestData(testData); + ConsumeEvaluationTestData(testData); } [Fact] @@ -942,7 +976,432 @@ public void MatchWeeklyRecurrenceTest() false ) }; + ConsumeEvaluationTestData(testData); + } + + [Fact] + public void FindDailyPrevAndNextOccurrenceTest() + { + var testData = new List>() + { + ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-3-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2024-3-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily + }, + Range = new RecurrenceRange() + } + }, + true, + DateTimeOffset.MinValue, + DateTimeOffset.Parse("2024-3-1T00:00:00+08:00")), + + ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily + }, + Range = new RecurrenceRange() + } + }, + true, + DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), + + ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily, + Interval = 2 + }, + Range = new RecurrenceRange() + } + }, + true, + DateTimeOffset.Parse("2024-2-27T00:00:00+08:00"), + DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), + + ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily, + Interval = 3 + }, + Range = new RecurrenceRange() + } + }, + true, + DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + DateTimeOffset.Parse("2024-3-2T00:00:00+08:00")), + + ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily, + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 27 + } + } + }, + false, + default, + default), + + ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily, + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 28 + } + } + }, + true, + DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + DateTimeOffset.MaxValue), + + ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily, + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.EndDate, + EndDate = DateTimeOffset.Parse("2024-2-27T00:00:00+08:00") + } + } + }, + false, + default, + default), + + ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily, + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.EndDate, + EndDate = DateTimeOffset.Parse("2024-2-28T00:00:00+08:00") + } + } + }, + true, + DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + DateTimeOffset.MaxValue) + }; + ConsumeEvalutationTestData(testData); } + + [Fact] + public void FindWeeklyPrevAndNextOccurrenceTest() + { + var testData = new List>() + { + ( DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-29T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Thursday } + }, + Range = new RecurrenceRange() + } + }, + true, + DateTimeOffset.MinValue, + DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), + + ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-29T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Thursday } + }, + Range = new RecurrenceRange() + } + }, + true, + DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), + DateTimeOffset.Parse("2024-3-7T00:00:00+08:00")), + + ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T12:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T12:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Thursday } + }, + Range = new RecurrenceRange() + } + }, + true, + DateTimeOffset.Parse("2024-2-22T12:00:00+08:00"), + DateTimeOffset.Parse("2024-2-29T12:00:00+08:00")), + + ( DateTimeOffset.Parse("2024-3-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Thursday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + } + }, + true, + DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), + DateTimeOffset.Parse("2024-3-3T00:00:00+08:00")), + + ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Thursday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + } + }, + true, + DateTimeOffset.Parse("2024-2-25T00:00:00+08:00"), + DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), + + ( DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Thursday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + } + }, + true, + DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + DateTimeOffset.Parse("2024-2-11T00:00:00+08:00")), // Sunday in the third week + + ( DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 1 + } + } + }, + true, + DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), + DateTimeOffset.MaxValue), + + ( DateTimeOffset.Parse("2024-2-2T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 1 + } + } + }, + false, + default, + default), + + ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Thursday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 2 + } + } + }, + true, + DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), + DateTimeOffset.MaxValue), // Sunday in the third week + + ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List(){ DayOfWeek.Thursday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 2 + } + } + }, + true, + DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), + DateTimeOffset.MaxValue), // Sunday in the third week + + ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + DaysOfWeek = new List(){ DayOfWeek.Thursday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.EndDate, + EndDate = DateTimeOffset.Parse("2024-2-8T00:00:00+08:00") + } + } + }, + false, + default, + default), + }; + + ConsumeEvalutationTestData(testData); + } + + [Fact] + public void RecurrenceEvaluationThroughCacheTest() + { + var mockedTimeWindowFilter = new MockedTimeWindowFilter(); + + + } } } diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index 3550c2dc..336f3a8b 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -23,7 +23,7 @@ } ] }, - "TimeWindowTestFeature": { + "RecurringTimeWindowTestFeature": { "EnabledFor": [ { "Name": "TimeWindow", @@ -42,8 +42,7 @@ "Friday", "Saturday" ], - "FirstDayOfWeek": "Monday", - "Index": "Last" + "FirstDayOfWeek": "Monday" }, "Range": { "Type": "NoEnd" From 0645a258d8d0f32353fd19e555e24f537b7235f9 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 29 Feb 2024 17:21:41 +0800 Subject: [PATCH 35/52] add more test --- .../FeatureManagement.cs | 1 - tests/Tests.FeatureManagement/Features.cs | 2 +- .../RecurrenceEvaluator.cs | 217 +++++++++++++++++- 3 files changed, 212 insertions(+), 8 deletions(-) diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 342662ce..e5c0ba06 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -348,7 +348,6 @@ public async Task TimeWindow() { Assert.True(await featureManager.IsEnabledAsync(Features.RecurringTimeWindowTestFeature)); } - } [Fact] diff --git a/tests/Tests.FeatureManagement/Features.cs b/tests/Tests.FeatureManagement/Features.cs index 6ad57472..4fa4fe91 100644 --- a/tests/Tests.FeatureManagement/Features.cs +++ b/tests/Tests.FeatureManagement/Features.cs @@ -5,7 +5,7 @@ namespace Tests.FeatureManagement { static class Features { - public const string RecurringTimeWindowTestFeature = "TimeWindowTestFeature"; + public const string RecurringTimeWindowTestFeature = "RecurringTimeWindowTestFeature"; public const string TargetingTestFeature = "TargetingTestFeature"; public const string TargetingTestFeatureWithExclusion = "TargetingTestFeatureWithExclusion"; public const string OnTestFeature = "OnTestFeature"; diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs index 03ce7689..2e720897 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Generic; @@ -1271,7 +1272,7 @@ public void FindWeeklyPrevAndNextOccurrenceTest() }, true, DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), - DateTimeOffset.Parse("2024-2-11T00:00:00+08:00")), // Sunday in the third week + DateTimeOffset.Parse("2024-2-11T00:00:00+08:00")), // Sunday in the 3rd week ( DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1341,8 +1342,8 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } }, true, - DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), - DateTimeOffset.MaxValue), // Sunday in the third week + DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), // Sunday in the 3rd week + DateTimeOffset.MaxValue), ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1366,8 +1367,108 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } }, true, - DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), - DateTimeOffset.MaxValue), // Sunday in the third week + DateTimeOffset.Parse("2024-2-4T00:00:00+08:00"), // Sunday in the 1st week + DateTimeOffset.MaxValue), + + ( DateTimeOffset.Parse("2024-2-12T00:00:00+08:00"), // Monday in the 3rd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Thursday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 3 + } + } + }, + true, + DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), // Sunday in the 3rd week + DateTimeOffset.Parse("2024-2-15T00:00:00+08:00")), // Thursday in the 3rd week + + ( DateTimeOffset.Parse("2024-2-12T00:00:00+08:00"), // Monday in the 3rd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List(){ DayOfWeek.Thursday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 3 + } + } + }, + true, + DateTimeOffset.Parse("2024-2-4T00:00:00+08:00"), // Sunday in the 1st week + DateTimeOffset.Parse("2024-2-15T00:00:00+08:00")), // Thursday in the 3rd week + + ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), // Sunday in the 3rd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T12:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T12:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 3 + } + } + }, + true, + DateTimeOffset.Parse("2024-2-3T12:00:00+08:00"), // Saturday in the 1st week + DateTimeOffset.MaxValue), + + ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), // Sunday in the 2nd week + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T12:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T12:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 3 + } + } + }, + false, + default, + default), ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1390,7 +1491,7 @@ public void FindWeeklyPrevAndNextOccurrenceTest() }, false, default, - default), + default) }; ConsumeEvalutationTestData(testData); @@ -1401,7 +1502,111 @@ public void RecurrenceEvaluationThroughCacheTest() { var mockedTimeWindowFilter = new MockedTimeWindowFilter(); + var context = new FeatureFilterEvaluationContext() + { + Settings = new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T12:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily, + Interval = 2 + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.EndDate, + EndDate = DateTimeOffset.Parse("2024-2-5T12:00:00+08:00") + } + } + } + }; + + DateTimeOffset now = DateTimeOffset.Parse("2024-2-2T23:00:00+08:00"); + + Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + + for (int i = 0; i < 13; i++) + { + now = now.AddHours(1); + Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + } + now = DateTimeOffset.Parse("2024-2-3T12:00:01+08:00"); + Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + + now = DateTimeOffset.Parse("2024-2-5T00:00:00+08:00"); + Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + + now = DateTimeOffset.Parse("2024-2-5T12:00:01+08:00"); + Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + + now = DateTimeOffset.Parse("2024-2-7T00:00:00+08:00"); + Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + + for (int i = 0; i < 10; i++ ) + { + now = now.AddDays(1); + Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + } + + context = new FeatureFilterEvaluationContext() + { + Settings = new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), // Thursday + End = DateTimeOffset.Parse("2024-2-1T12:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List() { DayOfWeek.Thursday, DayOfWeek.Sunday } + }, + Range = new RecurrenceRange() + { + Type = RecurrenceRangeType.Numbered, + NumberOfOccurrences = 2 + } + } + } + }; + + now = DateTimeOffset.Parse("2024-1-31T23:00:00+08:00"); + Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + + for (int i = 0; i < 13; i++) + { + now = now.AddHours(1); + Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + } + + now = DateTimeOffset.Parse("2024-2-1T12:00:01+08:00"); + Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + + now = DateTimeOffset.Parse("2024-2-2T00:00:00+08:00"); // Friday + Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + + now = DateTimeOffset.Parse("2024-2-4T00:00:00+08:00"); // Sunday + Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + + now = DateTimeOffset.Parse("2024-2-4T06:00:00+08:00"); + Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + + now = DateTimeOffset.Parse("2024-2-4T12:01:00+08:00"); + Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + + now = DateTimeOffset.Parse("2024-2-8T00:00:00+08:00"); + Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + + for (int i = 0; i < 10; i++) + { + now = now.AddDays(1); + Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + } } } } From acdc16bb46bf5dce5b3801b21ac2781f87be0e9b Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 7 Mar 2024 14:59:51 +0800 Subject: [PATCH 36/52] not include the end of a time window --- .../FeatureFilters/Recurrence/RecurrenceEvaluator.cs | 2 +- .../FeatureFilters/TimeWindowFilter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index bffb5e54..f7e38cb7 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -36,7 +36,7 @@ public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings if (TryFindPreviousOccurrence(time, settings, out DateTimeOffset previousOccurrence, out int _)) { - return time <= previousOccurrence + (settings.End.Value - settings.Start.Value); + return time < previousOccurrence + (settings.End.Value - settings.Start.Value); } return false; diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index d79f56a6..4aa5b86a 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -102,7 +102,7 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) return Task.FromResult(false); } - if (now <= cachedTime + (settings.End.Value - settings.Start.Value)) + if (now < cachedTime + (settings.End.Value - settings.Start.Value)) { return Task.FromResult(true); } From ace075ca3265d01246ab1547fe79c3049bf2f458 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 20 Mar 2024 11:10:22 +0800 Subject: [PATCH 37/52] move recurrence validation to RecurrenceValidator --- .../Recurrence/RecurrenceEvaluator.cs | 407 +----------------- .../Recurrence/RecurrenceRange.cs | 2 +- .../Recurrence/RecurrenceValidator.cs | 393 +++++++++++++++++ .../FeatureFilters/TimeWindowFilter.cs | 2 +- .../MockedTimeWindowFilter.cs | 4 +- ...ceEvaluator.cs => RecurrenceEvaluation.cs} | 53 +-- 6 files changed, 445 insertions(+), 416 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs rename tests/Tests.FeatureManagement/{RecurrenceEvaluator.cs => RecurrenceEvaluation.cs} (99%) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index f7e38cb7..d85c2554 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -10,14 +10,6 @@ namespace Microsoft.FeatureManagement.FeatureFilters { static class RecurrenceEvaluator { - // - // Error Message - const string ValueOutOfRange = "The value is out of the accepted range."; - const string UnrecognizableValue = "The value is unrecognizable."; - const string RequiredParameter = "Value cannot be null or empty."; - const string StartNotMatched = "Start date is not a valid first occurrence."; - const string TimeWindowDurationOutOfRange = "Time window duration cannot be longer than how frequently it occurs"; - const int DaysPerWeek = 7; /// @@ -43,7 +35,7 @@ public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings } /// - /// Try to find the closest previous recurrence occurrence (if any) before the provided timestamp and the next occurrence behind it. + /// Try to find the closest previous recurrence occurrence (if any) before the provided timestamp and the next occurrence. /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. If there is no previous occurrence, it will be set to . @@ -110,304 +102,33 @@ public static bool TryFindPrevAndNextOccurrences(DateTimeOffset time, TimeWindow } /// - /// Perform validation of time window settings. - /// The settings of time window filter. - /// The name of the invalid setting, if any. - /// The reason that the setting is invalid. - /// True if the provided settings are valid. False if the provided settings are invalid. + /// Calculate the offset in days between two given days of the week. + /// A day of week. + /// A day of week. + /// The number of days to be added to day2 to reach day1 /// - public static bool TryValidateSettings(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - if (settings == null) - { - throw new ArgumentNullException(nameof(settings)); - } - - if (settings.Recurrence != null) - { - return TryValidateRecurrenceRequiredParameter(settings, out paramName, out reason) && - TryValidateRecurrencePattern(settings, out paramName, out reason) && - TryValidateRecurrenceRange(settings, out paramName, out reason); - } - - paramName = null; - - reason = null; - - return true; - } - - private static bool TryValidateRecurrenceRequiredParameter(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - Debug.Assert(settings != null); - Debug.Assert(settings.Recurrence != null); - - if (settings.Start == null) - { - paramName = nameof(settings.Start); - - reason = RequiredParameter; - - return false; - } - - if (settings.End == null) - { - paramName = nameof(settings.End); - - reason = RequiredParameter; - - return false; - } - - Recurrence recurrence = settings.Recurrence; - - if (recurrence.Pattern == null) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(recurrence.Pattern)}"; - - reason = RequiredParameter; - - return false; - } - - if (recurrence.Range == null) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(recurrence.Range)}"; - - reason = RequiredParameter; - - return false; - } - - if (settings.End.Value <= settings.Start.Value) - { - paramName = nameof(settings.End); - - reason = ValueOutOfRange; - - return false; - } - - paramName = null; - - reason = null; - - return true; - } - - private static bool TryValidateRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - Debug.Assert(settings != null); - Debug.Assert(settings.Start != null); - Debug.Assert(settings.End != null); - Debug.Assert(settings.Recurrence != null); - Debug.Assert(settings.Recurrence.Pattern != null); - - if (!TryValidateInterval(settings, out paramName, out reason)) - { - return false; - } - - switch (settings.Recurrence.Pattern.Type) - { - case RecurrencePatternType.Daily: - return TryValidateDailyRecurrencePattern(settings, out paramName, out reason); - - case RecurrencePatternType.Weekly: - return TryValidateWeeklyRecurrencePattern(settings, out paramName, out reason); - - default: - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Type)}"; - - reason = UnrecognizableValue; - - return false; - } - } - - private static bool TryValidateDailyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - Debug.Assert(settings.Recurrence.Pattern.Interval > 0); - - // - // No required parameter for "Daily" pattern - // "Start" is always a valid first occurrence for "Daily" pattern - - TimeSpan intervalDuration = TimeSpan.FromDays(settings.Recurrence.Pattern.Interval); - - TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; - - // - // Time window duration must be shorter than how frequently it occurs - if (timeWindowDuration > intervalDuration) - { - paramName = $"{nameof(settings.End)}"; - - reason = TimeWindowDurationOutOfRange; - - return false; - } - - paramName = null; - - reason = null; - - return true; - } - - private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - RecurrencePattern pattern = settings.Recurrence.Pattern; - - Debug.Assert(pattern.Interval > 0); - - // - // Required parameters - if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) - { - return false; - } - - TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * DaysPerWeek); - - TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; - - // - // Time window duration must be shorter than how frequently it occurs - if (timeWindowDuration > intervalDuration || - !IsDurationCompliantWithDaysOfWeek(timeWindowDuration, pattern.Interval, pattern.DaysOfWeek, pattern.FirstDayOfWeek)) - { - paramName = $"{nameof(settings.End)}"; - - reason = TimeWindowDurationOutOfRange; - - return false; - } - - // - // Check whether "Start" is a valid first occurrence - DateTimeOffset start = settings.Start.Value; - - if (!pattern.DaysOfWeek.Any(day => - day == start.DayOfWeek)) - { - paramName = nameof(settings.Start); - - reason = StartNotMatched; - - return false; - } - - return true; - } - - private static bool TryValidateRecurrenceRange(TimeWindowFilterSettings settings, out string paramName, out string reason) + public static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) { - Debug.Assert(settings != null); - Debug.Assert(settings.Start != null); - Debug.Assert(settings.Recurrence != null); - Debug.Assert(settings.Recurrence.Range != null); - - switch(settings.Recurrence.Range.Type) - { - case RecurrenceRangeType.NoEnd: - paramName = null; - - reason = null; - - return true; - - case RecurrenceRangeType.EndDate: - return TryValidateEndDate(settings, out paramName, out reason); - - case RecurrenceRangeType.Numbered: - return TryValidateNumberOfOccurrences(settings, out paramName, out reason); - - default: - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.Type)}"; - - reason = UnrecognizableValue; - - return false; - } - } - - private static bool TryValidateInterval(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Interval)}"; - - if (settings.Recurrence.Pattern.Interval <= 0) - { - reason = ValueOutOfRange; - - return false; - } - - reason = null; - - return true; - } - - private static bool TryValidateDaysOfWeek(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DaysOfWeek)}"; - - if (settings.Recurrence.Pattern.DaysOfWeek == null || !settings.Recurrence.Pattern.DaysOfWeek.Any()) - { - reason = RequiredParameter; - - return false; - } - - reason = null; - - return true; + return ((int)day1 - (int)day2 + DaysPerWeek) % DaysPerWeek; } - private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out string paramName, out string reason) - { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.EndDate)}"; - - if (settings.Start == null) - { - paramName = nameof(settings.Start); - - reason = RequiredParameter; - - return false; - } - - DateTimeOffset start = settings.Start.Value; - - DateTimeOffset endDate = settings.Recurrence.Range.EndDate; - - if (endDate < start) - { - reason = ValueOutOfRange; - - return false; - } - reason = null; - - return true; - } - - private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings settings, out string paramName, out string reason) + /// + /// Sort a collection of days of week based on their offsets from a specified first day of week. + /// A collection of days of week. + /// The first day of week. + /// The sorted days of week. + /// + public static List SortDaysOfWeek(IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) { - paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.NumberOfOccurrences)}"; - - if (settings.Recurrence.Range.NumberOfOccurrences < 1) - { - reason = ValueOutOfRange; - - return false; - } + List result = daysOfWeek.ToList(); - reason = null; + result.Sort((x, y) => + CalculateWeeklyDayOffset(x, firstDayOfWeek) + .CompareTo( + CalculateWeeklyDayOffset(y, firstDayOfWeek))); - return true; + return result; } /// @@ -518,7 +239,7 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow List sortedDaysOfWeek = SortDaysOfWeek(pattern.DaysOfWeek, pattern.FirstDayOfWeek); // - // substract the day before the start in the first week + // Subtract the days before the start in the first week. numberOfOccurrences = numberOfInterval * sortedDaysOfWeek.Count - sortedDaysOfWeek.IndexOf(start.DayOfWeek); // @@ -605,91 +326,5 @@ private static DateTimeOffset GetWeeklyNextOccurrence(DateTimeOffset previousOcc return previousOccurrence.AddDays( pattern.Interval * DaysPerWeek - CalculateWeeklyDayOffset(previousOccurrence.DayOfWeek, sortedDaysOfWeek.First())); } - - /// - /// Check whether the duration is shorter than the minimum gap between recurrence of days of week. - /// - /// The time span of the duration. - /// The recurrence interval. - /// The days of the week when the recurrence will occur. - /// The first day of the week. - /// True if the duration is compliant with days of week, false otherwise. - private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int interval, IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) - { - Debug.Assert(interval > 0); - - if (daysOfWeek.Count() == 1) - { - return true; - } - - DateTime firstDayOfThisWeek = DateTime.Today.AddDays( - DaysPerWeek - CalculateWeeklyDayOffset(DateTime.Today.DayOfWeek, firstDayOfWeek)); - - List sortedDaysOfWeek = SortDaysOfWeek(daysOfWeek, firstDayOfWeek); - - DateTime prev = DateTime.MinValue; - - TimeSpan minGap = TimeSpan.FromDays(DaysPerWeek); - - foreach(DayOfWeek dayOfWeek in sortedDaysOfWeek) - { - if (prev == DateTime.MinValue) - { - prev = firstDayOfThisWeek.AddDays( - CalculateWeeklyDayOffset(dayOfWeek, firstDayOfWeek)); - } - else - { - DateTime date = firstDayOfThisWeek.AddDays( - CalculateWeeklyDayOffset(dayOfWeek, firstDayOfWeek)); - - TimeSpan gap = date - prev; - - if (gap < minGap) - { - minGap = gap; - } - - prev = date; - } - } - - // - // It may across weeks. Check the next week if the interval is one week. - if (interval == 1) - { - DateTime firstDayOfNextWeek = firstDayOfThisWeek.AddDays(DaysPerWeek); - - DateTime firstOccurrenceInNextWeek = firstDayOfNextWeek.AddDays( - CalculateWeeklyDayOffset(sortedDaysOfWeek.First(), firstDayOfWeek)); - - TimeSpan gap = firstOccurrenceInNextWeek - prev; - - if (gap < minGap) - { - minGap = gap; - } - } - - return minGap >= duration; - } - - private static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) - { - return ((int)day1 - (int)day2 + DaysPerWeek) % DaysPerWeek; - } - - private static List SortDaysOfWeek(IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) - { - List result = daysOfWeek.ToList(); - - result.Sort((x, y) => - CalculateWeeklyDayOffset(x, firstDayOfWeek) - .CompareTo( - CalculateWeeklyDayOffset(y, firstDayOfWeek))); - - return result; - } } } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs index 8c83b320..ba852a0e 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceRange.cs @@ -23,6 +23,6 @@ public class RecurrenceRange /// /// The number of times to repeat the time window. /// - public int NumberOfOccurrences { get; set; } = 1; + public int NumberOfOccurrences { get; set; } = int.MaxValue; } } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs new file mode 100644 index 00000000..7e93f9f2 --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs @@ -0,0 +1,393 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.FeatureManagement.FeatureFilters +{ + static class RecurrenceValidator + { + const int DaysPerWeek = 7; + + // + // Error Message + const string ValueOutOfRange = "The value is out of the accepted range."; + const string UnrecognizableValue = "The value is unrecognizable."; + const string RequiredParameter = "Value cannot be null or empty."; + const string StartNotMatched = "Start date is not a valid first occurrence."; + const string TimeWindowDurationOutOfRange = "Time window duration cannot be longer than how frequently it occurs"; + + /// + /// Perform validation of time window settings. + /// The settings of time window filter. + /// The name of the invalid setting, if any. + /// The reason that the setting is invalid. + /// True if the provided settings are valid. False if the provided settings are invalid. + /// + public static bool TryValidateSettings(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (settings.Recurrence != null) + { + return TryValidateRecurrenceRequiredParameter(settings, out paramName, out reason) && + TryValidateRecurrencePattern(settings, out paramName, out reason) && + TryValidateRecurrenceRange(settings, out paramName, out reason); + } + + paramName = null; + + reason = null; + + return true; + } + + private static bool TryValidateRecurrenceRequiredParameter(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + Debug.Assert(settings != null); + Debug.Assert(settings.Recurrence != null); + + if (settings.Start == null) + { + paramName = nameof(settings.Start); + + reason = RequiredParameter; + + return false; + } + + if (settings.End == null) + { + paramName = nameof(settings.End); + + reason = RequiredParameter; + + return false; + } + + Recurrence recurrence = settings.Recurrence; + + if (recurrence.Pattern == null) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(recurrence.Pattern)}"; + + reason = RequiredParameter; + + return false; + } + + if (recurrence.Range == null) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(recurrence.Range)}"; + + reason = RequiredParameter; + + return false; + } + + if (settings.End.Value <= settings.Start.Value) + { + paramName = nameof(settings.End); + + reason = ValueOutOfRange; + + return false; + } + + paramName = null; + + reason = null; + + return true; + } + + private static bool TryValidateRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + Debug.Assert(settings != null); + Debug.Assert(settings.Start != null); + Debug.Assert(settings.End != null); + Debug.Assert(settings.Recurrence != null); + Debug.Assert(settings.Recurrence.Pattern != null); + + if (!TryValidateInterval(settings, out paramName, out reason)) + { + return false; + } + + switch (settings.Recurrence.Pattern.Type) + { + case RecurrencePatternType.Daily: + return TryValidateDailyRecurrencePattern(settings, out paramName, out reason); + + case RecurrencePatternType.Weekly: + return TryValidateWeeklyRecurrencePattern(settings, out paramName, out reason); + + default: + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Type)}"; + + reason = UnrecognizableValue; + + return false; + } + } + + private static bool TryValidateDailyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + Debug.Assert(settings.Recurrence.Pattern.Interval > 0); + + // + // No required parameter for "Daily" pattern + // "Start" is always a valid first occurrence for "Daily" pattern + + TimeSpan intervalDuration = TimeSpan.FromDays(settings.Recurrence.Pattern.Interval); + + TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + + // + // Time window duration must be shorter than how frequently it occurs + if (timeWindowDuration > intervalDuration) + { + paramName = $"{nameof(settings.End)}"; + + reason = TimeWindowDurationOutOfRange; + + return false; + } + + paramName = null; + + reason = null; + + return true; + } + + private static bool TryValidateWeeklyRecurrencePattern(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + RecurrencePattern pattern = settings.Recurrence.Pattern; + + Debug.Assert(pattern.Interval > 0); + + // + // Required parameters + if (!TryValidateDaysOfWeek(settings, out paramName, out reason)) + { + return false; + } + + TimeSpan intervalDuration = TimeSpan.FromDays(pattern.Interval * DaysPerWeek); + + TimeSpan timeWindowDuration = settings.End.Value - settings.Start.Value; + + // + // Time window duration must be shorter than how frequently it occurs + if (timeWindowDuration > intervalDuration || + !IsDurationCompliantWithDaysOfWeek(timeWindowDuration, pattern.Interval, pattern.DaysOfWeek, pattern.FirstDayOfWeek)) + { + paramName = $"{nameof(settings.End)}"; + + reason = TimeWindowDurationOutOfRange; + + return false; + } + + // + // Check whether "Start" is a valid first occurrence + DateTimeOffset start = settings.Start.Value; + + if (!pattern.DaysOfWeek.Any(day => + day == start.DayOfWeek)) + { + paramName = nameof(settings.Start); + + reason = StartNotMatched; + + return false; + } + + return true; + } + + private static bool TryValidateRecurrenceRange(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + Debug.Assert(settings != null); + Debug.Assert(settings.Start != null); + Debug.Assert(settings.Recurrence != null); + Debug.Assert(settings.Recurrence.Range != null); + + switch (settings.Recurrence.Range.Type) + { + case RecurrenceRangeType.NoEnd: + paramName = null; + + reason = null; + + return true; + + case RecurrenceRangeType.EndDate: + return TryValidateEndDate(settings, out paramName, out reason); + + case RecurrenceRangeType.Numbered: + return TryValidateNumberOfOccurrences(settings, out paramName, out reason); + + default: + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.Type)}"; + + reason = UnrecognizableValue; + + return false; + } + } + + private static bool TryValidateInterval(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.Interval)}"; + + if (settings.Recurrence.Pattern.Interval <= 0) + { + reason = ValueOutOfRange; + + return false; + } + + reason = null; + + return true; + } + + private static bool TryValidateDaysOfWeek(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Pattern)}.{nameof(settings.Recurrence.Pattern.DaysOfWeek)}"; + + if (settings.Recurrence.Pattern.DaysOfWeek == null || !settings.Recurrence.Pattern.DaysOfWeek.Any()) + { + reason = RequiredParameter; + + return false; + } + + reason = null; + + return true; + } + + private static bool TryValidateEndDate(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.EndDate)}"; + + if (settings.Start == null) + { + paramName = nameof(settings.Start); + + reason = RequiredParameter; + + return false; + } + + DateTimeOffset start = settings.Start.Value; + + DateTimeOffset endDate = settings.Recurrence.Range.EndDate; + + if (endDate < start) + { + reason = ValueOutOfRange; + + return false; + } + + reason = null; + + return true; + } + + private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings settings, out string paramName, out string reason) + { + paramName = $"{nameof(settings.Recurrence)}.{nameof(settings.Recurrence.Range)}.{nameof(settings.Recurrence.Range.NumberOfOccurrences)}"; + + if (settings.Recurrence.Range.NumberOfOccurrences < 1) + { + reason = ValueOutOfRange; + + return false; + } + + reason = null; + + return true; + } + + /// + /// Check whether the duration is shorter than the minimum gap between recurrence of days of week. + /// + /// The time span of the duration. + /// The recurrence interval. + /// The days of the week when the recurrence will occur. + /// The first day of the week. + /// True if the duration is compliant with days of week, false otherwise. + private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int interval, IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) + { + Debug.Assert(interval > 0); + + if (daysOfWeek.Count() == 1) + { + return true; + } + + DateTime firstDayOfThisWeek = DateTime.Today.AddDays( + DaysPerWeek - RecurrenceEvaluator.CalculateWeeklyDayOffset(DateTime.Today.DayOfWeek, firstDayOfWeek)); + + List sortedDaysOfWeek = RecurrenceEvaluator.SortDaysOfWeek(daysOfWeek, firstDayOfWeek); + + DateTime prev = DateTime.MinValue; + + TimeSpan minGap = TimeSpan.FromDays(DaysPerWeek); + + foreach (DayOfWeek dayOfWeek in sortedDaysOfWeek) + { + if (prev == DateTime.MinValue) + { + prev = firstDayOfThisWeek.AddDays( + RecurrenceEvaluator.CalculateWeeklyDayOffset(dayOfWeek, firstDayOfWeek)); + } + else + { + DateTime date = firstDayOfThisWeek.AddDays( + RecurrenceEvaluator.CalculateWeeklyDayOffset(dayOfWeek, firstDayOfWeek)); + + TimeSpan gap = date - prev; + + if (gap < minGap) + { + minGap = gap; + } + + prev = date; + } + } + + // + // It may across weeks. Check the next week if the interval is one week. + if (interval == 1) + { + DateTime firstDayOfNextWeek = firstDayOfThisWeek.AddDays(DaysPerWeek); + + DateTime firstOccurrenceInNextWeek = firstDayOfNextWeek.AddDays( + RecurrenceEvaluator.CalculateWeeklyDayOffset(sortedDaysOfWeek.First(), firstDayOfWeek)); + + TimeSpan gap = firstOccurrenceInNextWeek - prev; + + if (gap < minGap) + { + minGap = gap; + } + } + + return minGap >= duration; + } + } +} diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 4aa5b86a..17bced52 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -40,7 +40,7 @@ public object BindParameters(IConfiguration filterParameters) { var settings = filterParameters.Get() ?? new TimeWindowFilterSettings(); - if (!RecurrenceEvaluator.TryValidateSettings(settings, out string paramName, out string reason)) + if (!RecurrenceValidator.TryValidateSettings(settings, out string paramName, out string reason)) { throw new ArgumentException(reason, paramName); } diff --git a/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs b/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs index 31078713..568de60d 100644 --- a/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs +++ b/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs @@ -1,11 +1,9 @@ using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Threading.Tasks; namespace Tests.FeatureManagement { @@ -22,7 +20,7 @@ public object BindParameters(IConfiguration filterParameters) { var settings = filterParameters.Get() ?? new TimeWindowFilterSettings(); - if (!RecurrenceEvaluator.TryValidateSettings(settings, out string paramName, out string reason)) + if (!RecurrenceValidator.TryValidateSettings(settings, out string paramName, out string reason)) { throw new ArgumentException(reason, paramName); } diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs similarity index 99% rename from tests/Tests.FeatureManagement/RecurrenceEvaluator.cs rename to tests/Tests.FeatureManagement/RecurrenceEvaluation.cs index 2e720897..1cb848be 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluator.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs @@ -38,41 +38,19 @@ class ParamName public const string EndDate = "Recurrence.Range.EndDate"; } - public class RecurrenceEvaluatorTest + public class RecurrenceValidatorTest { private static void ConsumeValidationTestData(List> testData) { foreach ((TimeWindowFilterSettings settings, string paramNameRef, string errorMessageRef) in testData) { - RecurrenceEvaluator.TryValidateSettings(settings, out string paramName, out string errorMessage); + RecurrenceValidator.TryValidateSettings(settings, out string paramName, out string errorMessage); Assert.Equal(paramNameRef, paramName); Assert.Equal(errorMessageRef, errorMessage); } } - private static void ConsumeEvaluationTestData(List> testData) - { - foreach ((DateTimeOffset time, TimeWindowFilterSettings settings, bool expected) in testData) - { - Assert.Equal(RecurrenceEvaluator.MatchRecurrence(time, settings), expected); - } - } - - private static void ConsumeEvalutationTestData(List> testData) - { - foreach ((DateTimeOffset time, TimeWindowFilterSettings settings, bool expectedRes, DateTimeOffset expectedPrev, DateTimeOffset expectedNext) in testData) - { - Assert.Equal(expectedRes, RecurrenceEvaluator.TryFindPrevAndNextOccurrences(time, settings, out DateTimeOffset prev, out DateTimeOffset next)); - - if (expectedRes) - { - Assert.Equal(expectedPrev, prev); - Assert.Equal(expectedNext, next); - } - } - } - [Fact] public void GeneralRequiredParameterTest() { @@ -359,7 +337,7 @@ public void ValidTimeWindowAcrossWeeks() } }; - Assert.False(RecurrenceEvaluator.TryValidateSettings(settings, out string paramName, out string errorMessage)); + Assert.False(RecurrenceValidator.TryValidateSettings(settings, out string paramName, out string errorMessage)); Assert.Equal(ParamName.End, paramName); Assert.Equal(ErrorMessage.TimeWindowDurationOutOfRange, errorMessage); } @@ -415,6 +393,31 @@ public void WeeklyPatternStartNotMatchedTest() ConsumeValidationTestData(testData); } + } + + public class RecurrenceEvaluatorTest + { + private static void ConsumeEvaluationTestData(List> testData) + { + foreach ((DateTimeOffset time, TimeWindowFilterSettings settings, bool expected) in testData) + { + Assert.Equal(RecurrenceEvaluator.MatchRecurrence(time, settings), expected); + } + } + + private static void ConsumeEvalutationTestData(List> testData) + { + foreach ((DateTimeOffset time, TimeWindowFilterSettings settings, bool expectedRes, DateTimeOffset expectedPrev, DateTimeOffset expectedNext) in testData) + { + Assert.Equal(expectedRes, RecurrenceEvaluator.TryFindPrevAndNextOccurrences(time, settings, out DateTimeOffset prev, out DateTimeOffset next)); + + if (expectedRes) + { + Assert.Equal(expectedPrev, prev); + Assert.Equal(expectedNext, next); + } + } + } [Fact] public void MatchDailyRecurrenceTest() From 856e183852846ade7d632c3d29a7a7c80ddcb876 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 22 Mar 2024 13:25:21 +0800 Subject: [PATCH 38/52] README updated --- README.md | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 61e63032..353399c4 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ The feature management library supports appsettings.json as a feature flag sourc "Name": "TimeWindow", "Parameters": { "Start": "Wed, 01 May 2019 13:59:59 GMT", - "End": "Mon, 01 July 2019 00:00:00 GMT" + "End": "Mon, 01 Jul 2019 00:00:00 GMT" } } ] @@ -131,7 +131,7 @@ A `RequirementType` of `All` changes the traversal. First, if there are no filte "Name": "TimeWindow", "Parameters": { "Start": "Mon, 01 May 2023 13:59:59 GMT", - "End": "Sat, 01 July 2023 00:00:00 GMT" + "End": "Sat, 01 Jul 2023 00:00:00 GMT" } }, { @@ -163,7 +163,7 @@ The feature management library also supports the usage of the [`Microsoft Featur "name": "Microsoft.TimeWindow", "parameters": { "Start": "Mon, 01 May 2023 13:59:59 GMT", - "End": "Sat, 01 July 2023 00:00:00 GMT" + "End": "Sat, 01 Jul 2023 00:00:00 GMT" } } ] @@ -565,13 +565,117 @@ This filter provides the capability to enable a feature based on a time window. "Name": "Microsoft.TimeWindow", "Parameters": { "Start": "Wed, 01 May 2019 13:59:59 GMT", - "End": "Mon, 01 July 2019 00:00:00 GMT" + "End": "Mon, 01 Jul 2019 00:00:00 GMT" } } ] } ``` +The time window can be configured to recur periodically. This can be useful for the scenarios where one may need to turn on a feature during a low or high traffic period of a day or certain days of a week. To expand the individual time window to recurring time windows, the recurrence rule should be specified in the `Recurrence` parameter. + +**Note:** `Start` and `End` must be both specified to enable `Recurrence`. + +``` JavaScript +"EnhancedPipeline": { + "EnabledFor": [ + { + "Name": "Microsoft.TimeWindow", + "Parameters": { + "Start": "Fri, 22 Mar 2024 20:00:00 GMT", + "End": "Sat, 23 Mar 2024 02:00:00 GMT", + "Recurrence": { + "Pattern": { + "Type": "Daily", + "Interval": 1 + }, + "Range": { + "Type": "NoEnd" + } + } + } + } + ] +} +``` + +The `Recurrence` settings is made up of two parts: `Pattern` (how often the time window will repeat) and `Range` (for how long the recurrence pattern will repeat). + +#### Recurrence Pattern + +There are two possible recurrence pattern types: `Daily` and `Weekly`. For example, a time window could repeat "every day", "every 3 days", "every Monday" or "on Friday per 2 weeks". + +Depending on the type, certain fields of the `Pattern` are required, optional, or ignored. + +- `Daily` + + The daily recurrence pattern causes the time window to repeat based on a number of days between each occurrence. + + | Property | Relevance | Description | + |----------|-----------|-------------| + | **Type** | Required | Must be set to `Daily`. | + | **Interval** | Optional | Specifies the number of days between each occurrence. Default value is 1. | + +- `Weekly` + + The weekly recurrence pattern causes the time window to repeat on the same day or days of the week, based on the number of weeks between each set of occurrences. + + | Property | Relevance | Description | + |----------|-----------|-------------| + | **Type** | Required | Must be set to `Weekly`. | + | **DaysOfWeek** | Required | Specifies on which day(s) of the week the event occurs. | + | **Interval** | Optional | Specifies the number of weeks between each set of occurrences. Default value is 1. | + | **FirstDayOfWeek** | Optional | Specifies which day is considered the first day of the week. Default value is `Sunday`. | + + The following example will repeat the time window every other Monday and Tuesday + + ``` javascript + "Pattern": { + "Type": "Weekly", + "Interval": 2, + "DaysOfWeek": [ + "Monday", + "Tuesday" + ] + } + ``` + +**Note:** `Start` must be a valid first occurrence which fits the recurrence pattern. Additionally, the duration of the time window cannot be longer than how frequently it occurs. For example, it is invalid to have a 25-hour time window recur every day. + +#### Recurrence Range + +There are three possible recurrence range type: `NoEnd`, `EndDate` and `Numbered`. + +- `NoEnd` + + The `NoEnd` range causes the recurrence to occur indefinitely. + + | Property | Relevance | Description | + |----------|-----------|-------------| + | **Type** | Required | Must be set to `EndDate`. | + +- `EndDate` + + The `EndDate` range causes the time window to occur on all days that fit the applicable pattern until the end date. + + | Property | Relevance | Description | + |----------|-----------|-------------| + | **Type** | Required | Must be set to `EndDate`. | + | **EndDate** | Required | Specifies the date time to stop applying the pattern. Note that as long as the start time of the last occurrence falls before the end date, the end time of that occurrence is allowed to extend beyond it. | + +- `Numbered` + + The `Numbered` range causes the time window to occur a fixed number of times (based on the pattern). + + | Property | Relevance | Description | + |----------|-----------|-------------| + | **Type** | Required | Must be set to `Numbered`. | + | **NumberOfOccurrences** | Required | Specifies the number of occurrences. | + +To create a recurrence rule, you must specify both `Pattern` and `Range`. Any pattern type can work with any range type. + +**Advanced:** The time zone offset of the `Start` will apply to the recurrence settings. + ### Microsoft.Targeting This filter provides the capability to enable a feature for a target audience. An in-depth explanation of targeting is explained in the [targeting](./README.md#Targeting) section below. The filter parameters include an audience object which describes users, groups, excluded users/groups, and a default percentage of the user base that should have access to the feature. Each group object that is listed in the target audience must also specify what percentage of the group's members should have access. If a user is specified in the exclusion section, either directly or if the user is in an excluded group, the feature will be disabled. Otherwise, if a user is specified in the users section directly, or if the user is in the included percentage of any of the group rollouts, or if the user falls into the default rollout percentage then that user will have the feature enabled. From b1c06ad54f4ad7f1dea53e8d3847c14227365f04 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Sun, 7 Apr 2024 13:34:31 +0800 Subject: [PATCH 39/52] update readme --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 353399c4..c8e59665 100644 --- a/README.md +++ b/README.md @@ -631,12 +631,9 @@ Depending on the type, certain fields of the `Pattern` are required, optional, o ``` javascript "Pattern": { - "Type": "Weekly", - "Interval": 2, - "DaysOfWeek": [ - "Monday", - "Tuesday" - ] + "Type": "Weekly", + "Interval": 2, + "DaysOfWeek": ["Monday", "Tuesday"] } ``` @@ -652,7 +649,7 @@ There are three possible recurrence range type: `NoEnd`, `EndDate` and `Numbered | Property | Relevance | Description | |----------|-----------|-------------| - | **Type** | Required | Must be set to `EndDate`. | + | **Type** | Required | Must be set to `NoEnd`. | - `EndDate` @@ -663,6 +660,23 @@ There are three possible recurrence range type: `NoEnd`, `EndDate` and `Numbered | **Type** | Required | Must be set to `EndDate`. | | **EndDate** | Required | Specifies the date time to stop applying the pattern. Note that as long as the start time of the last occurrence falls before the end date, the end time of that occurrence is allowed to extend beyond it. | + The following example will repeat the time window every day until the last occurrence happens on April 1st, 2024. + + ``` javascript + "Start": "Fri, 22 Mar 2024 18:00:00 GMT", + "End": "Fri, 22 Mar 2024 20:00:00 GMT", + "Recurrence":{ + "Pattern": { + "Type": "Daily", + "Interval": 1 + }, + "Range": { + "Type": "EndDate", + "EndDate": "Mon, 1 Apr 2024 20:00:00 GMT" + } + } + ``` + - `Numbered` The `Numbered` range causes the time window to occur a fixed number of times (based on the pattern). @@ -672,9 +686,27 @@ There are three possible recurrence range type: `NoEnd`, `EndDate` and `Numbered | **Type** | Required | Must be set to `Numbered`. | | **NumberOfOccurrences** | Required | Specifies the number of occurrences. | + The following example will repeat the time window on Monday and Tuesday until the there are 3 occurrences, which respectively happens on April 1st(Mon), April 2nd(Tue) and April 8th(Mon). + + ``` javascript + "Start": "Mon, 1 Apr 2024 18:00:00 GMT", + "End": "Mon, 1 Apr 2024 20:00:00 GMT", + "Recurrence":{ + "Pattern": { + "Type": "Weekly", + "Interval": 1 + "DaysOfWeek": ["Monday", "Tuesday"], + }, + "Range": { + "Type": "Numbered", + "NumberOfOccurrences": 3 + } + } + ``` + To create a recurrence rule, you must specify both `Pattern` and `Range`. Any pattern type can work with any range type. -**Advanced:** The time zone offset of the `Start` will apply to the recurrence settings. +**Advanced:** The time zone offset of the `Start` property will apply to the recurrence settings. ### Microsoft.Targeting From 0bb801790d2435afe59214ead97dd78aba0f13a7 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Sun, 7 Apr 2024 16:47:33 +0800 Subject: [PATCH 40/52] update CalculateSurroundingOccurrences method --- .../Recurrence/RecurrenceEvaluator.cs | 99 +++++++++---------- .../FeatureFilters/TimeWindowFilter.cs | 84 +++++++++------- .../MockedTimeWindowFilter.cs | 85 +++++++++------- .../RecurrenceEvaluation.cs | 78 +++++---------- 4 files changed, 167 insertions(+), 179 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index d85c2554..74603744 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -21,6 +21,12 @@ static class RecurrenceEvaluator /// public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings settings) { + Debug.Assert(settings != null); + Debug.Assert(settings.Start != null); + Debug.Assert(settings.Recurrence != null); + Debug.Assert(settings.Recurrence.Pattern != null); + Debug.Assert(settings.Recurrence.Range != null); + if (time < settings.Start.Value) { return false; @@ -35,46 +41,45 @@ public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings } /// - /// Try to find the closest previous recurrence occurrence (if any) before the provided timestamp and the next occurrence. + /// Calculate the closest previous recurrence occurrence (if any) before the provided timestamp and the next occurrence (if any) after the provided timestamp. /// A timestamp. /// The settings of time window filter. - /// The closest previous occurrence. If there is no previous occurrence, it will be set to . - /// The next occurrence. - /// True if the closest previous occurrence is within the recurrence range or the time is before the first occurrence, false otherwise. + /// The closest previous occurrence. Note that prev occurrence can be null even if the time is past the start date, because the recurrence range may have surpassed its end. + /// The next occurrence. Note that next occurrence can be null even if the prev occurrence is not null, because the recurrence range may have reached its end. /// - public static bool TryFindPrevAndNextOccurrences(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset prevOccurrence, out DateTimeOffset nextOccurrence) + public static void CalculateSurroundingOccurrences(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrence) { - prevOccurrence = DateTimeOffset.MinValue; + Debug.Assert(settings != null); + Debug.Assert(settings.Start != null); + Debug.Assert(settings.Recurrence != null); + Debug.Assert(settings.Recurrence.Pattern != null); + Debug.Assert(settings.Recurrence.Range != null); + + prevOccurrence = null; - nextOccurrence = DateTimeOffset.MaxValue; + nextOccurrence = null; if (time < settings.Start.Value) { - // - // The time is before the first occurrence. nextOccurrence = settings.Start.Value; - return true; + return; } - if (TryFindPreviousOccurrence(time, settings, out prevOccurrence, out int numberOfOccurrences)) + if (TryFindPreviousOccurrence(time, settings, out DateTimeOffset prev, out int numberOfOccurrences)) { + prevOccurrence = prev; + RecurrencePattern pattern = settings.Recurrence.Pattern; - switch (pattern.Type) + if (pattern.Type == RecurrencePatternType.Daily) { - case RecurrencePatternType.Daily: - nextOccurrence = prevOccurrence.AddDays(pattern.Interval); - - break; - - case RecurrencePatternType.Weekly: - nextOccurrence = GetWeeklyNextOccurrence(prevOccurrence, settings); - - break; + nextOccurrence = prev.AddDays(pattern.Interval); + } - default: - return false; + if (pattern.Type == RecurrencePatternType.Weekly) + { + nextOccurrence = CalculateWeeklyNextOccurrence(prev, settings); } RecurrenceRange range = settings.Recurrence.Range; @@ -83,7 +88,7 @@ public static bool TryFindPrevAndNextOccurrences(DateTimeOffset time, TimeWindow { if (nextOccurrence > range.EndDate) { - nextOccurrence = DateTimeOffset.MaxValue; + nextOccurrence = null; } } @@ -91,14 +96,10 @@ public static bool TryFindPrevAndNextOccurrences(DateTimeOffset time, TimeWindow { if (numberOfOccurrences >= range.NumberOfOccurrences) { - nextOccurrence = DateTimeOffset.MaxValue; + nextOccurrence = null; } } - - return true; } - - return false; } /// @@ -132,7 +133,7 @@ public static List SortDaysOfWeek(IEnumerable daysOfWeek, } /// - /// Try to find the closest previous recurrence occurrence before the provided timestamp according to the recurrence pattern. + /// Try to find the closest previous recurrence occurrence before the provided timestamp according to the recurrence pattern. The given time should be later than the recurrence start. /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. @@ -141,30 +142,22 @@ public static List SortDaysOfWeek(IEnumerable daysOfWeek, /// private static bool TryFindPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { - Debug.Assert(settings.Start != null); - Debug.Assert(settings.Recurrence != null); - Debug.Assert(settings.Recurrence.Pattern != null); - Debug.Assert(settings.Recurrence.Range != null); Debug.Assert(settings.Start.Value <= time); previousOccurrence = DateTimeOffset.MinValue; numberOfOccurrences = 0; - switch (settings.Recurrence.Pattern.Type) - { - case RecurrencePatternType.Daily: - FindDailyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); - - break; - - case RecurrencePatternType.Weekly: - FindWeeklyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + RecurrencePattern pattern = settings.Recurrence.Pattern; - break; + if (pattern.Type == RecurrencePatternType.Daily) + { + FindDailyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); + } - default: - return false; + if (pattern.Type == RecurrencePatternType.Weekly) + { + FindWeeklyPreviousOccurrence(time, settings, out previousOccurrence, out numberOfOccurrences); } RecurrenceRange range = settings.Recurrence.Range; @@ -183,7 +176,7 @@ private static bool TryFindPreviousOccurrence(DateTimeOffset time, TimeWindowFil } /// - /// Find the closest previous recurrence occurrence before the provided timestamp according to the "Daily" recurrence pattern. + /// Find the closest previous recurrence occurrence before the provided timestamp according to the "Daily" recurrence pattern. The given time should be later than the recurrence start. /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. @@ -191,12 +184,12 @@ private static bool TryFindPreviousOccurrence(DateTimeOffset time, TimeWindowFil /// private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { + Debug.Assert(settings.Start.Value <= time); + RecurrencePattern pattern = settings.Recurrence.Pattern; DateTimeOffset start = settings.Start.Value; - Debug.Assert(time >= start); - int interval = pattern.Interval; TimeSpan timeGap = time - start; @@ -211,7 +204,7 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF } /// - /// Find the closest previous recurrence occurrence before the provided timestamp according to the "Weekly" recurrence pattern. + /// Find the closest previous recurrence occurrence before the provided timestamp according to the "Weekly" recurrence pattern. The given time should be later than the recurrence start. /// A timestamp. /// The settings of time window filter. /// The closest previous occurrence. @@ -219,12 +212,12 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF /// private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset previousOccurrence, out int numberOfOccurrences) { + Debug.Assert(settings.Start.Value <= time); + RecurrencePattern pattern = settings.Recurrence.Pattern; DateTimeOffset start = settings.Start.Value; - Debug.Assert(time >= start); - int interval = pattern.Interval; DateTimeOffset firstDayOfStartWeek = start.AddDays( @@ -309,7 +302,7 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow /// The previous occurrence. /// The settings of time window filter. /// - private static DateTimeOffset GetWeeklyNextOccurrence(DateTimeOffset previousOccurrence, TimeWindowFilterSettings settings) + private static DateTimeOffset CalculateWeeklyNextOccurrence(DateTimeOffset previousOccurrence, TimeWindowFilterSettings settings) { RecurrencePattern pattern = settings.Recurrence.Pattern; diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 7fe000ac..e17d6a10 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -81,58 +82,67 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) // The reference of the object will be used for hash key. // If there is no pre-bounded settings attached to the context, there will be no cached filter settings and each call will have a unique settings object. // In this case, the cache for recurrence settings won't work. - if (context.Settings != null) + if (context.Settings == null) { - DateTimeOffset cachedTime = _recurrenceCache.GetOrAdd( - settings, - (_) => + return Task.FromResult(RecurrenceEvaluator.MatchRecurrence(now, settings)); + } + + DateTimeOffset cachedTime = _recurrenceCache.GetOrAdd( + settings, + (_) => + { + RecurrenceEvaluator.CalculateSurroundingOccurrences(now, settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrence); + + if (now < settings.Start.Value) { - if (RecurrenceEvaluator.TryFindPrevAndNextOccurrences(now, settings, out DateTimeOffset prevOccurrence, out DateTimeOffset _)) - { - return prevOccurrence; - } + return nextOccurrence.Value; + } - // - // There is no previous occurrence within the reccurrence range. - return DateTimeOffset.MaxValue; - }); + if (prevOccurrence != null) + { + return prevOccurrence.Value; + } - if (now < cachedTime) - { - return Task.FromResult(false); - } + // + // There is no previous occurrence within the reccurrence range. + return DateTimeOffset.MaxValue; + }); - if (now < cachedTime + (settings.End.Value - settings.Start.Value)) - { - return Task.FromResult(true); - } + if (now < cachedTime) + { + return Task.FromResult(false); + } - if (RecurrenceEvaluator.TryFindPrevAndNextOccurrences(now, settings, out DateTimeOffset prevOccurrence, out DateTimeOffset nextOccurrrence)) - { - bool isWithinPreviousTimeWindow = - now <= prevOccurrence + (settings.End.Value - settings.Start.Value); + if (now < cachedTime + (settings.End.Value - settings.Start.Value)) + { + return Task.FromResult(true); + } - _recurrenceCache.AddOrUpdate( - settings, - (_) => throw new KeyNotFoundException(), - (_, _) => isWithinPreviousTimeWindow ? - prevOccurrence : - nextOccurrrence); + RecurrenceEvaluator.CalculateSurroundingOccurrences(now, settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrrence); - return Task.FromResult(isWithinPreviousTimeWindow); - } + if (prevOccurrence != null) + { + Debug.Assert(now > settings.Start.Value); + + bool isWithinPreviousTimeWindow = + now <= prevOccurrence.Value + (settings.End.Value - settings.Start.Value); - // - // There is no previous occurrence within the reccurrence range. _recurrenceCache.AddOrUpdate( settings, (_) => throw new KeyNotFoundException(), - (_, _) => DateTimeOffset.MaxValue); + (_, _) => isWithinPreviousTimeWindow ? + prevOccurrence.Value : + nextOccurrrence ?? DateTimeOffset.MaxValue); - return Task.FromResult(false); + return Task.FromResult(isWithinPreviousTimeWindow); } - return Task.FromResult(RecurrenceEvaluator.MatchRecurrence(now, settings)); + // + // There is no previous occurrence within the recurrence range. + _recurrenceCache.AddOrUpdate( + settings, + (_) => throw new KeyNotFoundException(), + (_, _) => DateTimeOffset.MaxValue); } return Task.FromResult(false); diff --git a/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs b/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs index 568de60d..84f8b935 100644 --- a/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs +++ b/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; namespace Tests.FeatureManagement { @@ -52,58 +54,67 @@ public bool Evaluate(DateTimeOffset now, FeatureFilterEvaluationContext context) // The reference of the object will be used for hash key. // If there is no pre-bounded settings attached to the context, there will be no cached filter settings and each call will have a unique settings object. // In this case, the cache for recurrence settings won't work. - if (context.Settings != null) + if (context.Settings == null) { - DateTimeOffset cachedTime = _recurrenceCache.GetOrAdd( - settings, - (_) => + return RecurrenceEvaluator.MatchRecurrence(now, settings); + } + + DateTimeOffset cachedTime = _recurrenceCache.GetOrAdd( + settings, + (_) => + { + RecurrenceEvaluator.CalculateSurroundingOccurrences(now, settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrence); + + if (now < settings.Start.Value) { - if (RecurrenceEvaluator.TryFindPrevAndNextOccurrences(now, settings, out DateTimeOffset prevOccurrence, out DateTimeOffset _)) - { - return prevOccurrence; - } + return nextOccurrence.Value; + } - // - // There is no previous occurrence within the reccurrence range. - return DateTimeOffset.MaxValue; - }); + if (prevOccurrence != null) + { + return prevOccurrence.Value; + } - if (now < cachedTime) - { - return false; - } + // + // There is no previous occurrence within the reccurrence range. + return DateTimeOffset.MaxValue; + }); - if (now <= cachedTime + (settings.End.Value - settings.Start.Value)) - { - return true; - } + if (now < cachedTime) + { + return false; + } - if (RecurrenceEvaluator.TryFindPrevAndNextOccurrences(now, settings, out DateTimeOffset prevOccurrence, out DateTimeOffset nextOccurrrence)) - { - bool isWithinPreviousTimeWindow = - now <= prevOccurrence + (settings.End.Value - settings.Start.Value); + if (now < cachedTime + (settings.End.Value - settings.Start.Value)) + { + return true; + } - _recurrenceCache.AddOrUpdate( - settings, - (_) => throw new KeyNotFoundException(), - (_, _) => isWithinPreviousTimeWindow ? - prevOccurrence : - nextOccurrrence); + RecurrenceEvaluator.CalculateSurroundingOccurrences(now, settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrrence); - return isWithinPreviousTimeWindow; - } + if (prevOccurrence != null) + { + Debug.Assert(now > settings.Start.Value); + + bool isWithinPreviousTimeWindow = + now <= prevOccurrence.Value + (settings.End.Value - settings.Start.Value); - // - // There is no previous occurrence within the reccurrence range. _recurrenceCache.AddOrUpdate( settings, (_) => throw new KeyNotFoundException(), - (_, _) => DateTimeOffset.MaxValue); + (_, _) => isWithinPreviousTimeWindow ? + prevOccurrence.Value : + nextOccurrrence ?? DateTimeOffset.MaxValue); - return false; + return isWithinPreviousTimeWindow; } - return RecurrenceEvaluator.MatchRecurrence(now, settings); + // + // There is no previous occurrence within the recurrence range. + _recurrenceCache.AddOrUpdate( + settings, + (_) => throw new KeyNotFoundException(), + (_, _) => DateTimeOffset.MaxValue); } return false; diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs index 1cb848be..51018c2e 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs @@ -405,17 +405,14 @@ private static void ConsumeEvaluationTestData(List> testData) + private static void ConsumeEvalutationTestData(List> testData) { - foreach ((DateTimeOffset time, TimeWindowFilterSettings settings, bool expectedRes, DateTimeOffset expectedPrev, DateTimeOffset expectedNext) in testData) + foreach ((DateTimeOffset time, TimeWindowFilterSettings settings, DateTimeOffset? expectedPrev, DateTimeOffset? expectedNext) in testData) { - Assert.Equal(expectedRes, RecurrenceEvaluator.TryFindPrevAndNextOccurrences(time, settings, out DateTimeOffset prev, out DateTimeOffset next)); - - if (expectedRes) - { - Assert.Equal(expectedPrev, prev); - Assert.Equal(expectedNext, next); - } + RecurrenceEvaluator.CalculateSurroundingOccurrences(time, settings, out DateTimeOffset? prev, out DateTimeOffset? next); + + Assert.Equal(expectedPrev, prev); + Assert.Equal(expectedNext, next); } } @@ -986,7 +983,7 @@ public void MatchWeeklyRecurrenceTest() [Fact] public void FindDailyPrevAndNextOccurrenceTest() { - var testData = new List>() + var testData = new List>() { ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1002,8 +999,7 @@ public void FindDailyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - true, - DateTimeOffset.MinValue, + null, DateTimeOffset.Parse("2024-3-1T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), @@ -1020,7 +1016,6 @@ public void FindDailyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - true, DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), @@ -1039,7 +1034,6 @@ public void FindDailyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - true, DateTimeOffset.Parse("2024-2-27T00:00:00+08:00"), DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), @@ -1058,7 +1052,6 @@ public void FindDailyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - true, DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), DateTimeOffset.Parse("2024-3-2T00:00:00+08:00")), @@ -1080,9 +1073,8 @@ public void FindDailyPrevAndNextOccurrenceTest() } } }, - false, - default, - default), + null, + null), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1102,9 +1094,8 @@ public void FindDailyPrevAndNextOccurrenceTest() } } }, - true, DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), - DateTimeOffset.MaxValue), + null), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1124,9 +1115,8 @@ public void FindDailyPrevAndNextOccurrenceTest() } } }, - false, - default, - default), + null, + null), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1146,9 +1136,8 @@ public void FindDailyPrevAndNextOccurrenceTest() } } }, - true, DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), - DateTimeOffset.MaxValue) + null) }; ConsumeEvalutationTestData(testData); @@ -1157,7 +1146,7 @@ public void FindDailyPrevAndNextOccurrenceTest() [Fact] public void FindWeeklyPrevAndNextOccurrenceTest() { - var testData = new List>() + var testData = new List>() { ( DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1174,8 +1163,7 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - true, - DateTimeOffset.MinValue, + null, DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), @@ -1193,7 +1181,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - true, DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), DateTimeOffset.Parse("2024-3-7T00:00:00+08:00")), @@ -1212,7 +1199,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - true, DateTimeOffset.Parse("2024-2-22T12:00:00+08:00"), DateTimeOffset.Parse("2024-2-29T12:00:00+08:00")), @@ -1231,7 +1217,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - true, DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), DateTimeOffset.Parse("2024-3-3T00:00:00+08:00")), @@ -1252,7 +1237,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - true, DateTimeOffset.Parse("2024-2-25T00:00:00+08:00"), DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), @@ -1273,7 +1257,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - true, DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), DateTimeOffset.Parse("2024-2-11T00:00:00+08:00")), // Sunday in the 3rd week @@ -1296,9 +1279,8 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - true, DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), - DateTimeOffset.MaxValue), + null), ( DateTimeOffset.Parse("2024-2-2T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1319,9 +1301,8 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - false, - default, - default), + null, + null), ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1344,9 +1325,8 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - true, DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), // Sunday in the 3rd week - DateTimeOffset.MaxValue), + null), ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1369,9 +1349,8 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - true, DateTimeOffset.Parse("2024-2-4T00:00:00+08:00"), // Sunday in the 1st week - DateTimeOffset.MaxValue), + null), ( DateTimeOffset.Parse("2024-2-12T00:00:00+08:00"), // Monday in the 3rd week new TimeWindowFilterSettings() @@ -1394,7 +1373,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - true, DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), // Sunday in the 3rd week DateTimeOffset.Parse("2024-2-15T00:00:00+08:00")), // Thursday in the 3rd week @@ -1419,7 +1397,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - true, DateTimeOffset.Parse("2024-2-4T00:00:00+08:00"), // Sunday in the 1st week DateTimeOffset.Parse("2024-2-15T00:00:00+08:00")), // Thursday in the 3rd week @@ -1444,9 +1421,8 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - true, DateTimeOffset.Parse("2024-2-3T12:00:00+08:00"), // Saturday in the 1st week - DateTimeOffset.MaxValue), + null), ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), // Sunday in the 2nd week new TimeWindowFilterSettings() @@ -1469,9 +1445,8 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - false, - default, - default), + null, + null), ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1492,9 +1467,8 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - false, - default, - default) + null, + null) }; ConsumeEvalutationTestData(testData); From b5b37f0e8beda91958a486c710a22d47bf209e54 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 9 Apr 2024 16:09:34 +0800 Subject: [PATCH 41/52] add CalculateClosestStart method --- .../Recurrence/RecurrenceEvaluator.cs | 96 +++++++++++++------ .../FeatureFilters/TimeWindowFilter.cs | 63 ++++-------- .../MockedTimeWindowFilter.cs | 90 ++++------------- .../RecurrenceEvaluation.cs | 24 +++-- 4 files changed, 116 insertions(+), 157 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 74603744..5f782e03 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Runtime; namespace Microsoft.FeatureManagement.FeatureFilters { @@ -19,7 +20,7 @@ static class RecurrenceEvaluator /// The settings of time window filter. /// True if the timestamp is within any recurring time window, false otherwise. /// - public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings settings) + public static bool IsMatch(DateTimeOffset time, TimeWindowFilterSettings settings) { Debug.Assert(settings != null); Debug.Assert(settings.Start != null); @@ -40,6 +41,70 @@ public static bool MatchRecurrence(DateTimeOffset time, TimeWindowFilterSettings return false; } + /// + /// Calculate the start time of the closest active time window. + /// A timestamp. + /// The settings of time window filter. + /// The start time of the closest active time window or null if the recurrence range surpasses its end. + /// + public static DateTimeOffset? CalculateClosestStart(DateTimeOffset time, TimeWindowFilterSettings settings) + { + CalculateSurroundingOccurrences(time, settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrence); + + if (time < settings.Start.Value) + { + return nextOccurrence.Value; + } + + if (prevOccurrence != null) + { + bool isWithinPreviousTimeWindow = + time < prevOccurrence.Value + (settings.End.Value - settings.Start.Value); + + if (isWithinPreviousTimeWindow) + { + return prevOccurrence.Value; + } + + if (nextOccurrence != null) + { + return nextOccurrence.Value; + } + } + + return null; + } + + /// + /// Calculate the offset in days between two given days of the week. + /// A day of week. + /// A day of week. + /// The number of days to be added to day2 to reach day1 + /// + public static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) + { + return ((int)day1 - (int)day2 + DaysPerWeek) % DaysPerWeek; + } + + + /// + /// Sort a collection of days of week based on their offsets from a specified first day of week. + /// A collection of days of week. + /// The first day of week. + /// The sorted days of week. + /// + public static List SortDaysOfWeek(IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) + { + List result = daysOfWeek.ToList(); + + result.Sort((x, y) => + CalculateWeeklyDayOffset(x, firstDayOfWeek) + .CompareTo( + CalculateWeeklyDayOffset(y, firstDayOfWeek))); + + return result; + } + /// /// Calculate the closest previous recurrence occurrence (if any) before the provided timestamp and the next occurrence (if any) after the provided timestamp. /// A timestamp. @@ -102,35 +167,6 @@ public static void CalculateSurroundingOccurrences(DateTimeOffset time, TimeWind } } - /// - /// Calculate the offset in days between two given days of the week. - /// A day of week. - /// A day of week. - /// The number of days to be added to day2 to reach day1 - /// - public static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) - { - return ((int)day1 - (int)day2 + DaysPerWeek) % DaysPerWeek; - } - - - /// - /// Sort a collection of days of week based on their offsets from a specified first day of week. - /// A collection of days of week. - /// The first day of week. - /// The sorted days of week. - /// - public static List SortDaysOfWeek(IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) - { - List result = daysOfWeek.ToList(); - - result.Sort((x, y) => - CalculateWeeklyDayOffset(x, firstDayOfWeek) - .CompareTo( - CalculateWeeklyDayOffset(y, firstDayOfWeek))); - - return result; - } /// /// Try to find the closest previous recurrence occurrence before the provided timestamp according to the recurrence pattern. The given time should be later than the recurrence start. diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index e17d6a10..0348a813 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -20,7 +20,7 @@ public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder { private const string Alias = "Microsoft.TimeWindow"; private readonly ILogger _logger; - private readonly ConcurrentDictionary _recurrenceCache; + private readonly ConcurrentDictionary _recurrenceCache; /// /// Creates a time window based feature filter. @@ -29,7 +29,7 @@ public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder public TimeWindowFilter(ILoggerFactory loggerFactory = null) { _logger = loggerFactory?.CreateLogger(); - _recurrenceCache = new ConcurrentDictionary(); + _recurrenceCache = new ConcurrentDictionary(); } /// @@ -84,65 +84,36 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) // In this case, the cache for recurrence settings won't work. if (context.Settings == null) { - return Task.FromResult(RecurrenceEvaluator.MatchRecurrence(now, settings)); + return Task.FromResult(RecurrenceEvaluator.IsMatch(now, settings)); } - DateTimeOffset cachedTime = _recurrenceCache.GetOrAdd( + DateTimeOffset? closestStart = _recurrenceCache.GetOrAdd( settings, - (_) => - { - RecurrenceEvaluator.CalculateSurroundingOccurrences(now, settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrence); - - if (now < settings.Start.Value) - { - return nextOccurrence.Value; - } - - if (prevOccurrence != null) - { - return prevOccurrence.Value; - } - - // - // There is no previous occurrence within the reccurrence range. - return DateTimeOffset.MaxValue; - }); - - if (now < cachedTime) + RecurrenceEvaluator.CalculateClosestStart(now, settings)); + + if (closestStart == null || now < closestStart.Value) { return Task.FromResult(false); } - if (now < cachedTime + (settings.End.Value - settings.Start.Value)) + if (now < closestStart.Value + (settings.End.Value - settings.Start.Value)) { return Task.FromResult(true); } - RecurrenceEvaluator.CalculateSurroundingOccurrences(now, settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrrence); - - if (prevOccurrence != null) - { - Debug.Assert(now > settings.Start.Value); - - bool isWithinPreviousTimeWindow = - now <= prevOccurrence.Value + (settings.End.Value - settings.Start.Value); - - _recurrenceCache.AddOrUpdate( - settings, - (_) => throw new KeyNotFoundException(), - (_, _) => isWithinPreviousTimeWindow ? - prevOccurrence.Value : - nextOccurrrence ?? DateTimeOffset.MaxValue); - - return Task.FromResult(isWithinPreviousTimeWindow); - } + closestStart = RecurrenceEvaluator.CalculateClosestStart(now, settings); - // - // There is no previous occurrence within the recurrence range. _recurrenceCache.AddOrUpdate( settings, (_) => throw new KeyNotFoundException(), - (_, _) => DateTimeOffset.MaxValue); + (_, _) => closestStart); + + if (closestStart == null || now < closestStart.Value) + { + return Task.FromResult(false); + } + + return Task.FromResult(now < closestStart.Value + (settings.End.Value - settings.Start.Value)); } return Task.FromResult(false); diff --git a/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs b/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs index 84f8b935..7b941fe3 100644 --- a/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs +++ b/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs @@ -1,40 +1,26 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.FeatureManagement; +using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Threading.Tasks; namespace Tests.FeatureManagement { public class MockedTimeWindowFilter { - private readonly ConcurrentDictionary _recurrenceCache; + private readonly ConcurrentDictionary _recurrenceCache; public MockedTimeWindowFilter() { - _recurrenceCache = new ConcurrentDictionary(); - } - - public object BindParameters(IConfiguration filterParameters) - { - var settings = filterParameters.Get() ?? new TimeWindowFilterSettings(); - - if (!RecurrenceValidator.TryValidateSettings(settings, out string paramName, out string reason)) - { - throw new ArgumentException(reason, paramName); - } - - return settings; + _recurrenceCache = new ConcurrentDictionary(); } public bool Evaluate(DateTimeOffset now, FeatureFilterEvaluationContext context) { - // - // Check if prebound settings available, otherwise bind from parameters. - TimeWindowFilterSettings settings = (TimeWindowFilterSettings)context.Settings ?? (TimeWindowFilterSettings)BindParameters(context.Parameters); + Debug.Assert(context.Settings != null); + + TimeWindowFilterSettings settings = (TimeWindowFilterSettings)context.Settings; if (!settings.Start.HasValue && !settings.End.HasValue) { @@ -50,71 +36,31 @@ public bool Evaluate(DateTimeOffset now, FeatureFilterEvaluationContext context) if (settings.Recurrence != null) { - // - // The reference of the object will be used for hash key. - // If there is no pre-bounded settings attached to the context, there will be no cached filter settings and each call will have a unique settings object. - // In this case, the cache for recurrence settings won't work. - if (context.Settings == null) - { - return RecurrenceEvaluator.MatchRecurrence(now, settings); - } - - DateTimeOffset cachedTime = _recurrenceCache.GetOrAdd( - settings, - (_) => - { - RecurrenceEvaluator.CalculateSurroundingOccurrences(now, settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrence); - - if (now < settings.Start.Value) - { - return nextOccurrence.Value; - } + DateTimeOffset? closestStart = _recurrenceCache.GetOrAdd(settings, RecurrenceEvaluator.CalculateClosestStart(now, settings)); - if (prevOccurrence != null) - { - return prevOccurrence.Value; - } - - // - // There is no previous occurrence within the reccurrence range. - return DateTimeOffset.MaxValue; - }); - - if (now < cachedTime) + if (closestStart == null || now < closestStart.Value) { return false; } - if (now < cachedTime + (settings.End.Value - settings.Start.Value)) + if (now < closestStart.Value + (settings.End.Value - settings.Start.Value)) { return true; } - RecurrenceEvaluator.CalculateSurroundingOccurrences(now, settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrrence); - - if (prevOccurrence != null) - { - Debug.Assert(now > settings.Start.Value); + closestStart = RecurrenceEvaluator.CalculateClosestStart(now, settings); - bool isWithinPreviousTimeWindow = - now <= prevOccurrence.Value + (settings.End.Value - settings.Start.Value); - - _recurrenceCache.AddOrUpdate( - settings, - (_) => throw new KeyNotFoundException(), - (_, _) => isWithinPreviousTimeWindow ? - prevOccurrence.Value : - nextOccurrrence ?? DateTimeOffset.MaxValue); - - return isWithinPreviousTimeWindow; - } - - // - // There is no previous occurrence within the recurrence range. _recurrenceCache.AddOrUpdate( settings, (_) => throw new KeyNotFoundException(), - (_, _) => DateTimeOffset.MaxValue); + (_, _) => closestStart); + + if (closestStart == null || now < closestStart.Value) + { + return false; + } + + return now < closestStart.Value + (settings.End.Value - settings.Start.Value); } return false; diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs index 51018c2e..d64d1c66 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs @@ -297,7 +297,7 @@ public void ValidTimeWindowAcrossWeeks() // // The settings is valid. No exception should be thrown. - RecurrenceEvaluator.MatchRecurrence(DateTimeOffset.Now, settings); + RecurrenceEvaluator.IsMatch(DateTimeOffset.Now, settings); settings = new TimeWindowFilterSettings() { @@ -318,7 +318,7 @@ public void ValidTimeWindowAcrossWeeks() // // The settings is valid. No exception should be thrown. - RecurrenceEvaluator.MatchRecurrence(DateTimeOffset.Now, settings); + RecurrenceEvaluator.IsMatch(DateTimeOffset.Now, settings); settings = new TimeWindowFilterSettings() { @@ -401,7 +401,7 @@ private static void ConsumeEvaluationTestData(List Date: Tue, 9 Apr 2024 16:21:25 +0800 Subject: [PATCH 42/52] testcase updated --- .../RecurrenceEvaluation.cs | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs index d64d1c66..3bcb64e3 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs @@ -405,14 +405,13 @@ private static void ConsumeEvaluationTestData(List> testData) + private static void ConsumeEvalutationTestData(List> testData) { - foreach ((DateTimeOffset time, TimeWindowFilterSettings settings, DateTimeOffset? expectedPrev, DateTimeOffset? expectedNext) in testData) + foreach ((DateTimeOffset time, TimeWindowFilterSettings settings, DateTimeOffset? expected) in testData) { - RecurrenceEvaluator.CalculateSurroundingOccurrences(time, settings, out DateTimeOffset? prev, out DateTimeOffset? next); + DateTimeOffset? res = RecurrenceEvaluator.CalculateClosestStart(time, settings); - Assert.Equal(expectedPrev, prev); - Assert.Equal(expectedNext, next); + Assert.Equal(expected, res); } } @@ -981,9 +980,9 @@ public void MatchWeeklyRecurrenceTest() } [Fact] - public void FindDailyPrevAndNextOccurrenceTest() + public void FindDailyClosestStartTest() { - var testData = new List>() + var testData = new List>() { ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -999,7 +998,6 @@ public void FindDailyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - null, DateTimeOffset.Parse("2024-3-1T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), @@ -1016,8 +1014,7 @@ public void FindDailyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), - DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), + DateTimeOffset.Parse("2024-2-28T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1034,7 +1031,6 @@ public void FindDailyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - DateTimeOffset.Parse("2024-2-27T00:00:00+08:00"), DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), @@ -1052,8 +1048,7 @@ public void FindDailyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), - DateTimeOffset.Parse("2024-3-2T00:00:00+08:00")), + DateTimeOffset.Parse("2024-2-28T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1073,7 +1068,6 @@ public void FindDailyPrevAndNextOccurrenceTest() } } }, - null, null), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), @@ -1094,8 +1088,7 @@ public void FindDailyPrevAndNextOccurrenceTest() } } }, - DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), - null), + DateTimeOffset.Parse("2024-2-28T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1115,7 +1108,6 @@ public void FindDailyPrevAndNextOccurrenceTest() } } }, - null, null), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), @@ -1136,17 +1128,16 @@ public void FindDailyPrevAndNextOccurrenceTest() } } }, - DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), - null) + DateTimeOffset.Parse("2024-2-28T00:00:00+08:00")) }; ConsumeEvalutationTestData(testData); } [Fact] - public void FindWeeklyPrevAndNextOccurrenceTest() + public void FindWeeklyClosestStartTest() { - var testData = new List>() + var testData = new List>() { ( DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1163,7 +1154,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - null, DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), @@ -1181,8 +1171,7 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), - DateTimeOffset.Parse("2024-3-7T00:00:00+08:00")), + DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1199,7 +1188,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - DateTimeOffset.Parse("2024-2-22T12:00:00+08:00"), DateTimeOffset.Parse("2024-2-29T12:00:00+08:00")), ( DateTimeOffset.Parse("2024-3-1T00:00:00+08:00"), @@ -1217,7 +1205,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - DateTimeOffset.Parse("2024-2-29T00:00:00+08:00"), DateTimeOffset.Parse("2024-3-3T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-28T00:00:00+08:00"), @@ -1237,7 +1224,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - DateTimeOffset.Parse("2024-2-25T00:00:00+08:00"), DateTimeOffset.Parse("2024-2-29T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), @@ -1257,8 +1243,7 @@ public void FindWeeklyPrevAndNextOccurrenceTest() Range = new RecurrenceRange() } }, - DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), - DateTimeOffset.Parse("2024-2-11T00:00:00+08:00")), // Sunday in the 3rd week + DateTimeOffset.Parse("2024-2-1T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1279,8 +1264,7 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - DateTimeOffset.Parse("2024-2-1T00:00:00+08:00"), - null), + DateTimeOffset.Parse("2024-2-1T00:00:00+08:00")), ( DateTimeOffset.Parse("2024-2-2T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1301,7 +1285,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - null, null), ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), @@ -1325,8 +1308,7 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), // Sunday in the 3rd week - null), + DateTimeOffset.Parse("2024-2-11T00:00:00+08:00")), // Sunday in the 3rd week ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), new TimeWindowFilterSettings() @@ -1349,7 +1331,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - DateTimeOffset.Parse("2024-2-4T00:00:00+08:00"), // Sunday in the 1st week null), ( DateTimeOffset.Parse("2024-2-12T00:00:00+08:00"), // Monday in the 3rd week @@ -1373,7 +1354,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), // Sunday in the 3rd week DateTimeOffset.Parse("2024-2-15T00:00:00+08:00")), // Thursday in the 3rd week ( DateTimeOffset.Parse("2024-2-12T00:00:00+08:00"), // Monday in the 3rd week @@ -1397,7 +1377,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - DateTimeOffset.Parse("2024-2-4T00:00:00+08:00"), // Sunday in the 1st week DateTimeOffset.Parse("2024-2-15T00:00:00+08:00")), // Thursday in the 3rd week ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), // Sunday in the 3rd week @@ -1421,7 +1400,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - DateTimeOffset.Parse("2024-2-3T12:00:00+08:00"), // Saturday in the 1st week null), ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), // Sunday in the 2nd week @@ -1445,7 +1423,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - null, null), ( DateTimeOffset.Parse("2024-2-11T00:00:00+08:00"), @@ -1467,7 +1444,6 @@ public void FindWeeklyPrevAndNextOccurrenceTest() } } }, - null, null) }; From 6196e216fc7c4197c7649ced99261ee4a4575363 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 10 Apr 2024 14:59:09 +0800 Subject: [PATCH 43/52] update --- .../FeatureFilters/ITimeProvider.cs | 19 +++++ .../Recurrence/RecurrenceEvaluator.cs | 62 +++++++------- .../Recurrence/RecurrenceValidator.cs | 40 +++++++-- .../FeatureFilters/TimeWindowFilter.cs | 58 ++++++++++--- .../FeatureManagementBuilder.cs | 33 ++++++++ .../ServiceCollectionExtensions.cs | 50 ++++++----- .../MockedTimeWindowFilter.cs | 69 --------------- .../OnDemandTimeProvider.cs | 15 ++++ .../RecurrenceEvaluation.cs | 83 ++++++++++--------- 9 files changed, 254 insertions(+), 175 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/ITimeProvider.cs delete mode 100644 tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs create mode 100644 tests/Tests.FeatureManagement/OnDemandTimeProvider.cs diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/ITimeProvider.cs b/src/Microsoft.FeatureManagement/FeatureFilters/ITimeProvider.cs new file mode 100644 index 00000000..0555d3d8 --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureFilters/ITimeProvider.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +using System; + +namespace Microsoft.FeatureManagement.FeatureFilters +{ + /// + /// Provides the current time. This was implemented to allow the time window filter in our test suite to use simulated current time. + /// + internal interface ITimeProvider + { + /// + /// Gets the current time. + /// + public DateTimeOffset GetTime(); + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 5f782e03..8a73ec84 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -75,36 +75,6 @@ public static bool IsMatch(DateTimeOffset time, TimeWindowFilterSettings setting return null; } - /// - /// Calculate the offset in days between two given days of the week. - /// A day of week. - /// A day of week. - /// The number of days to be added to day2 to reach day1 - /// - public static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) - { - return ((int)day1 - (int)day2 + DaysPerWeek) % DaysPerWeek; - } - - - /// - /// Sort a collection of days of week based on their offsets from a specified first day of week. - /// A collection of days of week. - /// The first day of week. - /// The sorted days of week. - /// - public static List SortDaysOfWeek(IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) - { - List result = daysOfWeek.ToList(); - - result.Sort((x, y) => - CalculateWeeklyDayOffset(x, firstDayOfWeek) - .CompareTo( - CalculateWeeklyDayOffset(y, firstDayOfWeek))); - - return result; - } - /// /// Calculate the closest previous recurrence occurrence (if any) before the provided timestamp and the next occurrence (if any) after the provided timestamp. /// A timestamp. @@ -112,7 +82,7 @@ public static List SortDaysOfWeek(IEnumerable daysOfWeek, /// The closest previous occurrence. Note that prev occurrence can be null even if the time is past the start date, because the recurrence range may have surpassed its end. /// The next occurrence. Note that next occurrence can be null even if the prev occurrence is not null, because the recurrence range may have reached its end. /// - public static void CalculateSurroundingOccurrences(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrence) + private static void CalculateSurroundingOccurrences(DateTimeOffset time, TimeWindowFilterSettings settings, out DateTimeOffset? prevOccurrence, out DateTimeOffset? nextOccurrence) { Debug.Assert(settings != null); Debug.Assert(settings.Start != null); @@ -355,5 +325,35 @@ private static DateTimeOffset CalculateWeeklyNextOccurrence(DateTimeOffset previ return previousOccurrence.AddDays( pattern.Interval * DaysPerWeek - CalculateWeeklyDayOffset(previousOccurrence.DayOfWeek, sortedDaysOfWeek.First())); } + + /// + /// Calculate the offset in days between two given days of the week. + /// A day of week. + /// A day of week. + /// The number of days to be added to day2 to reach day1 + /// + private static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) + { + return ((int)day1 - (int)day2 + DaysPerWeek) % DaysPerWeek; + } + + + /// + /// Sort a collection of days of week based on their offsets from a specified first day of week. + /// A collection of days of week. + /// The first day of week. + /// The sorted days of week. + /// + private static List SortDaysOfWeek(IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) + { + List result = daysOfWeek.ToList(); + + result.Sort((x, y) => + CalculateWeeklyDayOffset(x, firstDayOfWeek) + .CompareTo( + CalculateWeeklyDayOffset(y, firstDayOfWeek))); + + return result; + } } } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs index 7e93f9f2..203d1271 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs @@ -339,9 +339,9 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int } DateTime firstDayOfThisWeek = DateTime.Today.AddDays( - DaysPerWeek - RecurrenceEvaluator.CalculateWeeklyDayOffset(DateTime.Today.DayOfWeek, firstDayOfWeek)); + DaysPerWeek - CalculateWeeklyDayOffset(DateTime.Today.DayOfWeek, firstDayOfWeek)); - List sortedDaysOfWeek = RecurrenceEvaluator.SortDaysOfWeek(daysOfWeek, firstDayOfWeek); + List sortedDaysOfWeek = SortDaysOfWeek(daysOfWeek, firstDayOfWeek); DateTime prev = DateTime.MinValue; @@ -352,12 +352,12 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int if (prev == DateTime.MinValue) { prev = firstDayOfThisWeek.AddDays( - RecurrenceEvaluator.CalculateWeeklyDayOffset(dayOfWeek, firstDayOfWeek)); + CalculateWeeklyDayOffset(dayOfWeek, firstDayOfWeek)); } else { DateTime date = firstDayOfThisWeek.AddDays( - RecurrenceEvaluator.CalculateWeeklyDayOffset(dayOfWeek, firstDayOfWeek)); + CalculateWeeklyDayOffset(dayOfWeek, firstDayOfWeek)); TimeSpan gap = date - prev; @@ -377,7 +377,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int DateTime firstDayOfNextWeek = firstDayOfThisWeek.AddDays(DaysPerWeek); DateTime firstOccurrenceInNextWeek = firstDayOfNextWeek.AddDays( - RecurrenceEvaluator.CalculateWeeklyDayOffset(sortedDaysOfWeek.First(), firstDayOfWeek)); + CalculateWeeklyDayOffset(sortedDaysOfWeek.First(), firstDayOfWeek)); TimeSpan gap = firstOccurrenceInNextWeek - prev; @@ -389,5 +389,35 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int return minGap >= duration; } + + /// + /// Calculate the offset in days between two given days of the week. + /// A day of week. + /// A day of week. + /// The number of days to be added to day2 to reach day1 + /// + private static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) + { + return ((int)day1 - (int)day2 + DaysPerWeek) % DaysPerWeek; + } + + + /// + /// Sort a collection of days of week based on their offsets from a specified first day of week. + /// A collection of days of week. + /// The first day of week. + /// The sorted days of week. + /// + private static List SortDaysOfWeek(IEnumerable daysOfWeek, DayOfWeek firstDayOfWeek) + { + List result = daysOfWeek.ToList(); + + result.Sort((x, y) => + CalculateWeeklyDayOffset(x, firstDayOfWeek) + .CompareTo( + CalculateWeeklyDayOffset(y, firstDayOfWeek))); + + return result; + } } } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 0348a813..3b244923 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -18,9 +16,11 @@ namespace Microsoft.FeatureManagement.FeatureFilters [FilterAlias(Alias)] public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder { + private readonly TimeSpan ParametersCacheSlidingExpiration = TimeSpan.FromMinutes(5); + private readonly TimeSpan ParametersCacheAbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1); + private const string Alias = "Microsoft.TimeWindow"; private readonly ILogger _logger; - private readonly ConcurrentDictionary _recurrenceCache; /// /// Creates a time window based feature filter. @@ -29,9 +29,18 @@ public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder public TimeWindowFilter(ILoggerFactory loggerFactory = null) { _logger = loggerFactory?.CreateLogger(); - _recurrenceCache = new ConcurrentDictionary(); } + /// + /// The application memory cache to store the start time of the closest active time window. By caching this time, the time window can minimize redundant computations when evaluating recurrence. + /// + public IMemoryCache Cache { get; init; } + + /// + /// This property allows the time window filter in our test suite to use simulated current time. + /// + internal ITimeProvider TimeProvider { get; init; } + /// /// Binds configuration representing filter parameters to . /// @@ -62,6 +71,11 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) DateTimeOffset now = DateTimeOffset.UtcNow; + if (TimeProvider != null) + { + now = TimeProvider.GetTime(); + } + if (!settings.Start.HasValue && !settings.End.HasValue) { _logger?.LogWarning($"The '{Alias}' feature filter is not valid for feature '{context.FeatureName}'. It must specify either '{nameof(settings.Start)}', '{nameof(settings.End)}', or both."); @@ -82,14 +96,29 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) // The reference of the object will be used for hash key. // If there is no pre-bounded settings attached to the context, there will be no cached filter settings and each call will have a unique settings object. // In this case, the cache for recurrence settings won't work. - if (context.Settings == null) + if (context.Settings == null || Cache == null) { return Task.FromResult(RecurrenceEvaluator.IsMatch(now, settings)); } - DateTimeOffset? closestStart = _recurrenceCache.GetOrAdd( - settings, - RecurrenceEvaluator.CalculateClosestStart(now, settings)); + // + // The start time of the closest active time window. It could be null if the recurrence range surpasses its end. + DateTimeOffset? closestStart; + + if (!Cache.TryGetValue(settings, out closestStart)) + { + closestStart = RecurrenceEvaluator.CalculateClosestStart(now, settings); + + Cache.Set( + settings, + closestStart, + new MemoryCacheEntryOptions + { + SlidingExpiration = ParametersCacheSlidingExpiration, + AbsoluteExpirationRelativeToNow = ParametersCacheAbsoluteExpirationRelativeToNow, + Size = 1 + }); + } if (closestStart == null || now < closestStart.Value) { @@ -103,10 +132,15 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) closestStart = RecurrenceEvaluator.CalculateClosestStart(now, settings); - _recurrenceCache.AddOrUpdate( + Cache.Set( settings, - (_) => throw new KeyNotFoundException(), - (_, _) => closestStart); + closestStart, + new MemoryCacheEntryOptions + { + SlidingExpiration = ParametersCacheSlidingExpiration, + AbsoluteExpirationRelativeToNow = ParametersCacheAbsoluteExpirationRelativeToNow, + Size = 1 + }); if (closestStart == null || now < closestStart.Value) { diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs index 031d6f42..640db4a5 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.FeatureManagement.FeatureFilters; namespace Microsoft.FeatureManagement { @@ -52,6 +53,38 @@ public IFeatureManagementBuilder AddFeatureFilter() where T : IFeatureFilterM return this; } + public IFeatureManagementBuilder AddFeatureFilter(Func implementationFactory) where T : IFeatureFilterMetadata + { + Type serviceType = typeof(IFeatureFilterMetadata); + + Type implementationType = typeof(T); + + IEnumerable featureFilterImplementations = implementationType.GetInterfaces() + .Where(i => i == typeof(IFeatureFilter) || + (i.IsGenericType && i.GetGenericTypeDefinition().IsAssignableFrom(typeof(IContextualFeatureFilter<>)))); + + if (featureFilterImplementations.Count() > 1) + { + throw new ArgumentException($"A single feature filter cannot implement more than one feature filter interface.", nameof(T)); + } + + if (!Services.Any(descriptor => descriptor.ServiceType == serviceType && descriptor.ImplementationType == implementationType)) + { + // + // Register the feature filter with the same lifetime as the feature manager + if (Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) + { + Services.AddScoped(serviceType, implementationFactory); + } + else + { + Services.AddSingleton(serviceType, implementationFactory); + } + } + + return this; + } + public IFeatureManagementBuilder AddSessionManager() where T : ISessionManager { // diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 02225d2c..b0a9bdd6 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -42,15 +42,16 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec // Add required services services.TryAddSingleton(); - services.AddSingleton(sp => new FeatureManager( - sp.GetRequiredService(), - sp.GetRequiredService>().Value) - { - FeatureFilters = sp.GetRequiredService>(), - SessionManagers = sp.GetRequiredService>(), - Cache = sp.GetRequiredService(), - Logger = sp.GetRequiredService().CreateLogger() - }); + services.AddSingleton(sp => + new FeatureManager( + sp.GetRequiredService(), + sp.GetRequiredService>().Value) + { + FeatureFilters = sp.GetRequiredService>(), + SessionManagers = sp.GetRequiredService>(), + Cache = sp.GetRequiredService(), + Logger = sp.GetRequiredService().CreateLogger() + }); services.AddScoped(); @@ -60,7 +61,11 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec // Add built-in feature filters builder.AddFeatureFilter(); - builder.AddFeatureFilter(); + builder.AddFeatureFilter(sp => + new TimeWindowFilter() + { + Cache = sp.GetRequiredService() + }); builder.AddFeatureFilter(); @@ -114,15 +119,16 @@ public static IFeatureManagementBuilder AddScopedFeatureManagement(this IService // Add required services services.TryAddSingleton(); - services.AddScoped(sp => new FeatureManager( - sp.GetRequiredService(), - sp.GetRequiredService>().Value) - { - FeatureFilters = sp.GetRequiredService>(), - SessionManagers = sp.GetRequiredService>(), - Cache = sp.GetRequiredService(), - Logger = sp.GetRequiredService().CreateLogger() - }); + services.AddScoped(sp => + new FeatureManager( + sp.GetRequiredService(), + sp.GetRequiredService>().Value) + { + FeatureFilters = sp.GetRequiredService>(), + SessionManagers = sp.GetRequiredService>(), + Cache = sp.GetRequiredService(), + Logger = sp.GetRequiredService().CreateLogger() + }); services.AddScoped(); @@ -132,7 +138,11 @@ public static IFeatureManagementBuilder AddScopedFeatureManagement(this IService // Add built-in feature filters builder.AddFeatureFilter(); - builder.AddFeatureFilter(); + builder.AddFeatureFilter(sp => + new TimeWindowFilter() + { + Cache = sp.GetRequiredService() + }); builder.AddFeatureFilter(); diff --git a/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs b/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs deleted file mode 100644 index 7b941fe3..00000000 --- a/tests/Tests.FeatureManagement/MockedTimeWindowFilter.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Microsoft.FeatureManagement; -using Microsoft.FeatureManagement.FeatureFilters; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Tests.FeatureManagement -{ - public class MockedTimeWindowFilter - { - private readonly ConcurrentDictionary _recurrenceCache; - - public MockedTimeWindowFilter() - { - _recurrenceCache = new ConcurrentDictionary(); - } - - public bool Evaluate(DateTimeOffset now, FeatureFilterEvaluationContext context) - { - Debug.Assert(context.Settings != null); - - TimeWindowFilterSettings settings = (TimeWindowFilterSettings)context.Settings; - - if (!settings.Start.HasValue && !settings.End.HasValue) - { - return false; - } - - // - // Hit the first occurrence of the time window - if ((!settings.Start.HasValue || now >= settings.Start.Value) && (!settings.End.HasValue || now < settings.End.Value)) - { - return true; - } - - if (settings.Recurrence != null) - { - DateTimeOffset? closestStart = _recurrenceCache.GetOrAdd(settings, RecurrenceEvaluator.CalculateClosestStart(now, settings)); - - if (closestStart == null || now < closestStart.Value) - { - return false; - } - - if (now < closestStart.Value + (settings.End.Value - settings.Start.Value)) - { - return true; - } - - closestStart = RecurrenceEvaluator.CalculateClosestStart(now, settings); - - _recurrenceCache.AddOrUpdate( - settings, - (_) => throw new KeyNotFoundException(), - (_, _) => closestStart); - - if (closestStart == null || now < closestStart.Value) - { - return false; - } - - return now < closestStart.Value + (settings.End.Value - settings.Start.Value); - } - - return false; - } - } -} diff --git a/tests/Tests.FeatureManagement/OnDemandTimeProvider.cs b/tests/Tests.FeatureManagement/OnDemandTimeProvider.cs new file mode 100644 index 00000000..afeb8863 --- /dev/null +++ b/tests/Tests.FeatureManagement/OnDemandTimeProvider.cs @@ -0,0 +1,15 @@ +using Microsoft.FeatureManagement.FeatureFilters; +using System; + +namespace Tests.FeatureManagement +{ + internal class OnDemandTimeProvider : ITimeProvider + { + public DateTimeOffset Now { get; set; } + + public DateTimeOffset GetTime() + { + return Now; + } + } +} diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs index 3bcb64e3..6c6f1858 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Microsoft.Extensions.Caching.Memory; using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.FeatureFilters; using System; @@ -1451,9 +1452,15 @@ public void FindWeeklyClosestStartTest() } [Fact] - public void RecurrenceEvaluationThroughCacheTest() + public async void RecurrenceEvaluationThroughCacheTest() { - var mockedTimeWindowFilter = new MockedTimeWindowFilter(); + OnDemandTimeProvider mockedTimeProvider = new OnDemandTimeProvider(); + + var mockedTimeWindowFilter = new TimeWindowFilter() + { + Cache = new MemoryCache(new MemoryCacheOptions()), + TimeProvider = mockedTimeProvider + }; var context = new FeatureFilterEvaluationContext() { @@ -1477,35 +1484,35 @@ public void RecurrenceEvaluationThroughCacheTest() } }; - DateTimeOffset now = DateTimeOffset.Parse("2024-2-2T23:00:00+08:00"); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-2T23:00:00+08:00"); - Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); for (int i = 0; i < 12; i++) { - now = now.AddHours(1); - //Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = mockedTimeProvider.Now.AddHours(1); + Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); } - now = DateTimeOffset.Parse("2024-2-3T11:59:59+08:00"); - //Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-3T11:59:59+08:00"); + Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); - now = DateTimeOffset.Parse("2024-2-3T12:00:00+08:00"); - Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-3T12:00:00+08:00"); + Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); - now = DateTimeOffset.Parse("2024-2-5T00:00:00+08:00"); - Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-5T00:00:00+08:00"); + Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); - now = DateTimeOffset.Parse("2024-2-5T12:00:00+08:00"); - Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-5T12:00:00+08:00"); + Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); - now = DateTimeOffset.Parse("2024-2-7T00:00:00+08:00"); - Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-7T00:00:00+08:00"); + Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); for (int i = 0; i < 10; i++ ) { - now = now.AddDays(1); - Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = mockedTimeProvider.Now.AddDays(1); + Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); } context = new FeatureFilterEvaluationContext() @@ -1531,40 +1538,40 @@ public void RecurrenceEvaluationThroughCacheTest() } }; - now = DateTimeOffset.Parse("2024-1-31T23:00:00+08:00"); - Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-1-31T23:00:00+08:00"); + Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); for (int i = 0; i < 12; i++) { - now = now.AddHours(1); - Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = mockedTimeProvider.Now.AddHours(1); + Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); } - now = DateTimeOffset.Parse("2024-2-1T11:59:59+08:00"); - Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-1T11:59:59+08:00"); + Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); - now = DateTimeOffset.Parse("2024-2-1T12:00:00+08:00"); - Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-1T12:00:00+08:00"); + Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); - now = DateTimeOffset.Parse("2024-2-2T00:00:00+08:00"); // Friday - Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-2T00:00:00+08:00"); // Friday + Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); - now = DateTimeOffset.Parse("2024-2-4T00:00:00+08:00"); // Sunday - Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-4T00:00:00+08:00"); // Sunday + Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); - now = DateTimeOffset.Parse("2024-2-4T06:00:00+08:00"); - Assert.True(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-4T06:00:00+08:00"); + Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); - now = DateTimeOffset.Parse("2024-2-4T12:01:00+08:00"); - Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-4T12:01:00+08:00"); + Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); - now = DateTimeOffset.Parse("2024-2-8T00:00:00+08:00"); - Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-8T00:00:00+08:00"); + Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); for (int i = 0; i < 10; i++) { - now = now.AddDays(1); - Assert.False(mockedTimeWindowFilter.Evaluate(now, context)); + mockedTimeProvider.Now = mockedTimeProvider.Now.AddDays(1); + Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); } } } From 188ce443514c4f040baf28b0dc059cb86aad69b0 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 10 Apr 2024 16:34:57 +0800 Subject: [PATCH 44/52] use ISystemClock for testing & add limit on time window duration --- .../FeatureFilters/ISystemClock.cs | 20 +++++++++ .../FeatureFilters/ITimeProvider.cs | 19 -------- .../Recurrence/RecurrenceValidator.cs | 11 ++++- .../FeatureFilters/TimeWindowFilter.cs | 11 ++--- .../Tests.FeatureManagement/OnDemandClock.cs | 10 +++++ .../OnDemandTimeProvider.cs | 15 ------- .../RecurrenceEvaluation.cs | 44 +++++++++---------- 7 files changed, 65 insertions(+), 65 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/ISystemClock.cs delete mode 100644 src/Microsoft.FeatureManagement/FeatureFilters/ITimeProvider.cs create mode 100644 tests/Tests.FeatureManagement/OnDemandClock.cs delete mode 100644 tests/Tests.FeatureManagement/OnDemandTimeProvider.cs diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/ISystemClock.cs b/src/Microsoft.FeatureManagement/FeatureFilters/ISystemClock.cs new file mode 100644 index 00000000..50c6743d --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureFilters/ISystemClock.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +using System; + +namespace Microsoft.FeatureManagement.FeatureFilters +{ + /// + /// Abstracts the system clock to facilitate testing. + /// .NET8 offers an abstract class TimeProvider. After we stop supporting .NET version less than .NET8, this ISystemClock should retire. + /// + internal interface ISystemClock + { + /// + /// Retrieves the current system time in UTC. + /// + public DateTimeOffset UtcNow { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/ITimeProvider.cs b/src/Microsoft.FeatureManagement/FeatureFilters/ITimeProvider.cs deleted file mode 100644 index 0555d3d8..00000000 --- a/src/Microsoft.FeatureManagement/FeatureFilters/ITimeProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// - -using System; - -namespace Microsoft.FeatureManagement.FeatureFilters -{ - /// - /// Provides the current time. This was implemented to allow the time window filter in our test suite to use simulated current time. - /// - internal interface ITimeProvider - { - /// - /// Gets the current time. - /// - public DateTimeOffset GetTime(); - } -} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs index 203d1271..63c202f0 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs @@ -18,7 +18,7 @@ static class RecurrenceValidator const string UnrecognizableValue = "The value is unrecognizable."; const string RequiredParameter = "Value cannot be null or empty."; const string StartNotMatched = "Start date is not a valid first occurrence."; - const string TimeWindowDurationOutOfRange = "Time window duration cannot be longer than how frequently it occurs"; + const string TimeWindowDurationOutOfRange = "Time window duration cannot be longer than how frequently it occurs or be longer than 10 years."; /// /// Perform validation of time window settings. @@ -100,6 +100,15 @@ private static bool TryValidateRecurrenceRequiredParameter(TimeWindowFilterSetti return false; } + if (settings.End.Value - settings.Start.Value >= TimeSpan.FromDays(3650)) + { + paramName = nameof(settings.End); + + reason = TimeWindowDurationOutOfRange; + + return false; + } + paramName = null; reason = null; diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 3b244923..1fd62b42 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -37,9 +37,9 @@ public TimeWindowFilter(ILoggerFactory loggerFactory = null) public IMemoryCache Cache { get; init; } /// - /// This property allows the time window filter in our test suite to use simulated current time. + /// This property allows the time window filter in our test suite to use simulated time. /// - internal ITimeProvider TimeProvider { get; init; } + internal ISystemClock SystemClock { get; init; } /// /// Binds configuration representing filter parameters to . @@ -69,12 +69,7 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) // Check if prebound settings available, otherwise bind from parameters. TimeWindowFilterSettings settings = (TimeWindowFilterSettings)context.Settings ?? (TimeWindowFilterSettings)BindParameters(context.Parameters); - DateTimeOffset now = DateTimeOffset.UtcNow; - - if (TimeProvider != null) - { - now = TimeProvider.GetTime(); - } + DateTimeOffset now = SystemClock?.UtcNow ?? DateTimeOffset.UtcNow; if (!settings.Start.HasValue && !settings.End.HasValue) { diff --git a/tests/Tests.FeatureManagement/OnDemandClock.cs b/tests/Tests.FeatureManagement/OnDemandClock.cs new file mode 100644 index 00000000..ffe4da22 --- /dev/null +++ b/tests/Tests.FeatureManagement/OnDemandClock.cs @@ -0,0 +1,10 @@ +using Microsoft.FeatureManagement.FeatureFilters; +using System; + +namespace Tests.FeatureManagement +{ + internal class OnDemandClock : ISystemClock + { + public DateTimeOffset UtcNow { get; set; } + } +} diff --git a/tests/Tests.FeatureManagement/OnDemandTimeProvider.cs b/tests/Tests.FeatureManagement/OnDemandTimeProvider.cs deleted file mode 100644 index afeb8863..00000000 --- a/tests/Tests.FeatureManagement/OnDemandTimeProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.FeatureManagement.FeatureFilters; -using System; - -namespace Tests.FeatureManagement -{ - internal class OnDemandTimeProvider : ITimeProvider - { - public DateTimeOffset Now { get; set; } - - public DateTimeOffset GetTime() - { - return Now; - } - } -} diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs index 6c6f1858..20b23bef 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs @@ -298,7 +298,7 @@ public void ValidTimeWindowAcrossWeeks() // // The settings is valid. No exception should be thrown. - RecurrenceEvaluator.IsMatch(DateTimeOffset.Now, settings); + RecurrenceEvaluator.IsMatch(DateTimeOffset.UtcNow, settings); settings = new TimeWindowFilterSettings() { @@ -319,7 +319,7 @@ public void ValidTimeWindowAcrossWeeks() // // The settings is valid. No exception should be thrown. - RecurrenceEvaluator.IsMatch(DateTimeOffset.Now, settings); + RecurrenceEvaluator.IsMatch(DateTimeOffset.UtcNow, settings); settings = new TimeWindowFilterSettings() { @@ -1454,12 +1454,12 @@ public void FindWeeklyClosestStartTest() [Fact] public async void RecurrenceEvaluationThroughCacheTest() { - OnDemandTimeProvider mockedTimeProvider = new OnDemandTimeProvider(); + OnDemandClock mockedTimeProvider = new OnDemandClock(); var mockedTimeWindowFilter = new TimeWindowFilter() { Cache = new MemoryCache(new MemoryCacheOptions()), - TimeProvider = mockedTimeProvider + SystemClock = mockedTimeProvider }; var context = new FeatureFilterEvaluationContext() @@ -1484,34 +1484,34 @@ public async void RecurrenceEvaluationThroughCacheTest() } }; - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-2T23:00:00+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-2T23:00:00+08:00"); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); for (int i = 0; i < 12; i++) { - mockedTimeProvider.Now = mockedTimeProvider.Now.AddHours(1); + mockedTimeProvider.UtcNow = mockedTimeProvider.UtcNow.AddHours(1); Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); } - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-3T11:59:59+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-3T11:59:59+08:00"); Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-3T12:00:00+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-3T12:00:00+08:00"); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-5T00:00:00+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-5T00:00:00+08:00"); Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-5T12:00:00+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-5T12:00:00+08:00"); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-7T00:00:00+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-7T00:00:00+08:00"); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); for (int i = 0; i < 10; i++ ) { - mockedTimeProvider.Now = mockedTimeProvider.Now.AddDays(1); + mockedTimeProvider.UtcNow = mockedTimeProvider.UtcNow.AddDays(1); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); } @@ -1538,39 +1538,39 @@ public async void RecurrenceEvaluationThroughCacheTest() } }; - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-1-31T23:00:00+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-1-31T23:00:00+08:00"); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); for (int i = 0; i < 12; i++) { - mockedTimeProvider.Now = mockedTimeProvider.Now.AddHours(1); + mockedTimeProvider.UtcNow = mockedTimeProvider.UtcNow.AddHours(1); Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); } - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-1T11:59:59+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-1T11:59:59+08:00"); Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-1T12:00:00+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-1T12:00:00+08:00"); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-2T00:00:00+08:00"); // Friday + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-2T00:00:00+08:00"); // Friday Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-4T00:00:00+08:00"); // Sunday + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-4T00:00:00+08:00"); // Sunday Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-4T06:00:00+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-4T06:00:00+08:00"); Assert.True(await mockedTimeWindowFilter.EvaluateAsync(context)); - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-4T12:01:00+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-4T12:01:00+08:00"); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); - mockedTimeProvider.Now = DateTimeOffset.Parse("2024-2-8T00:00:00+08:00"); + mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-8T00:00:00+08:00"); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); for (int i = 0; i < 10; i++) { - mockedTimeProvider.Now = mockedTimeProvider.Now.AddDays(1); + mockedTimeProvider.UtcNow = mockedTimeProvider.UtcNow.AddDays(1); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); } } From cd94d26e2831a64ec0cdf5f93f2872d1edb68477 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 10 Apr 2024 16:55:00 +0800 Subject: [PATCH 45/52] add testcase for timezone --- .../RecurrenceEvaluation.cs | 168 +++++++++++++++++- 1 file changed, 165 insertions(+), 3 deletions(-) diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs index 20b23bef..50c6e449 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs @@ -17,7 +17,7 @@ class ErrorMessage public const string UnrecognizableValue = "The value is unrecognizable."; public const string RequiredParameter = "Value cannot be null or empty."; public const string StartNotMatched = "Start date is not a valid first occurrence."; - public const string TimeWindowDurationOutOfRange = "Time window duration cannot be longer than how frequently it occurs"; + public const string TimeWindowDurationOutOfRange = "Time window duration cannot be longer than how frequently it occurs or be longer than 10 years."; } class ParamName @@ -278,7 +278,7 @@ public void InvalidTimeWindowTest() } [Fact] - public void ValidTimeWindowAcrossWeeks() + public void InvalidTimeWindowAcrossWeeksTest() { var settings = new TimeWindowFilterSettings() { @@ -560,7 +560,41 @@ public void MatchDailyRecurrenceTest() } } }, - false ) + false ), + + ( DateTimeOffset.Parse("2023-9-2T16:00:00+00:00"), // 2023-9-3T00:00:00+08:00 + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T12:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily, + Interval = 2 + }, + Range = new RecurrenceRange() + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-2T15:59:59+00:00"), // 2023-9-2T23:59:59+08:00 + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), + End = DateTimeOffset.Parse("2023-9-1T12:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Daily, + Interval = 2 + }, + Range = new RecurrenceRange() + } + }, + false ), }; ConsumeEvaluationTestData(testData); @@ -589,6 +623,24 @@ public void MatchWeeklyRecurrenceTest() }, true ), + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // Friday in the 2nd week after the Start date + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } + }, + Range = new RecurrenceRange() + } + }, + true ), + ( DateTimeOffset.Parse("2023-9-8T00:00:00+08:00"), // Friday in the 2nd week after the Start date new TimeWindowFilterSettings() { @@ -974,6 +1026,116 @@ public void MatchWeeklyRecurrenceTest() } } }, + false ), + + ( DateTimeOffset.Parse("2023-9-3T16:00:00+00:00"), // Monday in the 2nd week after the Start date if timezone is UTC+8 + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } + }, + Range = new RecurrenceRange() + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-7T16:00:00+00:00"), // Friday in the 2nd week after the Start date if timezone is UTC+8 + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } + }, + Range = new RecurrenceRange() + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-3T15:59:59+00:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } + }, + Range = new RecurrenceRange() + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-7T15:59:59+00:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-1T00:00:00+08:00"), // Friday + End = DateTimeOffset.Parse("2023-9-1T00:00:01+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + // FirstDayOfWeek is Sunday by default. + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Friday } + }, + Range = new RecurrenceRange() + } + }, + false ), + + ( DateTimeOffset.Parse("2023-9-10T16:00:00+00:00"), // Within the recurring time window 2023-9-11T:00:00:00+08:00 ~ 2023-9-15T:00:00:00+08:00 + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) + }, + Range = new RecurrenceRange() + } + }, + true ), + + ( DateTimeOffset.Parse("2023-9-10T15:59:59+00:00"), + new TimeWindowFilterSettings() + { + Start = DateTimeOffset.Parse("2023-9-3T00:00:00+08:00"), // Sunday + End = DateTimeOffset.Parse("2023-9-7T00:00:00+08:00"), + Recurrence = new Recurrence() + { + Pattern = new RecurrencePattern() + { + Type = RecurrencePatternType.Weekly, + Interval = 2, + FirstDayOfWeek = DayOfWeek.Monday, + DaysOfWeek = new List(){ DayOfWeek.Monday, DayOfWeek.Sunday } // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 and 9-17 ~ 9-21 (3rd week) + }, + Range = new RecurrenceRange() + } + }, false ) }; From 6abaa0d609a907260be5b1d11f9ebda7871ce282 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 11 Apr 2024 12:21:49 +0800 Subject: [PATCH 46/52] update --- .../FeatureFilters/TimeWindowFilter.cs | 65 +++++++++---------- .../FeatureManagementBuilder.cs | 34 +++++----- 2 files changed, 46 insertions(+), 53 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 1fd62b42..7350755c 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -88,10 +88,10 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) if (settings.Recurrence != null) { // - // The reference of the object will be used for hash key. + // The reference of the object will be used for cache key. // If there is no pre-bounded settings attached to the context, there will be no cached filter settings and each call will have a unique settings object. // In this case, the cache for recurrence settings won't work. - if (context.Settings == null || Cache == null) + if (Cache == null || context.Settings == null) { return Task.FromResult(RecurrenceEvaluator.IsMatch(now, settings)); } @@ -100,52 +100,45 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) // The start time of the closest active time window. It could be null if the recurrence range surpasses its end. DateTimeOffset? closestStart; - if (!Cache.TryGetValue(settings, out closestStart)) + TimeSpan activeDuration = settings.End.Value - settings.Start.Value; + + // + // Recalculate the closest start if not yet calculated, + // Or if we have passed the cached time window. + if (!Cache.TryGetValue(settings, out closestStart) || + (closestStart.HasValue && now >= closestStart.Value + activeDuration)) { - closestStart = RecurrenceEvaluator.CalculateClosestStart(now, settings); - - Cache.Set( - settings, - closestStart, - new MemoryCacheEntryOptions - { - SlidingExpiration = ParametersCacheSlidingExpiration, - AbsoluteExpirationRelativeToNow = ParametersCacheAbsoluteExpirationRelativeToNow, - Size = 1 - }); + closestStart = ReloadClosestStart(settings); } - if (closestStart == null || now < closestStart.Value) + if (!closestStart.HasValue || now < closestStart.Value) { return Task.FromResult(false); } - if (now < closestStart.Value + (settings.End.Value - settings.Start.Value)) - { - return Task.FromResult(true); - } + return Task.FromResult(now < closestStart.Value + activeDuration); + } - closestStart = RecurrenceEvaluator.CalculateClosestStart(now, settings); + return Task.FromResult(false); + } - Cache.Set( - settings, - closestStart, - new MemoryCacheEntryOptions - { - SlidingExpiration = ParametersCacheSlidingExpiration, - AbsoluteExpirationRelativeToNow = ParametersCacheAbsoluteExpirationRelativeToNow, - Size = 1 - }); + private DateTimeOffset? ReloadClosestStart(TimeWindowFilterSettings settings) + { + DateTimeOffset now = SystemClock?.UtcNow ?? DateTimeOffset.UtcNow; - if (closestStart == null || now < closestStart.Value) - { - return Task.FromResult(false); - } + DateTimeOffset? closestStart = RecurrenceEvaluator.CalculateClosestStart(now, settings); - return Task.FromResult(now < closestStart.Value + (settings.End.Value - settings.Start.Value)); - } + Cache.Set( + settings, + closestStart, + new MemoryCacheEntryOptions + { + SlidingExpiration = ParametersCacheSlidingExpiration, + AbsoluteExpirationRelativeToNow = ParametersCacheAbsoluteExpirationRelativeToNow, + Size = 1 + }); - return Task.FromResult(false); + return closestStart; } } } diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs index 640db4a5..69dd3e06 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs @@ -53,7 +53,23 @@ public IFeatureManagementBuilder AddFeatureFilter() where T : IFeatureFilterM return this; } - public IFeatureManagementBuilder AddFeatureFilter(Func implementationFactory) where T : IFeatureFilterMetadata + public IFeatureManagementBuilder AddSessionManager() where T : ISessionManager + { + // + // Register the session manager with the same lifetime as the feature manager + if (Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) + { + Services.AddScoped(typeof(ISessionManager), typeof(T)); + } + else + { + Services.AddSingleton(typeof(ISessionManager), typeof(T)); + } + + return this; + } + + internal IFeatureManagementBuilder AddFeatureFilter(Func implementationFactory) where T : IFeatureFilterMetadata { Type serviceType = typeof(IFeatureFilterMetadata); @@ -84,21 +100,5 @@ public IFeatureManagementBuilder AddFeatureFilter(Func() where T : ISessionManager - { - // - // Register the session manager with the same lifetime as the feature manager - if (Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) - { - Services.AddScoped(typeof(ISessionManager), typeof(T)); - } - else - { - Services.AddSingleton(typeof(ISessionManager), typeof(T)); - } - - return this; - } } } From 341f78f61f4a8e3178fa3bdf526e39d0aa740099 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 15 Apr 2024 10:37:22 +0800 Subject: [PATCH 47/52] update comments --- .../Recurrence/RecurrenceEvaluator.cs | 32 +++++++++++-------- .../Recurrence/RecurrenceValidator.cs | 8 ++--- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 8a73ec84..cbfd1300 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -16,7 +16,7 @@ static class RecurrenceEvaluator /// /// Checks if a provided timestamp is within any recurring time window specified by the Recurrence section in the time window filter settings. /// If the time window filter has an invalid recurrence setting, an exception will be thrown. - /// A timestamp. + /// A datetime. /// The settings of time window filter. /// True if the timestamp is within any recurring time window, false otherwise. /// @@ -42,8 +42,8 @@ public static bool IsMatch(DateTimeOffset time, TimeWindowFilterSettings setting } /// - /// Calculate the start time of the closest active time window. - /// A timestamp. + /// Calculates the start time of the closest active time window. + /// A datetime. /// The settings of time window filter. /// The start time of the closest active time window or null if the recurrence range surpasses its end. /// @@ -76,8 +76,8 @@ public static bool IsMatch(DateTimeOffset time, TimeWindowFilterSettings setting } /// - /// Calculate the closest previous recurrence occurrence (if any) before the provided timestamp and the next occurrence (if any) after the provided timestamp. - /// A timestamp. + /// Calculates the closest previous recurrence occurrence (if any) before the given time and the next occurrence (if any) after it. + /// A datetime. /// The settings of time window filter. /// The closest previous occurrence. Note that prev occurrence can be null even if the time is past the start date, because the recurrence range may have surpassed its end. /// The next occurrence. Note that next occurrence can be null even if the prev occurrence is not null, because the recurrence range may have reached its end. @@ -139,8 +139,10 @@ private static void CalculateSurroundingOccurrences(DateTimeOffset time, TimeWin /// - /// Try to find the closest previous recurrence occurrence before the provided timestamp according to the recurrence pattern. The given time should be later than the recurrence start. - /// A timestamp. + /// Finds the closest previous recurrence occurrence before the given time according to the recurrence pattern. + /// The given time should be later than the recurrence start. + /// A return value indicates whether any previous occurrence can be found. + /// A datetime. /// The settings of time window filter. /// The closest previous occurrence. /// The number of occurrences between the time and the recurrence start. @@ -182,8 +184,9 @@ private static bool TryFindPreviousOccurrence(DateTimeOffset time, TimeWindowFil } /// - /// Find the closest previous recurrence occurrence before the provided timestamp according to the "Daily" recurrence pattern. The given time should be later than the recurrence start. - /// A timestamp. + /// Finds the closest previous recurrence occurrence before the given time according to the "Daily" recurrence pattern. + /// The given time should be later than the recurrence start. + /// A datetime. /// The settings of time window filter. /// The closest previous occurrence. /// The number of occurrences between the time and the recurrence start. @@ -210,8 +213,9 @@ private static void FindDailyPreviousOccurrence(DateTimeOffset time, TimeWindowF } /// - /// Find the closest previous recurrence occurrence before the provided timestamp according to the "Weekly" recurrence pattern. The given time should be later than the recurrence start. - /// A timestamp. + /// Finds the closest previous recurrence occurrence before the given time according to the "Weekly" recurrence pattern. + /// The given time should be later than the recurrence start. + /// A datetime. /// The settings of time window filter. /// The closest previous occurrence. /// The number of occurrences between the time and the recurrence start. @@ -304,7 +308,7 @@ private static void FindWeeklyPreviousOccurrence(DateTimeOffset time, TimeWindow } /// - /// Find the next recurrence occurrence after the provided previous occurrence according to the "Weekly" recurrence pattern. + /// Finds the next recurrence occurrence after the provided previous occurrence according to the "Weekly" recurrence pattern. /// The previous occurrence. /// The settings of time window filter. /// @@ -327,7 +331,7 @@ private static DateTimeOffset CalculateWeeklyNextOccurrence(DateTimeOffset previ } /// - /// Calculate the offset in days between two given days of the week. + /// Calculates the offset in days between two given days of the week. /// A day of week. /// A day of week. /// The number of days to be added to day2 to reach day1 @@ -339,7 +343,7 @@ private static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) /// - /// Sort a collection of days of week based on their offsets from a specified first day of week. + /// Sorts a collection of days of week based on their offsets from a specified first day of week. /// A collection of days of week. /// The first day of week. /// The sorted days of week. diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs index 63c202f0..fccaa2ab 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs @@ -21,7 +21,7 @@ static class RecurrenceValidator const string TimeWindowDurationOutOfRange = "Time window duration cannot be longer than how frequently it occurs or be longer than 10 years."; /// - /// Perform validation of time window settings. + /// Performs validation of time window settings. /// The settings of time window filter. /// The name of the invalid setting, if any. /// The reason that the setting is invalid. @@ -331,7 +331,7 @@ private static bool TryValidateNumberOfOccurrences(TimeWindowFilterSettings sett } /// - /// Check whether the duration is shorter than the minimum gap between recurrence of days of week. + /// Checks whether the duration is shorter than the minimum gap between recurrence of days of week. /// /// The time span of the duration. /// The recurrence interval. @@ -400,7 +400,7 @@ private static bool IsDurationCompliantWithDaysOfWeek(TimeSpan duration, int int } /// - /// Calculate the offset in days between two given days of the week. + /// Calculates the offset in days between two given days of the week. /// A day of week. /// A day of week. /// The number of days to be added to day2 to reach day1 @@ -412,7 +412,7 @@ private static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) /// - /// Sort a collection of days of week based on their offsets from a specified first day of week. + /// Sorts a collection of days of week based on their offsets from a specified first day of week. /// A collection of days of week. /// The first day of week. /// The sorted days of week. From 1a0415320717c19eae15f3b5ae31c335ad36c197 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 22 Apr 2024 13:46:10 +0800 Subject: [PATCH 48/52] change method type --- .../FeatureManagementBuilder.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs index 69dd3e06..640db4a5 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs @@ -53,23 +53,7 @@ public IFeatureManagementBuilder AddFeatureFilter() where T : IFeatureFilterM return this; } - public IFeatureManagementBuilder AddSessionManager() where T : ISessionManager - { - // - // Register the session manager with the same lifetime as the feature manager - if (Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) - { - Services.AddScoped(typeof(ISessionManager), typeof(T)); - } - else - { - Services.AddSingleton(typeof(ISessionManager), typeof(T)); - } - - return this; - } - - internal IFeatureManagementBuilder AddFeatureFilter(Func implementationFactory) where T : IFeatureFilterMetadata + public IFeatureManagementBuilder AddFeatureFilter(Func implementationFactory) where T : IFeatureFilterMetadata { Type serviceType = typeof(IFeatureFilterMetadata); @@ -100,5 +84,21 @@ internal IFeatureManagementBuilder AddFeatureFilter(Func() where T : ISessionManager + { + // + // Register the session manager with the same lifetime as the feature manager + if (Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) + { + Services.AddScoped(typeof(ISessionManager), typeof(T)); + } + else + { + Services.AddSingleton(typeof(ISessionManager), typeof(T)); + } + + return this; + } } } From 8e9f893f19e005faebea30df2a9b02ec6c74b93d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 22 Apr 2024 13:59:28 +0800 Subject: [PATCH 49/52] remove unused reference --- src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs | 1 - tests/Tests.FeatureManagement/OnDemandClock.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs index 640db4a5..1d1e699b 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; -using Microsoft.FeatureManagement.FeatureFilters; namespace Microsoft.FeatureManagement { diff --git a/tests/Tests.FeatureManagement/OnDemandClock.cs b/tests/Tests.FeatureManagement/OnDemandClock.cs index ffe4da22..c639a3e3 100644 --- a/tests/Tests.FeatureManagement/OnDemandClock.cs +++ b/tests/Tests.FeatureManagement/OnDemandClock.cs @@ -3,7 +3,7 @@ namespace Tests.FeatureManagement { - internal class OnDemandClock : ISystemClock + class OnDemandClock : ISystemClock { public DateTimeOffset UtcNow { get; set; } } From 7deda051568edde42593f40524ca0ff75b538d46 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Mon, 22 Apr 2024 14:20:58 +0800 Subject: [PATCH 50/52] rename variable --- .../FeatureFilters/TimeWindowFilter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 57932f5b..121bb4cf 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -16,8 +16,8 @@ namespace Microsoft.FeatureManagement.FeatureFilters [FilterAlias(Alias)] public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder { - private readonly TimeSpan ParametersCacheSlidingExpiration = TimeSpan.FromMinutes(5); - private readonly TimeSpan ParametersCacheAbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1); + private readonly TimeSpan CacheSlidingExpiration = TimeSpan.FromMinutes(5); + private readonly TimeSpan CacheAbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1); private const string Alias = "Microsoft.TimeWindow"; private readonly ILogger _logger; @@ -138,8 +138,8 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context) closestStart, new MemoryCacheEntryOptions { - SlidingExpiration = ParametersCacheSlidingExpiration, - AbsoluteExpirationRelativeToNow = ParametersCacheAbsoluteExpirationRelativeToNow, + SlidingExpiration = CacheSlidingExpiration, + AbsoluteExpirationRelativeToNow = CacheAbsoluteExpirationRelativeToNow, Size = 1 }); From bf992ffd712850be558232e4e9689e439965f093 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 22 Apr 2024 15:52:54 +0800 Subject: [PATCH 51/52] remove used reference --- .../FeatureFilters/Recurrence/RecurrenceEvaluator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index cbfd1300..216083e3 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Runtime; namespace Microsoft.FeatureManagement.FeatureFilters { From e1616a6aba04269c3f3a001b9d6093da9a83ce0e Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 22 Apr 2024 15:55:22 +0800 Subject: [PATCH 52/52] remove empty lines --- .../FeatureFilters/Recurrence/RecurrenceEvaluator.cs | 1 - .../FeatureFilters/Recurrence/RecurrenceValidator.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 216083e3..4dba080a 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -136,7 +136,6 @@ private static void CalculateSurroundingOccurrences(DateTimeOffset time, TimeWin } } - /// /// Finds the closest previous recurrence occurrence before the given time according to the recurrence pattern. /// The given time should be later than the recurrence start. diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs index fccaa2ab..992bea72 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceValidator.cs @@ -410,7 +410,6 @@ private static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) return ((int)day1 - (int)day2 + DaysPerWeek) % DaysPerWeek; } - /// /// Sorts a collection of days of week based on their offsets from a specified first day of week. /// A collection of days of week.