@mrsekut/ccron 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,121 @@
1
+ export type CalendarInterval = {
2
+ Hour: number;
3
+ Minute: number;
4
+ Weekday?: number; // 0=Sun, 1=Mon, ..., 6=Sat (launchd convention)
5
+ };
6
+
7
+ /**
8
+ * Parse a cron expression into launchd StartCalendarInterval entries.
9
+ *
10
+ * Format: "minute hour * * day-of-week"
11
+ *
12
+ * Constraints (launchd limitations):
13
+ * - Day-of-month and month fields must be "*"
14
+ * - Step values (e.g. *​/5) are not supported
15
+ * - Range values in minute/hour are not supported
16
+ *
17
+ * Examples:
18
+ * "15 17 * * *" → every day at 17:15
19
+ * "15 17 * * 1-5" → weekdays at 17:15
20
+ * "0 22 * * 5" → every Friday at 22:00
21
+ * "0 9 * * 1,3,5" → Mon/Wed/Fri at 9:00
22
+ */
23
+ export function parseSchedule(input: string): CalendarInterval[] {
24
+ const trimmed = input.trim();
25
+ if (!trimmed) {
26
+ throw new ScheduleParseError('Schedule string is empty');
27
+ }
28
+
29
+ const fields = trimmed.split(/\s+/);
30
+ if (fields.length !== 5) {
31
+ throw new ScheduleParseError(
32
+ `Invalid cron expression: "${input}". Expected 5 fields: "minute hour * * day-of-week"`,
33
+ );
34
+ }
35
+
36
+ const minuteField = fields[0]!;
37
+ const hourField = fields[1]!;
38
+ const dayOfMonth = fields[2]!;
39
+ const month = fields[3]!;
40
+ const dayOfWeek = fields[4]!;
41
+
42
+ if (dayOfMonth !== '*' || month !== '*') {
43
+ throw new ScheduleParseError(
44
+ `Day-of-month and month fields must be "*". ` +
45
+ `launchd StartCalendarInterval only supports hour/minute/weekday scheduling.`,
46
+ );
47
+ }
48
+
49
+ if (minuteField.includes('/') || hourField.includes('/')) {
50
+ throw new ScheduleParseError(
51
+ `Step values (e.g. "*/5") are not supported. ` +
52
+ `launchd StartCalendarInterval cannot express interval-based schedules.`,
53
+ );
54
+ }
55
+
56
+ if (minuteField.includes('-') || hourField.includes('-')) {
57
+ throw new ScheduleParseError(
58
+ `Range values (e.g. "9-17") in minute/hour are not supported. ` +
59
+ `launchd StartCalendarInterval can only express fixed times.`,
60
+ );
61
+ }
62
+
63
+ const minute = parseIntStrict(minuteField, 'minute', 0, 59);
64
+ const hour = parseIntStrict(hourField, 'hour', 0, 23);
65
+
66
+ const weekdays = parseDayOfWeek(dayOfWeek);
67
+
68
+ if (weekdays === null) {
69
+ return [{ Hour: hour, Minute: minute }];
70
+ }
71
+
72
+ return weekdays.map(day => ({ Hour: hour, Minute: minute, Weekday: day }));
73
+ }
74
+
75
+ function parseDayOfWeek(field: string): number[] | null {
76
+ if (field === '*') return null;
77
+
78
+ const parts = field.split(',');
79
+ const days: number[] = [];
80
+
81
+ for (const part of parts) {
82
+ if (part.includes('-')) {
83
+ const segments = part.split('-');
84
+ const start = parseIntStrict(segments[0]!, 'day-of-week', 0, 6);
85
+ const end = parseIntStrict(segments[1]!, 'day-of-week', 0, 6);
86
+ if (start <= end) {
87
+ for (let i = start; i <= end; i++) days.push(i);
88
+ } else {
89
+ // Wrap around: e.g. 5-1 → Fri,Sat,Sun,Mon
90
+ for (let i = start; i <= 6; i++) days.push(i);
91
+ for (let i = 0; i <= end; i++) days.push(i);
92
+ }
93
+ } else {
94
+ days.push(parseIntStrict(part, 'day-of-week', 0, 6));
95
+ }
96
+ }
97
+
98
+ return days;
99
+ }
100
+
101
+ function parseIntStrict(
102
+ value: string,
103
+ name: string,
104
+ min: number,
105
+ max: number,
106
+ ): number {
107
+ const num = Number(value);
108
+ if (!Number.isInteger(num) || num < min || num > max) {
109
+ throw new ScheduleParseError(
110
+ `Invalid ${name}: "${value}". Must be an integer between ${min} and ${max}.`,
111
+ );
112
+ }
113
+ return num;
114
+ }
115
+
116
+ export class ScheduleParseError extends Error {
117
+ constructor(message: string) {
118
+ super(message);
119
+ this.name = 'ScheduleParseError';
120
+ }
121
+ }