From f9e510a614d1d08a50436927d794d2cdb188fa51 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 10 Dec 2024 04:47:14 +0800 Subject: [PATCH 1/8] WIP: validation finished --- src/featureManager.ts | 6 +- src/filter/TargetingFilter.ts | 2 +- src/filter/TimeWindowFilter.ts | 65 +++++++++-- src/filter/recurrence/evaluator.ts | 11 ++ src/filter/recurrence/model.ts | 47 ++++++++ src/filter/recurrence/utils.ts | 26 +++++ src/filter/recurrence/validator.ts | 172 +++++++++++++++++++++++++++++ src/filter/utils.ts | 10 ++ 8 files changed, 326 insertions(+), 13 deletions(-) create mode 100644 src/filter/recurrence/evaluator.ts create mode 100644 src/filter/recurrence/model.ts create mode 100644 src/filter/recurrence/utils.ts create mode 100644 src/filter/recurrence/validator.ts create mode 100644 src/filter/utils.ts diff --git a/src/featureManager.ts b/src/featureManager.ts index 6c02faa..a7a9e88 100644 --- a/src/featureManager.ts +++ b/src/featureManager.ts @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { TimeWindowFilter } from "./filter/TimeWindowFilter.js"; -import { IFeatureFilter } from "./filter/FeatureFilter.js"; +import { TimeWindowFilter } from "./filter/timeWindowFilter.js"; +import { IFeatureFilter } from "./filter/featureFilter.js"; import { RequirementType } from "./schema/model.js"; import { IFeatureFlagProvider } from "./featureProvider.js"; -import { TargetingFilter } from "./filter/TargetingFilter.js"; +import { TargetingFilter } from "./filter/targetingFilter.js"; export class FeatureManager { #provider: IFeatureFlagProvider; diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index 406d846..eb401fb 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { IFeatureFilter } from "./FeatureFilter.js"; +import { IFeatureFilter } from "./featureFilter.js"; type TargetingFilterParameters = { Audience: { diff --git a/src/filter/TimeWindowFilter.ts b/src/filter/TimeWindowFilter.ts index e1442c8..b70839d 100644 --- a/src/filter/TimeWindowFilter.ts +++ b/src/filter/TimeWindowFilter.ts @@ -1,18 +1,36 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { IFeatureFilter } from "./FeatureFilter.js"; +import { IFeatureFilter } from "./featureFilter.js"; +import { Recurrence } from "./recurrence/model.js"; +import { parseRecurrenceParameter } from "./recurrence/validator.js"; +import { matchRecurrence } from "./recurrence/evaluator.js"; +import { UNRECOGNIZABLE_VALUE_ERROR_MESSAGE, buildInvalidParameterErrorMessage } from "./utils.js"; + +type TimeWindowFilterEvaluationContext = { + featureName: string; + parameters: TimeWindowParameters; +}; -// [Start, End) type TimeWindowParameters = { Start?: string; End?: string; -} + Recurrence?: RecurrenceParameter; +}; -type TimeWindowFilterEvaluationContext = { - featureName: string; - parameters: TimeWindowParameters; -} +export type RecurrenceParameter = { + Pattern: { + Type: string; + Interval?: number; + DaysOfWeek?: string[]; + FirstDayOfWeek?: string; + }, + Range: { + Type: string; + EndDate?: string; + NumberOfOccurrences?: number; + } +}; export class TimeWindowFilter implements IFeatureFilter { name: string = "Microsoft.TimeWindow"; @@ -22,12 +40,41 @@ export class TimeWindowFilter implements IFeatureFilter { const startTime = parameters.Start !== undefined ? new Date(parameters.Start) : undefined; const endTime = parameters.End !== undefined ? new Date(parameters.End) : undefined; + const baseErrorMessage = `The ${this.name} feature filter is not valid for feature ${featureName}. `; + if (startTime === undefined && endTime === undefined) { // If neither start nor end time is specified, then the filter is not applicable. - console.warn(`The ${this.name} feature filter is not valid for feature ${featureName}. It must specify either 'Start', 'End', or both.`); + console.warn(baseErrorMessage + "It must specify either 'Start', 'End', or both."); return false; } + + if (startTime !== undefined && isNaN(startTime.getTime())) { + console.warn(baseErrorMessage + buildInvalidParameterErrorMessage("Start", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + return false; + } + + if (endTime !== undefined && isNaN(endTime.getTime())) { + console.warn(baseErrorMessage + buildInvalidParameterErrorMessage("End", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + return false; + } + const now = new Date(); - return (startTime === undefined || startTime <= now) && (endTime === undefined || now < endTime); + + if ((startTime === undefined || startTime <= now) && (endTime === undefined || now < endTime)) { + return true; + } + + if (parameters.Recurrence !== undefined) { + let recurrence: Recurrence; + try { + recurrence = parseRecurrenceParameter(startTime, endTime, parameters.Recurrence); + } catch (error) { + console.warn(baseErrorMessage + error.message); + return false; + } + return matchRecurrence(now, recurrence); + } + + return false; } } diff --git a/src/filter/recurrence/evaluator.ts b/src/filter/recurrence/evaluator.ts new file mode 100644 index 0000000..b94ea72 --- /dev/null +++ b/src/filter/recurrence/evaluator.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Recurrence } from "./model.js"; + +export function matchRecurrence(time: Date, recurrence: Recurrence): boolean { + if (time < recurrence.StartTime) { + return false; + } + return false; +} diff --git a/src/filter/recurrence/model.ts b/src/filter/recurrence/model.ts new file mode 100644 index 0000000..7424c7a --- /dev/null +++ b/src/filter/recurrence/model.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export const DAYS_PER_WEEK = 7; +export const ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000; + +export enum DayOfWeek { + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6 +} + +export enum RecurrencePatternType { + Daily, + Weekly +} + +export enum RecurrenceRangeType { + NoEnd, + EndDate, + Numbered +} + +export type RecurrencePattern = { + Type: RecurrencePatternType; + Interval: number; + DaysOfWeek?: DayOfWeek[]; + FirstDayOfWeek?: DayOfWeek; +}; + +export type RecurrenceRange = { + Type: RecurrenceRangeType; + EndDate?: Date; + NumberOfOccurrences?: number; +}; + +export type Recurrence = { + StartTime: Date; + EndTime: Date; + Pattern: RecurrencePattern; + Range: RecurrenceRange; + TimeZoneOffset: number; +}; diff --git a/src/filter/recurrence/utils.ts b/src/filter/recurrence/utils.ts new file mode 100644 index 0000000..1aee819 --- /dev/null +++ b/src/filter/recurrence/utils.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { DayOfWeek, DAYS_PER_WEEK } from "./model.js"; + +/** + * Calculates the offset in days between two given days of the week. + * @param day1 A day of week + * @param day2 A day of week + * @returns The number of days to be added to day2 to reach day1 + */ +export function calculateWeeklyDayOffset(day1: DayOfWeek, day2: DayOfWeek): number { + return (day1 - day2 + DAYS_PER_WEEK) % DAYS_PER_WEEK; +} + +/** + * Sorts a collection of days of week based on their offsets from a specified first day of week. + * @param daysOfWeek A collection of days of week + * @param firstDayOfWeek The first day of week which will be the first element in the sorted result + * @returns The sorted days of week + */ +export function sortDaysOfWeek(daysOfWeek: DayOfWeek[], firstDayOfWeek: DayOfWeek): DayOfWeek[] { + const sortedDaysOfWeek = daysOfWeek.slice(); + sortedDaysOfWeek.sort((x, y) => calculateWeeklyDayOffset(x, firstDayOfWeek) - calculateWeeklyDayOffset(y, firstDayOfWeek)); + return sortedDaysOfWeek; +} diff --git a/src/filter/recurrence/validator.ts b/src/filter/recurrence/validator.ts new file mode 100644 index 0000000..244eb97 --- /dev/null +++ b/src/filter/recurrence/validator.ts @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { RecurrenceParameter } from "../timeWindowFilter.js"; +import { VALUE_OUT_OF_RANGE_ERROR_MESSAGE, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE, buildInvalidParameterErrorMessage } from "../utils.js"; +import { DayOfWeek, Recurrence, RecurrencePattern, RecurrenceRange, RecurrencePatternType, RecurrenceRangeType, DAYS_PER_WEEK, ONE_DAY_IN_MILLISECONDS } from "./model.js"; +import { calculateWeeklyDayOffset, sortDaysOfWeek } from "./utils.js"; + +const START_NOT_MATCHED_ERROR_MESSAGE = "Start date is not a valid first occurrence."; +const TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE = "Time window duration cannot be longer than how frequently it occurs or be longer than 10 years."; + +/** + * Parses @see RecurrenceParameter into a @see Recurrence object. If the parameter is invalid, an error will be thrown. + * @param startTime The start time of the base time window + * @param day2 The end time of the base time window + * @param recurrenceParameter The @see RecurrenceParameter to parse + * @param TimeZoneOffset The time zone offset in milliseconds, by default 0 + * @returns A @see Recurrence object + */ +export function parseRecurrenceParameter(startTime: Date | undefined, endTime: Date | undefined, recurrenceParameter: RecurrenceParameter, TimeZoneOffset: number = 0): Recurrence { + if (startTime === undefined) { + throw new Error(buildInvalidParameterErrorMessage("Start", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + } + if (endTime === undefined) { + throw new Error(buildInvalidParameterErrorMessage("End", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + } + if (startTime >= endTime) { + throw new Error(buildInvalidParameterErrorMessage("End", VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); + } + const timeWindowDuration = endTime.getTime() - startTime.getTime(); + if (timeWindowDuration > 10 * 365 * ONE_DAY_IN_MILLISECONDS) { // time window duration cannot be longer than 10 years + throw new Error(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); + } + + return { + StartTime: startTime, + EndTime: endTime, + Pattern: parseRecurrencePattern(startTime, endTime, recurrenceParameter, TimeZoneOffset), + Range: parseRecurrenceRange(startTime, recurrenceParameter), + TimeZoneOffset: TimeZoneOffset + }; +} + +function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParameter: RecurrenceParameter, TimeZoneOffset: number): RecurrencePattern { + const rawPattern = recurrenceParameter.Pattern; + if (rawPattern === undefined) { + throw new Error(buildInvalidParameterErrorMessage("Pattern", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + } + if (rawPattern.Type === undefined) { + throw new Error(buildInvalidParameterErrorMessage("Pattern.Type", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + } + const patternType = RecurrencePatternType[rawPattern.Type]; + if (patternType === undefined) { + throw new Error(buildInvalidParameterErrorMessage("Pattern.Type", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + } + let interval = rawPattern.Interval; + if (interval !== undefined) { + if (typeof interval !== "number") { + throw new Error(buildInvalidParameterErrorMessage("Pattern.Interval", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + } else if (interval <= 0 || !Number.isInteger(interval)) { + throw new Error(buildInvalidParameterErrorMessage("Pattern.Interval", VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); + } + } else { + interval = 1; + } + const parsedPattern: RecurrencePattern = { + Type: patternType, + Interval: interval + }; + const timeWindowDuration = endTime.getTime() - startTime.getTime(); + if (patternType === RecurrencePatternType.Daily) { + if (timeWindowDuration > interval * ONE_DAY_IN_MILLISECONDS) { + throw new Error(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); + } + } else if (patternType === RecurrencePatternType.Weekly) { + let firstDayOfWeek: DayOfWeek; + if (rawPattern.FirstDayOfWeek !== undefined) { + firstDayOfWeek = DayOfWeek[rawPattern.FirstDayOfWeek]; + if (firstDayOfWeek === undefined) { + throw new Error(buildInvalidParameterErrorMessage("Pattern.FirstDayOfWeek", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + } + } + else { + firstDayOfWeek = DayOfWeek.Sunday; + } + parsedPattern.FirstDayOfWeek = firstDayOfWeek; + + if (rawPattern.DaysOfWeek === undefined || rawPattern.DaysOfWeek.length === 0) { + throw new Error(buildInvalidParameterErrorMessage("Pattern.DaysOfWeek", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + } + const daysOfWeek = [...new Set(rawPattern.DaysOfWeek.map(day => DayOfWeek[day]))]; // dedup array + if (daysOfWeek.some(day => day === undefined)) { + throw new Error(buildInvalidParameterErrorMessage("Pattern.DaysOfWeek", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + } + if (timeWindowDuration > interval * DAYS_PER_WEEK * ONE_DAY_IN_MILLISECONDS || + !IsDurationCompliantWithDaysOfWeek(timeWindowDuration, interval, daysOfWeek, firstDayOfWeek)) { + throw new Error(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); + } + parsedPattern.DaysOfWeek = daysOfWeek; + + // check whether "Start" is a valid first occurrence + const alignedStartTime = new Date(startTime); + alignedStartTime.setUTCMilliseconds(alignedStartTime.getUTCMilliseconds() + TimeZoneOffset); + if (!daysOfWeek.find(day => day === alignedStartTime.getUTCDay())) { + throw new Error(buildInvalidParameterErrorMessage("Start", START_NOT_MATCHED_ERROR_MESSAGE)); + } + } + return parsedPattern; +} + +function parseRecurrenceRange(startTime: Date, recurrenceParameter: RecurrenceParameter): RecurrenceRange { + const rawRange = recurrenceParameter.Range; + if (rawRange === undefined) { + throw new Error(buildInvalidParameterErrorMessage("Range", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + } + if (rawRange.Type === undefined) { + throw new Error(buildInvalidParameterErrorMessage("Range.Type", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + } + const rangeType = RecurrenceRangeType[rawRange.Type]; + if (rangeType === undefined) { + throw new Error(buildInvalidParameterErrorMessage("Range.Type", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + } + const parsedRange: RecurrenceRange = { Type: rangeType }; + if (rangeType === RecurrenceRangeType.EndDate) { + let endDate: Date; + if (rawRange.EndDate !== undefined) { + endDate = new Date(rawRange.EndDate); + if (isNaN(endDate.getTime())) { + throw new Error(buildInvalidParameterErrorMessage("Range.EndDate", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + } + if (endDate < startTime) { + throw new Error(buildInvalidParameterErrorMessage("Range.EndDate", VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); + } + } else { + endDate = new Date(8.64e15); // the maximum date in ECMAScript: https://262.ecma-international.org/5.1/#sec-15.9.1.1 + } + parsedRange.EndDate = endDate; + } else if (rangeType === RecurrenceRangeType.Numbered) { + let numberOfOccurrences = rawRange.NumberOfOccurrences; + if (numberOfOccurrences !== undefined) { + if (typeof numberOfOccurrences !== "number") { + throw new Error(buildInvalidParameterErrorMessage("Range.NumberOfOccurrences", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + } else if (numberOfOccurrences <= 0 || !Number.isInteger(numberOfOccurrences)) { + throw new Error(buildInvalidParameterErrorMessage("Range.NumberOfOccurrences", VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); + } + } else { + numberOfOccurrences = Number.MAX_SAFE_INTEGER; + } + parsedRange.NumberOfOccurrences = numberOfOccurrences; + } + return parsedRange; +} + +function IsDurationCompliantWithDaysOfWeek(duration: number, interval: number, daysOfWeek: DayOfWeek[], firstDayOfWeek: DayOfWeek): boolean { + if (daysOfWeek.length === 1) { + return true; + } + const sortedDaysOfWeek = sortDaysOfWeek(daysOfWeek, firstDayOfWeek); + let prev = sortedDaysOfWeek[0]; // the closest occurrence day to the first day of week + let minGap = DAYS_PER_WEEK * ONE_DAY_IN_MILLISECONDS; + for (let i = 1; i < sortedDaysOfWeek.length; i++) { // skip the first day + const gap = calculateWeeklyDayOffset(sortedDaysOfWeek[i], prev) * ONE_DAY_IN_MILLISECONDS; + minGap = gap < minGap ? gap : minGap; + prev = sortedDaysOfWeek[i]; + } + // It may across weeks. Check the next week if the interval is one week. + if (interval == 1) { + const gap = calculateWeeklyDayOffset(sortedDaysOfWeek[0], prev) * ONE_DAY_IN_MILLISECONDS; + minGap = gap < minGap ? gap : minGap; + } + return minGap >= duration; +} diff --git a/src/filter/utils.ts b/src/filter/utils.ts new file mode 100644 index 0000000..f0c66c4 --- /dev/null +++ b/src/filter/utils.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export const VALUE_OUT_OF_RANGE_ERROR_MESSAGE = "The value is out of the accepted range."; +export const UNRECOGNIZABLE_VALUE_ERROR_MESSAGE = "The value is unrecognizable."; +export const REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE = "Value cannot be undefined or empty."; + +export function buildInvalidParameterErrorMessage(parameterName: string, additionalInfo?: string): string { + return `The ${parameterName} parameter is not valid.` + (additionalInfo ?? ""); +} From 92f7456cef3ce897773a8ed05911444c5c764633 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 10 Dec 2024 04:54:59 +0800 Subject: [PATCH 2/8] update filename --- .../{FeatureFilter.ts => featureFilter.ts} | 24 +- ...{TargetingFilter.ts => targetingFilter.ts} | 344 +++++++++--------- ...imeWindowFilter.ts => timeWindowFilter.ts} | 160 ++++---- 3 files changed, 264 insertions(+), 264 deletions(-) rename src/filter/{FeatureFilter.ts => featureFilter.ts} (96%) rename src/filter/{TargetingFilter.ts => targetingFilter.ts} (97%) rename src/filter/{TimeWindowFilter.ts => timeWindowFilter.ts} (97%) diff --git a/src/filter/FeatureFilter.ts b/src/filter/featureFilter.ts similarity index 96% rename from src/filter/FeatureFilter.ts rename to src/filter/featureFilter.ts index 4c259b9..f7e572b 100644 --- a/src/filter/FeatureFilter.ts +++ b/src/filter/featureFilter.ts @@ -1,12 +1,12 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -export interface IFeatureFilter { - name: string; // e.g. Microsoft.TimeWindow - evaluate(context: IFeatureFilterEvaluationContext, appContext?: unknown): boolean | Promise; -} - -export interface IFeatureFilterEvaluationContext { - featureName: string; - parameters?: unknown; -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export interface IFeatureFilter { + name: string; // e.g. Microsoft.TimeWindow + evaluate(context: IFeatureFilterEvaluationContext, appContext?: unknown): boolean | Promise; +} + +export interface IFeatureFilterEvaluationContext { + featureName: string; + parameters?: unknown; +} diff --git a/src/filter/TargetingFilter.ts b/src/filter/targetingFilter.ts similarity index 97% rename from src/filter/TargetingFilter.ts rename to src/filter/targetingFilter.ts index eb401fb..fb766ee 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/targetingFilter.ts @@ -1,172 +1,172 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { IFeatureFilter } from "./featureFilter.js"; - -type TargetingFilterParameters = { - Audience: { - DefaultRolloutPercentage: number; - Users?: string[]; - Groups?: { - Name: string; - RolloutPercentage: number; - }[]; - Exclusion?: { - Users?: string[]; - Groups?: string[]; - }; - } -} - -type TargetingFilterEvaluationContext = { - featureName: string; - parameters: TargetingFilterParameters; -} - -type TargetingFilterAppContext = { - userId?: string; - groups?: string[]; -} - -export class TargetingFilter implements IFeatureFilter { - name: string = "Microsoft.Targeting"; - - async evaluate(context: TargetingFilterEvaluationContext, appContext?: TargetingFilterAppContext): Promise { - const { featureName, parameters } = context; - TargetingFilter.#validateParameters(parameters); - - if (appContext === undefined) { - throw new Error("The app context is required for targeting filter."); - } - - if (parameters.Audience.Exclusion !== undefined) { - // check if the user is in the exclusion list - if (appContext?.userId !== undefined && - parameters.Audience.Exclusion.Users !== undefined && - parameters.Audience.Exclusion.Users.includes(appContext.userId)) { - return false; - } - // check if the user is in a group within exclusion list - if (appContext?.groups !== undefined && - parameters.Audience.Exclusion.Groups !== undefined) { - for (const excludedGroup of parameters.Audience.Exclusion.Groups) { - if (appContext.groups.includes(excludedGroup)) { - return false; - } - } - } - } - - // check if the user is being targeted directly - if (appContext?.userId !== undefined && - parameters.Audience.Users !== undefined && - parameters.Audience.Users.includes(appContext.userId)) { - return true; - } - - // check if the user is in a group that is being targeted - if (appContext?.groups !== undefined && - parameters.Audience.Groups !== undefined) { - for (const group of parameters.Audience.Groups) { - if (appContext.groups.includes(group.Name)) { - const audienceContextId = constructAudienceContextId(featureName, appContext.userId, group.Name); - const rolloutPercentage = group.RolloutPercentage; - if (await TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) { - return true; - } - } - } - } - - // check if the user is being targeted by a default rollout percentage - const defaultContextId = constructAudienceContextId(featureName, appContext?.userId); - return TargetingFilter.#isTargeted(defaultContextId, parameters.Audience.DefaultRolloutPercentage); - } - - static async #isTargeted(audienceContextId: string, rolloutPercentage: number): Promise { - if (rolloutPercentage === 100) { - return true; - } - // Cryptographic hashing algorithms ensure adequate entropy across hash values. - const contextMarker = await stringToUint32(audienceContextId); - const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100; - return contextPercentage < rolloutPercentage; - } - - static #validateParameters(parameters: TargetingFilterParameters): void { - if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) { - throw new Error("Audience.DefaultRolloutPercentage must be a number between 0 and 100."); - } - // validate RolloutPercentage for each group - if (parameters.Audience.Groups !== undefined) { - for (const group of parameters.Audience.Groups) { - if (group.RolloutPercentage < 0 || group.RolloutPercentage > 100) { - throw new Error(`RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`); - } - } - } - } -} - -/** - * Constructs the context id for the audience. - * The context id is used to determine if the user is part of the audience for a feature. - * If groupName is provided, the context id is constructed as follows: - * userId + "\n" + featureName + "\n" + groupName - * Otherwise, the context id is constructed as follows: - * userId + "\n" + featureName - * - * @param featureName name of the feature - * @param userId userId from app context - * @param groupName group name from app context - * @returns a string that represents the context id for the audience - */ -function constructAudienceContextId(featureName: string, userId: string | undefined, groupName?: string) { - let contextId = `${userId ?? ""}\n${featureName}`; - if (groupName !== undefined) { - contextId += `\n${groupName}`; - } - return contextId; -} - -async function stringToUint32(str: string): Promise { - let crypto; - - // Check for browser environment - if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) { - crypto = window.crypto; - } - // Check for Node.js environment - else if (typeof global !== "undefined" && global.crypto) { - crypto = global.crypto; - } - // Fallback to native Node.js crypto module - else { - try { - if (typeof module !== "undefined" && module.exports) { - crypto = require("crypto"); - } - else { - crypto = await import("crypto"); - } - } catch (error) { - console.error("Failed to load the crypto module:", error.message); - throw error; - } - } - - // In the browser, use crypto.subtle.digest - if (crypto.subtle) { - const data = new TextEncoder().encode(str); - const hashBuffer = await crypto.subtle.digest("SHA-256", data); - const dataView = new DataView(hashBuffer); - const uint32 = dataView.getUint32(0, true); - return uint32; - } - // In Node.js, use the crypto module's hash function - else { - const hash = crypto.createHash("sha256").update(str).digest(); - const uint32 = hash.readUInt32LE(0); - return uint32; - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { IFeatureFilter } from "./featureFilter.js"; + +type TargetingFilterParameters = { + Audience: { + DefaultRolloutPercentage: number; + Users?: string[]; + Groups?: { + Name: string; + RolloutPercentage: number; + }[]; + Exclusion?: { + Users?: string[]; + Groups?: string[]; + }; + } +} + +type TargetingFilterEvaluationContext = { + featureName: string; + parameters: TargetingFilterParameters; +} + +type TargetingFilterAppContext = { + userId?: string; + groups?: string[]; +} + +export class TargetingFilter implements IFeatureFilter { + name: string = "Microsoft.Targeting"; + + async evaluate(context: TargetingFilterEvaluationContext, appContext?: TargetingFilterAppContext): Promise { + const { featureName, parameters } = context; + TargetingFilter.#validateParameters(parameters); + + if (appContext === undefined) { + throw new Error("The app context is required for targeting filter."); + } + + if (parameters.Audience.Exclusion !== undefined) { + // check if the user is in the exclusion list + if (appContext?.userId !== undefined && + parameters.Audience.Exclusion.Users !== undefined && + parameters.Audience.Exclusion.Users.includes(appContext.userId)) { + return false; + } + // check if the user is in a group within exclusion list + if (appContext?.groups !== undefined && + parameters.Audience.Exclusion.Groups !== undefined) { + for (const excludedGroup of parameters.Audience.Exclusion.Groups) { + if (appContext.groups.includes(excludedGroup)) { + return false; + } + } + } + } + + // check if the user is being targeted directly + if (appContext?.userId !== undefined && + parameters.Audience.Users !== undefined && + parameters.Audience.Users.includes(appContext.userId)) { + return true; + } + + // check if the user is in a group that is being targeted + if (appContext?.groups !== undefined && + parameters.Audience.Groups !== undefined) { + for (const group of parameters.Audience.Groups) { + if (appContext.groups.includes(group.Name)) { + const audienceContextId = constructAudienceContextId(featureName, appContext.userId, group.Name); + const rolloutPercentage = group.RolloutPercentage; + if (await TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) { + return true; + } + } + } + } + + // check if the user is being targeted by a default rollout percentage + const defaultContextId = constructAudienceContextId(featureName, appContext?.userId); + return TargetingFilter.#isTargeted(defaultContextId, parameters.Audience.DefaultRolloutPercentage); + } + + static async #isTargeted(audienceContextId: string, rolloutPercentage: number): Promise { + if (rolloutPercentage === 100) { + return true; + } + // Cryptographic hashing algorithms ensure adequate entropy across hash values. + const contextMarker = await stringToUint32(audienceContextId); + const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100; + return contextPercentage < rolloutPercentage; + } + + static #validateParameters(parameters: TargetingFilterParameters): void { + if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) { + throw new Error("Audience.DefaultRolloutPercentage must be a number between 0 and 100."); + } + // validate RolloutPercentage for each group + if (parameters.Audience.Groups !== undefined) { + for (const group of parameters.Audience.Groups) { + if (group.RolloutPercentage < 0 || group.RolloutPercentage > 100) { + throw new Error(`RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`); + } + } + } + } +} + +/** + * Constructs the context id for the audience. + * The context id is used to determine if the user is part of the audience for a feature. + * If groupName is provided, the context id is constructed as follows: + * userId + "\n" + featureName + "\n" + groupName + * Otherwise, the context id is constructed as follows: + * userId + "\n" + featureName + * + * @param featureName name of the feature + * @param userId userId from app context + * @param groupName group name from app context + * @returns a string that represents the context id for the audience + */ +function constructAudienceContextId(featureName: string, userId: string | undefined, groupName?: string) { + let contextId = `${userId ?? ""}\n${featureName}`; + if (groupName !== undefined) { + contextId += `\n${groupName}`; + } + return contextId; +} + +async function stringToUint32(str: string): Promise { + let crypto; + + // Check for browser environment + if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) { + crypto = window.crypto; + } + // Check for Node.js environment + else if (typeof global !== "undefined" && global.crypto) { + crypto = global.crypto; + } + // Fallback to native Node.js crypto module + else { + try { + if (typeof module !== "undefined" && module.exports) { + crypto = require("crypto"); + } + else { + crypto = await import("crypto"); + } + } catch (error) { + console.error("Failed to load the crypto module:", error.message); + throw error; + } + } + + // In the browser, use crypto.subtle.digest + if (crypto.subtle) { + const data = new TextEncoder().encode(str); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const dataView = new DataView(hashBuffer); + const uint32 = dataView.getUint32(0, true); + return uint32; + } + // In Node.js, use the crypto module's hash function + else { + const hash = crypto.createHash("sha256").update(str).digest(); + const uint32 = hash.readUInt32LE(0); + return uint32; + } +} diff --git a/src/filter/TimeWindowFilter.ts b/src/filter/timeWindowFilter.ts similarity index 97% rename from src/filter/TimeWindowFilter.ts rename to src/filter/timeWindowFilter.ts index b70839d..668a27a 100644 --- a/src/filter/TimeWindowFilter.ts +++ b/src/filter/timeWindowFilter.ts @@ -1,80 +1,80 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { IFeatureFilter } from "./featureFilter.js"; -import { Recurrence } from "./recurrence/model.js"; -import { parseRecurrenceParameter } from "./recurrence/validator.js"; -import { matchRecurrence } from "./recurrence/evaluator.js"; -import { UNRECOGNIZABLE_VALUE_ERROR_MESSAGE, buildInvalidParameterErrorMessage } from "./utils.js"; - -type TimeWindowFilterEvaluationContext = { - featureName: string; - parameters: TimeWindowParameters; -}; - -type TimeWindowParameters = { - Start?: string; - End?: string; - Recurrence?: RecurrenceParameter; -}; - -export type RecurrenceParameter = { - Pattern: { - Type: string; - Interval?: number; - DaysOfWeek?: string[]; - FirstDayOfWeek?: string; - }, - Range: { - Type: string; - EndDate?: string; - NumberOfOccurrences?: number; - } -}; - -export class TimeWindowFilter implements IFeatureFilter { - name: string = "Microsoft.TimeWindow"; - - evaluate(context: TimeWindowFilterEvaluationContext): boolean { - const {featureName, parameters} = context; - const startTime = parameters.Start !== undefined ? new Date(parameters.Start) : undefined; - const endTime = parameters.End !== undefined ? new Date(parameters.End) : undefined; - - const baseErrorMessage = `The ${this.name} feature filter is not valid for feature ${featureName}. `; - - if (startTime === undefined && endTime === undefined) { - // If neither start nor end time is specified, then the filter is not applicable. - console.warn(baseErrorMessage + "It must specify either 'Start', 'End', or both."); - return false; - } - - if (startTime !== undefined && isNaN(startTime.getTime())) { - console.warn(baseErrorMessage + buildInvalidParameterErrorMessage("Start", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); - return false; - } - - if (endTime !== undefined && isNaN(endTime.getTime())) { - console.warn(baseErrorMessage + buildInvalidParameterErrorMessage("End", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); - return false; - } - - const now = new Date(); - - if ((startTime === undefined || startTime <= now) && (endTime === undefined || now < endTime)) { - return true; - } - - if (parameters.Recurrence !== undefined) { - let recurrence: Recurrence; - try { - recurrence = parseRecurrenceParameter(startTime, endTime, parameters.Recurrence); - } catch (error) { - console.warn(baseErrorMessage + error.message); - return false; - } - return matchRecurrence(now, recurrence); - } - - return false; - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { IFeatureFilter } from "./featureFilter.js"; +import { Recurrence } from "./recurrence/model.js"; +import { parseRecurrenceParameter } from "./recurrence/validator.js"; +import { matchRecurrence } from "./recurrence/evaluator.js"; +import { UNRECOGNIZABLE_VALUE_ERROR_MESSAGE, buildInvalidParameterErrorMessage } from "./utils.js"; + +type TimeWindowFilterEvaluationContext = { + featureName: string; + parameters: TimeWindowParameters; +}; + +type TimeWindowParameters = { + Start?: string; + End?: string; + Recurrence?: RecurrenceParameter; +}; + +export type RecurrenceParameter = { + Pattern: { + Type: string; + Interval?: number; + DaysOfWeek?: string[]; + FirstDayOfWeek?: string; + }, + Range: { + Type: string; + EndDate?: string; + NumberOfOccurrences?: number; + } +}; + +export class TimeWindowFilter implements IFeatureFilter { + name: string = "Microsoft.TimeWindow"; + + evaluate(context: TimeWindowFilterEvaluationContext): boolean { + const {featureName, parameters} = context; + const startTime = parameters.Start !== undefined ? new Date(parameters.Start) : undefined; + const endTime = parameters.End !== undefined ? new Date(parameters.End) : undefined; + + const baseErrorMessage = `The ${this.name} feature filter is not valid for feature ${featureName}. `; + + if (startTime === undefined && endTime === undefined) { + // If neither start nor end time is specified, then the filter is not applicable. + console.warn(baseErrorMessage + "It must specify either 'Start', 'End', or both."); + return false; + } + + if (startTime !== undefined && isNaN(startTime.getTime())) { + console.warn(baseErrorMessage + buildInvalidParameterErrorMessage("Start", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + return false; + } + + if (endTime !== undefined && isNaN(endTime.getTime())) { + console.warn(baseErrorMessage + buildInvalidParameterErrorMessage("End", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + return false; + } + + const now = new Date(); + + if ((startTime === undefined || startTime <= now) && (endTime === undefined || now < endTime)) { + return true; + } + + if (parameters.Recurrence !== undefined) { + let recurrence: Recurrence; + try { + recurrence = parseRecurrenceParameter(startTime, endTime, parameters.Recurrence); + } catch (error) { + console.warn(baseErrorMessage + error.message); + return false; + } + return matchRecurrence(now, recurrence); + } + + return false; + } +} From b767a4c8b7f345040f342f328a75fb2f1e5eb949 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 10 Dec 2024 18:38:55 +0800 Subject: [PATCH 3/8] WIP: evaluation done --- src/filter/recurrence/evaluator.ts | 143 ++++++++++++++++++++++++++++- src/filter/recurrence/model.ts | 92 ++++++++++++++++--- src/filter/recurrence/utils.ts | 23 +++++ src/filter/recurrence/validator.ts | 41 ++++----- src/filter/timeWindowFilter.ts | 4 +- src/index.ts | 2 +- 6 files changed, 264 insertions(+), 41 deletions(-) diff --git a/src/filter/recurrence/evaluator.ts b/src/filter/recurrence/evaluator.ts index b94ea72..884672a 100644 --- a/src/filter/recurrence/evaluator.ts +++ b/src/filter/recurrence/evaluator.ts @@ -1,11 +1,146 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { Recurrence } from "./model.js"; +import { RecurrenceSpec, RecurrencePatternType, RecurrenceRangeType, DAYS_PER_WEEK, ONE_DAY_IN_MILLISECONDS } from "./model.js"; +import { calculateWeeklyDayOffset, sortDaysOfWeek, getDayOfWeek, addDays } from "./utils.js"; -export function matchRecurrence(time: Date, recurrence: Recurrence): boolean { - if (time < recurrence.StartTime) { - return false; +type RecurrenceState = { + previousOccurrence: Date; + numberOfOccurrences: number; +} + +/** + * Checks if a provided datetime is within any recurring time window specified by the recurrence information + * @param time A datetime + * @param recurrenceSpec The recurrence spcification + * @returns True if the given time is within any recurring time window; otherwise, false + */ +export function matchRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): boolean { + const recurrenceState = FindPreviousRecurrence(time, recurrenceSpec); + if (recurrenceState) { + return time.getTime() < recurrenceState.previousOccurrence.getTime() + recurrenceSpec.duration; } return false; } + +/** + * Finds the closest previous recurrence occurrence before the given time according to the recurrence information + * @param time A datetime + * @param recurrenceSpec The recurrence specification + * @returns The recurrence state if any previous occurrence is found; otherwise, undefined + */ +function FindPreviousRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): RecurrenceState | undefined { + if (time < recurrenceSpec.startTime) { + return undefined; + } + let result: RecurrenceState; + const pattern = recurrenceSpec.pattern; + if (pattern.type === RecurrencePatternType.Daily) { + result = FindPreviousDailyRecurrence(time, recurrenceSpec); + } else if (pattern.type === RecurrencePatternType.Weekly) { + result = FindPreviousWeeklyRecurrence(time, recurrenceSpec); + } else { + throw new Error("Unsupported recurrence pattern type."); + } + const { previousOccurrence, numberOfOccurrences } = result; + + const range = recurrenceSpec.range; + if (range.type === RecurrenceRangeType.EndDate) { + if (previousOccurrence > range.endDate!) { + return undefined; + } + } else if (range.type === RecurrenceRangeType.Numbered) { + if (numberOfOccurrences > range.numberOfOccurrences!) { + return undefined; + } + } + return result; +} + +function FindPreviousDailyRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): RecurrenceState { + const startTime = recurrenceSpec.startTime; + const timeGap = time.getTime() - startTime.getTime(); + const pattern = recurrenceSpec.pattern; + const numberOfIntervals = Math.floor(timeGap / (pattern.interval * ONE_DAY_IN_MILLISECONDS)); + return { + previousOccurrence: addDays(startTime, numberOfIntervals * pattern.interval), + numberOfOccurrences: numberOfIntervals + 1 + }; +} + +function FindPreviousWeeklyRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): RecurrenceState { + /* + * Algorithm: + * 1. first find day 0 (d0), it's the day representing the start day on the week of `Start`. + * 2. find start day of the most recent occurring week d0 + floor((time - d0) / (interval * 7)) * (interval * 7) + * 3. if that's over 7 days ago, then previous occurence is the day with the max offset of the last occurring week + * 4. if gotten this far, then the current week is the most recent occurring week: + i. if time > day with min offset, then previous occurence is the day with max offset less than current + ii. if time < day with min offset, then previous occurence is the day with the max offset of previous occurring week + */ + const startTime = recurrenceSpec.startTime; + const startDay = getDayOfWeek(startTime, recurrenceSpec.timezoneOffset); + const pattern = recurrenceSpec.pattern; + const sortedDaysOfWeek = sortDaysOfWeek(pattern.daysOfWeek!, pattern.firstDayOfWeek!); + + /* + * Example: + * startTime = 2024-12-11 (Tue) + * pattern.interval = 2 pattern.firstDayOfWeek = Sun pattern.daysOfWeek = [Wed, Sun] + * sortedDaysOfWeek = [Sun, Wed] + * firstDayofStartWeek = 2024-12-08 (Sun) + * + * time = 2024-12-23 (Mon) timeGap = 15 days + * the most recent occurring week: 2024-12-22 ~ 2024-12-28 + * number of intervals before the most recent occurring week = 15 / (2 * 7) = 1 (2024-12-08 ~ 2023-12-21) + * number of occurrences before the most recent occurring week = 1 * 2 - 1 = 1 (2024-12-11) + * firstDayOfLastOccurringWeek = 2024-12-22 + */ + const firstDayofStartWeek = addDays(startTime, -calculateWeeklyDayOffset(startDay, pattern.firstDayOfWeek!)); + const timeGap = time.getTime() - firstDayofStartWeek.getTime(); + // number of intervals before the most recent occurring week + const numberOfIntervals = Math.floor(timeGap / (pattern.interval * DAYS_PER_WEEK * ONE_DAY_IN_MILLISECONDS)); + // number of occurrences before the most recent occurring week, it is possible to be negative + let numberOfOccurrences = numberOfIntervals * sortedDaysOfWeek.length - sortedDaysOfWeek.indexOf(startDay); + const firstDayOfLatestOccurringWeek = addDays(firstDayofStartWeek, numberOfIntervals * pattern.interval * DAYS_PER_WEEK); + + // the current time is out of the last occurring week + if (time > addDays(firstDayOfLatestOccurringWeek, DAYS_PER_WEEK)) { + numberOfOccurrences += sortDaysOfWeek.length; + // day with max offset in the last occurring week + const previousOccurrence = addDays(firstDayOfLatestOccurringWeek, calculateWeeklyDayOffset(sortedDaysOfWeek.at(-1)!, pattern.firstDayOfWeek!)); + return { + previousOccurrence: previousOccurrence, + numberOfOccurrences: numberOfOccurrences + }; + } + + let dayWithMinOffset = addDays(firstDayOfLatestOccurringWeek, calculateWeeklyDayOffset(sortedDaysOfWeek[0], pattern.firstDayOfWeek!)); + if (dayWithMinOffset < startTime) { + numberOfOccurrences = 0; + dayWithMinOffset = startTime; + } + let previousOccurrence; + if (time >= dayWithMinOffset) { + // the previous occurence is the day with max offset less than current + previousOccurrence = dayWithMinOffset; + numberOfOccurrences += 1; + const dayWithMinOffsetIndex = sortedDaysOfWeek.indexOf(getDayOfWeek(dayWithMinOffset, recurrenceSpec.timezoneOffset)); + for (let i = dayWithMinOffsetIndex + 1; i < sortedDaysOfWeek.length; i++) { + const day = addDays(firstDayOfLatestOccurringWeek, calculateWeeklyDayOffset(sortedDaysOfWeek[i], pattern.firstDayOfWeek!)); + if (time < day) { + break; + } + previousOccurrence = day; + numberOfOccurrences += 1; + } + } else { + const firstDayOfPreviousOccurringWeek = addDays(firstDayOfLatestOccurringWeek, -pattern.interval * DAYS_PER_WEEK); + // the previous occurence is the day with the max offset of previous occurring week + previousOccurrence = addDays(firstDayOfPreviousOccurringWeek, calculateWeeklyDayOffset(sortedDaysOfWeek.at(-1)!, pattern.firstDayOfWeek!)); + } + return { + previousOccurrence: previousOccurrence, + numberOfOccurrences: numberOfOccurrences + }; +} diff --git a/src/filter/recurrence/model.ts b/src/filter/recurrence/model.ts index 7424c7a..88d9602 100644 --- a/src/filter/recurrence/model.ts +++ b/src/filter/recurrence/model.ts @@ -14,34 +14,100 @@ export enum DayOfWeek { Saturday = 6 } +/** + * The recurrence pattern describes the frequency by which the time window repeats + */ export enum RecurrencePatternType { + /** + * The pattern where the time window will repeat based on the number of days specified by interval between occurrences + */ Daily, + /** + * The pattern where the time window will repeat on the same day or days of the week, based on the number of weeks between each set of occurrences + */ Weekly } +/** + * The recurrence range specifies the date range over which the time window repeats + */ export enum RecurrenceRangeType { + /** + * The recurrence has no end and repeats on all the days that fit the corresponding pattern + */ NoEnd, + /** + * The recurrence repeats on all the days that fit the corresponding pattern until or on the specified end date + */ EndDate, + /** + * The recurrence repeats for the specified number of occurrences that match the pattern + */ Numbered } +/** + * The recurrence pattern describes the frequency by which the time window repeats + */ export type RecurrencePattern = { - Type: RecurrencePatternType; - Interval: number; - DaysOfWeek?: DayOfWeek[]; - FirstDayOfWeek?: DayOfWeek; + /** + * The type of the recurrence pattern + */ + type: RecurrencePatternType; + /** + * The number of units between occurrences, where units can be in days or weeks, depending on the pattern type + */ + interval: number; + /** + * The days of the week when the time window occurs, which is only applicable for 'Weekly' pattern + */ + daysOfWeek?: DayOfWeek[]; + /** + * The first day of the week, which is only applicable for 'Weekly' pattern + */ + firstDayOfWeek?: DayOfWeek; }; +/** + * The recurrence range describes a date range over which the time window repeats + */ export type RecurrenceRange = { - Type: RecurrenceRangeType; - EndDate?: Date; - NumberOfOccurrences?: number; + /** + * The type of the recurrence range + */ + type: RecurrenceRangeType; + /** + * The date to stop applying the recurrence pattern, which is only applicable for 'EndDate' range + */ + endDate?: Date; + /** + * The number of times to repeat the time window, which is only applicable for 'Numbered' range + */ + numberOfOccurrences?: number; }; -export type Recurrence = { - StartTime: Date; - EndTime: Date; - Pattern: RecurrencePattern; - Range: RecurrenceRange; - TimeZoneOffset: number; +/** + * Specification defines the recurring time window + */ +export type RecurrenceSpec = { + /** + * The start time of the first/base time window + */ + startTime: Date; + /** + * The duration of each time window in milliseconds + */ + duration: number; + /** + * The recurrence pattern + */ + pattern: RecurrencePattern; + /** + * The recurrence range + */ + range: RecurrenceRange; + /** + * The timezone offset in milliseconds, which helps to determine the day of week of a given date + */ + timezoneOffset: number; }; diff --git a/src/filter/recurrence/utils.ts b/src/filter/recurrence/utils.ts index 1aee819..2ddb1a5 100644 --- a/src/filter/recurrence/utils.ts +++ b/src/filter/recurrence/utils.ts @@ -24,3 +24,26 @@ export function sortDaysOfWeek(daysOfWeek: DayOfWeek[], firstDayOfWeek: DayOfWee sortedDaysOfWeek.sort((x, y) => calculateWeeklyDayOffset(x, firstDayOfWeek) - calculateWeeklyDayOffset(y, firstDayOfWeek)); return sortedDaysOfWeek; } + +/** + * Gets the day of week of a given date based on the timezone offset. + * @param date A UTC date + * @param timezoneOffset The timezone offset in milliseconds + * @returns The day of week (0 for Sunday, 1 for Monday, ..., 6 for Saturday) + */ +export function getDayOfWeek(date: Date, timezoneOffset: number): number { + const alignedDate = new Date(date.getTime() + timezoneOffset); + return alignedDate.getUTCDay(); +} + +/** + * Adds a specified number of days to a given date. + * @param date The date to add days to + * @param days The number of days to add + * @returns The new date + */ +export function addDays(date: Date, days: number): Date { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; +} diff --git a/src/filter/recurrence/validator.ts b/src/filter/recurrence/validator.ts index 244eb97..757e0f1 100644 --- a/src/filter/recurrence/validator.ts +++ b/src/filter/recurrence/validator.ts @@ -3,21 +3,21 @@ import { RecurrenceParameter } from "../timeWindowFilter.js"; import { VALUE_OUT_OF_RANGE_ERROR_MESSAGE, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE, buildInvalidParameterErrorMessage } from "../utils.js"; -import { DayOfWeek, Recurrence, RecurrencePattern, RecurrenceRange, RecurrencePatternType, RecurrenceRangeType, DAYS_PER_WEEK, ONE_DAY_IN_MILLISECONDS } from "./model.js"; -import { calculateWeeklyDayOffset, sortDaysOfWeek } from "./utils.js"; +import { DayOfWeek, RecurrenceSpec, RecurrencePattern, RecurrenceRange, RecurrencePatternType, RecurrenceRangeType, DAYS_PER_WEEK, ONE_DAY_IN_MILLISECONDS } from "./model.js"; +import { calculateWeeklyDayOffset, sortDaysOfWeek, getDayOfWeek } from "./utils.js"; const START_NOT_MATCHED_ERROR_MESSAGE = "Start date is not a valid first occurrence."; const TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE = "Time window duration cannot be longer than how frequently it occurs or be longer than 10 years."; /** - * Parses @see RecurrenceParameter into a @see Recurrence object. If the parameter is invalid, an error will be thrown. + * Parses @see RecurrenceParameter into a @see RecurrenceSpec object. If the parameter is invalid, an error will be thrown. * @param startTime The start time of the base time window * @param day2 The end time of the base time window * @param recurrenceParameter The @see RecurrenceParameter to parse * @param TimeZoneOffset The time zone offset in milliseconds, by default 0 - * @returns A @see Recurrence object + * @returns A @see RecurrenceSpec object */ -export function parseRecurrenceParameter(startTime: Date | undefined, endTime: Date | undefined, recurrenceParameter: RecurrenceParameter, TimeZoneOffset: number = 0): Recurrence { +export function parseRecurrenceParameter(startTime: Date | undefined, endTime: Date | undefined, recurrenceParameter: RecurrenceParameter, TimeZoneOffset: number = 0): RecurrenceSpec { if (startTime === undefined) { throw new Error(buildInvalidParameterErrorMessage("Start", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); } @@ -33,15 +33,15 @@ export function parseRecurrenceParameter(startTime: Date | undefined, endTime: D } return { - StartTime: startTime, - EndTime: endTime, - Pattern: parseRecurrencePattern(startTime, endTime, recurrenceParameter, TimeZoneOffset), - Range: parseRecurrenceRange(startTime, recurrenceParameter), - TimeZoneOffset: TimeZoneOffset + startTime: startTime, + duration: timeWindowDuration, + pattern: parseRecurrencePattern(startTime, endTime, recurrenceParameter, TimeZoneOffset), + range: parseRecurrenceRange(startTime, recurrenceParameter), + timezoneOffset: TimeZoneOffset }; } -function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParameter: RecurrenceParameter, TimeZoneOffset: number): RecurrencePattern { +function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParameter: RecurrenceParameter, timeZoneOffset: number): RecurrencePattern { const rawPattern = recurrenceParameter.Pattern; if (rawPattern === undefined) { throw new Error(buildInvalidParameterErrorMessage("Pattern", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); @@ -64,8 +64,8 @@ function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParame interval = 1; } const parsedPattern: RecurrencePattern = { - Type: patternType, - Interval: interval + type: patternType, + interval: interval }; const timeWindowDuration = endTime.getTime() - startTime.getTime(); if (patternType === RecurrencePatternType.Daily) { @@ -83,7 +83,7 @@ function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParame else { firstDayOfWeek = DayOfWeek.Sunday; } - parsedPattern.FirstDayOfWeek = firstDayOfWeek; + parsedPattern.firstDayOfWeek = firstDayOfWeek; if (rawPattern.DaysOfWeek === undefined || rawPattern.DaysOfWeek.length === 0) { throw new Error(buildInvalidParameterErrorMessage("Pattern.DaysOfWeek", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); @@ -96,12 +96,11 @@ function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParame !IsDurationCompliantWithDaysOfWeek(timeWindowDuration, interval, daysOfWeek, firstDayOfWeek)) { throw new Error(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); } - parsedPattern.DaysOfWeek = daysOfWeek; + parsedPattern.daysOfWeek = daysOfWeek; // check whether "Start" is a valid first occurrence - const alignedStartTime = new Date(startTime); - alignedStartTime.setUTCMilliseconds(alignedStartTime.getUTCMilliseconds() + TimeZoneOffset); - if (!daysOfWeek.find(day => day === alignedStartTime.getUTCDay())) { + const alignedStartDay = getDayOfWeek(startTime, timeZoneOffset); + if (!daysOfWeek.find(day => day === alignedStartDay)) { throw new Error(buildInvalidParameterErrorMessage("Start", START_NOT_MATCHED_ERROR_MESSAGE)); } } @@ -120,7 +119,7 @@ function parseRecurrenceRange(startTime: Date, recurrenceParameter: RecurrencePa if (rangeType === undefined) { throw new Error(buildInvalidParameterErrorMessage("Range.Type", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); } - const parsedRange: RecurrenceRange = { Type: rangeType }; + const parsedRange: RecurrenceRange = { type: rangeType }; if (rangeType === RecurrenceRangeType.EndDate) { let endDate: Date; if (rawRange.EndDate !== undefined) { @@ -134,7 +133,7 @@ function parseRecurrenceRange(startTime: Date, recurrenceParameter: RecurrencePa } else { endDate = new Date(8.64e15); // the maximum date in ECMAScript: https://262.ecma-international.org/5.1/#sec-15.9.1.1 } - parsedRange.EndDate = endDate; + parsedRange.endDate = endDate; } else if (rangeType === RecurrenceRangeType.Numbered) { let numberOfOccurrences = rawRange.NumberOfOccurrences; if (numberOfOccurrences !== undefined) { @@ -146,7 +145,7 @@ function parseRecurrenceRange(startTime: Date, recurrenceParameter: RecurrencePa } else { numberOfOccurrences = Number.MAX_SAFE_INTEGER; } - parsedRange.NumberOfOccurrences = numberOfOccurrences; + parsedRange.numberOfOccurrences = numberOfOccurrences; } return parsedRange; } diff --git a/src/filter/timeWindowFilter.ts b/src/filter/timeWindowFilter.ts index 668a27a..d7694b9 100644 --- a/src/filter/timeWindowFilter.ts +++ b/src/filter/timeWindowFilter.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { IFeatureFilter } from "./featureFilter.js"; -import { Recurrence } from "./recurrence/model.js"; +import { RecurrenceSpec } from "./recurrence/model.js"; import { parseRecurrenceParameter } from "./recurrence/validator.js"; import { matchRecurrence } from "./recurrence/evaluator.js"; import { UNRECOGNIZABLE_VALUE_ERROR_MESSAGE, buildInvalidParameterErrorMessage } from "./utils.js"; @@ -65,7 +65,7 @@ export class TimeWindowFilter implements IFeatureFilter { } if (parameters.Recurrence !== undefined) { - let recurrence: Recurrence; + let recurrence: RecurrenceSpec; try { recurrence = parseRecurrenceParameter(startTime, endTime, parameters.Recurrence); } catch (error) { diff --git a/src/index.ts b/src/index.ts index 5b3db29..3fdcaa1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,5 +3,5 @@ export { FeatureManager } from "./featureManager.js"; export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from "./featureProvider.js"; -export { IFeatureFilter } from "./filter/FeatureFilter.js"; +export { IFeatureFilter } from "./filter/featureFilter.js"; export { VERSION } from "./version.js"; From dae956920cd655cd0637519680b3be076c1cc643 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 11 Dec 2024 03:30:36 +0800 Subject: [PATCH 4/8] add testcases --- package-lock.json | 317 +++++++++------ package.json | 3 +- src/filter/recurrence/validator.ts | 47 ++- src/filter/utils.ts | 2 +- test/featureManager.test.ts | 2 +- test/noFilters.test.ts | 2 +- test/recurrence.test.ts | 607 +++++++++++++++++++++++++++++ test/targetingFilter.test.ts | 2 +- test/timeWindowFilter.test.ts | 254 ++++++++++++ 9 files changed, 1104 insertions(+), 132 deletions(-) create mode 100644 test/recurrence.test.ts create mode 100644 test/timeWindowFilter.test.ts diff --git a/package-lock.json b/package-lock.json index 73b3276..dea5964 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "rimraf": "^5.0.5", "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.0", + "sinon": "^18.0.0", "tslib": "^2.6.2", "typescript": "^5.3.3" } @@ -737,6 +738,55 @@ "win32" ] }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -1028,10 +1078,11 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1294,10 +1345,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1308,12 +1360,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1355,10 +1408,11 @@ "dev": true }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -2123,6 +2177,13 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true, + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2160,6 +2221,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2225,12 +2293,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -2259,32 +2328,32 @@ } }, "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -2292,49 +2361,45 @@ }, "engines": { "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { - "node": "*" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2342,21 +2407,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2373,22 +2423,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -2396,6 +2435,30 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2531,6 +2594,16 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -2648,6 +2721,7 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -2825,7 +2899,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/semver": { "version": "7.5.4", @@ -2843,10 +2918,11 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -2884,6 +2960,25 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sinon": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.1.tgz", + "integrity": "sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3096,10 +3191,11 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "7.0.0", @@ -3176,10 +3272,11 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } diff --git a/package.json b/package.json index f7e5e01..08f7311 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "dev": "rollup --config --watch", "lint": "eslint src/ test/ --ignore-pattern test/browser/testcases.js", "fix-lint": "eslint src/ test/ --fix --ignore-pattern test/browser/testcases.js", - "test": "mocha out/*.test.{js,cjs,mjs} --parallel", + "test": "mocha out/test/*.test.{js,cjs,mjs} --parallel", "test-browser": "npx playwright install chromium && npx playwright test" }, "repository": { @@ -44,6 +44,7 @@ "rimraf": "^5.0.5", "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.0", + "sinon": "^18.0.0", "tslib": "^2.6.2", "typescript": "^5.3.3" }, diff --git a/src/filter/recurrence/validator.ts b/src/filter/recurrence/validator.ts index 757e0f1..6f24f69 100644 --- a/src/filter/recurrence/validator.ts +++ b/src/filter/recurrence/validator.ts @@ -6,8 +6,18 @@ import { VALUE_OUT_OF_RANGE_ERROR_MESSAGE, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE, R import { DayOfWeek, RecurrenceSpec, RecurrencePattern, RecurrenceRange, RecurrencePatternType, RecurrenceRangeType, DAYS_PER_WEEK, ONE_DAY_IN_MILLISECONDS } from "./model.js"; import { calculateWeeklyDayOffset, sortDaysOfWeek, getDayOfWeek } from "./utils.js"; -const START_NOT_MATCHED_ERROR_MESSAGE = "Start date is not a valid first occurrence."; -const TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE = "Time window duration cannot be longer than how frequently it occurs or be longer than 10 years."; +export const START_NOT_MATCHED_ERROR_MESSAGE = "Start date is not a valid first occurrence."; +export const TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE = "Time window duration cannot be longer than how frequently it occurs or be longer than 10 years."; +export const PATTERN = "Recurrence.Pattern"; +export const PATTERN_TYPE = "Recurrence.Pattern.Type"; +export const INTERVAL = "Recurrence.Pattern.Interval"; +export const DAYS_OF_WEEK = "Recurrence.Pattern.DaysOfWeek"; +export const FIRST_DAY_OF_WEEK = "Recurrence.Pattern.FirstDayOfWeek"; +export const RANGE = "Recurrence.Range"; +export const RANGE_TYPE = "Recurrence.Range.Type"; +export const END_DATE = "Recurrence.Range.EndDate"; +export const NUMBER_OF_OCCURRENCES = "Recurrence.Range.NumberOfOccurrences"; + /** * Parses @see RecurrenceParameter into a @see RecurrenceSpec object. If the parameter is invalid, an error will be thrown. @@ -44,21 +54,21 @@ export function parseRecurrenceParameter(startTime: Date | undefined, endTime: D function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParameter: RecurrenceParameter, timeZoneOffset: number): RecurrencePattern { const rawPattern = recurrenceParameter.Pattern; if (rawPattern === undefined) { - throw new Error(buildInvalidParameterErrorMessage("Pattern", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(PATTERN, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); } if (rawPattern.Type === undefined) { - throw new Error(buildInvalidParameterErrorMessage("Pattern.Type", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(PATTERN_TYPE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); } const patternType = RecurrencePatternType[rawPattern.Type]; if (patternType === undefined) { - throw new Error(buildInvalidParameterErrorMessage("Pattern.Type", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(PATTERN_TYPE, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); } let interval = rawPattern.Interval; if (interval !== undefined) { if (typeof interval !== "number") { - throw new Error(buildInvalidParameterErrorMessage("Pattern.Interval", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(INTERVAL, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); } else if (interval <= 0 || !Number.isInteger(interval)) { - throw new Error(buildInvalidParameterErrorMessage("Pattern.Interval", VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(INTERVAL, VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); } } else { interval = 1; @@ -77,7 +87,7 @@ function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParame if (rawPattern.FirstDayOfWeek !== undefined) { firstDayOfWeek = DayOfWeek[rawPattern.FirstDayOfWeek]; if (firstDayOfWeek === undefined) { - throw new Error(buildInvalidParameterErrorMessage("Pattern.FirstDayOfWeek", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(FIRST_DAY_OF_WEEK, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); } } else { @@ -86,11 +96,14 @@ function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParame parsedPattern.firstDayOfWeek = firstDayOfWeek; if (rawPattern.DaysOfWeek === undefined || rawPattern.DaysOfWeek.length === 0) { - throw new Error(buildInvalidParameterErrorMessage("Pattern.DaysOfWeek", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + } + if (!Array.isArray(rawPattern.DaysOfWeek)) { + throw new Error(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); } const daysOfWeek = [...new Set(rawPattern.DaysOfWeek.map(day => DayOfWeek[day]))]; // dedup array if (daysOfWeek.some(day => day === undefined)) { - throw new Error(buildInvalidParameterErrorMessage("Pattern.DaysOfWeek", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); } if (timeWindowDuration > interval * DAYS_PER_WEEK * ONE_DAY_IN_MILLISECONDS || !IsDurationCompliantWithDaysOfWeek(timeWindowDuration, interval, daysOfWeek, firstDayOfWeek)) { @@ -110,14 +123,14 @@ function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParame function parseRecurrenceRange(startTime: Date, recurrenceParameter: RecurrenceParameter): RecurrenceRange { const rawRange = recurrenceParameter.Range; if (rawRange === undefined) { - throw new Error(buildInvalidParameterErrorMessage("Range", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(RANGE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); } if (rawRange.Type === undefined) { - throw new Error(buildInvalidParameterErrorMessage("Range.Type", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(RANGE_TYPE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); } const rangeType = RecurrenceRangeType[rawRange.Type]; if (rangeType === undefined) { - throw new Error(buildInvalidParameterErrorMessage("Range.Type", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(RANGE_TYPE, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); } const parsedRange: RecurrenceRange = { type: rangeType }; if (rangeType === RecurrenceRangeType.EndDate) { @@ -125,10 +138,10 @@ function parseRecurrenceRange(startTime: Date, recurrenceParameter: RecurrencePa if (rawRange.EndDate !== undefined) { endDate = new Date(rawRange.EndDate); if (isNaN(endDate.getTime())) { - throw new Error(buildInvalidParameterErrorMessage("Range.EndDate", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(END_DATE, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); } if (endDate < startTime) { - throw new Error(buildInvalidParameterErrorMessage("Range.EndDate", VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(END_DATE, VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); } } else { endDate = new Date(8.64e15); // the maximum date in ECMAScript: https://262.ecma-international.org/5.1/#sec-15.9.1.1 @@ -138,9 +151,9 @@ function parseRecurrenceRange(startTime: Date, recurrenceParameter: RecurrencePa let numberOfOccurrences = rawRange.NumberOfOccurrences; if (numberOfOccurrences !== undefined) { if (typeof numberOfOccurrences !== "number") { - throw new Error(buildInvalidParameterErrorMessage("Range.NumberOfOccurrences", UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(NUMBER_OF_OCCURRENCES, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); } else if (numberOfOccurrences <= 0 || !Number.isInteger(numberOfOccurrences)) { - throw new Error(buildInvalidParameterErrorMessage("Range.NumberOfOccurrences", VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); + throw new Error(buildInvalidParameterErrorMessage(NUMBER_OF_OCCURRENCES, VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); } } else { numberOfOccurrences = Number.MAX_SAFE_INTEGER; diff --git a/src/filter/utils.ts b/src/filter/utils.ts index f0c66c4..62dde70 100644 --- a/src/filter/utils.ts +++ b/src/filter/utils.ts @@ -6,5 +6,5 @@ export const UNRECOGNIZABLE_VALUE_ERROR_MESSAGE = "The value is unrecognizable." export const REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE = "Value cannot be undefined or empty."; export function buildInvalidParameterErrorMessage(parameterName: string, additionalInfo?: string): string { - return `The ${parameterName} parameter is not valid.` + (additionalInfo ?? ""); + return `The ${parameterName} parameter is not valid. ` + (additionalInfo ?? ""); } diff --git a/test/featureManager.test.ts b/test/featureManager.test.ts index 0bfa331..19a21e4 100644 --- a/test/featureManager.test.ts +++ b/test/featureManager.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "../"; +import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "../src/index.js"; describe("feature manager", () => { it("should load from json string", () => { diff --git a/test/noFilters.test.ts b/test/noFilters.test.ts index aaac209..3e6c57a 100644 --- a/test/noFilters.test.ts +++ b/test/noFilters.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "../"; +import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "../src/index.js"; const featureFlagsDataObject = { "feature_management": { diff --git a/test/recurrence.test.ts b/test/recurrence.test.ts new file mode 100644 index 0000000..1dd5a2e --- /dev/null +++ b/test/recurrence.test.ts @@ -0,0 +1,607 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +chai.use(chaiAsPromised); +const expect = chai.expect; + +import { + parseRecurrenceParameter, + PATTERN, + PATTERN_TYPE, + INTERVAL, + DAYS_OF_WEEK, + FIRST_DAY_OF_WEEK, + RANGE, + RANGE_TYPE, + END_DATE, + NUMBER_OF_OCCURRENCES, + START_NOT_MATCHED_ERROR_MESSAGE, + TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE } from "../src/filter/recurrence/validator.js"; +import { VALUE_OUT_OF_RANGE_ERROR_MESSAGE, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE, buildInvalidParameterErrorMessage } from "../src/filter/utils.js"; +import { DayOfWeek, RecurrencePatternType, RecurrenceRangeType } from "../src/filter/recurrence/model"; +import { matchRecurrence } from "../src/filter/recurrence/evaluator.js"; + +describe("recurrence validator", () => { + it("should check general required parameter", () => { + let recurrence1 = { + Pattern: { + Type: "Daily" + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(undefined, new Date(), recurrence1)).to.throw(buildInvalidParameterErrorMessage("Start", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + expect(() => parseRecurrenceParameter(new Date(), undefined, recurrence1)).to.throw(buildInvalidParameterErrorMessage("End", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + let recurrence2 = { + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence2 as any)).to.throw(buildInvalidParameterErrorMessage(PATTERN, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + let recurrence3 = { + Pattern: { + Type: "Daily" + } + }; + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence3 as any)).to.throw(buildInvalidParameterErrorMessage(RANGE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + }); + + it("should check pattern and range required parameter", () => { + let recurrence1 = { + Pattern: {}, + Range: { + Type: "NoEnd" + } + } + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence1 as any)).to.throw(buildInvalidParameterErrorMessage(PATTERN_TYPE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + let recurrence2 = { + Pattern: { + Type: "Weekly" + }, + Range: { + Type: "NoEnd" + } + } + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence2)).to.throw(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + let recurrence3 = { + Pattern: { + Type: "Weekly", + DaysOfWeek: [] + }, + Range: { + Type: "NoEnd" + } + } + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence3)).to.throw(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + let recurrence4 = { + Pattern: { + Type: "Daily" + }, + Range: {} + } + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence4 as any)).to.throw(buildInvalidParameterErrorMessage(RANGE_TYPE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); + + }); + + it("should check invalid value", () => { + let recurrence1 = { + Pattern: { + Type: "Daily", + Interval: "1" + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence1 as any)).to.throw(buildInvalidParameterErrorMessage(INTERVAL, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + let recurrence2 = { + Pattern: { + Type: "Daily", + Interval: 0 + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence2)).to.throw(buildInvalidParameterErrorMessage(INTERVAL, VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); + let recurrence3 = { + Pattern: { + Type: "Daily" + }, + Range: { + Type: "Numbered", + NumberOfOccurrences: "1" + } + }; + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence3 as any)).to.throw(buildInvalidParameterErrorMessage(NUMBER_OF_OCCURRENCES, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + let recurrence4 = { + Pattern: { + Type: "Daily" + }, + Range: { + Type: "Numbered", + NumberOfOccurrences: 0 + } + }; + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence4)).to.throw(buildInvalidParameterErrorMessage(NUMBER_OF_OCCURRENCES, VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); + let recurrence5 = { + Pattern: { + Type: "Weekly", + DaysOfWeek: ["Monday", "Tue"] + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence5)).to.throw(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + let recurrence6 = { + Pattern: { + Type: "Weekly", + DaysOfWeek: "Monday" + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence6 as any)).to.throw(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + let recurrence7 = { + Pattern: { + Type: "Weekly", + DaysOfWeek: ["Monday"], + FirstDayOfWeek: "Mon" + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence7)).to.throw(buildInvalidParameterErrorMessage(FIRST_DAY_OF_WEEK, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + let recurrence8 = { + Pattern: { + Type: "Daily" + }, + Range: { + Type: "EndDate", + EndDate: "AppConfig" + } + }; + expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence8)).to.throw(buildInvalidParameterErrorMessage(END_DATE, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); + let recurrence9 = { + Pattern: { + Type: "Daily" + }, + Range: { + Type: "EndDate", + EndDate: "2024-12-10T00:00:00+0000" + } + }; + expect(() => parseRecurrenceParameter(new Date("2024-12-11T00:00:00+0000"), new Date("2024-12-11T00:00:01+0000"), recurrence9)).to.throw(buildInvalidParameterErrorMessage(END_DATE, VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); + }); + + it("should check time window duration", () => { + let recurrence1 = { + Pattern: { + Type: "Daily" + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date(2), new Date(1), recurrence1)).to.throw(buildInvalidParameterErrorMessage("End", VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); + let recurrence2 = { + Pattern: { + Type: "Daily" + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date("2024-12-10T00:00:00+0000"), new Date("2024-12-12T00:00:00+0000"), recurrence2)).to.throw(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); + let recurrence3 = { + Pattern: { + Type: "Weekly", + DaysOfWeek: ["Monday"] + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date("2024-12-09T00:00:00+0000"), new Date("2024-12-16T00:00:01+0000"), recurrence3)).to.throw(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); + let recurrence4 = { + Pattern: { + Type: "Weekly", + DaysOfWeek: ["Monday", "Thursday", "Sunday"] + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date("2024-12-09T00:00:00+0000"), new Date("2024-12-11T00:00:01+0000"), recurrence4)).to.throw(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); + let recurrence5 = { + Pattern: { + Type: "Weekly", + DaysOfWeek: ["Monday", "Saturday"] + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date("2024-12-09T00:00:00+0000"), new Date("2024-12-11T00:00:01+0000"), recurrence5)).to.throw(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); + let recurrence6 = { + Pattern: { + Type: "Weekly", + DaysOfWeek: ["Tuesday", "Saturday"] + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date("2024-01-16T00:00:00+0000"), new Date("2024-01-19T00:00:00+0000"), recurrence6)).to.not.throw(); + let recurrence7 = { + Pattern: { + Type: "Weekly", + Interval: 2, + DaysOfWeek: ["Monday", "Sunday"], + FirstDayOfWeek: "Monday" + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date("2024-01-15T00:00:00+0000"), new Date("2024-01-19T00:00:00+0000"), recurrence7)).to.not.throw(); + let recurrence8 = { + Pattern: { + Type: "Weekly", + Interval: 1, + DaysOfWeek: ["Monday", "Saturday"], + FirstDayOfWeek: "Sunday" + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date("2024-01-15T00:00:00+0000"), new Date("2024-01-17T00:00:00+0000"), recurrence8)).to.not.throw(); + expect(() => parseRecurrenceParameter(new Date("2024-01-15T00:00:00+0000"), new Date("2024-01-17T00:00:01+0000"), recurrence8)).to.throw(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); + let recurrence9 = { + Pattern: { + Type: "Weekly", + Interval: 1, + DaysOfWeek: ["Monday", "Sunday"], + FirstDayOfWeek: "Monday" + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter(new Date("2024-01-15T00:00:00+0000"), new Date("2024-01-19T00:00:00+0000"), recurrence9)).to.throw(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); + }); + + it("should check whether start is a valid first occurrence", () => { + let recurrence1 = { + Pattern: { + Type: "Weekly", + DaysOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Saturday", "Sunday"] + }, + Range: { + Type: "NoEnd" + } + }; + expect(() => parseRecurrenceParameter( + new Date("2023-09-01T00:00:00+08:00"), + new Date("2023-09-01T00:00:01+08:00"), + recurrence1, + 8 * 24 * 60 * 60 * 1000) + ).to.throw(buildInvalidParameterErrorMessage("Start", START_NOT_MATCHED_ERROR_MESSAGE)); + }); +}); + +describe("recurrence evaluator", () => { + it("should match daily recurrence", () => { + let spec1 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: {type: RecurrencePatternType.Daily, interval: 1}, + range: {type: RecurrenceRangeType.NoEnd} + }; + expect(matchRecurrence(new Date("2023-09-02T00:00:00+08:00"), spec1 as any)).to.be.true; + let spec2 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: {type: RecurrencePatternType.Daily, interval: 2}, + range: {type: RecurrenceRangeType.NoEnd} + }; + expect(matchRecurrence(new Date("2023-09-02T00:00:00+08:00"), spec2 as any)).to.be.false; + expect(matchRecurrence(new Date("2023-09-03T00:00:00+08:00"), spec2 as any)).to.be.true; + let spec3 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 2 * 24 * 60 * 60 * 1000, + pattern: {type: RecurrencePatternType.Daily, interval: 4}, + range: {type: RecurrenceRangeType.NoEnd} + }; + expect(matchRecurrence(new Date("2023-09-05T00:00:00+08:00"), spec3 as any)).to.be.true; + expect(matchRecurrence(new Date("2023-09-06T00:00:00+08:00"), spec3 as any)).to.be.true; + expect(matchRecurrence(new Date("2023-09-09T00:00:00+08:00"), spec3 as any)).to.be.true; + let spec4 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: {type: RecurrencePatternType.Daily, interval: 1}, + range: {type: RecurrenceRangeType.Numbered, numberOfOccurrences: 2} + }; + expect(matchRecurrence(new Date("2023-09-02T00:00:00+08:00"), spec4 as any)).to.be.true; + expect(matchRecurrence(new Date("2023-09-03T00:00:00+08:00"), spec4 as any)).to.be.false; + let spec5 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: {type: RecurrencePatternType.Daily, interval: 1}, + range: {type: RecurrenceRangeType.EndDate, endDate: new Date("2023-09-03T00:00:00+08:00")} + }; + expect(matchRecurrence(new Date("2023-09-04T00:00:00+08:00"), spec5 as any)).to.be.false; + let spec6 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 12 * 60 * 60 * 1000 + 1000, + pattern: {type: RecurrencePatternType.Daily, interval: 2}, + range: {type: RecurrenceRangeType.NoEnd} + }; + expect(matchRecurrence(new Date("2023-09-02T16:00:00+0000"), spec6 as any)).to.be.true; + expect(matchRecurrence(new Date("2023-09-02T15:59:59+0000"), spec6 as any)).to.be.false; + }); + it("should match weekly recurrence", () => { + let spec1 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 1, + daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Friday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.NoEnd}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-04T00:00:00+08:00"), spec1)).to.be.true; + expect(matchRecurrence(new Date("2023-09-08T00:00:00+08:00"), spec1)).to.be.true; + let spec2 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 2, + daysOfWeek: [DayOfWeek.Friday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.NoEnd}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-08T00:00:00+08:00"), spec2)).to.be.false; + expect(matchRecurrence(new Date("2023-09-15T00:00:00+08:00"), spec2)).to.be.true; + let spec3 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 1, + daysOfWeek: [DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.NoEnd}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-04T00:00:00+08:00"), spec3)).to.be.false; + let spec4 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 1, + daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.Numbered, numberOfOccurrences: 1}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-02T00:00:00+08:00"), spec4)).to.be.false; + let spec5 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 1, + daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.Numbered, numberOfOccurrences: 2}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-03T00:00:00+08:00"), spec5)).to.be.false; + let spec6 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 1, + daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.Numbered, numberOfOccurrences: 3}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-03T00:00:00+08:00"), spec6)).to.be.true; + let spec7 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 1, + daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.Numbered, numberOfOccurrences: 7}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-08T00:00:00+08:00"), spec7)).to.be.false; + let spec8 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 1, + daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.Numbered, numberOfOccurrences: 8}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-08T00:00:00+08:00"), spec8)).to.be.true; + let spec9 = { + startTime: new Date("2024-01-04T00:00:00+08:00"), + duration: 60 * 60 * 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 2, + daysOfWeek: [DayOfWeek.Tuesday, DayOfWeek.Thursday, DayOfWeek.Friday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.Numbered, numberOfOccurrences: 3}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2024-01-18T00:30:00+08:00"), spec9)).to.be.false; + let spec10 = { + startTime: new Date("2024-01-04T00:00:00+08:00"), + duration: 60 * 60 * 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 2, + daysOfWeek: [DayOfWeek.Tuesday, DayOfWeek.Thursday, DayOfWeek.Friday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.Numbered, numberOfOccurrences: 4}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2024-01-18T00:30:00+08:00"), spec10)).to.be.true; + let spec11 = { + startTime: new Date("2023-09-03T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 2, + daysOfWeek: [DayOfWeek.Sunday, DayOfWeek.Monday], + firstDayOfWeek: DayOfWeek.Monday + }, + range: {type: RecurrenceRangeType.NoEnd}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-04T00:00:00+08:00"), spec11)).to.be.false; + expect(matchRecurrence(new Date("2023-09-18T00:00:00+08:00"), spec11)).to.be.false; + let spec12 = { + startTime: new Date("2023-09-03T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 2, + daysOfWeek: [DayOfWeek.Sunday, DayOfWeek.Monday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.NoEnd}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-04T00:00:00+08:00"), spec12)).to.be.true; + expect(matchRecurrence(new Date("2023-09-18T00:00:00+08:00"), spec12)).to.be.true; + let spec13 = { + startTime: new Date("2023-09-03T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 2, + daysOfWeek: [DayOfWeek.Sunday, DayOfWeek.Monday], + firstDayOfWeek: DayOfWeek.Monday + }, + range: {type: RecurrenceRangeType.Numbered, numberOfOccurrences: 3}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-17T00:00:00+08:00"), spec13)).to.be.true; + let spec14 = { + startTime: new Date("2024-02-02T12:00:00+08:00"), + duration: 24 * 60 * 60 * 1000 + 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 2, + daysOfWeek: [DayOfWeek.Friday, DayOfWeek.Monday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.NoEnd}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2024-02-12T08:00:00+08:00"), spec14)).to.be.false; + let spec15 = { + startTime: new Date("2023-09-03T00:00:00+08:00"), + duration: 4 * 24 * 60 * 60 * 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 2, + daysOfWeek: [DayOfWeek.Sunday, DayOfWeek.Monday], + firstDayOfWeek: DayOfWeek.Monday + }, + range: {type: RecurrenceRangeType.NoEnd}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-13T00:00:00+08:00"), spec15)).to.be.true; + let spec16 = { + startTime: new Date("2023-09-03T00:00:00+08:00"), + duration: 4 * 24 * 60 * 60 * 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 2, + daysOfWeek: [DayOfWeek.Sunday, DayOfWeek.Monday], + firstDayOfWeek: DayOfWeek.Monday + }, + range: {type: RecurrenceRangeType.Numbered, numberOfOccurrences: 3}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-19T00:00:00+08:00"), spec16)).to.be.true; + let spec17 = { + startTime: new Date("2023-09-03T00:00:00+08:00"), + duration: 4 * 24 * 60 * 60 * 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 2, + daysOfWeek: [DayOfWeek.Sunday, DayOfWeek.Monday], + firstDayOfWeek: DayOfWeek.Monday + }, + range: {type: RecurrenceRangeType.Numbered, numberOfOccurrences: 2}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-19T00:00:00+08:00"), spec17)).to.be.false; + let spec18 = { + startTime: new Date("2023-09-01T00:00:00+08:00"), + duration: 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 1, + daysOfWeek: [DayOfWeek.Friday, DayOfWeek.Monday], + firstDayOfWeek: DayOfWeek.Sunday + }, + range: {type: RecurrenceRangeType.NoEnd}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-03T16:00:00+00:00"), spec18)).to.be.true; + expect(matchRecurrence(new Date("2023-09-07T16:00:00+00:00"), spec18)).to.be.true; + expect(matchRecurrence(new Date("2023-09-03T15:59:59+00:00"), spec18)).to.be.false; + expect(matchRecurrence(new Date("2023-09-07T15:59:59+00:00"), spec18)).to.be.false; + let spec19 = { + startTime: new Date("2023-09-03T00:00:00+08:00"), + duration: 4 * 24 * 60 * 60 * 1000, + pattern: { + type: RecurrencePatternType.Weekly, + interval: 2, + daysOfWeek: [DayOfWeek.Sunday, DayOfWeek.Monday], + firstDayOfWeek: DayOfWeek.Monday + }, + range: {type: RecurrenceRangeType.NoEnd}, + timezoneOffset: 8 * 60 * 60 * 1000 + }; + expect(matchRecurrence(new Date("2023-09-10T16:00:00+00:00"), spec19)).to.be.true; + expect(matchRecurrence(new Date("2023-09-10T15:59:59+00:00"), spec19)).to.be.false; + }); +}); \ No newline at end of file diff --git a/test/targetingFilter.test.ts b/test/targetingFilter.test.ts index ac33f88..328ede2 100644 --- a/test/targetingFilter.test.ts +++ b/test/targetingFilter.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { FeatureManager, ConfigurationMapFeatureFlagProvider } from "../"; +import { FeatureManager, ConfigurationMapFeatureFlagProvider } from "../src/index.js"; const complexTargetingFeature = { "id": "ComplexTargeting", diff --git a/test/timeWindowFilter.test.ts b/test/timeWindowFilter.test.ts new file mode 100644 index 0000000..e0d051a --- /dev/null +++ b/test/timeWindowFilter.test.ts @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +import * as sinon from "sinon"; +chai.use(chaiAsPromised); +const expect = chai.expect; + +import { FeatureManager, ConfigurationMapFeatureFlagProvider } from "../src/index.js"; + +const createTimeWindowFeature = (name: string, description: string, parameters: any) => { + const featureFlag = { + "id": name, + "description": description, + "enabled": true, + "conditions": { + "client_filters": [ + { + "name": "Microsoft.TimeWindow", + "parameters": parameters + } + ] + } + }; + + return featureFlag; +} + +describe("time window filter", () => { + it("should evaluate basic time window", async () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + createTimeWindowFeature("PastTimeWindow", + "A feature flag using a time window filter, that is active from 2023-06-29 07:00:00 to 2023-08-30 07:00:00. Will always return false as the current time is outside the time window.", + { + "Start": "Thu, 29 Jun 2023 07:00:00 GMT", + "End": "Wed, 30 Aug 2023 07:00:00 GMT" + } + ), + createTimeWindowFeature("FutureTimeWindow", + "A feature flag using a time window filter, that is active from 3023-06-27 06:00:00 to 3023-06-28 06:05:00. Will always return false as the time window has yet been reached.", + { + "Start": "Fri, 27 Jun 3023 06:00:00 GMT", + "End": "Sat, 28 Jun 3023 06:05:00 GMT" + } + ), + createTimeWindowFeature("PresentTimeWindow", + "A feature flag using a time window filter, that is active from 2023-06-27 06:00:00 to 3023-06-28 06:05:00. Will always return true as we are in the time window.", + { + "Start": "Thu, 29 Jun 2023 07:00:00 GMT", + "End": "Sat, 28 Jun 3023 06:05:00 GMT" + } + ), + createTimeWindowFeature("StartedTimeWindow", + "A feature flag using a time window filter, that will always return true as the current time is within the time window.", + { + "Start": "Tue, 27 Jun 2023 06:00:00 GMT" + } + ), + createTimeWindowFeature("WillEndTimeWindow", + "A feature flag using a time window filter, that will always return true as the current time is within the time window.", + { + "End": "Sat, 28 Jun 3023 06:05:00 GMT" + } + ) + ] + }); + const provider = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager = new FeatureManager(provider); + + // specify the date you want to mock + const fakeDate = new Date(2024, 12, 10); // Dec 10, 2024 + const clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("PastTimeWindow")).eq(false); + expect(await featureManager.isEnabled("PresentTimeWindow")).eq(true); + expect(await featureManager.isEnabled("FutureTimeWindow")).eq(false); + expect(await featureManager.isEnabled("StartedTimeWindow")).eq(true); + expect(await featureManager.isEnabled("WillEndTimeWindow")).eq(true); + clock.restore(); + }); + + it("should evaluate recurring time window", async () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + createTimeWindowFeature("DailyTimeWindow", + "A feature flag using a recurring time window filter, that is active from 18:00:00 to 20:00:00 every other day since 2024-12-10, until 2025-1-1.", + { + "Start": "Tue, 10 Dec 2024 18:00:00 GMT", + "End": "Tue, 10 Dec 2024 20:00:00 GMT", + "Recurrence": { + "Pattern": { + "Type": "Daily", + "Interval": 2 + }, + "Range": { + "Type": "EndDate", + "EndDate": "Wed, 1 Jan 2025 20:00:00 GMT" + } + } + } + ) + ] + }); + const provider = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager = new FeatureManager(provider); + + // daily recurring time window + let fakeDate = new Date("2024-12-10T17:59:59+0000"); + let clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(false); + clock.restore(); + + fakeDate = new Date("2024-12-10T18:00:00+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-10T19:59:59+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-10T20:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(false); + clock.restore(); + + fakeDate = new Date("2024-12-11T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(false); + clock.restore(); + + fakeDate = new Date("2024-12-12T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-12T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-12T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-24T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-25T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(false); + clock.restore(); + + fakeDate = new Date("2025-01-01T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2025-01-01T20:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(false); + clock.restore(); + + fakeDate = new Date("2025-01-03T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("DailyTimeWindow")).eq(false); + clock.restore(); + + // weekly recurring time window + dataSource.set("feature_management", { + feature_flags: [ + createTimeWindowFeature("WeeklyTimeWindow", + "A feature flag using a recurring time window filter, that is active from 18:00:00 to 20:00:00 every weekday since 2024-12-10, until the time window recurs for 10 times.", + { + "Start": "Tue, 10 Dec 2024 18:00:00 GMT", + "End": "Tue, 10 Dec 2024 20:00:00 GMT", + "Recurrence": { + "Pattern": { + "Type": "Weekly", + "Interval": 1, + "DaysOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] + }, + "Range": { + "Type": "Numbered", + "NumberOfOccurrences": 10 + } + } + } + ) + ] + }); + fakeDate = new Date("2024-12-10T17:59:59+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("WeeklyTimeWindow")).eq(false); + clock.restore(); + + fakeDate = new Date("2024-12-10T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("WeeklyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-11T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("WeeklyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-12T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("WeeklyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-13T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("WeeklyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-14T18:00:01+0000"); // Saturday + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("WeeklyTimeWindow")).eq(false); + clock.restore(); + + fakeDate = new Date("2024-12-15T18:00:01+0000"); // Sunday + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("WeeklyTimeWindow")).eq(false); + clock.restore(); + + fakeDate = new Date("2024-12-16T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("WeeklyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-16T20:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("WeeklyTimeWindow")).eq(false); + clock.restore(); + + fakeDate = new Date("2024-12-23T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("WeeklyTimeWindow")).eq(true); + clock.restore(); + + fakeDate = new Date("2024-12-24T18:00:01+0000"); + clock = sinon.useFakeTimers(fakeDate.getTime()); + expect(await featureManager.isEnabled("WeeklyTimeWindow")).eq(false); + clock.restore(); + }); +}); From 00bc029b672be7b13fcb08f6c55f331b5929fc63 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 11 Dec 2024 03:32:47 +0800 Subject: [PATCH 5/8] fix lint --- src/filter/recurrence/validator.ts | 1 - test/recurrence.test.ts | 124 ++++++++++++++--------------- test/timeWindowFilter.test.ts | 2 +- 3 files changed, 63 insertions(+), 64 deletions(-) diff --git a/src/filter/recurrence/validator.ts b/src/filter/recurrence/validator.ts index 6f24f69..778da7e 100644 --- a/src/filter/recurrence/validator.ts +++ b/src/filter/recurrence/validator.ts @@ -18,7 +18,6 @@ export const RANGE_TYPE = "Recurrence.Range.Type"; export const END_DATE = "Recurrence.Range.EndDate"; export const NUMBER_OF_OCCURRENCES = "Recurrence.Range.NumberOfOccurrences"; - /** * Parses @see RecurrenceParameter into a @see RecurrenceSpec object. If the parameter is invalid, an error will be thrown. * @param startTime The start time of the base time window diff --git a/test/recurrence.test.ts b/test/recurrence.test.ts index 1dd5a2e..97b40e2 100644 --- a/test/recurrence.test.ts +++ b/test/recurrence.test.ts @@ -6,8 +6,8 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { - parseRecurrenceParameter, +import { + parseRecurrenceParameter, PATTERN, PATTERN_TYPE, INTERVAL, @@ -17,7 +17,7 @@ import { RANGE_TYPE, END_DATE, NUMBER_OF_OCCURRENCES, - START_NOT_MATCHED_ERROR_MESSAGE, + START_NOT_MATCHED_ERROR_MESSAGE, TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE } from "../src/filter/recurrence/validator.js"; import { VALUE_OUT_OF_RANGE_ERROR_MESSAGE, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE, buildInvalidParameterErrorMessage } from "../src/filter/utils.js"; import { DayOfWeek, RecurrencePatternType, RecurrenceRangeType } from "../src/filter/recurrence/model"; @@ -25,7 +25,7 @@ import { matchRecurrence } from "../src/filter/recurrence/evaluator.js"; describe("recurrence validator", () => { it("should check general required parameter", () => { - let recurrence1 = { + const recurrence1 = { Pattern: { Type: "Daily" }, @@ -35,13 +35,13 @@ describe("recurrence validator", () => { }; expect(() => parseRecurrenceParameter(undefined, new Date(), recurrence1)).to.throw(buildInvalidParameterErrorMessage("Start", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); expect(() => parseRecurrenceParameter(new Date(), undefined, recurrence1)).to.throw(buildInvalidParameterErrorMessage("End", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); - let recurrence2 = { + const recurrence2 = { Range: { Type: "NoEnd" } }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence2 as any)).to.throw(buildInvalidParameterErrorMessage(PATTERN, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); - let recurrence3 = { + const recurrence3 = { Pattern: { Type: "Daily" } @@ -50,23 +50,23 @@ describe("recurrence validator", () => { }); it("should check pattern and range required parameter", () => { - let recurrence1 = { + const recurrence1 = { Pattern: {}, Range: { Type: "NoEnd" } - } + }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence1 as any)).to.throw(buildInvalidParameterErrorMessage(PATTERN_TYPE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); - let recurrence2 = { + const recurrence2 = { Pattern: { Type: "Weekly" }, Range: { Type: "NoEnd" } - } + }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence2)).to.throw(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); - let recurrence3 = { + const recurrence3 = { Pattern: { Type: "Weekly", DaysOfWeek: [] @@ -74,20 +74,20 @@ describe("recurrence validator", () => { Range: { Type: "NoEnd" } - } + }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence3)).to.throw(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); - let recurrence4 = { + const recurrence4 = { Pattern: { Type: "Daily" }, Range: {} - } + }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence4 as any)).to.throw(buildInvalidParameterErrorMessage(RANGE_TYPE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); - + }); it("should check invalid value", () => { - let recurrence1 = { + const recurrence1 = { Pattern: { Type: "Daily", Interval: "1" @@ -97,7 +97,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence1 as any)).to.throw(buildInvalidParameterErrorMessage(INTERVAL, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); - let recurrence2 = { + const recurrence2 = { Pattern: { Type: "Daily", Interval: 0 @@ -107,7 +107,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence2)).to.throw(buildInvalidParameterErrorMessage(INTERVAL, VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); - let recurrence3 = { + const recurrence3 = { Pattern: { Type: "Daily" }, @@ -117,7 +117,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence3 as any)).to.throw(buildInvalidParameterErrorMessage(NUMBER_OF_OCCURRENCES, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); - let recurrence4 = { + const recurrence4 = { Pattern: { Type: "Daily" }, @@ -127,7 +127,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence4)).to.throw(buildInvalidParameterErrorMessage(NUMBER_OF_OCCURRENCES, VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); - let recurrence5 = { + const recurrence5 = { Pattern: { Type: "Weekly", DaysOfWeek: ["Monday", "Tue"] @@ -137,7 +137,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence5)).to.throw(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); - let recurrence6 = { + const recurrence6 = { Pattern: { Type: "Weekly", DaysOfWeek: "Monday" @@ -147,7 +147,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence6 as any)).to.throw(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); - let recurrence7 = { + const recurrence7 = { Pattern: { Type: "Weekly", DaysOfWeek: ["Monday"], @@ -158,7 +158,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence7)).to.throw(buildInvalidParameterErrorMessage(FIRST_DAY_OF_WEEK, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); - let recurrence8 = { + const recurrence8 = { Pattern: { Type: "Daily" }, @@ -168,7 +168,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date(1), new Date(2), recurrence8)).to.throw(buildInvalidParameterErrorMessage(END_DATE, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); - let recurrence9 = { + const recurrence9 = { Pattern: { Type: "Daily" }, @@ -181,7 +181,7 @@ describe("recurrence validator", () => { }); it("should check time window duration", () => { - let recurrence1 = { + const recurrence1 = { Pattern: { Type: "Daily" }, @@ -190,7 +190,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date(2), new Date(1), recurrence1)).to.throw(buildInvalidParameterErrorMessage("End", VALUE_OUT_OF_RANGE_ERROR_MESSAGE)); - let recurrence2 = { + const recurrence2 = { Pattern: { Type: "Daily" }, @@ -199,7 +199,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date("2024-12-10T00:00:00+0000"), new Date("2024-12-12T00:00:00+0000"), recurrence2)).to.throw(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); - let recurrence3 = { + const recurrence3 = { Pattern: { Type: "Weekly", DaysOfWeek: ["Monday"] @@ -209,7 +209,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date("2024-12-09T00:00:00+0000"), new Date("2024-12-16T00:00:01+0000"), recurrence3)).to.throw(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); - let recurrence4 = { + const recurrence4 = { Pattern: { Type: "Weekly", DaysOfWeek: ["Monday", "Thursday", "Sunday"] @@ -219,7 +219,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date("2024-12-09T00:00:00+0000"), new Date("2024-12-11T00:00:01+0000"), recurrence4)).to.throw(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); - let recurrence5 = { + const recurrence5 = { Pattern: { Type: "Weekly", DaysOfWeek: ["Monday", "Saturday"] @@ -229,7 +229,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date("2024-12-09T00:00:00+0000"), new Date("2024-12-11T00:00:01+0000"), recurrence5)).to.throw(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); - let recurrence6 = { + const recurrence6 = { Pattern: { Type: "Weekly", DaysOfWeek: ["Tuesday", "Saturday"] @@ -239,7 +239,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date("2024-01-16T00:00:00+0000"), new Date("2024-01-19T00:00:00+0000"), recurrence6)).to.not.throw(); - let recurrence7 = { + const recurrence7 = { Pattern: { Type: "Weekly", Interval: 2, @@ -251,7 +251,7 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter(new Date("2024-01-15T00:00:00+0000"), new Date("2024-01-19T00:00:00+0000"), recurrence7)).to.not.throw(); - let recurrence8 = { + const recurrence8 = { Pattern: { Type: "Weekly", Interval: 1, @@ -264,7 +264,7 @@ describe("recurrence validator", () => { }; expect(() => parseRecurrenceParameter(new Date("2024-01-15T00:00:00+0000"), new Date("2024-01-17T00:00:00+0000"), recurrence8)).to.not.throw(); expect(() => parseRecurrenceParameter(new Date("2024-01-15T00:00:00+0000"), new Date("2024-01-17T00:00:01+0000"), recurrence8)).to.throw(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); - let recurrence9 = { + const recurrence9 = { Pattern: { Type: "Weekly", Interval: 1, @@ -279,7 +279,7 @@ describe("recurrence validator", () => { }); it("should check whether start is a valid first occurrence", () => { - let recurrence1 = { + const recurrence1 = { Pattern: { Type: "Weekly", DaysOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Saturday", "Sunday"] @@ -289,8 +289,8 @@ describe("recurrence validator", () => { } }; expect(() => parseRecurrenceParameter( - new Date("2023-09-01T00:00:00+08:00"), - new Date("2023-09-01T00:00:01+08:00"), + new Date("2023-09-01T00:00:00+08:00"), + new Date("2023-09-01T00:00:01+08:00"), recurrence1, 8 * 24 * 60 * 60 * 1000) ).to.throw(buildInvalidParameterErrorMessage("Start", START_NOT_MATCHED_ERROR_MESSAGE)); @@ -299,14 +299,14 @@ describe("recurrence validator", () => { describe("recurrence evaluator", () => { it("should match daily recurrence", () => { - let spec1 = { + const spec1 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: {type: RecurrencePatternType.Daily, interval: 1}, range: {type: RecurrenceRangeType.NoEnd} }; expect(matchRecurrence(new Date("2023-09-02T00:00:00+08:00"), spec1 as any)).to.be.true; - let spec2 = { + const spec2 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: {type: RecurrencePatternType.Daily, interval: 2}, @@ -314,7 +314,7 @@ describe("recurrence evaluator", () => { }; expect(matchRecurrence(new Date("2023-09-02T00:00:00+08:00"), spec2 as any)).to.be.false; expect(matchRecurrence(new Date("2023-09-03T00:00:00+08:00"), spec2 as any)).to.be.true; - let spec3 = { + const spec3 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 2 * 24 * 60 * 60 * 1000, pattern: {type: RecurrencePatternType.Daily, interval: 4}, @@ -323,7 +323,7 @@ describe("recurrence evaluator", () => { expect(matchRecurrence(new Date("2023-09-05T00:00:00+08:00"), spec3 as any)).to.be.true; expect(matchRecurrence(new Date("2023-09-06T00:00:00+08:00"), spec3 as any)).to.be.true; expect(matchRecurrence(new Date("2023-09-09T00:00:00+08:00"), spec3 as any)).to.be.true; - let spec4 = { + const spec4 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: {type: RecurrencePatternType.Daily, interval: 1}, @@ -331,14 +331,14 @@ describe("recurrence evaluator", () => { }; expect(matchRecurrence(new Date("2023-09-02T00:00:00+08:00"), spec4 as any)).to.be.true; expect(matchRecurrence(new Date("2023-09-03T00:00:00+08:00"), spec4 as any)).to.be.false; - let spec5 = { + const spec5 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: {type: RecurrencePatternType.Daily, interval: 1}, range: {type: RecurrenceRangeType.EndDate, endDate: new Date("2023-09-03T00:00:00+08:00")} }; expect(matchRecurrence(new Date("2023-09-04T00:00:00+08:00"), spec5 as any)).to.be.false; - let spec6 = { + const spec6 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 12 * 60 * 60 * 1000 + 1000, pattern: {type: RecurrencePatternType.Daily, interval: 2}, @@ -348,7 +348,7 @@ describe("recurrence evaluator", () => { expect(matchRecurrence(new Date("2023-09-02T15:59:59+0000"), spec6 as any)).to.be.false; }); it("should match weekly recurrence", () => { - let spec1 = { + const spec1 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: { @@ -362,7 +362,7 @@ describe("recurrence evaluator", () => { }; expect(matchRecurrence(new Date("2023-09-04T00:00:00+08:00"), spec1)).to.be.true; expect(matchRecurrence(new Date("2023-09-08T00:00:00+08:00"), spec1)).to.be.true; - let spec2 = { + const spec2 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: { @@ -376,7 +376,7 @@ describe("recurrence evaluator", () => { }; expect(matchRecurrence(new Date("2023-09-08T00:00:00+08:00"), spec2)).to.be.false; expect(matchRecurrence(new Date("2023-09-15T00:00:00+08:00"), spec2)).to.be.true; - let spec3 = { + const spec3 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: { @@ -389,7 +389,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2023-09-04T00:00:00+08:00"), spec3)).to.be.false; - let spec4 = { + const spec4 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: { @@ -402,7 +402,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2023-09-02T00:00:00+08:00"), spec4)).to.be.false; - let spec5 = { + const spec5 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: { @@ -415,7 +415,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2023-09-03T00:00:00+08:00"), spec5)).to.be.false; - let spec6 = { + const spec6 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: { @@ -428,7 +428,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2023-09-03T00:00:00+08:00"), spec6)).to.be.true; - let spec7 = { + const spec7 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: { @@ -441,7 +441,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2023-09-08T00:00:00+08:00"), spec7)).to.be.false; - let spec8 = { + const spec8 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: { @@ -454,7 +454,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2023-09-08T00:00:00+08:00"), spec8)).to.be.true; - let spec9 = { + const spec9 = { startTime: new Date("2024-01-04T00:00:00+08:00"), duration: 60 * 60 * 1000, pattern: { @@ -467,7 +467,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2024-01-18T00:30:00+08:00"), spec9)).to.be.false; - let spec10 = { + const spec10 = { startTime: new Date("2024-01-04T00:00:00+08:00"), duration: 60 * 60 * 1000, pattern: { @@ -480,7 +480,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2024-01-18T00:30:00+08:00"), spec10)).to.be.true; - let spec11 = { + const spec11 = { startTime: new Date("2023-09-03T00:00:00+08:00"), duration: 1000, pattern: { @@ -494,7 +494,7 @@ describe("recurrence evaluator", () => { }; expect(matchRecurrence(new Date("2023-09-04T00:00:00+08:00"), spec11)).to.be.false; expect(matchRecurrence(new Date("2023-09-18T00:00:00+08:00"), spec11)).to.be.false; - let spec12 = { + const spec12 = { startTime: new Date("2023-09-03T00:00:00+08:00"), duration: 1000, pattern: { @@ -508,7 +508,7 @@ describe("recurrence evaluator", () => { }; expect(matchRecurrence(new Date("2023-09-04T00:00:00+08:00"), spec12)).to.be.true; expect(matchRecurrence(new Date("2023-09-18T00:00:00+08:00"), spec12)).to.be.true; - let spec13 = { + const spec13 = { startTime: new Date("2023-09-03T00:00:00+08:00"), duration: 1000, pattern: { @@ -521,7 +521,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2023-09-17T00:00:00+08:00"), spec13)).to.be.true; - let spec14 = { + const spec14 = { startTime: new Date("2024-02-02T12:00:00+08:00"), duration: 24 * 60 * 60 * 1000 + 1000, pattern: { @@ -534,7 +534,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2024-02-12T08:00:00+08:00"), spec14)).to.be.false; - let spec15 = { + const spec15 = { startTime: new Date("2023-09-03T00:00:00+08:00"), duration: 4 * 24 * 60 * 60 * 1000, pattern: { @@ -547,7 +547,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2023-09-13T00:00:00+08:00"), spec15)).to.be.true; - let spec16 = { + const spec16 = { startTime: new Date("2023-09-03T00:00:00+08:00"), duration: 4 * 24 * 60 * 60 * 1000, pattern: { @@ -560,7 +560,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2023-09-19T00:00:00+08:00"), spec16)).to.be.true; - let spec17 = { + const spec17 = { startTime: new Date("2023-09-03T00:00:00+08:00"), duration: 4 * 24 * 60 * 60 * 1000, pattern: { @@ -573,7 +573,7 @@ describe("recurrence evaluator", () => { timezoneOffset: 8 * 60 * 60 * 1000 }; expect(matchRecurrence(new Date("2023-09-19T00:00:00+08:00"), spec17)).to.be.false; - let spec18 = { + const spec18 = { startTime: new Date("2023-09-01T00:00:00+08:00"), duration: 1000, pattern: { @@ -589,7 +589,7 @@ describe("recurrence evaluator", () => { expect(matchRecurrence(new Date("2023-09-07T16:00:00+00:00"), spec18)).to.be.true; expect(matchRecurrence(new Date("2023-09-03T15:59:59+00:00"), spec18)).to.be.false; expect(matchRecurrence(new Date("2023-09-07T15:59:59+00:00"), spec18)).to.be.false; - let spec19 = { + const spec19 = { startTime: new Date("2023-09-03T00:00:00+08:00"), duration: 4 * 24 * 60 * 60 * 1000, pattern: { @@ -604,4 +604,4 @@ describe("recurrence evaluator", () => { expect(matchRecurrence(new Date("2023-09-10T16:00:00+00:00"), spec19)).to.be.true; expect(matchRecurrence(new Date("2023-09-10T15:59:59+00:00"), spec19)).to.be.false; }); -}); \ No newline at end of file +}); diff --git a/test/timeWindowFilter.test.ts b/test/timeWindowFilter.test.ts index e0d051a..60610d6 100644 --- a/test/timeWindowFilter.test.ts +++ b/test/timeWindowFilter.test.ts @@ -25,7 +25,7 @@ const createTimeWindowFeature = (name: string, description: string, parameters: }; return featureFlag; -} +}; describe("time window filter", () => { it("should evaluate basic time window", async () => { From 28d27ce6054b6c1cd25ece403889aafa4bea03e7 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 12 Dec 2024 11:19:34 +0800 Subject: [PATCH 6/8] update method name --- src/filter/recurrence/evaluator.ts | 12 ++++++------ src/filter/recurrence/utils.ts | 6 +++--- src/filter/recurrence/validator.ts | 24 ++++++++++++------------ src/filter/timeWindowFilter.ts | 4 ++-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/filter/recurrence/evaluator.ts b/src/filter/recurrence/evaluator.ts index 884672a..73c6fe1 100644 --- a/src/filter/recurrence/evaluator.ts +++ b/src/filter/recurrence/evaluator.ts @@ -16,7 +16,7 @@ type RecurrenceState = { * @returns True if the given time is within any recurring time window; otherwise, false */ export function matchRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): boolean { - const recurrenceState = FindPreviousRecurrence(time, recurrenceSpec); + const recurrenceState = findPreviousRecurrence(time, recurrenceSpec); if (recurrenceState) { return time.getTime() < recurrenceState.previousOccurrence.getTime() + recurrenceSpec.duration; } @@ -29,16 +29,16 @@ export function matchRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): boo * @param recurrenceSpec The recurrence specification * @returns The recurrence state if any previous occurrence is found; otherwise, undefined */ -function FindPreviousRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): RecurrenceState | undefined { +function findPreviousRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): RecurrenceState | undefined { if (time < recurrenceSpec.startTime) { return undefined; } let result: RecurrenceState; const pattern = recurrenceSpec.pattern; if (pattern.type === RecurrencePatternType.Daily) { - result = FindPreviousDailyRecurrence(time, recurrenceSpec); + result = findPreviousDailyRecurrence(time, recurrenceSpec); } else if (pattern.type === RecurrencePatternType.Weekly) { - result = FindPreviousWeeklyRecurrence(time, recurrenceSpec); + result = findPreviousWeeklyRecurrence(time, recurrenceSpec); } else { throw new Error("Unsupported recurrence pattern type."); } @@ -57,7 +57,7 @@ function FindPreviousRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): Rec return result; } -function FindPreviousDailyRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): RecurrenceState { +function findPreviousDailyRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): RecurrenceState { const startTime = recurrenceSpec.startTime; const timeGap = time.getTime() - startTime.getTime(); const pattern = recurrenceSpec.pattern; @@ -68,7 +68,7 @@ function FindPreviousDailyRecurrence(time: Date, recurrenceSpec: RecurrenceSpec) }; } -function FindPreviousWeeklyRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): RecurrenceState { +function findPreviousWeeklyRecurrence(time: Date, recurrenceSpec: RecurrenceSpec): RecurrenceState { /* * Algorithm: * 1. first find day 0 (d0), it's the day representing the start day on the week of `Start`. diff --git a/src/filter/recurrence/utils.ts b/src/filter/recurrence/utils.ts index 2ddb1a5..41acc66 100644 --- a/src/filter/recurrence/utils.ts +++ b/src/filter/recurrence/utils.ts @@ -28,11 +28,11 @@ export function sortDaysOfWeek(daysOfWeek: DayOfWeek[], firstDayOfWeek: DayOfWee /** * Gets the day of week of a given date based on the timezone offset. * @param date A UTC date - * @param timezoneOffset The timezone offset in milliseconds + * @param timezoneOffsetInMs The timezone offset in milliseconds * @returns The day of week (0 for Sunday, 1 for Monday, ..., 6 for Saturday) */ -export function getDayOfWeek(date: Date, timezoneOffset: number): number { - const alignedDate = new Date(date.getTime() + timezoneOffset); +export function getDayOfWeek(date: Date, timezoneOffsetInMs: number): number { + const alignedDate = new Date(date.getTime() + timezoneOffsetInMs); return alignedDate.getUTCDay(); } diff --git a/src/filter/recurrence/validator.ts b/src/filter/recurrence/validator.ts index 778da7e..d10ae23 100644 --- a/src/filter/recurrence/validator.ts +++ b/src/filter/recurrence/validator.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { RecurrenceParameter } from "../timeWindowFilter.js"; +import { RecurrenceParameters } from "../timeWindowFilter.js"; import { VALUE_OUT_OF_RANGE_ERROR_MESSAGE, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE, buildInvalidParameterErrorMessage } from "../utils.js"; import { DayOfWeek, RecurrenceSpec, RecurrencePattern, RecurrenceRange, RecurrencePatternType, RecurrenceRangeType, DAYS_PER_WEEK, ONE_DAY_IN_MILLISECONDS } from "./model.js"; import { calculateWeeklyDayOffset, sortDaysOfWeek, getDayOfWeek } from "./utils.js"; @@ -19,14 +19,14 @@ export const END_DATE = "Recurrence.Range.EndDate"; export const NUMBER_OF_OCCURRENCES = "Recurrence.Range.NumberOfOccurrences"; /** - * Parses @see RecurrenceParameter into a @see RecurrenceSpec object. If the parameter is invalid, an error will be thrown. + * Parses @see RecurrenceParameters into a @see RecurrenceSpec object. If the parameter is invalid, an error will be thrown. * @param startTime The start time of the base time window * @param day2 The end time of the base time window - * @param recurrenceParameter The @see RecurrenceParameter to parse + * @param recurrenceParameters The @see RecurrenceParameters to parse * @param TimeZoneOffset The time zone offset in milliseconds, by default 0 * @returns A @see RecurrenceSpec object */ -export function parseRecurrenceParameter(startTime: Date | undefined, endTime: Date | undefined, recurrenceParameter: RecurrenceParameter, TimeZoneOffset: number = 0): RecurrenceSpec { +export function parseRecurrenceParameter(startTime: Date | undefined, endTime: Date | undefined, recurrenceParameters: RecurrenceParameters, TimeZoneOffset: number = 0): RecurrenceSpec { if (startTime === undefined) { throw new Error(buildInvalidParameterErrorMessage("Start", REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); } @@ -44,14 +44,14 @@ export function parseRecurrenceParameter(startTime: Date | undefined, endTime: D return { startTime: startTime, duration: timeWindowDuration, - pattern: parseRecurrencePattern(startTime, endTime, recurrenceParameter, TimeZoneOffset), - range: parseRecurrenceRange(startTime, recurrenceParameter), + pattern: parseRecurrencePattern(startTime, endTime, recurrenceParameters, TimeZoneOffset), + range: parseRecurrenceRange(startTime, recurrenceParameters), timezoneOffset: TimeZoneOffset }; } -function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParameter: RecurrenceParameter, timeZoneOffset: number): RecurrencePattern { - const rawPattern = recurrenceParameter.Pattern; +function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParameters: RecurrenceParameters, timeZoneOffset: number): RecurrencePattern { + const rawPattern = recurrenceParameters.Pattern; if (rawPattern === undefined) { throw new Error(buildInvalidParameterErrorMessage(PATTERN, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); } @@ -105,7 +105,7 @@ function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParame throw new Error(buildInvalidParameterErrorMessage(DAYS_OF_WEEK, UNRECOGNIZABLE_VALUE_ERROR_MESSAGE)); } if (timeWindowDuration > interval * DAYS_PER_WEEK * ONE_DAY_IN_MILLISECONDS || - !IsDurationCompliantWithDaysOfWeek(timeWindowDuration, interval, daysOfWeek, firstDayOfWeek)) { + !isDurationCompliantWithDaysOfWeek(timeWindowDuration, interval, daysOfWeek, firstDayOfWeek)) { throw new Error(buildInvalidParameterErrorMessage("End", TIME_WINDOW_DURATION_OUT_OF_RANGE_ERROR_MESSAGE)); } parsedPattern.daysOfWeek = daysOfWeek; @@ -119,8 +119,8 @@ function parseRecurrencePattern(startTime: Date, endTime: Date, recurrenceParame return parsedPattern; } -function parseRecurrenceRange(startTime: Date, recurrenceParameter: RecurrenceParameter): RecurrenceRange { - const rawRange = recurrenceParameter.Range; +function parseRecurrenceRange(startTime: Date, recurrenceParameters: RecurrenceParameters): RecurrenceRange { + const rawRange = recurrenceParameters.Range; if (rawRange === undefined) { throw new Error(buildInvalidParameterErrorMessage(RANGE, REQUIRED_PARAMETER_MISSING_ERROR_MESSAGE)); } @@ -162,7 +162,7 @@ function parseRecurrenceRange(startTime: Date, recurrenceParameter: RecurrencePa return parsedRange; } -function IsDurationCompliantWithDaysOfWeek(duration: number, interval: number, daysOfWeek: DayOfWeek[], firstDayOfWeek: DayOfWeek): boolean { +function isDurationCompliantWithDaysOfWeek(duration: number, interval: number, daysOfWeek: DayOfWeek[], firstDayOfWeek: DayOfWeek): boolean { if (daysOfWeek.length === 1) { return true; } diff --git a/src/filter/timeWindowFilter.ts b/src/filter/timeWindowFilter.ts index d7694b9..dd4fa02 100644 --- a/src/filter/timeWindowFilter.ts +++ b/src/filter/timeWindowFilter.ts @@ -15,10 +15,10 @@ type TimeWindowFilterEvaluationContext = { type TimeWindowParameters = { Start?: string; End?: string; - Recurrence?: RecurrenceParameter; + Recurrence?: RecurrenceParameters; }; -export type RecurrenceParameter = { +export type RecurrenceParameters = { Pattern: { Type: string; Interval?: number; From aa722977344e61322bc314e561b5bc8460a1ef76 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 20 Dec 2024 14:46:01 +0800 Subject: [PATCH 7/8] update --- sdk/feature-management/test/featureEvaluation.test.ts | 2 +- sdk/feature-management/test/variant.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/feature-management/test/featureEvaluation.test.ts b/sdk/feature-management/test/featureEvaluation.test.ts index 2729a2e..b7e857e 100644 --- a/sdk/feature-management/test/featureEvaluation.test.ts +++ b/sdk/feature-management/test/featureEvaluation.test.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import * as chai from "chai"; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider, EvaluationResult, VariantAssignmentReason } from "../"; +import { FeatureManager, ConfigurationObjectFeatureFlagProvider, EvaluationResult, VariantAssignmentReason } from "../src/index.js"; const expect = chai.expect; let called: number = 0; diff --git a/sdk/feature-management/test/variant.test.ts b/sdk/feature-management/test/variant.test.ts index ddfd90f..2b40612 100644 --- a/sdk/feature-management/test/variant.test.ts +++ b/sdk/feature-management/test/variant.test.ts @@ -3,7 +3,7 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "../"; +import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "../src/index.js"; import { Features, featureFlagsConfigurationObject } from "./sampleFeatureFlags.js"; chai.use(chaiAsPromised); const expect = chai.expect; From 224d72c9dabb7d3960d12dd270375a3095ec93a7 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 12 Aug 2025 09:58:52 +0800 Subject: [PATCH 8/8] revert change --- src/feature-management/src/filter/timeWindowFilter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/feature-management/src/filter/timeWindowFilter.ts b/src/feature-management/src/filter/timeWindowFilter.ts index dd4fa02..975f123 100644 --- a/src/feature-management/src/filter/timeWindowFilter.ts +++ b/src/feature-management/src/filter/timeWindowFilter.ts @@ -33,7 +33,7 @@ export type RecurrenceParameters = { }; export class TimeWindowFilter implements IFeatureFilter { - name: string = "Microsoft.TimeWindow"; + readonly name: string = "Microsoft.TimeWindow"; evaluate(context: TimeWindowFilterEvaluationContext): boolean { const {featureName, parameters} = context;