Skip to content

Commit 6387a69

Browse files
committed
Mimic the cron expression from the dotnet library.
Signed-off-by: Alexander Trauzzi <[email protected]>
1 parent 340b251 commit 6387a69

File tree

6 files changed

+511
-12
lines changed

6 files changed

+511
-12
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/*
2+
Copyright 2025 The Dapr Authors
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
export enum CronPeriod {
15+
Second,
16+
Minute,
17+
Hour,
18+
DayOfWeek,
19+
DayOfMonth,
20+
Month,
21+
}
22+
23+
export type TimeValue = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59;
24+
25+
export type DayOfMonth = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31;
26+
27+
export enum DayOfWeek {
28+
Sunday = "SUN",
29+
Monday = "MON",
30+
Tuesday = "TUE",
31+
Wednesday = "WED",
32+
Thursday = "THU",
33+
Friday = "FRI",
34+
Saturday = "SAT",
35+
}
36+
37+
export enum Month {
38+
January = "JAN",
39+
February = "FEB",
40+
March = "MAR",
41+
April = "APR",
42+
May = "MAY",
43+
June = "JUN",
44+
July = "JUL",
45+
August = "AUG",
46+
September = "SEP",
47+
October = "OCT",
48+
November = "NOV",
49+
December = "DEC",
50+
}
51+
52+
export class CronExpressionBuilder {
53+
54+
public static on(period: CronPeriod.Second, ...values: (TimeValue[] | TimeValue)[]): CronExpression;
55+
public static on(period: CronPeriod.Minute, ...values: (TimeValue[] | TimeValue)[]): CronExpression;
56+
public static on(period: CronPeriod.Hour, ...values: (TimeValue[] | TimeValue)[]): CronExpression;
57+
public static on(period: CronPeriod.DayOfWeek, ...values: (DayOfWeek[] | DayOfWeek)[]): CronExpression;
58+
public static on(period: CronPeriod.DayOfMonth, ...values: (DayOfMonth[] | DayOfMonth)[]): CronExpression;
59+
public static on(period: CronPeriod.Month, ...values: (Month[] | Month)[]): CronExpression;
60+
public static on(period: any, ...values: (any[] | any)[]) {
61+
return new CronExpression().on(period, values.flat());
62+
}
63+
64+
public static through(period: CronPeriod.Second, from: TimeValue, to: TimeValue): CronExpression;
65+
public static through(period: CronPeriod.Minute, from: TimeValue, to: TimeValue): CronExpression;
66+
public static through(period: CronPeriod.Hour, from: TimeValue, to: TimeValue): CronExpression;
67+
public static through(period: CronPeriod.DayOfWeek, from: DayOfWeek, to: DayOfWeek): CronExpression;
68+
public static through(period: CronPeriod.DayOfMonth, from: DayOfMonth, to: DayOfMonth): CronExpression;
69+
public static through(period: CronPeriod.Month, from: Month, to: Month): CronExpression;
70+
public static through(period: any, from: any, to: any) {
71+
return new CronExpression().through(period, from, to);
72+
}
73+
74+
public static each(period: CronPeriod) {
75+
return new CronExpression().every(period);
76+
}
77+
}
78+
79+
export class CronExpression {
80+
private static readonly SecondsAndMinutesRegexText = /([0-5]?\d-[0-5]?\d)|([0-5]?\d,?)|(\*(\/[0-5]?\d)?)/;
81+
private static readonly HoursRegexText =
82+
/(([0-1]?\d)|(2[0-3])-([0-1]?\d)|(2[0-3]))|(([0-1]?\d)|(2[0-3]),?)|(\*(\/([0-1]?\d)|(2[0-3]))?)/;
83+
private static readonly DayOfMonthRegexText =
84+
/\*|(\*\/(([0-2]?\d)|(3[0-1])))|(((([0-2]?\d)|(3[0-1]))(-(([0-2]?\d)|(3[0-1])))?))/;
85+
private static readonly MonthRegexText =
86+
/(^(\*\/)?((0?\d)|(1[0-2]))$)|(^\*$)|(^((0?\d)|(1[0-2]))(-((0?\d)|(1[0-2]))?)$)|(^(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:-(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?(?:,(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:-(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)*$)/;
87+
private static readonly DayOfWeekRegexText =
88+
/\*|(\*\/(0?[0-6])|(0?[0-6](-0?[0-6])?)|((,?(SUN|MON|TUE|WED|THU|FRI|SAT))+)|((SUN|MON|TUE|WED|THU|FRI|SAT)(-(SUN|MON|TUE|WED|THU|FRI|SAT))?))/;
89+
90+
private static readonly cronExpressionRegex = new RegExp(
91+
`${CronExpression.SecondsAndMinutesRegexText.source} ${CronExpression.SecondsAndMinutesRegexText.source} ${CronExpression.HoursRegexText.source} ${CronExpression.DayOfMonthRegexText.source} ${CronExpression.MonthRegexText.source} ${CronExpression.DayOfWeekRegexText.source}`,
92+
);
93+
94+
public static IsCronExpression(value: string) {
95+
return CronExpression.cronExpressionRegex.test(value);
96+
}
97+
98+
private seconds = "*";
99+
private minutes = "*";
100+
private hours = "*";
101+
private dayOfMonth = "*";
102+
private month = "*";
103+
private dayOfWeek = "*";
104+
105+
public on(period: CronPeriod.Second, ...values: (TimeValue[] | TimeValue)[]): this;
106+
public on(period: CronPeriod.Minute, ...values: (TimeValue[] | TimeValue)[]): this;
107+
public on(period: CronPeriod.Hour, ...values: (TimeValue[] | TimeValue)[]): this;
108+
public on(period: CronPeriod.DayOfWeek, ...values: (DayOfWeek[] | DayOfWeek)[]): this;
109+
public on(period: CronPeriod.DayOfMonth, ...values: (DayOfMonth[] | DayOfMonth)[]): this;
110+
public on(period: CronPeriod.Month, ...values: (Month[] | Month)[]): this;
111+
public on(period: any, ...values: (any[] | any)[]): this {
112+
const fixedValues = values.flat();
113+
114+
switch (period) {
115+
case CronPeriod.Second:
116+
this.seconds = this.prepareTimeValueList(fixedValues);
117+
break;
118+
case CronPeriod.Minute:
119+
this.minutes = this.prepareTimeValueList(fixedValues);
120+
break;
121+
case CronPeriod.Hour:
122+
this.hours = this.prepareTimeValueList(fixedValues);
123+
break;
124+
case CronPeriod.DayOfWeek:
125+
this.dayOfWeek = this.prepareDayOfWeekValueList(fixedValues);
126+
break;
127+
case CronPeriod.DayOfMonth:
128+
this.dayOfMonth = this.prepareDayOfMonthValueList(fixedValues);
129+
break;
130+
case CronPeriod.Month:
131+
this.month = this.prepareMonthValueList(fixedValues);
132+
break;
133+
}
134+
135+
return this;
136+
}
137+
138+
public through(period: CronPeriod.Second, from: TimeValue, to: TimeValue): CronExpression;
139+
public through(period: CronPeriod.Minute, from: TimeValue, to: TimeValue): CronExpression;
140+
public through(period: CronPeriod.Hour, from: TimeValue, to: TimeValue): CronExpression;
141+
public through(period: CronPeriod.DayOfWeek, from: DayOfWeek, to: DayOfWeek): CronExpression;
142+
public through(period: CronPeriod.DayOfMonth, from: DayOfMonth, to: DayOfMonth): CronExpression;
143+
public through(period: CronPeriod.Month, from: Month, to: Month): CronExpression;
144+
public through(period: any, from: any, to: any) {
145+
146+
switch (period) {
147+
case CronPeriod.Second:
148+
this.seconds = this.prepareTimeValueRange(from, to);
149+
break;
150+
case CronPeriod.Minute:
151+
this.minutes = this.prepareTimeValueRange(from, to);
152+
break;
153+
case CronPeriod.Hour:
154+
this.hours = this.prepareTimeValueRange(from, to);
155+
break;
156+
case CronPeriod.DayOfWeek:
157+
this.dayOfWeek = this.prepareDayOfWeekValueRange(from, to);
158+
break;
159+
case CronPeriod.DayOfMonth:
160+
this.dayOfMonth = this.prepareDayOfMonthValueRange(from, to);
161+
break;
162+
case CronPeriod.Month:
163+
this.month = this.prepareMonthValueRange(from, to);
164+
break;
165+
}
166+
167+
return this;
168+
}
169+
170+
public every(period: CronPeriod): this {
171+
switch (period) {
172+
case CronPeriod.Second:
173+
this.seconds = "*";
174+
break;
175+
case CronPeriod.Minute:
176+
this.minutes = "*";
177+
break;
178+
case CronPeriod.Hour:
179+
this.hours = "*";
180+
break;
181+
case CronPeriod.DayOfWeek:
182+
this.dayOfWeek = "*";
183+
break;
184+
case CronPeriod.DayOfMonth:
185+
this.dayOfMonth = "*";
186+
break;
187+
case CronPeriod.Month:
188+
this.month = "*";
189+
break;
190+
}
191+
return this;
192+
}
193+
194+
public toString() {
195+
return `${this.seconds} ${this.minutes} ${this.hours} ${this.dayOfMonth} ${this.month} ${this.dayOfWeek}`;
196+
}
197+
198+
private prepareTimeValueRange(from: TimeValue, to: TimeValue) {
199+
if (from >= to)
200+
throw new Error("Invalid range: 'from' must be less than 'to'");
201+
202+
if ([from, to].some((v) => v < 0 || v > 59))
203+
throw new RangeError("Time values must be within 0 and 59, inclusively.");
204+
205+
return `${from}-${to}`;
206+
}
207+
208+
private prepareDayOfMonthValueRange(from: DayOfMonth, to: DayOfMonth) {
209+
if (from >= to)
210+
throw new Error("Invalid range: 'from' must be less than 'to'");
211+
212+
if ([from, to].some((v) => v < 1 || v > 31))
213+
throw new RangeError("Day of month values must be within 1 and 31, inclusively.");
214+
215+
return `${from}-${to}`;
216+
}
217+
218+
private prepareMonthValueRange(from: Month, to: Month) {
219+
if (Object.values(Month).indexOf(from) >= Object.values(Month).indexOf(to))
220+
throw new Error("Invalid range: 'from' must be less than 'to'");
221+
222+
return `${from}-${to}`;
223+
}
224+
225+
private prepareDayOfWeekValueRange(from: DayOfWeek, to: DayOfWeek) {
226+
if (Object.values(DayOfWeek).indexOf(from) >= Object.values(DayOfWeek).indexOf(to))
227+
throw new Error("Invalid range: 'from' must be less than 'to'");
228+
229+
return `${from}-${to}`;
230+
}
231+
232+
private prepareTimeValueList(timeValues: TimeValue[]) {
233+
if (timeValues.some((v) => v < 0 || v > 59))
234+
throw new RangeError("Time values must be within 0 and 59, inclusively.");
235+
236+
return [...timeValues].sort((a, b) => a - b).join(",");
237+
}
238+
239+
private prepareDayOfMonthValueList(dayOfMonthValues: DayOfMonth[]) {
240+
if (dayOfMonthValues.some((v) => v < 1 || v > 31))
241+
throw new RangeError("Day of month values must be within 1 and 31, inclusively.");
242+
243+
return [...dayOfMonthValues].sort((a, b) => a - b).join(",");
244+
}
245+
246+
private prepareMonthValueList(monthValues: Month[]) {
247+
return [...monthValues]
248+
.sort((left, right) => Object.values(Month).indexOf(left) - Object.values(Month).indexOf(right))
249+
.join(",");
250+
}
251+
252+
private prepareDayOfWeekValueList(dayValues: DayOfWeek[]) {
253+
return [...dayValues]
254+
.sort((left, right) => Object.values(DayOfWeek).indexOf(left) - Object.values(DayOfWeek).indexOf(right))
255+
.join(",");
256+
}
257+
}

src/types/jobs/JobSchedule.type.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,37 @@ See the License for the specific language governing permissions and
1111
limitations under the License.
1212
*/
1313

14-
export enum FixedPeriod {
15-
Yearly = "@yearly",
16-
Monthly = "@monthly",
17-
Weekly = "@weekly",
18-
Daily = "@daily",
19-
Hourly = "@hourly",
14+
export enum PrefixedPeriodExpression {
15+
Yearly = "@yearly",
16+
Monthly = "@monthly",
17+
Weekly = "@weekly",
18+
Daily = "@daily",
19+
Hourly = "@hourly",
2020
}
2121

2222
type EveryPeriod = `@every ${string}`;
2323

2424
// note: This can get crazy, more than TS can really handle.
2525
type SystemDCronExpression = `${string} ${string} ${string} ${string} ${string} ${string}`;
2626

27-
type Schedule = FixedPeriod | EveryPeriod | SystemDCronExpression;
27+
type ScheduleString = PrefixedPeriodExpression | EveryPeriod | SystemDCronExpression;
2828

29-
export type JobSchedule = Schedule;
29+
class Schedule {
30+
31+
private static readonly IsEveryExpression = /^@every (d+(m?s|m|h))+$/;
32+
33+
private readonly value: ScheduleString;
34+
35+
constructor(scheduleString: ScheduleString) {
36+
this.value = scheduleString;
37+
}
38+
39+
public get isPrefixedPeriodExpression() {
40+
return (
41+
Object.values(PrefixedPeriodExpression).includes(this.value as PrefixedPeriodExpression)
42+
|| this.value.match(Schedule.IsEveryExpression)
43+
);
44+
}
45+
}
46+
47+
export type JobSchedule = ScheduleString | Schedule;

test/e2e/jobs/jobs.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616

1717
import IClientJobs from "../../../src/interfaces/Client/IClientJobs";
1818

19-
describe("Jobs", async () => {
19+
describe("JobsE2E", async () => {
2020

2121
let jobsClient: IClientJobs
2222

0 commit comments

Comments
 (0)