diff --git a/sdk/spring/spring-cloud-azure-feature-management/README.md b/sdk/spring/spring-cloud-azure-feature-management/README.md
index 127001dd28d2..5cad25d9b221 100644
--- a/sdk/spring/spring-cloud-azure-feature-management/README.md
+++ b/sdk/spring/spring-cloud-azure-feature-management/README.md
@@ -41,10 +41,10 @@ feature-management:
feature-v:
enabled-for:
-
- name: TimeWindowFilter
+ name: Microsoft.TimeWindow
parameters:
- time-window-filter-setting-start: "Wed, 01 May 2019 13:59:59 GMT"
- time-window-filter-setting-end: "Mon, 01 July 2019 00:00:00 GMT"
+ start: "Wed, 01 May 2019 13:59:59 GMT"
+ end: "Mon, 01 July 2019 00:00:00 GMT"
feature-w:
evaluate: false
enabled-for:
@@ -182,7 +182,7 @@ feature-management:
### TimeWindowFilter
-This filter provides the capability to enable a feature based on a time window. If only `time-window-filter-setting-end` is specified, the feature will be considered on until that time. If only start is specified, the feature will be considered on at all points after that time. If both are specified the feature will be considered valid between the two times.
+This filter provides the capability to enable a feature based on a time window. If only `end` is specified, the feature will be considered enabled until that time. If only `start` is specified, the feature will be considered enabled at all points after that time. If both are specified the feature will be considered enabled between the two times.
```yaml
feature-management:
@@ -190,12 +190,175 @@ feature-management:
feature-v:
enabled-for:
-
- name: TimeWindowFilter
+ name: Microsoft.TimeWindow
parameters:
- time-window-filter-setting-start: "Wed, 01 May 2019 13:59:59 GMT",
- time-window-filter-setting-end: "Mon, 01 July 2019 00:00:00 GMT"
+ start: "Wed, 01 May 2019 13:59:59 GMT"
+ end: "Mon, 01 July 2019 00:00:00 GMT"
```
+The time window can be configured to recur periodically. This can be useful in the scenario where you have to enable/disable a feature during low or high traffic periods of a day or for certain days of the week. To expand the individual time window to recurring time windows, the recurrence rule should be specified in the `recurrence` parameter.
+
+
+```yaml
+feature-management:
+ feature-flags:
+ feature-v:
+ enabled-for:
+ -
+ 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: "Numbered"
+ numberOfOccurrences: 3
+```
+To create a recurrence rule, you need to specify 3 parts: `start`, `end` and `recurrence`.
+The `start` and `end` parameters define the time window which need to recur periodically.
+The `recurrence` parameter is made up of two parts: `pattern` (how often the time window will repeat) and `range` (for how long the recurrence pattern will repeat). To create a recurrence rule, you must specify both `pattern` and `range`. Any pattern type can work with any range type.
+The time zone offset of the `start` property will apply to the recurrence settings.
+
+**Note:** `start` must be a valid first occurrence which fits the recurrence pattern. For example, if we define to repeat on every other Monday and Tuesday, then the start time should be in Monday or Tuesday. 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 Pattern
+
+There are two possible recurrence pattern types: `Daily` and `Weekly`.
+
+`Daily` causes an event to repeat based on a number of days between each occurrence. For example, "every day" or "every 3 days".
+
+`Weekly` causes an event to repeat on the same day or days of the week, based on the number of weeks between each set of occurrences. For example, "every Monday" or "every other Friday".
+
+* Parameters
+
+ Property | Relevance | Description
+ -----------|-------|-----
+ **type** | Required | `Daily`/`Weekly`.
+ **interval** | Optional | Specifies the interval between each start time of occurrence. Default value is 1. For example, if the interval is 2 with `Daily` type, and current occurrence is 2:00 AM ~ 3:00 AM on 2024/05/11, then the next occurrence should be 2:00 AM ~ 3:00 AM on 2024/05/13
+ **daysOfWeek** | Required/Optional | Specifies on which day(s) of the week the event occurs. It's required when `Weekly` type, negatively for `Daily` type.
+ **firstDayOfWeek** | Optional | Specifies which day is considered the first day of the week. Default value is `Sunday`. Negatively for `Daily` type.
+
+ * Example
+ * Daily
+
+ The following example will repeat from 2:00 AM to 3:00 AM on every 2 days
+
+ ```yaml
+ start: "Mon, 13 May 2024 02:00:00 GMT"
+ end: "Mon, 13 May 2024 03:00:00 GMT"
+ recurrence:
+ pattern:
+ type: "Daily"
+ interval: 2
+ range:
+ type: "NoEnd"
+ ```
+
+ * Weekly
+
+ The following example will repeat from 2:00 AM to 3:00 AM on every other Monday and Tuesday
+
+ ```yaml
+ start: "Mon, 13 May 2024 02:00:00 GMT"
+ end: "Mon, 13 May 2024 03:00:00 GMT"
+ recurrence:
+ pattern:
+ type: "Weekly"
+ interval: 2
+ daysOfWeek:
+ - Monday
+ - Tuesday
+ range:
+ type: "NoEnd"
+ ```
+ The following example shows a time window starts on one day and ends on another, repeat every week.
+
+ ```yaml
+ start: "Mon, 13 May 2024 02:00:00 GMT"
+ end: "Mon, 14 May 2024 03:00:00 GMT"
+ recurrence:
+ pattern:
+ type: "Weekly"
+ interval: 1
+ daysOfWeek:
+ - Monday
+ range:
+ type: "NoEnd"
+ ```
+
+
+#### Recurrence Range
+
+There are three possible recurrence range type: `NoEnd`, `EndDate` and `Numbered`.
+
+* Parameter
+
+ Property | Relevance | Description |
+ -----------|---------------|-------------
+ **type** | Required | `NoEnd`/`EndDate`/`Numbered`.
+ **endDate** | Required/Optional | 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. It's required for `EndDate` type, negatively for `NoEnd` and `Numbered` type.
+ **NumberOfOccurrences** | Required/Optional | Specifies the number of days that it will occur. It's required for `Numbered` type, negatively for `NoEnd` and `EndDate` type.
+
+ * Example
+ * `NoEnd`
+
+ The `NoEnd` range causes the recurrence to occur indefinitely.
+
+ The following example will repeat from 6:00 PM to 8:00 PM every day.
+
+ ``` yaml
+ 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: "NoEnd"
+ ```
+
+ * `EndDate`
+
+ The `EndDate` range causes the time window to occur on all days that fit the applicable pattern until the end date.
+
+ The following example will repeat from 6:00 PM to 8:00 PM every day until the last occurrence happens on April 1st, 2024.
+
+ ``` yaml
+ 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 days (based on the pattern).
+
+ The following example will repeat from 6:00 PM to 8:00 PM on Monday and Tuesday until the there are 3 occurrences, which respectively happens on 2024/04/01(Mon), 2024/04/02(Tue) and 2024/04/08(Mon).
+
+ ``` yaml
+ 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
+ ```
+
### TargetingFilter
This filter provides the capability to enable a feature for a target audience. An in-depth explanation of targeting is explained in the targeting section below. The filter parameters include an audience object which describes users, groups, and a default percentage of the user base that should have access to the feature, and an exclusion object for users and groups that should never be targeted. 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 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.
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java
index be1dd9872764..963568485e86 100644
--- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java
@@ -7,13 +7,12 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.HashMap;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Optional;
+import com.azure.spring.cloud.feature.management.implementation.FeatureFilterUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
@@ -84,7 +83,7 @@ public class TargetingFilter implements FeatureFilter {
/**
* Filter for targeting a user/group/percentage of users.
- *
+ *
* @param contextAccessor Accessor for identifying the current user/group when evaluating
*/
public TargetingFilter(TargetingContextAccessor contextAccessor) {
@@ -94,7 +93,7 @@ public TargetingFilter(TargetingContextAccessor contextAccessor) {
/**
* `Microsoft.TargetingFilter` evaluates a user/group/overall rollout of a feature.
- *
+ *
* @param contextAccessor Context for evaluating the users/groups.
* @param options enables customization of the filter.
*/
@@ -126,13 +125,13 @@ public boolean evaluate(FeatureFilterEvaluationContext context) {
parameters = (Map) audienceObject;
}
- updateValueFromMapToList(parameters, USERS);
- updateValueFromMapToList(parameters, GROUPS);
+ FeatureFilterUtils.updateValueFromMapToList(parameters, USERS);
+ FeatureFilterUtils.updateValueFromMapToList(parameters, GROUPS);
Audience audience;
- String exclusionValue = getKeyCase(parameters, EXCLUSION_CAMEL);
- String exclusionUserValue = getKeyCase((Map) parameters.get(exclusionValue), "Users");
- String exclusionGroupsValue = getKeyCase((Map) parameters.get(exclusionValue), "Groups");
+ String exclusionValue = FeatureFilterUtils.getKeyCase(parameters, EXCLUSION_CAMEL);
+ String exclusionUserValue = FeatureFilterUtils.getKeyCase((Map) parameters.get(exclusionValue), "Users");
+ String exclusionGroupsValue = FeatureFilterUtils.getKeyCase((Map) parameters.get(exclusionValue), "Groups");
if (((Map) parameters.getOrDefault(exclusionValue, new HashMap<>()))
.get(exclusionUserValue) instanceof List) {
@@ -194,13 +193,6 @@ public boolean evaluate(FeatureFilterEvaluationContext context) {
return isTargeted(defaultContextId, audience.getDefaultRolloutPercentage());
}
- private String getKeyCase(Map parameters, String key) {
- if (parameters != null && parameters.containsKey(key)) {
- return key;
- }
- return key.toLowerCase(Locale.getDefault());
- }
-
private boolean targetUser(String userId, List users) {
return userId != null && users != null && users.stream().anyMatch(user -> equals(userId, user));
}
@@ -234,7 +226,7 @@ private boolean validateTargetingContext(TargetingFilterContext targetingContext
/**
* Computes the percentage that the contextId falls into.
- *
+ *
* @param contextId Id of the context being targeted
* @return the bucket value of the context id
* @throws TargetingException Unable to create hash of target context
@@ -265,8 +257,8 @@ private boolean isTargeted(String contextId, double percentage) {
/**
* Validates the settings of a targeting filter.
- *
- * @param settings targeting filter settings
+ *
+ * @param audience targeting filter settings
* @throws TargetingException when a required parameter is missing or percentage value is greater than 100.
*/
void validateSettings(Audience audience) {
@@ -303,7 +295,7 @@ void validateSettings(Audience audience) {
/**
* Checks if two strings are equal, ignores case if configured to.
- *
+ *
* @param s1 string to compare
* @param s2 string to compare
* @return true if the strings are equal
@@ -314,21 +306,4 @@ private boolean equals(String s1, String s2) {
}
return s1.equals(s2);
}
-
- /**
- * Looks at the given key in the parameters and coverts it to a list if it is currently a map. Used for updating
- * fields in the targeting filter.
- *
- * @param Type of object inside of parameters for the given key
- * @param parameters map of generic objects
- * @param key key of object int the parameters map
- */
- @SuppressWarnings("unchecked")
- private void updateValueFromMapToList(Map parameters, String key) {
- Object objectMap = parameters.get(key);
- if (objectMap instanceof Map) {
- Collection toType = ((Map) objectMap).values();
- parameters.put(key, toType);
- }
- }
}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilter.java
index feffbc5a8ffd..ef313aa329f0 100644
--- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilter.java
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilter.java
@@ -2,16 +2,22 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.feature.management.filters;
+import com.azure.spring.cloud.feature.management.implementation.FeatureFilterUtils;
+import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings;
+import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants;
+import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceEvaluator;
import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.util.StringUtils;
import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
+import java.util.Map;
import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_END;
+import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_RECURRENCE;
import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_START;
/**
@@ -26,6 +32,8 @@ public TimeWindowFilter() {
}
private static final Logger LOGGER = LoggerFactory.getLogger(TimeWindowFilter.class);
+ private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
+ .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();
/**
* Evaluates whether a feature is enabled based on a configurable time window.
@@ -35,38 +43,42 @@ public TimeWindowFilter() {
*/
@Override
public boolean evaluate(FeatureFilterEvaluationContext context) {
- String start = (String) context.getParameters().get(TIME_WINDOW_FILTER_SETTING_START);
- String end = (String) context.getParameters().get(TIME_WINDOW_FILTER_SETTING_END);
+ final Map parameters = context.getParameters();
+ final Object recurrenceObject = parameters.get(FeatureFilterUtils.getKeyCase(parameters, TIME_WINDOW_FILTER_SETTING_RECURRENCE));
+ if (recurrenceObject != null) {
+ final Map recurrenceParameters = (Map) recurrenceObject;
+ final Object patternObj = recurrenceParameters.get(FeatureFilterUtils.getKeyCase(recurrenceParameters, RecurrenceConstants.RECURRENCE_PATTERN));
+ if (patternObj != null) {
+ FeatureFilterUtils.updateValueFromMapToList((Map) patternObj, FeatureFilterUtils.getKeyCase((Map) patternObj, RecurrenceConstants.RECURRENCE_PATTERN_DAYS_OF_WEEK));
+ }
+ }
- ZonedDateTime now = ZonedDateTime.now();
+ final TimeWindowFilterSettings settings = OBJECT_MAPPER.convertValue(context.getParameters(), TimeWindowFilterSettings.class);
+ final ZonedDateTime now = ZonedDateTime.now();
- if (!StringUtils.hasText(start) && !StringUtils.hasText(end)) {
+ if (settings.getStart() == null && settings.getEnd() == null) {
LOGGER.warn("The {} feature filter is not valid for feature {}. It must specify either {}, {}, or both.",
this.getClass().getSimpleName(), context.getName(), TIME_WINDOW_FILTER_SETTING_START,
TIME_WINDOW_FILTER_SETTING_END);
return false;
}
-
- ZonedDateTime startTime = null;
- ZonedDateTime endTime = null;
-
- try {
- startTime = StringUtils.hasText(start)
- ? ZonedDateTime.parse(start, DateTimeFormatter.ISO_DATE_TIME)
- : null;
- endTime = StringUtils.hasText(end)
- ? ZonedDateTime.parse(end, DateTimeFormatter.ISO_DATE_TIME)
- : null;
- } catch (DateTimeParseException e) {
- startTime = StringUtils.hasText(start)
- ? ZonedDateTime.parse(start, DateTimeFormatter.RFC_1123_DATE_TIME)
- : null;
- endTime = StringUtils.hasText(end)
- ? ZonedDateTime.parse(end, DateTimeFormatter.RFC_1123_DATE_TIME)
- : null;
+ if (settings.getRecurrence() != null) {
+ if (settings.getStart() != null && settings.getEnd() != null) {
+ try {
+ return RecurrenceEvaluator.isMatch(settings, now);
+ } catch (final IllegalArgumentException e) {
+ LOGGER.warn("The {} feature filter is not valid for feature {}. {}",
+ this.getClass().getSimpleName(), context.getName(), e.getMessage());
+ throw e;
+ }
+ }
+ LOGGER.warn("The {} feature filter is not valid for feature {}. It must specify both {} and {} when Recurrence is not null.",
+ this.getClass().getSimpleName(), context.getName(), TIME_WINDOW_FILTER_SETTING_START,
+ TIME_WINDOW_FILTER_SETTING_END);
+ return false;
}
- return (!StringUtils.hasText(start) || now.isAfter(startTime))
- && (!StringUtils.hasText(end) || now.isBefore(endTime));
- }
+ return (settings.getStart() == null || now.isAfter(settings.getStart()))
+ && (settings.getEnd() == null || now.isBefore(settings.getEnd()));
+ }
}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java
new file mode 100644
index 000000000000..335d14624101
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.implementation;
+
+import org.springframework.util.StringUtils;
+
+import java.util.Collection;
+import java.util.Map;
+
+public class FeatureFilterUtils {
+ /**
+ * Looks at the given key in the parameters and coverts it to a list if it is currently a map.
+ *
+ * @param parameters map of generic objects
+ * @param key key of object int the parameters map
+ */
+ public static void updateValueFromMapToList(Map parameters, String key) {
+ Object objectMap = parameters.get(key);
+ if (objectMap instanceof Map) {
+ Collection toType = ((Map) objectMap).values();
+ parameters.put(key, toType);
+ }
+ }
+
+ public static String getKeyCase(Map parameters, String key) {
+ if (parameters != null && parameters.containsKey(key)) {
+ return key;
+ }
+ return StringUtils.uncapitalize(key);
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/Recurrence.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/Recurrence.java
new file mode 100644
index 000000000000..02c3060fc476
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/Recurrence.java
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.implementation.models;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * A recurrence definition describing how time window recurs
+ * */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Recurrence {
+ /**
+ * The recurrence pattern specifying how often the time window repeats
+ * */
+ private RecurrencePattern pattern;
+ /**
+ * The recurrence range specifying how long the recurrence pattern repeats
+ * */
+ private RecurrenceRange range;
+
+ /**
+ * @return the recurrence pattern specifying how often the time window repeats
+ * */
+ public RecurrencePattern getPattern() {
+ return pattern;
+ }
+
+ /**
+ * @param pattern the recurrence pattern to be set
+ * */
+ public void setPattern(RecurrencePattern pattern) {
+ this.pattern = pattern;
+ }
+
+ /**
+ * @return the recurrence range specifying how long the recurrence pattern repeats
+ * */
+ public RecurrenceRange getRange() {
+ return range;
+ }
+
+ /**
+ * @param range the recurrence range to be set
+ * */
+ public void setRange(RecurrenceRange range) {
+ this.range = range;
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrencePattern.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrencePattern.java
new file mode 100644
index 000000000000..330a89d82763
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrencePattern.java
@@ -0,0 +1,130 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.implementation.models;
+
+import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import java.time.DayOfWeek;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The recurrence pattern specifying how often the time window repeats
+ * */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class RecurrencePattern {
+ /**
+ * The recurrence pattern type
+ */
+ private RecurrencePatternType type = RecurrencePatternType.DAILY;
+
+ /**
+ * The number of units between occurrences, where units can be in days, weeks, months, or years,
+ * depending on the pattern type. Default value is 1
+ */
+ private Integer interval = 1;
+
+ /**
+ * The days of the week on which the time window occurs
+ */
+ private List daysOfWeek = new ArrayList<>();
+
+ /**
+ * The first day of the week
+ * */
+ private DayOfWeek firstDayOfWeek = DayOfWeek.SUNDAY;
+
+ /**
+ * @return the recurrence pattern type
+ * */
+ public RecurrencePatternType getType() {
+ return type;
+ }
+
+ /**
+ * @param type pattern type to be set
+ * @throws IllegalArgumentException if type is invalid
+ * */
+ public void setType(String type) throws IllegalArgumentException {
+ try {
+ this.type = RecurrencePatternType.valueOf(type.toUpperCase());
+ } catch (final IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ String.format(RecurrenceConstants.INVALID_VALUE, "Recurrence.Pattern.Type", Arrays.toString(RecurrencePatternType.values())));
+ }
+ }
+
+ /**
+ * @return the number of units between occurrences
+ * */
+ public Integer getInterval() {
+ return interval;
+ }
+
+ /**
+ * @param interval the time units to be set
+ * @throws IllegalArgumentException if interval is invalid
+ * */
+ public void setInterval(Integer interval) throws IllegalArgumentException {
+ if (interval == null || interval <= 0) {
+ throw new IllegalArgumentException(
+ String.format(RecurrenceConstants.OUT_OF_RANGE, "Recurrence.Pattern.Interval"));
+ }
+ this.interval = interval;
+ }
+
+ /**
+ * @return the days of the week on which the time window occurs
+ * */
+ public List getDaysOfWeek() {
+ return daysOfWeek;
+ }
+
+ /**
+ * @param daysOfWeek the days that time window occurs
+ * @throws IllegalArgumentException if daysOfWeek is null/empty, or has invalid value
+ * */
+ public void setDaysOfWeek(List daysOfWeek) throws IllegalArgumentException {
+ if (daysOfWeek == null || daysOfWeek.size() == 0) {
+ throw new IllegalArgumentException(
+ String.format(RecurrenceConstants.REQUIRED_PARAMETER, "Recurrence.Pattern.DaysOfWeek"));
+ }
+
+ try {
+ for (String dayOfWeek : daysOfWeek) {
+ this.daysOfWeek.add(DayOfWeek.valueOf(dayOfWeek.toUpperCase()));
+ }
+ } catch (final IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ String.format(RecurrenceConstants.INVALID_VALUE, "Recurrence.Pattern.DaysOfWeek", Arrays.toString(DayOfWeek.values())));
+ }
+ }
+
+ /**
+ * @return the first day of the week
+ * */
+ public DayOfWeek getFirstDayOfWeek() {
+ return firstDayOfWeek;
+ }
+
+ /**
+ * @param firstDayOfWeek the first day of the week
+ * @throws IllegalArgumentException if firstDayOfWeek is invalid
+ * */
+ public void setFirstDayOfWeek(String firstDayOfWeek) throws IllegalArgumentException {
+ if (firstDayOfWeek == null) {
+ throw new IllegalArgumentException(
+ String.format(RecurrenceConstants.REQUIRED_PARAMETER, "Recurrence.Pattern.FirstDayOfWeek"));
+ }
+
+ try {
+ this.firstDayOfWeek = DayOfWeek.valueOf(firstDayOfWeek.toUpperCase());
+ } catch (final IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ String.format(RecurrenceConstants.INVALID_VALUE, "Recurrence.Pattern.FirstDayOfWeek", Arrays.toString(DayOfWeek.values())));
+ }
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrencePatternType.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrencePatternType.java
new file mode 100644
index 000000000000..512cb151da75
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrencePatternType.java
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.implementation.models;
+
+/**
+ * The type of {@link RecurrencePattern} 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("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("Weekly");
+
+ private final String type;
+
+ RecurrencePatternType(final String type) {
+ this.type = type;
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrenceRange.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrenceRange.java
new file mode 100644
index 000000000000..a9e833aed7bb
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrenceRange.java
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.implementation.models;
+
+import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowUtils;
+import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+
+/**
+ * The recurrence range specifying how long the recurrence pattern repeats
+ * */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class RecurrenceRange {
+ /**
+ * The recurrence range type. Default value is "NoEnd"
+ * */
+ private RecurrenceRangeType type = RecurrenceRangeType.NOEND;
+
+ /**
+ * The date to stop applying the recurrence pattern
+ * */
+ private ZonedDateTime endDate;
+
+ /**
+ * The number of times to repeat the time window
+ */
+ private int numberOfOccurrences;
+
+ /**
+ * @return the recurrence range type
+ * */
+ public RecurrenceRangeType getType() {
+ return type;
+ }
+
+ /**
+ * @param type the range type to be set
+ * @throws IllegalArgumentException if type is invalid
+ * */
+ public void setType(String type) throws IllegalArgumentException {
+ try {
+ this.type = RecurrenceRangeType.valueOf(type.toUpperCase());
+ } catch (final IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ String.format(RecurrenceConstants.INVALID_VALUE, "Recurrence.Range.Type", Arrays.toString(RecurrenceRangeType.values())));
+ }
+ }
+
+ /**
+ * @return the date to stop applying the recurrence pattern
+ * */
+ public ZonedDateTime getEndDate() {
+ return endDate;
+ }
+
+ /**
+ * @param endDate the end date to be set
+ * */
+ public void setEndDate(String endDate) {
+ this.endDate = TimeWindowUtils.convertStringToDate(endDate);
+ }
+
+ /**
+ * @return the number of times to repeat the time window
+ * */
+ public int getNumberOfOccurrences() {
+ return numberOfOccurrences;
+ }
+
+ /**
+ * @param numberOfOccurrences the repeat times to be set
+ * @throws IllegalArgumentException if numberOfOccurrences is invalid
+ * */
+ public void setNumberOfOccurrences(int numberOfOccurrences) throws IllegalArgumentException {
+ if (numberOfOccurrences < 1) {
+ throw new IllegalArgumentException(
+ String.format(RecurrenceConstants.OUT_OF_RANGE, "Recurrence.Range.NumberOfOccurrences"));
+ }
+ this.numberOfOccurrences = numberOfOccurrences;
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrenceRangeType.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrenceRangeType.java
new file mode 100644
index 000000000000..06356355aad5
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrenceRangeType.java
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.implementation.models;
+
+/**
+ * The type of {@link RecurrenceRange}, 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 {@link RecurrencePattern}.
+ */
+ NOEND("NoEnd"),
+
+ /**
+ * The time window repeats on all the days that fit the corresponding {@link RecurrencePattern}
+ * before or on the end date specified in EndDate of {@link RecurrenceRange}.
+ */
+ ENDDATE("EndDate"),
+
+ /**
+ * The time window repeats for the number specified in the NumberOfOccurrences of {@link RecurrenceRange}
+ * that fit the corresponding {@link RecurrencePattern}.
+ */
+ NUMBERED("Numbered");
+
+ private final String type;
+
+ RecurrenceRangeType(final String type) {
+ this.type = type;
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowFilterSettings.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowFilterSettings.java
new file mode 100644
index 000000000000..5496bac40603
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowFilterSettings.java
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.azure.spring.cloud.feature.management.implementation.timewindow;
+
+import com.azure.spring.cloud.feature.management.implementation.models.Recurrence;
+
+import java.time.ZonedDateTime;
+
+public class TimeWindowFilterSettings {
+ /**
+ * An optional start time used to determine when a feature should be enabled.
+ * If no start time is specified the time window is considered to have already started.
+ * */
+ private ZonedDateTime start;
+ /**
+ * An optional end time used to determine when a feature should be disabled.
+ * If no end time is specified the time window is considered to never end.
+ * */
+ private ZonedDateTime end;
+ /**
+ * 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.
+ * */
+ private Recurrence recurrence;
+
+ /**
+ * @param startTime the start time to determine when a feature should be enabled
+ * */
+ public void setStart(String startTime) {
+ this.start = TimeWindowUtils.convertStringToDate(startTime);
+ }
+
+ /**
+ * @return start time
+ * */
+ public ZonedDateTime getStart() {
+ return start;
+ }
+
+ /**
+ * @param endTime the end time to determine when a feature should be disabled
+ * */
+ public void setEnd(String endTime) {
+ this.end = TimeWindowUtils.convertStringToDate(endTime);
+ }
+
+ /**
+ * @return end time
+ * */
+ public ZonedDateTime getEnd() {
+ return end;
+ }
+
+ /**
+ * @param recurrence the recurrence rule to specify both how often the time window repeats and for how long
+ * */
+ public void setRecurrence(Recurrence recurrence) {
+ this.recurrence = recurrence;
+ }
+
+ /**
+ * @return the recurrence rule
+ * */
+ public Recurrence getRecurrence() {
+ return recurrence;
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowUtils.java
new file mode 100644
index 000000000000..3ee3fd0883cd
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowUtils.java
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.implementation.timewindow;
+
+import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants;
+import org.springframework.util.StringUtils;
+
+import java.time.DayOfWeek;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class TimeWindowUtils {
+ public static ZonedDateTime convertStringToDate(String timeStr) {
+ if (!StringUtils.hasText(timeStr)) {
+ return null;
+ }
+ try {
+ return ZonedDateTime.parse(timeStr, DateTimeFormatter.ISO_DATE_TIME);
+ } catch (final DateTimeParseException e) {
+ return ZonedDateTime.parse(timeStr, DateTimeFormatter.RFC_1123_DATE_TIME);
+ }
+ }
+
+ /**
+ * Calculates the offset in days between two given days of the week.
+ * @param today DayOfWeek enum of today
+ * @param firstDayOfWeek the start day of the week
+ * @return the number of days passed
+ * */
+ public static int getPassedWeekDays(DayOfWeek today, DayOfWeek firstDayOfWeek) {
+ return (today.getValue() - firstDayOfWeek.getValue() + RecurrenceConstants.DAYS_PER_WEEK) % 7;
+ }
+
+ public static List sortDaysOfWeek(List daysOfWeek, DayOfWeek firstDayOfWeek) {
+ final List result = new ArrayList<>(daysOfWeek);
+
+ Collections.sort(result, Comparator.comparingInt(a -> getPassedWeekDays(a, firstDayOfWeek)));
+ return result;
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceConstants.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceConstants.java
new file mode 100644
index 000000000000..f825090183d2
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceConstants.java
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence;
+
+
+public final class RecurrenceConstants {
+ private RecurrenceConstants() {
+ }
+ public static final int DAYS_PER_WEEK = 7;
+ public static final int TEN_YEARS = 3650;
+
+ // parameters
+ public static final String RECURRENCE_PATTERN = "Pattern";
+ public static final String RECURRENCE_PATTERN_DAYS_OF_WEEK = "DaysOfWeek";
+ public static final String RECURRENCE_RANGE = "Range";
+
+ // Error Message
+ public static final String OUT_OF_RANGE = "The value of parameter %s is out of the accepted range.";
+ public static final String INVALID_VALUE = "The value of parameter %s should be one of [%s].";
+ public static final String REQUIRED_PARAMETER = "Value cannot be null for required parameter: %s";
+ public static final String NOT_MATCHED = "%s date is not a valid first occurrence.";
+ public static final String TIME_WINDOW_DURATION_TEN_YEARS = "Time window duration cannot be longer than 10 years.";
+ public static final String TIME_WINDOW_DURATION_OUT_OF_RANGE = "The time window between Start and End should be shorter than the minimum gap between %s";
+
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceEvaluator.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceEvaluator.java
new file mode 100644
index 000000000000..17621fc0842b
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceEvaluator.java
@@ -0,0 +1,152 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence;
+
+import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePattern;
+import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePatternType;
+import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRange;
+import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRangeType;
+import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings;
+import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowUtils;
+
+import java.time.DayOfWeek;
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+public class RecurrenceEvaluator {
+ /**
+ * Checks if a provided timestamp is within any recurring time window specified
+ * by the Recurrence section in the time window filter settings.
+ * @return True if the time stamp is within any recurring time window, false otherwise.
+ */
+ public static boolean isMatch(TimeWindowFilterSettings settings, ZonedDateTime now) {
+ RecurrenceValidator.validateSettings(settings);
+
+ final ZonedDateTime previousOccurrence = getPreviousOccurrence(settings, now);
+ if (previousOccurrence == null) {
+ return false;
+ }
+
+ final ZonedDateTime occurrenceEndDate = previousOccurrence.plus(
+ Duration.between(settings.getStart(), settings.getEnd()));
+ return now.isBefore(occurrenceEndDate);
+ }
+
+ /**
+ * Find the most recent recurrence occurrence before the provided time stamp.
+ *
+ * @return The closest previous occurrence.
+ */
+ private static ZonedDateTime getPreviousOccurrence(TimeWindowFilterSettings settings, ZonedDateTime now) {
+ ZonedDateTime start = settings.getStart();
+ if (now.isBefore(start)) {
+ return null;
+ }
+
+ final RecurrencePatternType patternType = settings.getRecurrence().getPattern().getType();
+ OccurrenceInfo occurrenceInfo;
+ if (patternType == RecurrencePatternType.DAILY) {
+ occurrenceInfo = getDailyPreviousOccurrence(settings, now);
+ } else {
+ occurrenceInfo = getWeeklyPreviousOccurrence(settings, now);
+ }
+
+ final RecurrenceRange range = settings.getRecurrence().getRange();
+ final RecurrenceRangeType rangeType = range.getType();
+ if (rangeType == RecurrenceRangeType.ENDDATE
+ && occurrenceInfo.previousOccurrence != null
+ && occurrenceInfo.previousOccurrence.isAfter(range.getEndDate())) {
+ return null;
+ }
+ if (rangeType == RecurrenceRangeType.NUMBERED
+ && occurrenceInfo.numberOfOccurrences > range.getNumberOfOccurrences()) {
+ return null;
+ }
+
+ return occurrenceInfo.previousOccurrence;
+ }
+
+ /**
+ * Find the closest previous recurrence occurrence before the provided time stamp according to the "Daily" recurrence pattern.
+ *
+ * @return The return result contains two property, one is previousOccurrence, the other is numberOfOccurrences.
+ * previousOccurrence: The closest previous occurrence.
+ * numberOfOccurrences: The number of complete recurrence intervals which have occurred between the time and the recurrence start.
+ */
+ private static OccurrenceInfo getDailyPreviousOccurrence(TimeWindowFilterSettings settings, ZonedDateTime now) {
+ final ZonedDateTime start = settings.getStart();
+ final int interval = settings.getRecurrence().getPattern().getInterval();
+ final int numberOfOccurrences = (int) (Duration.between(start, now).getSeconds() / Duration.ofDays(interval).getSeconds());
+ return new OccurrenceInfo(start.plusDays((long) numberOfOccurrences * interval), numberOfOccurrences + 1);
+ }
+
+ /**
+ * Find the closest previous recurrence occurrence before the provided time stamp according to the "Weekly" recurrence pattern.
+ *
+ * @return The return result contains two property, one is previousOccurrence, the other is numberOfOccurrences.
+ * previousOccurrence: The closest previous occurrence.
+ * numberOfOccurrences: The number of recurring days of week which have occurred between the time and the recurrence start.
+ */
+ private static OccurrenceInfo getWeeklyPreviousOccurrence(TimeWindowFilterSettings settings, ZonedDateTime now) {
+ final RecurrencePattern pattern = settings.getRecurrence().getPattern();
+ final int interval = pattern.getInterval();
+ final ZonedDateTime start = settings.getStart();
+ final ZonedDateTime firstDayOfFirstWeek = start.minusDays(
+ TimeWindowUtils.getPassedWeekDays(start.getDayOfWeek(), pattern.getFirstDayOfWeek()));
+
+ final long numberOfInterval = Duration.between(firstDayOfFirstWeek, now).toSeconds()
+ / Duration.ofDays((long) interval * RecurrenceConstants.DAYS_PER_WEEK).toSeconds();
+ final ZonedDateTime firstDayOfMostRecentOccurringWeek = firstDayOfFirstWeek.plusDays(
+ numberOfInterval * (interval * RecurrenceConstants.DAYS_PER_WEEK));
+ final List sortedDaysOfWeek = TimeWindowUtils.sortDaysOfWeek(pattern.getDaysOfWeek(), pattern.getFirstDayOfWeek());
+ final int maxDayOffset = TimeWindowUtils.getPassedWeekDays(sortedDaysOfWeek.get(sortedDaysOfWeek.size() - 1), pattern.getFirstDayOfWeek());
+ final int minDayOffset = TimeWindowUtils.getPassedWeekDays(sortedDaysOfWeek.get(0), pattern.getFirstDayOfWeek());
+ ZonedDateTime mostRecentOccurrence;
+ int numberOfOccurrences = (int) (numberOfInterval * sortedDaysOfWeek.size()
+ - (sortedDaysOfWeek.indexOf(start.getDayOfWeek())));
+
+ // now is not within the most recent occurring week
+ if (now.isAfter(firstDayOfMostRecentOccurringWeek.plusDays(RecurrenceConstants.DAYS_PER_WEEK))) {
+ numberOfOccurrences += sortedDaysOfWeek.size();
+ mostRecentOccurrence = firstDayOfMostRecentOccurringWeek.plusDays(maxDayOffset);
+ return new OccurrenceInfo(mostRecentOccurrence, numberOfOccurrences);
+ }
+
+ // day with the min offset in the most recent occurring week
+ ZonedDateTime dayWithMinOffset = firstDayOfMostRecentOccurringWeek.plusDays(minDayOffset);
+ if (start.isAfter(dayWithMinOffset)) {
+ numberOfOccurrences = 0;
+ dayWithMinOffset = start;
+ }
+ if (now.isBefore(dayWithMinOffset)) {
+ // move to the last occurrence in the previous occurring week
+ mostRecentOccurrence = firstDayOfMostRecentOccurringWeek.minusDays(interval * RecurrenceConstants.DAYS_PER_WEEK).plusDays(maxDayOffset);
+ } else {
+ mostRecentOccurrence = dayWithMinOffset;
+ numberOfOccurrences++;
+
+ for (int i = sortedDaysOfWeek.indexOf(dayWithMinOffset.getDayOfWeek()) + 1; i < sortedDaysOfWeek.size(); i++) {
+ dayWithMinOffset = firstDayOfMostRecentOccurringWeek.plusDays(
+ TimeWindowUtils.getPassedWeekDays(sortedDaysOfWeek.get(i), pattern.getFirstDayOfWeek()));
+ if (now.isBefore(dayWithMinOffset)) {
+ break;
+ }
+ mostRecentOccurrence = dayWithMinOffset;
+ numberOfOccurrences++;
+ }
+ }
+ return new OccurrenceInfo(mostRecentOccurrence, numberOfOccurrences);
+ }
+
+ private static class OccurrenceInfo {
+ private final ZonedDateTime previousOccurrence;
+ private final int numberOfOccurrences;
+
+ OccurrenceInfo(ZonedDateTime dateTime, int num) {
+ this.previousOccurrence = dateTime;
+ this.numberOfOccurrences = num;
+ }
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceValidator.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceValidator.java
new file mode 100644
index 000000000000..a09f16c3c432
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceValidator.java
@@ -0,0 +1,173 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence;
+
+import com.azure.spring.cloud.feature.management.implementation.models.Recurrence;
+import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePattern;
+import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePatternType;
+import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRangeType;
+import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings;
+import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowUtils;
+
+import java.time.DayOfWeek;
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+
+import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_END;
+import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_RECURRENCE;
+import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_START;
+
+public class RecurrenceValidator {
+ public static void validateSettings(TimeWindowFilterSettings settings) {
+ validateRecurrenceRequiredParameter(settings);
+ validateRecurrencePattern(settings);
+ validateRecurrenceRange(settings);
+ }
+
+ private static void validateRecurrenceRequiredParameter(TimeWindowFilterSettings settings) {
+ final Recurrence recurrence = settings.getRecurrence();
+ String paramName = "";
+ String reason = "";
+ if (recurrence.getPattern() == null) {
+ paramName = String.format("%s.%s", TIME_WINDOW_FILTER_SETTING_RECURRENCE, RecurrenceConstants.RECURRENCE_PATTERN);
+ reason = RecurrenceConstants.REQUIRED_PARAMETER;
+ }
+ if (recurrence.getRange() == null) {
+ paramName = String.format("%s.%s", TIME_WINDOW_FILTER_SETTING_RECURRENCE, RecurrenceConstants.RECURRENCE_RANGE);
+ reason = RecurrenceConstants.REQUIRED_PARAMETER;
+ }
+ if (!settings.getEnd().isAfter(settings.getStart())) {
+ paramName = TIME_WINDOW_FILTER_SETTING_END;
+ reason = RecurrenceConstants.OUT_OF_RANGE;
+ }
+ if (settings.getEnd().isAfter(settings.getStart().plusDays(RecurrenceConstants.TEN_YEARS))) {
+ paramName = TIME_WINDOW_FILTER_SETTING_END;
+ reason = RecurrenceConstants.TIME_WINDOW_DURATION_TEN_YEARS;
+ }
+
+ if (!paramName.isEmpty()) {
+ throw new IllegalArgumentException(String.format(reason, paramName));
+ }
+ }
+
+ private static void validateRecurrencePattern(TimeWindowFilterSettings settings) {
+ final RecurrencePatternType patternType = settings.getRecurrence().getPattern().getType();
+
+ if (patternType == RecurrencePatternType.DAILY) {
+ validateDailyRecurrencePattern(settings);
+ } else {
+ validateWeeklyRecurrencePattern(settings);
+ }
+ }
+
+ private static void validateRecurrenceRange(TimeWindowFilterSettings settings) {
+ RecurrenceRangeType rangeType = settings.getRecurrence().getRange().getType();
+ if (RecurrenceRangeType.ENDDATE.equals(rangeType)) {
+ validateEndDate(settings);
+ }
+ }
+
+ private static void validateDailyRecurrencePattern(TimeWindowFilterSettings settings) {
+ // "Start" is always a valid first occurrence for "Daily" pattern.
+ // Only need to check if time window validated
+ validateTimeWindowDuration(settings);
+ }
+
+ private static void validateWeeklyRecurrencePattern(TimeWindowFilterSettings settings) {
+ validateDaysOfWeek(settings);
+
+ // Check whether "Start" is a valid first occurrence
+ final RecurrencePattern pattern = settings.getRecurrence().getPattern();
+ if (pattern.getDaysOfWeek().stream().noneMatch((dayOfWeekStr) ->
+ settings.getStart().getDayOfWeek() == dayOfWeekStr)) {
+ throw new IllegalArgumentException(String.format(RecurrenceConstants.NOT_MATCHED, TIME_WINDOW_FILTER_SETTING_START));
+ }
+
+ // Time window duration must be shorter than how frequently it occurs
+ validateTimeWindowDuration(settings);
+
+ // Check whether the time window duration is shorter than the minimum gap between days of week
+ if (!isDurationCompliantWithDaysOfWeek(settings)) {
+ throw new IllegalArgumentException(String.format(RecurrenceConstants.TIME_WINDOW_DURATION_OUT_OF_RANGE, "Recurrence.Pattern.DaysOfWeek"));
+ }
+ }
+
+ /**
+ * Validate if time window duration is shorter than how frequently it occurs
+ */
+ private static void validateTimeWindowDuration(TimeWindowFilterSettings settings) {
+ final RecurrencePattern pattern = settings.getRecurrence().getPattern();
+ final Duration intervalDuration = RecurrencePatternType.DAILY.equals(pattern.getType())
+ ? Duration.ofDays(pattern.getInterval())
+ : Duration.ofDays((long) pattern.getInterval() * RecurrenceConstants.DAYS_PER_WEEK);
+ final Duration timeWindowDuration = Duration.between(settings.getStart(), settings.getEnd());
+ if (timeWindowDuration.compareTo(intervalDuration) > 0) {
+ throw new IllegalArgumentException(String.format(RecurrenceConstants.TIME_WINDOW_DURATION_OUT_OF_RANGE, "Recurrence.Pattern.Interval"));
+ }
+ }
+
+ private static void validateDaysOfWeek(TimeWindowFilterSettings settings) {
+ final List daysOfWeek = settings.getRecurrence().getPattern().getDaysOfWeek();
+ if (daysOfWeek == null || daysOfWeek.size() == 0) {
+ throw new IllegalArgumentException(String.format(RecurrenceConstants.REQUIRED_PARAMETER, "Recurrence.Pattern.DaysOfWeek"));
+ }
+ }
+
+ private static void validateEndDate(TimeWindowFilterSettings settings) {
+ if (settings.getRecurrence().getRange().getEndDate().isBefore(settings.getStart())) {
+ throw new IllegalArgumentException("The Recurrence.Range.EndDate should be after the Start");
+ }
+ }
+
+ /**
+ * Check whether the duration is shorter than the minimum gap between recurrence of days of week.
+ *
+ * @param settings time window filter settings
+ * @return True if the duration is compliant with days of week, false otherwise.
+ */
+ private static boolean isDurationCompliantWithDaysOfWeek(TimeWindowFilterSettings settings) {
+ final List daysOfWeek = settings.getRecurrence().getPattern().getDaysOfWeek();
+ if (daysOfWeek.size() == 1) {
+ return true;
+ }
+
+ // Get the date of first day of the week
+ final ZonedDateTime today = ZonedDateTime.now();
+ final DayOfWeek firstDayOfWeek = settings.getRecurrence().getPattern().getFirstDayOfWeek();
+ final int offset = TimeWindowUtils.getPassedWeekDays(today.getDayOfWeek(), firstDayOfWeek);
+ final ZonedDateTime firstDateOfWeek = today.minusDays(offset).truncatedTo(ChronoUnit.DAYS);
+ final List sortedDaysOfWeek = TimeWindowUtils.sortDaysOfWeek(daysOfWeek, firstDayOfWeek);
+
+ // Loop the whole week to get the min gap between the two consecutive recurrences
+ ZonedDateTime date;
+ ZonedDateTime prevOccurrence = null;
+ Duration minGap = Duration.ofDays(RecurrenceConstants.DAYS_PER_WEEK);
+
+ for (DayOfWeek day: sortedDaysOfWeek) {
+ date = firstDateOfWeek.plusDays(TimeWindowUtils.getPassedWeekDays(day, firstDayOfWeek));
+ if (prevOccurrence != null) {
+ final Duration currentGap = Duration.between(prevOccurrence, date);
+ if (currentGap.compareTo(minGap) < 0) {
+ minGap = currentGap;
+ }
+ }
+ prevOccurrence = date;
+ }
+
+ if (settings.getRecurrence().getPattern().getInterval() == 1) {
+ // It may across weeks. Check the adjacent week
+ date = firstDateOfWeek.plusDays(RecurrenceConstants.DAYS_PER_WEEK)
+ .plusDays(TimeWindowUtils.getPassedWeekDays(sortedDaysOfWeek.get(0), firstDayOfWeek));
+ final Duration currentGap = Duration.between(prevOccurrence, date);
+ if (currentGap.compareTo(minGap) < 0) {
+ minGap = currentGap;
+ }
+ }
+
+ final Duration timeWindowDuration = Duration.between(settings.getStart(), settings.getEnd());
+ return minGap.compareTo(timeWindowDuration) >= 0;
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FilterParameters.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FilterParameters.java
index f7ec1afab92d..3bcc61919b5c 100644
--- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FilterParameters.java
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FilterParameters.java
@@ -6,9 +6,9 @@
* Parameters for the predefined filters.
*/
public final class FilterParameters {
-
+
private FilterParameters() {
-
+
}
/**
@@ -26,4 +26,10 @@ private FilterParameters() {
*/
public static final String TIME_WINDOW_FILTER_SETTING_END = "End";
+ /**
+ * 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 static final String TIME_WINDOW_FILTER_SETTING_RECURRENCE = "Recurrence";
+
}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java
index 2f30f601308d..396be785dc14 100644
--- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java
@@ -11,8 +11,10 @@
import static org.mockito.Mockito.when;
import java.util.HashMap;
+import java.util.List;
import java.util.concurrent.ExecutionException;
+import com.azure.spring.cloud.feature.management.filters.TimeWindowFilter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -203,6 +205,39 @@ public void oneOffAll() {
assertFalse(featureManager.isEnabledAsync("On").block());
}
+ @Test
+ public void timeWindowFilter() {
+ final HashMap features = new HashMap<>();
+ final HashMap filters = new HashMap();
+
+ final HashMap parameters = new HashMap<>();
+ parameters.put("Start", "Sun, 14 Jan 2024 00:00:00 GMT");
+ parameters.put("End", "Mon, 15 Jan 2024 00:00:00 GMT");
+ final HashMap pattern = new HashMap<>();
+ pattern.put("Type", "Weekly");
+ pattern.put("DaysOfWeek", List.of("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"));
+ final HashMap range = new HashMap<>();
+ range.put("Type", "NoEnd");
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("Pattern", pattern);
+ recurrence.put("Range", range);
+ parameters.put("Recurrence", recurrence);
+
+ final FeatureFilterEvaluationContext weeklyAlwaysOn = new FeatureFilterEvaluationContext();
+ weeklyAlwaysOn.setName("TimeWindowFilter");
+ weeklyAlwaysOn.setParameters(parameters);
+ filters.put(0, weeklyAlwaysOn);
+
+ final Feature weeklyAlwaysOnFeature = new Feature();
+ weeklyAlwaysOnFeature.setEnabledFor(filters);
+ features.put("Alpha", weeklyAlwaysOnFeature);
+
+ when(featureManagementPropertiesMock.getFeatureManagement()).thenReturn(features);
+ when(context.getBean(Mockito.matches("TimeWindowFilter"))).thenReturn(new TimeWindowFilter());
+
+ assertTrue(featureManager.isEnabled("Alpha"));
+ }
+
class AlwaysOnFilter implements FeatureFilter {
@Override
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilterTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilterTest.java
index 4d52015e0e28..7be1af4cc933 100644
--- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilterTest.java
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilterTest.java
@@ -9,7 +9,9 @@
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -88,4 +90,76 @@ public void noInputsTest() {
assertFalse(filter.evaluate(context));
}
+ @Test
+ public void weeklyTest() {
+ final TimeWindowFilter filter = new TimeWindowFilter();
+ final FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext();
+ final ZonedDateTime now = ZonedDateTime.now();
+
+ final HashMap parameters = new HashMap<>();
+ parameters.put("Start", now.minusDays(7).format(DateTimeFormatter.RFC_1123_DATE_TIME));
+ parameters.put("End", now.minusDays(7).plusHours(2).format(DateTimeFormatter.RFC_1123_DATE_TIME));
+ final HashMap pattern = new HashMap<>();
+ pattern.put("Type", "Weekly");
+ pattern.put("DaysOfWeek", List.of(now.minusDays(7).getDayOfWeek().name()));
+ final HashMap range = new HashMap<>();
+ range.put("Type", "NoEnd");
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("Pattern", pattern);
+ recurrence.put("Range", range);
+ parameters.put("Recurrence", recurrence);
+
+ context.setParameters(parameters);
+ assertTrue(filter.evaluate(context));
+ }
+
+ @Test
+ public void weeklyDaysOfWeekMapTest() {
+ final TimeWindowFilter filter = new TimeWindowFilter();
+ final FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext();
+ final ZonedDateTime now = ZonedDateTime.now();
+
+ final HashMap parameters = new HashMap<>();
+ parameters.put("Start", now.minusDays(7).format(DateTimeFormatter.RFC_1123_DATE_TIME));
+ parameters.put("End", now.minusDays(7).plusHours(2).format(DateTimeFormatter.RFC_1123_DATE_TIME));
+ final HashMap daysOfWeekMap = new HashMap<>();
+ daysOfWeekMap.put("0", now.minusDays(7).getDayOfWeek().name());
+ final HashMap pattern = new HashMap<>();
+ pattern.put("Type", "Weekly");
+ pattern.put("DaysOfWeek", daysOfWeekMap);
+ final HashMap range = new HashMap<>();
+ range.put("Type", "NoEnd");
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("Pattern", pattern);
+ recurrence.put("Range", range);
+ parameters.put("Recurrence", recurrence);
+
+ context.setParameters(parameters);
+ assertTrue(filter.evaluate(context));
+ }
+
+ @Test
+ public void weeklyLowerCamelCaseTest() {
+ final TimeWindowFilter filter = new TimeWindowFilter();
+ final FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext();
+ final ZonedDateTime now = ZonedDateTime.now();
+
+ final HashMap parameters = new HashMap<>();
+ parameters.put("start", now.minusDays(7).format(DateTimeFormatter.RFC_1123_DATE_TIME));
+ parameters.put("end", now.minusDays(7).plusHours(2).format(DateTimeFormatter.RFC_1123_DATE_TIME));
+ final HashMap daysOfWeekMap = new HashMap<>();
+ daysOfWeekMap.put("0", now.minusDays(7).getDayOfWeek().name());
+ final HashMap pattern = new HashMap<>();
+ pattern.put("type", "Weekly");
+ pattern.put("daysOfWeek", daysOfWeekMap);
+ final HashMap range = new HashMap<>();
+ range.put("type", "NoEnd");
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("pattern", pattern);
+ recurrence.put("range", range);
+ parameters.put("recurrence", recurrence);
+
+ context.setParameters(parameters);
+ assertTrue(filter.evaluate(context));
+ }
}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceEvaluatorTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceEvaluatorTest.java
new file mode 100644
index 000000000000..12d9fd6c6308
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceEvaluatorTest.java
@@ -0,0 +1,798 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.filters.recurrence;
+
+import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings;
+import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceEvaluator;
+import com.azure.spring.cloud.feature.management.implementation.models.Recurrence;
+import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePattern;
+import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRange;
+import org.junit.jupiter.api.Test;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class RecurrenceEvaluatorTest {
+
+ @Test
+ public void dailyTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-02T00:00:00+08:00");
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void dailyMultiIntervalTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-05T00:00:00+08:00");
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ pattern.setInterval(4);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-03T00:00:00+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void dailyMultiIntervalTrue2() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-06T00:00:00+08:00");
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ pattern.setInterval(4);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-03T00:00:00+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void dailyMultiIntervalTrue3() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-09T00:00:00+08:00"); // Within the recurring time window 2023-09-09T00:00:00+08:00 ~ 2023-09-11T00:00:00+08:00
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ pattern.setInterval(4);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-03T00:00:00+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void dailyMultiIntervalFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-03T00:00:00+08:00");
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ pattern.setInterval(4);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-03T00:00:00+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void dailyNumberedRangeTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-03T00:00:00+08:00"); // second occurrences
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ pattern.setInterval(2);
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void dailyNumberedRangeFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-03T00:00:00+08:00"); // third occurrences
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void dailyEndDateTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-06T00:00:00+08:00");
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ pattern.setInterval(3);
+ range.setType("EndDate");
+ range.setEndDate("2023-09-04T00:00:00+08:00");
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-03T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void dailyEndDateFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-04T00:00:00+08:00");
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ range.setType("EndDate");
+ range.setEndDate("2023-09-03T00:00:00+08:00");
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void dailyDiffTimeZoneTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-02T16:00:00+00:00"); // 2023-09-03T00:00:00+08:00
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ pattern.setInterval(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-01T00:12:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void dailyDiffTimeZoneFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-02T15:59:00+00:00"); // 2023-09-02T23:59:00+08:00
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ pattern.setInterval(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-01T00:12:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+
+ @Test
+ public void dailyRFCFormatTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("Sat, 02 Sep 2023 00:00:00 +0800", DateTimeFormatter.RFC_1123_DATE_TIME);
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("Fri, 01 Sep 2023 00:00:00 +0800");
+ settings.setEnd("Fri, 01 Sep 2023 00:00:01 +0800");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void dailyRFCFormatFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("Sun, 03 Sep 2023 00:00:00 +0800", DateTimeFormatter.RFC_1123_DATE_TIME); // third occurrences
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Daily");
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("Fri, 01 Sep 2023 00:00:00 +0800");
+ settings.setEnd("Fri, 01 Sep 2023 00:00:01 +0800");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-04T00:00:00+08:00"); // Monday in the 2nd week
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Friday"));
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00"); // Friday
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDayOfWeekFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-04T00:00:00+08:00"); // Monday
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Sunday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")); // No Monday
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00"); // Friday
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekIntervalTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-04T00:00:00+08:00"); // Monday in the first week
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Sunday"));
+ pattern.setInterval(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00"); // Sunday
+ settings.setEnd("2023-09-03T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDayOfWeekIntervalTrue2() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-15T00:00:00+08:00"); // Friday in the third week after the start date
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Friday"));
+ pattern.setInterval(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00"); // Friday
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDayOfWeekIntervalTrue3() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-04T00:00:00+08:00"); // Monday in the 1st week after the Start date
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Sunday"));
+ pattern.setInterval(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00"); // Sunday
+ settings.setEnd("2023-09-03T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekIntervalFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-04T00:00:00+08:00"); // Monday in the second week
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Sunday"));
+ pattern.setInterval(2);
+ pattern.setFirstDayOfWeek("Monday");
+ range.setType("NoEnd");
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00"); // Sunday
+ settings.setEnd("2023-09-03T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDayOfWeekIntervalFalse2() {
+ final ZonedDateTime now = ZonedDateTime.parse("2024-02-12T08:00:00+08:00"); // Monday in the 3rd week after the Start date
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Friday", "Monday"));
+ pattern.setFirstDayOfWeek("Sunday");
+ pattern.setInterval(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2024-02-02T12:00:00+08:00"); // Friday
+ settings.setEnd("2024-02-03T12:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDayOfWeekIntervalFalse3() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-04T00:00:00+08:00"); // Monday in the 2nd week after the Start date
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Sunday"));
+ pattern.setFirstDayOfWeek("Monday");
+ pattern.setInterval(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00"); // Sunday
+ settings.setEnd("2023-09-03T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekNumberedRangeTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-03T00:00:00+08:00"); // Third occurrence
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"));
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(3);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00"); // Friday
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekNumberedRangeTrue2() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-22T00:00:00+08:00"); // 4th occurrence
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Friday"));
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(4);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00"); // Friday
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekNumberedRangeTrue3() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-25T00:00:00+08:00"); // 4th occurrence
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Sunday"));
+ pattern.setFirstDayOfWeek("Monday");
+ pattern.setInterval(2);
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(4);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00"); // Sunday
+ settings.setEnd("2023-09-03T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekNumberedRangeFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-04T00:00:00+08:00"); // 4th occurrence
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"));
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(3);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00"); // Friday
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekNumberedRangeFalse2() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-29T00:00:00+08:00"); // 5th occurrence
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Friday"));
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(4);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00"); // Friday
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekNumberedRangeFalse3() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-10-01T00:00:00+08:00"); // 5th occurrence
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Sunday"));
+ pattern.setFirstDayOfWeek("Monday");
+ pattern.setInterval(2);
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(4);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00"); // Sunday
+ settings.setEnd("2023-09-03T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekEndDateRangeTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-29T00:00:00+08:00");
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Friday"));
+ range.setType("EndDate");
+ range.setEndDate("2023-09-29T00:00:00+08:00");
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00"); // Friday
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekEndDateRangeTrue2() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-30T00:00:00+08:00");
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Friday"));
+ range.setType("EndDate");
+ range.setEndDate("2023-09-29T00:00:00+08:00");
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00"); // Friday
+ settings.setEnd("2023-09-02T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekEndDateRangeFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-10-06T00:00:00+08:00");
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Friday"));
+ range.setType("EndDate");
+ range.setEndDate("2023-09-29T00:00:00+08:00");
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00"); // Friday
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDaysOfWeekIntervalNumberedRangeTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-17T00:00:00+08:00"); // Sunday in the 3rd week
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Sunday"));
+ pattern.setFirstDayOfWeek("Monday");
+ pattern.setInterval(2);
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(3);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00"); // Sunday
+ settings.setEnd("2023-09-03T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyTimeWindowAcrossDaysIntervalTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-13T08:00:00+08:00"); // Within the recurring time window 2023-09-11T:00:00:00+08:00 ~ 2023-09-15T:00:00:00+08:00
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Sunday", "Monday")); // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 (3rd week) and 9-17 ~ 9-21 (3rd week)
+ pattern.setFirstDayOfWeek("Monday");
+ pattern.setInterval(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00");
+ settings.setEnd("2023-09-07T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyTimeWindowAcrossDaysNumberedRangeTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-19T00:00:00+08:00"); // The 3rd occurrence: 2023-9-17T:00:00:00+08:00 ~ 2023-9-21T:00:00:00+08:00
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Sunday")); // Time window occurrences: 9-3 ~ 9-7 (1st week), 9-11 ~ 9-15 (3rd week) and 9-17 ~ 9-21 (3rd week)
+ pattern.setFirstDayOfWeek("Monday");
+ pattern.setInterval(2);
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(3);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00"); // Sunday
+ settings.setEnd("2023-09-07T00:00:00+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyTimeWindowAcrossDaysNumberedRangeFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-19T00:00:00+08:00"); // 3rd occurrence
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Sunday")); // Time window occurrences: 9-3 ~ 9-7 (1st occurrence), 9-11 ~ 9-15 (2nd occurrence) and 9-17 ~ 9-21 (3rd occurrence)
+ pattern.setFirstDayOfWeek("Monday");
+ pattern.setInterval(2);
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00"); // Sunday
+ settings.setEnd("2023-09-07T00:00:00+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDiffTimeZoneTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-03T16:00:00+00:00"); // Monday in the 2nd week after the Start date if timezone is UTC+8
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Friday", "Monday"));
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDiffTimeZoneTrue2() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-07T16:00:00+00:00"); // Friday in the 2nd week after the Start date if timezone is UTC+8
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Friday", "Monday"));
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDiffTimeZoneFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-03T15:59:00+00:00"); // Sunday in the 2nd week after the Start date if timezone is UTC+8
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Friday", "Monday"));
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDiffTimeZoneFalse2() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-07T15:59:00+00:00"); // Thursday in the 2nd week after the Start date if timezone is UTC+8
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Friday", "Monday"));
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-01T00:00:00+08:00");
+ settings.setEnd("2023-09-01T00:00:01+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyDiffTimeZoneIntervalTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-10T16:00:00+00:00"); // Within the recurring time window 2023-09-11T:00:00:00+08:00 ~ 2023-9-15T:00:00:00+08:00
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Sunday", "Monday")); // Time window occurrences: 9-3 ~ 9-7, 9-11 ~ 9-15 and 9-17 ~ 9-21
+ pattern.setInterval(2);
+ pattern.setFirstDayOfWeek("Monday");
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00");
+ settings.setEnd("2023-09-07T00:00:00+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyDiffTimeZoneIntervalFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("2023-09-10T15:59:00+00:00"); // Within the recurring time window 2023-09-11T:00:00:00+08:00 ~ 2023-9-15T:00:00:00+08:00
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Sunday", "Monday")); // Time window occurrences: 9-3 ~ 9-7, 9-11 ~ 9-15 and 9-17 ~ 9-21
+ pattern.setInterval(2);
+ pattern.setFirstDayOfWeek("Monday");
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("2023-09-03T00:00:00+08:00");
+ settings.setEnd("2023-09-07T00:00:00+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ @Test
+ public void weeklyRFCFormatTrue() {
+ final ZonedDateTime now = ZonedDateTime.parse("Mon, 04 Sep 2023 00:00:00 +0800", DateTimeFormatter.RFC_1123_DATE_TIME); // Monday in the 2nd week
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Friday"));
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("Fri, 01 Sep 2023 00:00:00 +0800"); // Friday
+ settings.setEnd("Fri, 01 Sep 2023 00:00:01 +0800");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, true);
+ }
+
+ @Test
+ public void weeklyRFCFormatFalse() {
+ final ZonedDateTime now = ZonedDateTime.parse("Tue, 19 Sep 2023 00:00:00 GMT", DateTimeFormatter.RFC_1123_DATE_TIME); // 3rd occurrence
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ final Recurrence recurrence = new Recurrence();
+ pattern.setType("Weekly");
+ pattern.setDaysOfWeek(List.of("Monday", "Sunday")); // Time window occurrences: 9-3 ~ 9-7 (1st occurrence), 9-11 ~ 9-15 (2nd occurrence) and 9-17 ~ 9-21 (3rd occurrence)
+ pattern.setFirstDayOfWeek("Monday");
+ pattern.setInterval(2);
+ range.setType("Numbered");
+ range.setNumberOfOccurrences(2);
+ recurrence.setRange(range);
+ recurrence.setPattern(pattern);
+ settings.setStart("Sun, 03 Sep 2023 00:00:00 +0800");
+ settings.setEnd("2023-09-07T00:00:00+08:00");
+ settings.setRecurrence(recurrence);
+ consumeEvaluationTestData(settings, now, false);
+ }
+
+ private void consumeEvaluationTestData(TimeWindowFilterSettings settings, ZonedDateTime now, boolean isEnabled) {
+ assertEquals(RecurrenceEvaluator.isMatch(settings, now), isEnabled);
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceValidatorTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceValidatorTest.java
new file mode 100644
index 000000000000..a4122ca06a63
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceValidatorTest.java
@@ -0,0 +1,329 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.cloud.feature.management.filters.recurrence;
+
+import com.azure.spring.cloud.feature.management.filters.TimeWindowFilter;
+import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings;
+import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants;
+import com.azure.spring.cloud.feature.management.implementation.models.Recurrence;
+import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePattern;
+import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRange;
+import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;
+import com.azure.spring.cloud.feature.management.models.FilterParameters;
+import org.junit.jupiter.api.Test;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RecurrenceValidatorTest {
+ private final String recurrencePatter = "Recurrence.Pattern";
+ private final String recurrenceRange = "Recurrence.Range";
+ private final String recurrencePatternInterval = "Recurrence.Pattern.Interval";
+ private final String recurrencePatternDaysOfWeek = "Recurrence.Pattern.DaysOfWeek";
+ private final String recurrenceRangeNumberOfOccurrences = "Recurrence.Range.NumberOfOccurrences";
+ private final String recurrenceRangeEndDate = "Recurrence.Range.EndDate";
+
+ @Test
+ public void generalRequiredParameterTest() {
+ final ZonedDateTime startTime = ZonedDateTime.now();
+ final ZonedDateTime endTime = startTime.plusHours(2);
+
+ // no pattern in recurrence parameter
+ final HashMap range1 = new HashMap<>();
+ range1.put("Type", "NoEnd");
+ final HashMap recurrence1 = new HashMap<>();
+ recurrence1.put("Range", range1);
+ final Map parameters1 = new LinkedHashMap<>();
+ parameters1.put("Start", startTime.format(DateTimeFormatter.RFC_1123_DATE_TIME));
+ parameters1.put("End", endTime.format(DateTimeFormatter.RFC_1123_DATE_TIME));
+ parameters1.put("Recurrence", recurrence1);
+ consumeValidationTestData(parameters1, recurrencePatter);
+
+ // no range in recurrence parameter
+ final HashMap pattern2 = new HashMap<>();
+ pattern2.put("Type", "Daily");
+ final HashMap recurrence2 = new HashMap<>();
+ recurrence2.put("Pattern", pattern2);
+ final Map parameters2 = new LinkedHashMap<>();
+ parameters2.put("Start", startTime.format(DateTimeFormatter.RFC_1123_DATE_TIME));
+ parameters2.put("End", endTime.format(DateTimeFormatter.RFC_1123_DATE_TIME));
+ parameters2.put("Recurrence", recurrence2);
+ consumeValidationTestData(parameters2, recurrenceRange);
+ }
+
+ @Test
+ public void invalidPatternTypeTest() {
+ final HashMap pattern = new HashMap<>();
+ pattern.put("Type", "");
+
+ final HashMap range = new HashMap<>();
+ range.put("Type", "NoEnd");
+
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("Pattern", pattern);
+ recurrence.put("Range", range);
+
+ final Map parameters = new LinkedHashMap<>();
+ parameters.put("Start", "2023-09-01T00:00:00+08:00");
+ parameters.put("End", "2023-09-01T02:00:00+08:00");
+ parameters.put("Recurrence", recurrence);
+
+ consumeValidationTestData(parameters, "Recurrence.Pattern.Type");
+ }
+
+ @Test
+ public void invalidPatternIntervalTest() {
+ final HashMap pattern = new HashMap<>();
+ pattern.put("Type", "Daily");
+ pattern.put("Interval", 0);
+
+ final HashMap range = new HashMap<>();
+ range.put("Type", "NoEnd");
+
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("Pattern", pattern);
+ recurrence.put("Range", range);
+
+ final Map parameters = new LinkedHashMap<>();
+ parameters.put("Start", "2023-09-01T00:00:00+08:00");
+ parameters.put("End", "2023-09-01T02:00:00+08:00");
+ parameters.put("Recurrence", recurrence);
+
+ consumeValidationTestData(parameters, recurrencePatternInterval);
+ }
+
+ @Test
+ public void invalidPatternFirstDayOfWeekTest() {
+ final HashMap pattern = new HashMap<>();
+ pattern.put("Type", "Weekly");
+ pattern.put("FirstDayOfWeek", "");
+ pattern.put("DaysOfWeek", List.of("Monday"));
+
+ final HashMap range = new HashMap<>();
+ range.put("Type", "NoEnd");
+
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("Pattern", pattern);
+ recurrence.put("Range", range);
+
+ final Map parameters = new LinkedHashMap<>();
+ parameters.put("Start", "2023-09-01T00:00:00+08:00");
+ parameters.put("End", "2023-09-01T02:00:00+08:00");
+ parameters.put("Recurrence", recurrence);
+
+ consumeValidationTestData(parameters, "Recurrence.Pattern.FirstDayOfWeek");
+ }
+
+ @Test
+ public void invalidPatternDaysOfWeekTest() {
+ final HashMap pattern = new HashMap<>();
+ pattern.put("Type", "Weekly");
+ pattern.put("DaysOfWeek", List.of("day"));
+
+ final HashMap range = new HashMap<>();
+ range.put("Type", "NoEnd");
+
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("Pattern", pattern);
+ recurrence.put("Range", range);
+
+ final Map parameters = new LinkedHashMap<>();
+ parameters.put("Start", "2023-09-01T00:00:00+08:00");
+ parameters.put("End", "2023-09-01T02:00:00+08:00");
+ parameters.put("Recurrence", recurrence);
+
+ consumeValidationTestData(parameters, "Recurrence.Pattern.DaysOfWeek");
+ }
+
+ @Test
+ public void invalidRangeTypeTest() {
+ final HashMap pattern = new HashMap<>();
+ pattern.put("Type", "Daily");
+
+ final HashMap range = new HashMap<>();
+ range.put("Type", "");
+
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("Pattern", pattern);
+ recurrence.put("Range", range);
+
+ final Map parameters = new LinkedHashMap<>();
+ parameters.put("Start", "2023-09-01T00:00:00+08:00");
+ parameters.put("End", "2023-09-01T02:00:00+08:00");
+ parameters.put("Recurrence", recurrence);
+
+ consumeValidationTestData(parameters, "Recurrence.Range.Type");
+ }
+
+ @Test
+ public void invalidRangeNumberOfOccurrencesTest() {
+ final HashMap pattern = new HashMap<>();
+ pattern.put("Type", "Daily");
+
+ final HashMap range = new HashMap<>();
+ range.put("Type", "Numbered");
+ range.put("NumberOfOccurrences", 0);
+
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("Pattern", pattern);
+ recurrence.put("Range", range);
+
+ final Map parameters = new LinkedHashMap<>();
+ parameters.put("Start", "2023-09-01T00:00:00+08:00");
+ parameters.put("End", "2023-09-01T02:00:00+08:00");
+ parameters.put("Recurrence", recurrence);
+
+ consumeValidationTestData(parameters, recurrenceRangeNumberOfOccurrences);
+ }
+
+ @Test
+ public void invalidTimeWindowTest() {
+ final TimeWindowFilterSettings settings = new TimeWindowFilterSettings();
+ final Recurrence recurrence = new Recurrence();
+ final RecurrencePattern pattern = new RecurrencePattern();
+ final RecurrenceRange range = new RecurrenceRange();
+ recurrence.setPattern(pattern);
+ recurrence.setRange(range);
+ settings.setRecurrence(recurrence);
+
+ // time window is zero
+ final HashMap pattern1 = new HashMap<>();
+ pattern1.put("Type", "Daily");
+ final HashMap range1 = new HashMap<>();
+ range1.put("Type", "NoEnd");
+ final HashMap recurrence1 = new HashMap<>();
+ recurrence1.put("Pattern", pattern1);
+ recurrence1.put("Range", range1);
+ final Map parameters1 = new LinkedHashMap<>();
+ parameters1.put("Start", "2023-09-25T00:00:00+08:00");
+ parameters1.put("End", "2023-09-25T00:00:00+08:00");
+ parameters1.put("Recurrence", recurrence1);
+ consumeValidationTestData(parameters1, FilterParameters.TIME_WINDOW_FILTER_SETTING_END);
+
+ // time window is bigger than interval when pattern is daily
+ final HashMap pattern2 = new HashMap<>();
+ pattern2.put("Type", "Daily");
+ pattern2.put("Interval", 2);
+ final HashMap range2 = new HashMap<>();
+ range2.put("Type", "NoEnd");
+ final HashMap recurrence2 = new HashMap<>();
+ recurrence2.put("Pattern", pattern2);
+ recurrence2.put("Range", range2);
+ final Map parameters2 = new LinkedHashMap<>();
+ parameters2.put("Start", "2023-09-25T00:00:00+08:00");
+ parameters2.put("End", "2023-09-27T00:00:01+08:00");
+ parameters2.put("Recurrence", recurrence2);
+ consumeValidationTestData(parameters2, FilterParameters.TIME_WINDOW_FILTER_SETTING_END);
+
+ // time window is bigger than interval when pattern is weekly
+ final HashMap pattern3 = new HashMap<>();
+ pattern3.put("Type", "WeekLy");
+ pattern3.put("Interval", 1);
+ pattern3.put("DaysOfWeek", List.of("Monday"));
+ final HashMap range3 = new HashMap<>();
+ range3.put("Type", "NoEnd");
+ final HashMap recurrence3 = new HashMap<>();
+ recurrence3.put("Pattern", pattern3);
+ recurrence3.put("Range", range3);
+ final Map parameters3 = new LinkedHashMap<>();
+ parameters3.put("Start", "2023-09-25T00:00:00+08:00");
+ parameters3.put("End", "2023-10-02T00:00:01+08:00");
+ parameters3.put("Recurrence", recurrence3);
+ consumeValidationTestData(parameters3, FilterParameters.TIME_WINDOW_FILTER_SETTING_END);
+
+ // time window is bigger than interval when pattern is weekly
+ final HashMap pattern4 = new HashMap<>();
+ pattern4.put("Type", "WeekLy");
+ pattern4.put("Interval", 1);
+ pattern4.put("DaysOfWeek", List.of("Monday", "Thursday", "Sunday"));
+ final HashMap range4 = new HashMap<>();
+ range4.put("Type", "NoEnd");
+ final HashMap recurrence4 = new HashMap<>();
+ recurrence4.put("Pattern", pattern4);
+ recurrence4.put("Range", range4);
+ final Map parameters4 = new LinkedHashMap<>();
+ parameters4.put("Start", "2023-09-25T00:00:00+08:00");
+ parameters4.put("End", "2023-09-27T00:00:01+08:00");
+ parameters4.put("Recurrence", recurrence4);
+ consumeValidationTestData(parameters4, FilterParameters.TIME_WINDOW_FILTER_SETTING_END);
+
+ // endDate is before first start time
+ final HashMap pattern5 = new HashMap<>();
+ pattern5.put("Type", "Daily");
+ final HashMap range5 = new HashMap<>();
+ range5.put("Type", "EndDate");
+ range5.put("EndDate", "2023-08-31T00:00:00+08:00");
+ final HashMap recurrence5 = new HashMap<>();
+ recurrence5.put("Pattern", pattern5);
+ recurrence5.put("Range", range5);
+ final Map parameters5 = new LinkedHashMap<>();
+ parameters5.put("Start", "2023-09-01T00:00:00+08:00");
+ parameters5.put("End", "2023-09-01T00:00:01+08:00");
+ parameters5.put("Recurrence", recurrence5);
+ consumeValidationTestData(parameters5, recurrenceRangeEndDate);
+ }
+
+ @Test
+ public void weeklyPatternRequiredParameterTest() {
+ final HashMap pattern = new HashMap<>();
+ pattern.put("Type", "Weekly");
+
+ final HashMap range = new HashMap<>();
+ range.put("Type", "NoEnd");
+
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("Pattern", pattern);
+ recurrence.put("Range", range);
+
+ final Map parameters = new LinkedHashMap<>();
+ parameters.put("Start", "2023-09-01T00:00:00+08:00");
+ parameters.put("End", "2023-09-01T02:00:00+08:00");
+ parameters.put("Recurrence", recurrence);
+
+ // daysOfWeek parameter is required
+ consumeValidationTestData(parameters, recurrencePatternDaysOfWeek);
+ }
+
+ @Test
+ public void startParameterNotMatchTest() {
+ final HashMap pattern = new HashMap<>();
+ pattern.put("Type", "Weekly");
+ pattern.put("DaysOfWeek", List.of("Monday", "Tuesday", "Wednesday", "Thursday", "Saturday", "Sunday"));
+
+ final HashMap range = new HashMap<>();
+ range.put("Type", "NoEnd");
+
+ final HashMap recurrence = new HashMap<>();
+ recurrence.put("Pattern", pattern);
+ recurrence.put("Range", range);
+
+ final Map parameters = new LinkedHashMap<>();
+ parameters.put("Start", "2023-09-01T00:00:00+08:00");
+ parameters.put("End", "2023-09-01T02:00:00+08:00");
+ parameters.put("Recurrence", recurrence);
+
+ consumeValidationTestData(parameters, String.format(RecurrenceConstants.NOT_MATCHED, FilterParameters.TIME_WINDOW_FILTER_SETTING_START));
+ }
+
+
+ private void consumeValidationTestData(Map parameters, String errorMessage) {
+ final TimeWindowFilter filter = new TimeWindowFilter();
+ final FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext();
+
+ final IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ context.setParameters(parameters);
+ filter.evaluate(context);
+ });
+ assertTrue(exception.getMessage().contains(errorMessage));
+ }
+}