hron-ts 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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/index.cjs +1988 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +167 -0
- package/dist/index.d.ts +167 -0
- package/dist/index.js +1959 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1988 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
HronError: () => HronError,
|
|
24
|
+
Schedule: () => Schedule,
|
|
25
|
+
Temporal: () => import_polyfill2.Temporal
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/ast.ts
|
|
30
|
+
function weekdayNumber(day) {
|
|
31
|
+
const map = {
|
|
32
|
+
monday: 1,
|
|
33
|
+
tuesday: 2,
|
|
34
|
+
wednesday: 3,
|
|
35
|
+
thursday: 4,
|
|
36
|
+
friday: 5,
|
|
37
|
+
saturday: 6,
|
|
38
|
+
sunday: 7
|
|
39
|
+
};
|
|
40
|
+
return map[day];
|
|
41
|
+
}
|
|
42
|
+
function cronDowNumber(day) {
|
|
43
|
+
const map = {
|
|
44
|
+
sunday: 0,
|
|
45
|
+
monday: 1,
|
|
46
|
+
tuesday: 2,
|
|
47
|
+
wednesday: 3,
|
|
48
|
+
thursday: 4,
|
|
49
|
+
friday: 5,
|
|
50
|
+
saturday: 6
|
|
51
|
+
};
|
|
52
|
+
return map[day];
|
|
53
|
+
}
|
|
54
|
+
function monthNumber(month) {
|
|
55
|
+
const map = {
|
|
56
|
+
jan: 1,
|
|
57
|
+
feb: 2,
|
|
58
|
+
mar: 3,
|
|
59
|
+
apr: 4,
|
|
60
|
+
may: 5,
|
|
61
|
+
jun: 6,
|
|
62
|
+
jul: 7,
|
|
63
|
+
aug: 8,
|
|
64
|
+
sep: 9,
|
|
65
|
+
oct: 10,
|
|
66
|
+
nov: 11,
|
|
67
|
+
dec: 12
|
|
68
|
+
};
|
|
69
|
+
return map[month];
|
|
70
|
+
}
|
|
71
|
+
function parseWeekday(s) {
|
|
72
|
+
const map = {
|
|
73
|
+
monday: "monday",
|
|
74
|
+
mon: "monday",
|
|
75
|
+
tuesday: "tuesday",
|
|
76
|
+
tue: "tuesday",
|
|
77
|
+
wednesday: "wednesday",
|
|
78
|
+
wed: "wednesday",
|
|
79
|
+
thursday: "thursday",
|
|
80
|
+
thu: "thursday",
|
|
81
|
+
friday: "friday",
|
|
82
|
+
fri: "friday",
|
|
83
|
+
saturday: "saturday",
|
|
84
|
+
sat: "saturday",
|
|
85
|
+
sunday: "sunday",
|
|
86
|
+
sun: "sunday"
|
|
87
|
+
};
|
|
88
|
+
return map[s.toLowerCase()] ?? null;
|
|
89
|
+
}
|
|
90
|
+
function parseMonthName(s) {
|
|
91
|
+
const map = {
|
|
92
|
+
january: "jan",
|
|
93
|
+
jan: "jan",
|
|
94
|
+
february: "feb",
|
|
95
|
+
feb: "feb",
|
|
96
|
+
march: "mar",
|
|
97
|
+
mar: "mar",
|
|
98
|
+
april: "apr",
|
|
99
|
+
apr: "apr",
|
|
100
|
+
may: "may",
|
|
101
|
+
june: "jun",
|
|
102
|
+
jun: "jun",
|
|
103
|
+
july: "jul",
|
|
104
|
+
jul: "jul",
|
|
105
|
+
august: "aug",
|
|
106
|
+
aug: "aug",
|
|
107
|
+
september: "sep",
|
|
108
|
+
sep: "sep",
|
|
109
|
+
october: "oct",
|
|
110
|
+
oct: "oct",
|
|
111
|
+
november: "nov",
|
|
112
|
+
nov: "nov",
|
|
113
|
+
december: "dec",
|
|
114
|
+
dec: "dec"
|
|
115
|
+
};
|
|
116
|
+
return map[s.toLowerCase()] ?? null;
|
|
117
|
+
}
|
|
118
|
+
function expandDaySpec(spec) {
|
|
119
|
+
if (spec.type === "single") {
|
|
120
|
+
return [spec.day];
|
|
121
|
+
}
|
|
122
|
+
const result = [];
|
|
123
|
+
for (let d = spec.start; d <= spec.end; d++) {
|
|
124
|
+
result.push(d);
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
function expandMonthTarget(target) {
|
|
129
|
+
if (target.type === "days") {
|
|
130
|
+
return target.specs.flatMap(expandDaySpec);
|
|
131
|
+
}
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
function ordinalToN(ord) {
|
|
135
|
+
const map = {
|
|
136
|
+
first: 1,
|
|
137
|
+
second: 2,
|
|
138
|
+
third: 3,
|
|
139
|
+
fourth: 4,
|
|
140
|
+
fifth: 5
|
|
141
|
+
};
|
|
142
|
+
return map[ord];
|
|
143
|
+
}
|
|
144
|
+
function newScheduleData(expr) {
|
|
145
|
+
return {
|
|
146
|
+
expr,
|
|
147
|
+
timezone: null,
|
|
148
|
+
except: [],
|
|
149
|
+
until: null,
|
|
150
|
+
anchor: null,
|
|
151
|
+
during: []
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/error.ts
|
|
156
|
+
var HronError = class _HronError extends Error {
|
|
157
|
+
kind;
|
|
158
|
+
span;
|
|
159
|
+
input;
|
|
160
|
+
suggestion;
|
|
161
|
+
constructor(kind, message, span, input, suggestion) {
|
|
162
|
+
super(message);
|
|
163
|
+
this.name = "HronError";
|
|
164
|
+
this.kind = kind;
|
|
165
|
+
this.span = span;
|
|
166
|
+
this.input = input;
|
|
167
|
+
this.suggestion = suggestion;
|
|
168
|
+
}
|
|
169
|
+
static lex(message, span, input) {
|
|
170
|
+
return new _HronError("lex", message, span, input);
|
|
171
|
+
}
|
|
172
|
+
static parse(message, span, input, suggestion) {
|
|
173
|
+
return new _HronError("parse", message, span, input, suggestion);
|
|
174
|
+
}
|
|
175
|
+
static eval(message) {
|
|
176
|
+
return new _HronError("eval", message);
|
|
177
|
+
}
|
|
178
|
+
static cron(message) {
|
|
179
|
+
return new _HronError("cron", message);
|
|
180
|
+
}
|
|
181
|
+
displayRich() {
|
|
182
|
+
if ((this.kind === "lex" || this.kind === "parse") && this.span && this.input) {
|
|
183
|
+
let out = `error: ${this.message}
|
|
184
|
+
`;
|
|
185
|
+
out += ` ${this.input}
|
|
186
|
+
`;
|
|
187
|
+
const padding = " ".repeat(this.span.start + 2);
|
|
188
|
+
const underline = "^".repeat(Math.max(this.span.end - this.span.start, 1));
|
|
189
|
+
out += padding + underline;
|
|
190
|
+
if (this.suggestion) {
|
|
191
|
+
out += ` try: "${this.suggestion}"`;
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
return `error: ${this.message}`;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// src/lexer.ts
|
|
200
|
+
function tokenize(input) {
|
|
201
|
+
const lexer = new Lexer(input);
|
|
202
|
+
return lexer.tokenize();
|
|
203
|
+
}
|
|
204
|
+
var Lexer = class {
|
|
205
|
+
input;
|
|
206
|
+
pos;
|
|
207
|
+
afterIn;
|
|
208
|
+
constructor(input) {
|
|
209
|
+
this.input = input;
|
|
210
|
+
this.pos = 0;
|
|
211
|
+
this.afterIn = false;
|
|
212
|
+
}
|
|
213
|
+
tokenize() {
|
|
214
|
+
const tokens = [];
|
|
215
|
+
while (true) {
|
|
216
|
+
this.skipWhitespace();
|
|
217
|
+
if (this.pos >= this.input.length) break;
|
|
218
|
+
if (this.afterIn) {
|
|
219
|
+
this.afterIn = false;
|
|
220
|
+
tokens.push(this.lexTimezone());
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const start = this.pos;
|
|
224
|
+
const ch = this.input[this.pos];
|
|
225
|
+
if (ch === ",") {
|
|
226
|
+
this.pos++;
|
|
227
|
+
tokens.push({ kind: { type: "comma" }, span: { start, end: this.pos } });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (isDigit(ch)) {
|
|
231
|
+
tokens.push(this.lexNumberOrTimeOrDate());
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (isAlpha(ch)) {
|
|
235
|
+
tokens.push(this.lexWord());
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
throw HronError.lex(
|
|
239
|
+
`unexpected character '${ch}'`,
|
|
240
|
+
{ start, end: start + 1 },
|
|
241
|
+
this.input
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return tokens;
|
|
245
|
+
}
|
|
246
|
+
skipWhitespace() {
|
|
247
|
+
while (this.pos < this.input.length && isWhitespace(this.input[this.pos])) {
|
|
248
|
+
this.pos++;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
lexTimezone() {
|
|
252
|
+
this.skipWhitespace();
|
|
253
|
+
const start = this.pos;
|
|
254
|
+
while (this.pos < this.input.length && !isWhitespace(this.input[this.pos])) {
|
|
255
|
+
this.pos++;
|
|
256
|
+
}
|
|
257
|
+
const tz = this.input.slice(start, this.pos);
|
|
258
|
+
if (tz.length === 0) {
|
|
259
|
+
throw HronError.lex(
|
|
260
|
+
"expected timezone after 'in'",
|
|
261
|
+
{ start, end: start + 1 },
|
|
262
|
+
this.input
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return { kind: { type: "timezone", tz }, span: { start, end: this.pos } };
|
|
266
|
+
}
|
|
267
|
+
lexNumberOrTimeOrDate() {
|
|
268
|
+
const start = this.pos;
|
|
269
|
+
const numStart = this.pos;
|
|
270
|
+
while (this.pos < this.input.length && isDigit(this.input[this.pos])) {
|
|
271
|
+
this.pos++;
|
|
272
|
+
}
|
|
273
|
+
const digits = this.input.slice(numStart, this.pos);
|
|
274
|
+
if (digits.length === 4 && this.pos < this.input.length && this.input[this.pos] === "-") {
|
|
275
|
+
const remaining = this.input.slice(start);
|
|
276
|
+
if (remaining.length >= 10 && remaining[4] === "-" && isDigit(remaining[5]) && isDigit(remaining[6]) && remaining[7] === "-" && isDigit(remaining[8]) && isDigit(remaining[9])) {
|
|
277
|
+
this.pos = start + 10;
|
|
278
|
+
return {
|
|
279
|
+
kind: { type: "isoDate", date: this.input.slice(start, this.pos) },
|
|
280
|
+
span: { start, end: this.pos }
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if ((digits.length === 1 || digits.length === 2) && this.pos < this.input.length && this.input[this.pos] === ":") {
|
|
285
|
+
this.pos++;
|
|
286
|
+
const minStart = this.pos;
|
|
287
|
+
while (this.pos < this.input.length && isDigit(this.input[this.pos])) {
|
|
288
|
+
this.pos++;
|
|
289
|
+
}
|
|
290
|
+
const minDigits = this.input.slice(minStart, this.pos);
|
|
291
|
+
if (minDigits.length === 2) {
|
|
292
|
+
const hour = parseInt(digits, 10);
|
|
293
|
+
const minute = parseInt(minDigits, 10);
|
|
294
|
+
if (hour > 23 || minute > 59) {
|
|
295
|
+
throw HronError.lex(
|
|
296
|
+
"invalid time",
|
|
297
|
+
{ start, end: this.pos },
|
|
298
|
+
this.input
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
kind: { type: "time", hour, minute },
|
|
303
|
+
span: { start, end: this.pos }
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const num = parseInt(digits, 10);
|
|
308
|
+
if (isNaN(num)) {
|
|
309
|
+
throw HronError.lex(
|
|
310
|
+
"invalid number",
|
|
311
|
+
{ start, end: this.pos },
|
|
312
|
+
this.input
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (this.pos + 1 < this.input.length) {
|
|
316
|
+
const suffix = this.input.slice(this.pos, this.pos + 2).toLowerCase();
|
|
317
|
+
if (suffix === "st" || suffix === "nd" || suffix === "rd" || suffix === "th") {
|
|
318
|
+
this.pos += 2;
|
|
319
|
+
return {
|
|
320
|
+
kind: { type: "ordinalNumber", value: num },
|
|
321
|
+
span: { start, end: this.pos }
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
kind: { type: "number", value: num },
|
|
327
|
+
span: { start, end: this.pos }
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
lexWord() {
|
|
331
|
+
const start = this.pos;
|
|
332
|
+
while (this.pos < this.input.length && (isAlphanumeric(this.input[this.pos]) || this.input[this.pos] === "_")) {
|
|
333
|
+
this.pos++;
|
|
334
|
+
}
|
|
335
|
+
const word = this.input.slice(start, this.pos).toLowerCase();
|
|
336
|
+
const span = { start, end: this.pos };
|
|
337
|
+
const kind = KEYWORD_MAP[word];
|
|
338
|
+
if (kind === void 0) {
|
|
339
|
+
throw HronError.lex(`unknown keyword '${word}'`, span, this.input);
|
|
340
|
+
}
|
|
341
|
+
if (kind.type === "in") {
|
|
342
|
+
this.afterIn = true;
|
|
343
|
+
}
|
|
344
|
+
return { kind, span };
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
var KEYWORD_MAP = {
|
|
348
|
+
every: { type: "every" },
|
|
349
|
+
on: { type: "on" },
|
|
350
|
+
at: { type: "at" },
|
|
351
|
+
from: { type: "from" },
|
|
352
|
+
to: { type: "to" },
|
|
353
|
+
in: { type: "in" },
|
|
354
|
+
of: { type: "of" },
|
|
355
|
+
the: { type: "the" },
|
|
356
|
+
last: { type: "last" },
|
|
357
|
+
except: { type: "except" },
|
|
358
|
+
until: { type: "until" },
|
|
359
|
+
starting: { type: "starting" },
|
|
360
|
+
during: { type: "during" },
|
|
361
|
+
year: { type: "year" },
|
|
362
|
+
day: { type: "day" },
|
|
363
|
+
weekday: { type: "weekday" },
|
|
364
|
+
weekdays: { type: "weekday" },
|
|
365
|
+
weekend: { type: "weekend" },
|
|
366
|
+
weekends: { type: "weekend" },
|
|
367
|
+
weeks: { type: "weeks" },
|
|
368
|
+
week: { type: "weeks" },
|
|
369
|
+
month: { type: "month" },
|
|
370
|
+
monday: { type: "dayName", name: "monday" },
|
|
371
|
+
mon: { type: "dayName", name: "monday" },
|
|
372
|
+
tuesday: { type: "dayName", name: "tuesday" },
|
|
373
|
+
tue: { type: "dayName", name: "tuesday" },
|
|
374
|
+
wednesday: { type: "dayName", name: "wednesday" },
|
|
375
|
+
wed: { type: "dayName", name: "wednesday" },
|
|
376
|
+
thursday: { type: "dayName", name: "thursday" },
|
|
377
|
+
thu: { type: "dayName", name: "thursday" },
|
|
378
|
+
friday: { type: "dayName", name: "friday" },
|
|
379
|
+
fri: { type: "dayName", name: "friday" },
|
|
380
|
+
saturday: { type: "dayName", name: "saturday" },
|
|
381
|
+
sat: { type: "dayName", name: "saturday" },
|
|
382
|
+
sunday: { type: "dayName", name: "sunday" },
|
|
383
|
+
sun: { type: "dayName", name: "sunday" },
|
|
384
|
+
january: { type: "monthName", name: "jan" },
|
|
385
|
+
jan: { type: "monthName", name: "jan" },
|
|
386
|
+
february: { type: "monthName", name: "feb" },
|
|
387
|
+
feb: { type: "monthName", name: "feb" },
|
|
388
|
+
march: { type: "monthName", name: "mar" },
|
|
389
|
+
mar: { type: "monthName", name: "mar" },
|
|
390
|
+
april: { type: "monthName", name: "apr" },
|
|
391
|
+
apr: { type: "monthName", name: "apr" },
|
|
392
|
+
may: { type: "monthName", name: "may" },
|
|
393
|
+
june: { type: "monthName", name: "jun" },
|
|
394
|
+
jun: { type: "monthName", name: "jun" },
|
|
395
|
+
july: { type: "monthName", name: "jul" },
|
|
396
|
+
jul: { type: "monthName", name: "jul" },
|
|
397
|
+
august: { type: "monthName", name: "aug" },
|
|
398
|
+
aug: { type: "monthName", name: "aug" },
|
|
399
|
+
september: { type: "monthName", name: "sep" },
|
|
400
|
+
sep: { type: "monthName", name: "sep" },
|
|
401
|
+
october: { type: "monthName", name: "oct" },
|
|
402
|
+
oct: { type: "monthName", name: "oct" },
|
|
403
|
+
november: { type: "monthName", name: "nov" },
|
|
404
|
+
nov: { type: "monthName", name: "nov" },
|
|
405
|
+
december: { type: "monthName", name: "dec" },
|
|
406
|
+
dec: { type: "monthName", name: "dec" },
|
|
407
|
+
first: { type: "ordinal", name: "first" },
|
|
408
|
+
second: { type: "ordinal", name: "second" },
|
|
409
|
+
third: { type: "ordinal", name: "third" },
|
|
410
|
+
fourth: { type: "ordinal", name: "fourth" },
|
|
411
|
+
fifth: { type: "ordinal", name: "fifth" },
|
|
412
|
+
min: { type: "intervalUnit", unit: "min" },
|
|
413
|
+
mins: { type: "intervalUnit", unit: "min" },
|
|
414
|
+
minute: { type: "intervalUnit", unit: "min" },
|
|
415
|
+
minutes: { type: "intervalUnit", unit: "min" },
|
|
416
|
+
hour: { type: "intervalUnit", unit: "hours" },
|
|
417
|
+
hours: { type: "intervalUnit", unit: "hours" },
|
|
418
|
+
hr: { type: "intervalUnit", unit: "hours" },
|
|
419
|
+
hrs: { type: "intervalUnit", unit: "hours" }
|
|
420
|
+
};
|
|
421
|
+
function isDigit(ch) {
|
|
422
|
+
return ch >= "0" && ch <= "9";
|
|
423
|
+
}
|
|
424
|
+
function isAlpha(ch) {
|
|
425
|
+
return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z";
|
|
426
|
+
}
|
|
427
|
+
function isAlphanumeric(ch) {
|
|
428
|
+
return isDigit(ch) || isAlpha(ch);
|
|
429
|
+
}
|
|
430
|
+
function isWhitespace(ch) {
|
|
431
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r";
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/parser.ts
|
|
435
|
+
var Parser = class {
|
|
436
|
+
tokens;
|
|
437
|
+
pos;
|
|
438
|
+
input;
|
|
439
|
+
constructor(tokens, input) {
|
|
440
|
+
this.tokens = tokens;
|
|
441
|
+
this.pos = 0;
|
|
442
|
+
this.input = input;
|
|
443
|
+
}
|
|
444
|
+
peek() {
|
|
445
|
+
return this.tokens[this.pos];
|
|
446
|
+
}
|
|
447
|
+
peekKind() {
|
|
448
|
+
return this.tokens[this.pos]?.kind;
|
|
449
|
+
}
|
|
450
|
+
advance() {
|
|
451
|
+
const tok = this.tokens[this.pos];
|
|
452
|
+
if (tok) this.pos++;
|
|
453
|
+
return tok;
|
|
454
|
+
}
|
|
455
|
+
currentSpan() {
|
|
456
|
+
const tok = this.peek();
|
|
457
|
+
if (tok) return tok.span;
|
|
458
|
+
const last = this.tokens[this.tokens.length - 1];
|
|
459
|
+
if (last) return { start: last.span.end, end: last.span.end };
|
|
460
|
+
return { start: 0, end: 0 };
|
|
461
|
+
}
|
|
462
|
+
error(message, span) {
|
|
463
|
+
return HronError.parse(message, span, this.input);
|
|
464
|
+
}
|
|
465
|
+
errorAtEnd(message) {
|
|
466
|
+
const span = this.tokens.length > 0 ? {
|
|
467
|
+
start: this.tokens[this.tokens.length - 1].span.end,
|
|
468
|
+
end: this.tokens[this.tokens.length - 1].span.end
|
|
469
|
+
} : { start: 0, end: 0 };
|
|
470
|
+
return HronError.parse(message, span, this.input);
|
|
471
|
+
}
|
|
472
|
+
consumeKind(expected, check) {
|
|
473
|
+
const span = this.currentSpan();
|
|
474
|
+
const tok = this.peek();
|
|
475
|
+
if (tok && check(tok.kind)) {
|
|
476
|
+
this.pos++;
|
|
477
|
+
return tok;
|
|
478
|
+
}
|
|
479
|
+
if (tok) {
|
|
480
|
+
throw this.error(`expected ${expected}, got ${tok.kind.type}`, span);
|
|
481
|
+
}
|
|
482
|
+
throw this.errorAtEnd(`expected ${expected}`);
|
|
483
|
+
}
|
|
484
|
+
// --- Grammar productions ---
|
|
485
|
+
parseExpression() {
|
|
486
|
+
const span = this.currentSpan();
|
|
487
|
+
const kind = this.peekKind();
|
|
488
|
+
let expr;
|
|
489
|
+
if (kind?.type === "every") {
|
|
490
|
+
this.advance();
|
|
491
|
+
expr = this.parseEvery();
|
|
492
|
+
} else if (kind?.type === "on") {
|
|
493
|
+
this.advance();
|
|
494
|
+
expr = this.parseOn();
|
|
495
|
+
} else if (kind?.type === "ordinal" || kind?.type === "last") {
|
|
496
|
+
expr = this.parseOrdinalRepeat();
|
|
497
|
+
} else {
|
|
498
|
+
throw this.error(
|
|
499
|
+
"expected 'every', 'on', or an ordinal (first, second, ...)",
|
|
500
|
+
span
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
return this.parseTrailingClauses(expr);
|
|
504
|
+
}
|
|
505
|
+
parseTrailingClauses(expr) {
|
|
506
|
+
const schedule = newScheduleData(expr);
|
|
507
|
+
if (this.peekKind()?.type === "except") {
|
|
508
|
+
this.advance();
|
|
509
|
+
schedule.except = this.parseExceptionList();
|
|
510
|
+
}
|
|
511
|
+
if (this.peekKind()?.type === "until") {
|
|
512
|
+
this.advance();
|
|
513
|
+
schedule.until = this.parseUntilSpec();
|
|
514
|
+
}
|
|
515
|
+
if (this.peekKind()?.type === "starting") {
|
|
516
|
+
this.advance();
|
|
517
|
+
const k = this.peekKind();
|
|
518
|
+
if (k?.type === "isoDate") {
|
|
519
|
+
schedule.anchor = k.date;
|
|
520
|
+
this.advance();
|
|
521
|
+
} else {
|
|
522
|
+
throw this.error(
|
|
523
|
+
"expected ISO date (YYYY-MM-DD) after 'starting'",
|
|
524
|
+
this.currentSpan()
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (this.peekKind()?.type === "during") {
|
|
529
|
+
this.advance();
|
|
530
|
+
schedule.during = this.parseMonthList();
|
|
531
|
+
}
|
|
532
|
+
if (this.peekKind()?.type === "in") {
|
|
533
|
+
this.advance();
|
|
534
|
+
const k = this.peekKind();
|
|
535
|
+
if (k?.type === "timezone") {
|
|
536
|
+
schedule.timezone = k.tz;
|
|
537
|
+
this.advance();
|
|
538
|
+
} else {
|
|
539
|
+
throw this.error("expected timezone after 'in'", this.currentSpan());
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return schedule;
|
|
543
|
+
}
|
|
544
|
+
parseExceptionList() {
|
|
545
|
+
const exceptions = [this.parseException()];
|
|
546
|
+
while (this.peekKind()?.type === "comma") {
|
|
547
|
+
this.advance();
|
|
548
|
+
exceptions.push(this.parseException());
|
|
549
|
+
}
|
|
550
|
+
return exceptions;
|
|
551
|
+
}
|
|
552
|
+
parseException() {
|
|
553
|
+
const k = this.peekKind();
|
|
554
|
+
if (k?.type === "isoDate") {
|
|
555
|
+
const date = k.date;
|
|
556
|
+
this.advance();
|
|
557
|
+
return { type: "iso", date };
|
|
558
|
+
}
|
|
559
|
+
if (k?.type === "monthName") {
|
|
560
|
+
const month = parseMonthName(
|
|
561
|
+
k.name
|
|
562
|
+
);
|
|
563
|
+
this.advance();
|
|
564
|
+
const day = this.parseDayNumber(
|
|
565
|
+
"expected day number after month name in exception"
|
|
566
|
+
);
|
|
567
|
+
return { type: "named", month, day };
|
|
568
|
+
}
|
|
569
|
+
throw this.error(
|
|
570
|
+
"expected ISO date or month-day in exception",
|
|
571
|
+
this.currentSpan()
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
parseUntilSpec() {
|
|
575
|
+
const k = this.peekKind();
|
|
576
|
+
if (k?.type === "isoDate") {
|
|
577
|
+
const date = k.date;
|
|
578
|
+
this.advance();
|
|
579
|
+
return { type: "iso", date };
|
|
580
|
+
}
|
|
581
|
+
if (k?.type === "monthName") {
|
|
582
|
+
const month = parseMonthName(
|
|
583
|
+
k.name
|
|
584
|
+
);
|
|
585
|
+
this.advance();
|
|
586
|
+
const day = this.parseDayNumber(
|
|
587
|
+
"expected day number after month name in until"
|
|
588
|
+
);
|
|
589
|
+
return { type: "named", month, day };
|
|
590
|
+
}
|
|
591
|
+
throw this.error(
|
|
592
|
+
"expected ISO date or month-day after 'until'",
|
|
593
|
+
this.currentSpan()
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
parseDayNumber(errorMsg) {
|
|
597
|
+
const k = this.peekKind();
|
|
598
|
+
if (k?.type === "number") {
|
|
599
|
+
const n = k.value;
|
|
600
|
+
this.advance();
|
|
601
|
+
return n;
|
|
602
|
+
}
|
|
603
|
+
if (k?.type === "ordinalNumber") {
|
|
604
|
+
const n = k.value;
|
|
605
|
+
this.advance();
|
|
606
|
+
return n;
|
|
607
|
+
}
|
|
608
|
+
throw this.error(errorMsg, this.currentSpan());
|
|
609
|
+
}
|
|
610
|
+
// After "every": dispatch
|
|
611
|
+
parseEvery() {
|
|
612
|
+
if (!this.peek()) throw this.errorAtEnd("expected repeater");
|
|
613
|
+
const k = this.peekKind();
|
|
614
|
+
if (k.type === "year") {
|
|
615
|
+
this.advance();
|
|
616
|
+
return this.parseYearRepeat();
|
|
617
|
+
}
|
|
618
|
+
if (k.type === "day") {
|
|
619
|
+
return this.parseDayRepeat({ type: "every" });
|
|
620
|
+
}
|
|
621
|
+
if (k.type === "weekday") {
|
|
622
|
+
this.advance();
|
|
623
|
+
return this.parseDayRepeat({ type: "weekday" });
|
|
624
|
+
}
|
|
625
|
+
if (k.type === "weekend") {
|
|
626
|
+
this.advance();
|
|
627
|
+
return this.parseDayRepeat({ type: "weekend" });
|
|
628
|
+
}
|
|
629
|
+
if (k.type === "dayName") {
|
|
630
|
+
const days = this.parseDayList();
|
|
631
|
+
return this.parseDayRepeat({ type: "days", days });
|
|
632
|
+
}
|
|
633
|
+
if (k.type === "month") {
|
|
634
|
+
this.advance();
|
|
635
|
+
return this.parseMonthRepeat();
|
|
636
|
+
}
|
|
637
|
+
if (k.type === "number") {
|
|
638
|
+
return this.parseNumberRepeat();
|
|
639
|
+
}
|
|
640
|
+
throw this.error(
|
|
641
|
+
"expected day, weekday, weekend, year, day name, month, or number after 'every'",
|
|
642
|
+
this.currentSpan()
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
parseDayRepeat(days) {
|
|
646
|
+
if (days.type === "every") {
|
|
647
|
+
this.consumeKind("'day'", (k) => k.type === "day");
|
|
648
|
+
}
|
|
649
|
+
this.consumeKind("'at'", (k) => k.type === "at");
|
|
650
|
+
const times = this.parseTimeList();
|
|
651
|
+
return { type: "dayRepeat", days, times };
|
|
652
|
+
}
|
|
653
|
+
parseNumberRepeat() {
|
|
654
|
+
const k = this.peekKind();
|
|
655
|
+
const num = k.value;
|
|
656
|
+
this.advance();
|
|
657
|
+
const next = this.peekKind();
|
|
658
|
+
if (next?.type === "weeks") {
|
|
659
|
+
this.advance();
|
|
660
|
+
return this.parseWeekRepeat(num);
|
|
661
|
+
}
|
|
662
|
+
if (next?.type === "intervalUnit") {
|
|
663
|
+
return this.parseIntervalRepeat(num);
|
|
664
|
+
}
|
|
665
|
+
throw this.error(
|
|
666
|
+
"expected 'weeks', 'min', 'minutes', 'hour', or 'hours' after number",
|
|
667
|
+
this.currentSpan()
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
parseIntervalRepeat(interval) {
|
|
671
|
+
const k = this.peekKind();
|
|
672
|
+
const unitStr = k.unit;
|
|
673
|
+
this.advance();
|
|
674
|
+
const unit = unitStr === "min" ? "min" : "hours";
|
|
675
|
+
this.consumeKind("'from'", (k2) => k2.type === "from");
|
|
676
|
+
const from = this.parseTime();
|
|
677
|
+
this.consumeKind("'to'", (k2) => k2.type === "to");
|
|
678
|
+
const to = this.parseTime();
|
|
679
|
+
let dayFilter = null;
|
|
680
|
+
if (this.peekKind()?.type === "on") {
|
|
681
|
+
this.advance();
|
|
682
|
+
dayFilter = this.parseDayTarget();
|
|
683
|
+
}
|
|
684
|
+
return { type: "intervalRepeat", interval, unit, from, to, dayFilter };
|
|
685
|
+
}
|
|
686
|
+
parseWeekRepeat(interval) {
|
|
687
|
+
this.consumeKind("'on'", (k) => k.type === "on");
|
|
688
|
+
const days = this.parseDayList();
|
|
689
|
+
this.consumeKind("'at'", (k) => k.type === "at");
|
|
690
|
+
const times = this.parseTimeList();
|
|
691
|
+
return { type: "weekRepeat", interval, days, times };
|
|
692
|
+
}
|
|
693
|
+
parseMonthRepeat() {
|
|
694
|
+
this.consumeKind("'on'", (k2) => k2.type === "on");
|
|
695
|
+
this.consumeKind("'the'", (k2) => k2.type === "the");
|
|
696
|
+
let target;
|
|
697
|
+
const k = this.peekKind();
|
|
698
|
+
if (k?.type === "last") {
|
|
699
|
+
this.advance();
|
|
700
|
+
const next = this.peekKind();
|
|
701
|
+
if (next?.type === "day") {
|
|
702
|
+
this.advance();
|
|
703
|
+
target = { type: "lastDay" };
|
|
704
|
+
} else if (next?.type === "weekday") {
|
|
705
|
+
this.advance();
|
|
706
|
+
target = { type: "lastWeekday" };
|
|
707
|
+
} else {
|
|
708
|
+
throw this.error(
|
|
709
|
+
"expected 'day' or 'weekday' after 'last'",
|
|
710
|
+
this.currentSpan()
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
} else if (k?.type === "ordinalNumber") {
|
|
714
|
+
const specs = this.parseOrdinalDayList();
|
|
715
|
+
target = { type: "days", specs };
|
|
716
|
+
} else {
|
|
717
|
+
throw this.error(
|
|
718
|
+
"expected ordinal day (1st, 15th) or 'last' after 'the'",
|
|
719
|
+
this.currentSpan()
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
this.consumeKind("'at'", (k2) => k2.type === "at");
|
|
723
|
+
const times = this.parseTimeList();
|
|
724
|
+
return { type: "monthRepeat", target, times };
|
|
725
|
+
}
|
|
726
|
+
parseOrdinalRepeat() {
|
|
727
|
+
const ordinal = this.parseOrdinalPosition();
|
|
728
|
+
const k = this.peekKind();
|
|
729
|
+
if (k?.type !== "dayName") {
|
|
730
|
+
throw this.error("expected day name after ordinal", this.currentSpan());
|
|
731
|
+
}
|
|
732
|
+
const day = parseWeekday(
|
|
733
|
+
k.name
|
|
734
|
+
);
|
|
735
|
+
this.advance();
|
|
736
|
+
this.consumeKind("'of'", (k2) => k2.type === "of");
|
|
737
|
+
this.consumeKind("'every'", (k2) => k2.type === "every");
|
|
738
|
+
this.consumeKind("'month'", (k2) => k2.type === "month");
|
|
739
|
+
this.consumeKind("'at'", (k2) => k2.type === "at");
|
|
740
|
+
const times = this.parseTimeList();
|
|
741
|
+
return { type: "ordinalRepeat", ordinal, day, times };
|
|
742
|
+
}
|
|
743
|
+
parseYearRepeat() {
|
|
744
|
+
this.consumeKind("'on'", (k2) => k2.type === "on");
|
|
745
|
+
let target;
|
|
746
|
+
const k = this.peekKind();
|
|
747
|
+
if (k?.type === "the") {
|
|
748
|
+
this.advance();
|
|
749
|
+
target = this.parseYearTargetAfterThe();
|
|
750
|
+
} else if (k?.type === "monthName") {
|
|
751
|
+
const month = parseMonthName(
|
|
752
|
+
k.name
|
|
753
|
+
);
|
|
754
|
+
this.advance();
|
|
755
|
+
const day = this.parseDayNumber(
|
|
756
|
+
"expected day number after month name"
|
|
757
|
+
);
|
|
758
|
+
target = { type: "date", month, day };
|
|
759
|
+
} else {
|
|
760
|
+
throw this.error(
|
|
761
|
+
"expected month name or 'the' after 'every year on'",
|
|
762
|
+
this.currentSpan()
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
this.consumeKind("'at'", (k2) => k2.type === "at");
|
|
766
|
+
const times = this.parseTimeList();
|
|
767
|
+
return { type: "yearRepeat", target, times };
|
|
768
|
+
}
|
|
769
|
+
parseYearTargetAfterThe() {
|
|
770
|
+
const k = this.peekKind();
|
|
771
|
+
if (k?.type === "last") {
|
|
772
|
+
this.advance();
|
|
773
|
+
const next = this.peekKind();
|
|
774
|
+
if (next?.type === "weekday") {
|
|
775
|
+
this.advance();
|
|
776
|
+
this.consumeKind("'of'", (k2) => k2.type === "of");
|
|
777
|
+
const month = this.parseMonthNameToken();
|
|
778
|
+
return { type: "lastWeekday", month };
|
|
779
|
+
}
|
|
780
|
+
if (next?.type === "dayName") {
|
|
781
|
+
const weekday = parseWeekday(
|
|
782
|
+
next.name
|
|
783
|
+
);
|
|
784
|
+
this.advance();
|
|
785
|
+
this.consumeKind("'of'", (k2) => k2.type === "of");
|
|
786
|
+
const month = this.parseMonthNameToken();
|
|
787
|
+
return { type: "ordinalWeekday", ordinal: "last", weekday, month };
|
|
788
|
+
}
|
|
789
|
+
throw this.error(
|
|
790
|
+
"expected 'weekday' or day name after 'last' in yearly expression",
|
|
791
|
+
this.currentSpan()
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
if (k?.type === "ordinal") {
|
|
795
|
+
const ordinal = this.parseOrdinalPosition();
|
|
796
|
+
const next = this.peekKind();
|
|
797
|
+
if (next?.type === "dayName") {
|
|
798
|
+
const weekday = parseWeekday(
|
|
799
|
+
next.name
|
|
800
|
+
);
|
|
801
|
+
this.advance();
|
|
802
|
+
this.consumeKind("'of'", (k2) => k2.type === "of");
|
|
803
|
+
const month = this.parseMonthNameToken();
|
|
804
|
+
return { type: "ordinalWeekday", ordinal, weekday, month };
|
|
805
|
+
}
|
|
806
|
+
throw this.error(
|
|
807
|
+
"expected day name after ordinal in yearly expression",
|
|
808
|
+
this.currentSpan()
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
if (k?.type === "ordinalNumber") {
|
|
812
|
+
const day = k.value;
|
|
813
|
+
this.advance();
|
|
814
|
+
this.consumeKind("'of'", (k2) => k2.type === "of");
|
|
815
|
+
const month = this.parseMonthNameToken();
|
|
816
|
+
return { type: "dayOfMonth", day, month };
|
|
817
|
+
}
|
|
818
|
+
throw this.error(
|
|
819
|
+
"expected ordinal, day number, or 'last' after 'the' in yearly expression",
|
|
820
|
+
this.currentSpan()
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
parseMonthNameToken() {
|
|
824
|
+
const k = this.peekKind();
|
|
825
|
+
if (k?.type === "monthName") {
|
|
826
|
+
const month = parseMonthName(
|
|
827
|
+
k.name
|
|
828
|
+
);
|
|
829
|
+
this.advance();
|
|
830
|
+
return month;
|
|
831
|
+
}
|
|
832
|
+
throw this.error("expected month name", this.currentSpan());
|
|
833
|
+
}
|
|
834
|
+
parseOrdinalPosition() {
|
|
835
|
+
const span = this.currentSpan();
|
|
836
|
+
const k = this.peekKind();
|
|
837
|
+
if (k?.type === "ordinal") {
|
|
838
|
+
const name = k.name;
|
|
839
|
+
this.advance();
|
|
840
|
+
return name;
|
|
841
|
+
}
|
|
842
|
+
if (k?.type === "last") {
|
|
843
|
+
this.advance();
|
|
844
|
+
return "last";
|
|
845
|
+
}
|
|
846
|
+
throw this.error(
|
|
847
|
+
"expected ordinal (first, second, third, fourth, fifth, last)",
|
|
848
|
+
span
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
parseOn() {
|
|
852
|
+
const date = this.parseDateTarget();
|
|
853
|
+
this.consumeKind("'at'", (k) => k.type === "at");
|
|
854
|
+
const times = this.parseTimeList();
|
|
855
|
+
return { type: "singleDate", date, times };
|
|
856
|
+
}
|
|
857
|
+
parseDateTarget() {
|
|
858
|
+
const k = this.peekKind();
|
|
859
|
+
if (k?.type === "isoDate") {
|
|
860
|
+
const date = k.date;
|
|
861
|
+
this.advance();
|
|
862
|
+
return { type: "iso", date };
|
|
863
|
+
}
|
|
864
|
+
if (k?.type === "monthName") {
|
|
865
|
+
const month = parseMonthName(
|
|
866
|
+
k.name
|
|
867
|
+
);
|
|
868
|
+
this.advance();
|
|
869
|
+
const day = this.parseDayNumber(
|
|
870
|
+
"expected day number after month name"
|
|
871
|
+
);
|
|
872
|
+
return { type: "named", month, day };
|
|
873
|
+
}
|
|
874
|
+
throw this.error(
|
|
875
|
+
"expected date (ISO date or month name)",
|
|
876
|
+
this.currentSpan()
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
parseDayTarget() {
|
|
880
|
+
const k = this.peekKind();
|
|
881
|
+
if (k?.type === "day") {
|
|
882
|
+
this.advance();
|
|
883
|
+
return { type: "every" };
|
|
884
|
+
}
|
|
885
|
+
if (k?.type === "weekday") {
|
|
886
|
+
this.advance();
|
|
887
|
+
return { type: "weekday" };
|
|
888
|
+
}
|
|
889
|
+
if (k?.type === "weekend") {
|
|
890
|
+
this.advance();
|
|
891
|
+
return { type: "weekend" };
|
|
892
|
+
}
|
|
893
|
+
if (k?.type === "dayName") {
|
|
894
|
+
const days = this.parseDayList();
|
|
895
|
+
return { type: "days", days };
|
|
896
|
+
}
|
|
897
|
+
throw this.error(
|
|
898
|
+
"expected 'day', 'weekday', 'weekend', or day name",
|
|
899
|
+
this.currentSpan()
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
parseDayList() {
|
|
903
|
+
const k = this.peekKind();
|
|
904
|
+
if (k?.type !== "dayName") {
|
|
905
|
+
throw this.error("expected day name", this.currentSpan());
|
|
906
|
+
}
|
|
907
|
+
const days = [
|
|
908
|
+
parseWeekday(k.name)
|
|
909
|
+
];
|
|
910
|
+
this.advance();
|
|
911
|
+
while (this.peekKind()?.type === "comma") {
|
|
912
|
+
this.advance();
|
|
913
|
+
const next = this.peekKind();
|
|
914
|
+
if (next?.type !== "dayName") {
|
|
915
|
+
throw this.error("expected day name after ','", this.currentSpan());
|
|
916
|
+
}
|
|
917
|
+
days.push(
|
|
918
|
+
parseWeekday(
|
|
919
|
+
next.name
|
|
920
|
+
)
|
|
921
|
+
);
|
|
922
|
+
this.advance();
|
|
923
|
+
}
|
|
924
|
+
return days;
|
|
925
|
+
}
|
|
926
|
+
parseOrdinalDayList() {
|
|
927
|
+
const specs = [this.parseOrdinalDaySpec()];
|
|
928
|
+
while (this.peekKind()?.type === "comma") {
|
|
929
|
+
this.advance();
|
|
930
|
+
specs.push(this.parseOrdinalDaySpec());
|
|
931
|
+
}
|
|
932
|
+
return specs;
|
|
933
|
+
}
|
|
934
|
+
parseOrdinalDaySpec() {
|
|
935
|
+
const k = this.peekKind();
|
|
936
|
+
if (k?.type !== "ordinalNumber") {
|
|
937
|
+
throw this.error("expected ordinal day number", this.currentSpan());
|
|
938
|
+
}
|
|
939
|
+
const start = k.value;
|
|
940
|
+
this.advance();
|
|
941
|
+
if (this.peekKind()?.type === "to") {
|
|
942
|
+
this.advance();
|
|
943
|
+
const next = this.peekKind();
|
|
944
|
+
if (next?.type !== "ordinalNumber") {
|
|
945
|
+
throw this.error(
|
|
946
|
+
"expected ordinal day number after 'to'",
|
|
947
|
+
this.currentSpan()
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
const end = next.value;
|
|
951
|
+
this.advance();
|
|
952
|
+
return { type: "range", start, end };
|
|
953
|
+
}
|
|
954
|
+
return { type: "single", day: start };
|
|
955
|
+
}
|
|
956
|
+
parseMonthList() {
|
|
957
|
+
const months = [this.parseMonthNameToken()];
|
|
958
|
+
while (this.peekKind()?.type === "comma") {
|
|
959
|
+
this.advance();
|
|
960
|
+
months.push(this.parseMonthNameToken());
|
|
961
|
+
}
|
|
962
|
+
return months;
|
|
963
|
+
}
|
|
964
|
+
parseTimeList() {
|
|
965
|
+
const times = [this.parseTime()];
|
|
966
|
+
while (this.peekKind()?.type === "comma") {
|
|
967
|
+
this.advance();
|
|
968
|
+
times.push(this.parseTime());
|
|
969
|
+
}
|
|
970
|
+
return times;
|
|
971
|
+
}
|
|
972
|
+
parseTime() {
|
|
973
|
+
const span = this.currentSpan();
|
|
974
|
+
const k = this.peekKind();
|
|
975
|
+
if (k?.type === "time") {
|
|
976
|
+
const { hour, minute } = k;
|
|
977
|
+
this.advance();
|
|
978
|
+
return { hour, minute };
|
|
979
|
+
}
|
|
980
|
+
throw this.error("expected time (HH:MM)", span);
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
function parse(input) {
|
|
984
|
+
const tokens = tokenize(input);
|
|
985
|
+
if (tokens.length === 0) {
|
|
986
|
+
throw HronError.parse("empty expression", { start: 0, end: 0 }, input);
|
|
987
|
+
}
|
|
988
|
+
const parser = new Parser(tokens, input);
|
|
989
|
+
const schedule = parser.parseExpression();
|
|
990
|
+
if (parser.peek()) {
|
|
991
|
+
throw HronError.parse(
|
|
992
|
+
"unexpected tokens after expression",
|
|
993
|
+
parser.currentSpan(),
|
|
994
|
+
input
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
return schedule;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// src/display.ts
|
|
1001
|
+
function display(schedule) {
|
|
1002
|
+
let out = displayExpr(schedule.expr);
|
|
1003
|
+
if (schedule.except.length > 0) {
|
|
1004
|
+
out += " except ";
|
|
1005
|
+
out += schedule.except.map((exc) => {
|
|
1006
|
+
if (exc.type === "named") return `${exc.month} ${exc.day}`;
|
|
1007
|
+
return exc.date;
|
|
1008
|
+
}).join(", ");
|
|
1009
|
+
}
|
|
1010
|
+
if (schedule.until) {
|
|
1011
|
+
if (schedule.until.type === "iso") {
|
|
1012
|
+
out += ` until ${schedule.until.date}`;
|
|
1013
|
+
} else {
|
|
1014
|
+
out += ` until ${schedule.until.month} ${schedule.until.day}`;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (schedule.anchor) {
|
|
1018
|
+
out += ` starting ${schedule.anchor}`;
|
|
1019
|
+
}
|
|
1020
|
+
if (schedule.during.length > 0) {
|
|
1021
|
+
out += " during " + schedule.during.join(", ");
|
|
1022
|
+
}
|
|
1023
|
+
if (schedule.timezone) {
|
|
1024
|
+
out += ` in ${schedule.timezone}`;
|
|
1025
|
+
}
|
|
1026
|
+
return out;
|
|
1027
|
+
}
|
|
1028
|
+
function displayExpr(expr) {
|
|
1029
|
+
switch (expr.type) {
|
|
1030
|
+
case "intervalRepeat": {
|
|
1031
|
+
let out = `every ${expr.interval} ${unitDisplay(expr.interval, expr.unit)}`;
|
|
1032
|
+
out += ` from ${formatTime(expr.from)} to ${formatTime(expr.to)}`;
|
|
1033
|
+
if (expr.dayFilter) {
|
|
1034
|
+
out += ` on ${displayDayFilter(expr.dayFilter)}`;
|
|
1035
|
+
}
|
|
1036
|
+
return out;
|
|
1037
|
+
}
|
|
1038
|
+
case "dayRepeat":
|
|
1039
|
+
return `every ${displayDayFilter(expr.days)} at ${formatTimeList(expr.times)}`;
|
|
1040
|
+
case "weekRepeat":
|
|
1041
|
+
return `every ${expr.interval} weeks on ${formatDayList(expr.days)} at ${formatTimeList(expr.times)}`;
|
|
1042
|
+
case "monthRepeat": {
|
|
1043
|
+
let targetStr;
|
|
1044
|
+
if (expr.target.type === "days") {
|
|
1045
|
+
targetStr = formatOrdinalDaySpecs(expr.target.specs);
|
|
1046
|
+
} else if (expr.target.type === "lastDay") {
|
|
1047
|
+
targetStr = "last day";
|
|
1048
|
+
} else {
|
|
1049
|
+
targetStr = "last weekday";
|
|
1050
|
+
}
|
|
1051
|
+
return `every month on the ${targetStr} at ${formatTimeList(expr.times)}`;
|
|
1052
|
+
}
|
|
1053
|
+
case "ordinalRepeat":
|
|
1054
|
+
return `${expr.ordinal} ${expr.day} of every month at ${formatTimeList(expr.times)}`;
|
|
1055
|
+
case "singleDate": {
|
|
1056
|
+
let dateStr;
|
|
1057
|
+
if (expr.date.type === "named") {
|
|
1058
|
+
dateStr = `${expr.date.month} ${expr.date.day}`;
|
|
1059
|
+
} else {
|
|
1060
|
+
dateStr = expr.date.date;
|
|
1061
|
+
}
|
|
1062
|
+
return `on ${dateStr} at ${formatTimeList(expr.times)}`;
|
|
1063
|
+
}
|
|
1064
|
+
case "yearRepeat": {
|
|
1065
|
+
let targetStr;
|
|
1066
|
+
if (expr.target.type === "date") {
|
|
1067
|
+
targetStr = `${expr.target.month} ${expr.target.day}`;
|
|
1068
|
+
} else if (expr.target.type === "ordinalWeekday") {
|
|
1069
|
+
targetStr = `the ${expr.target.ordinal} ${expr.target.weekday} of ${expr.target.month}`;
|
|
1070
|
+
} else if (expr.target.type === "dayOfMonth") {
|
|
1071
|
+
targetStr = `the ${expr.target.day}${ordinalSuffix(expr.target.day)} of ${expr.target.month}`;
|
|
1072
|
+
} else {
|
|
1073
|
+
targetStr = `the last weekday of ${expr.target.month}`;
|
|
1074
|
+
}
|
|
1075
|
+
return `every year on ${targetStr} at ${formatTimeList(expr.times)}`;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function displayDayFilter(filter) {
|
|
1080
|
+
switch (filter.type) {
|
|
1081
|
+
case "every":
|
|
1082
|
+
return "day";
|
|
1083
|
+
case "weekday":
|
|
1084
|
+
return "weekday";
|
|
1085
|
+
case "weekend":
|
|
1086
|
+
return "weekend";
|
|
1087
|
+
case "days":
|
|
1088
|
+
return formatDayList(filter.days);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function formatTime(t) {
|
|
1092
|
+
return `${String(t.hour).padStart(2, "0")}:${String(t.minute).padStart(2, "0")}`;
|
|
1093
|
+
}
|
|
1094
|
+
function formatTimeList(times) {
|
|
1095
|
+
return times.map(formatTime).join(", ");
|
|
1096
|
+
}
|
|
1097
|
+
function formatDayList(days) {
|
|
1098
|
+
return days.join(", ");
|
|
1099
|
+
}
|
|
1100
|
+
function formatOrdinalDaySpecs(specs) {
|
|
1101
|
+
return specs.map((spec) => {
|
|
1102
|
+
if (spec.type === "single") {
|
|
1103
|
+
return `${spec.day}${ordinalSuffix(spec.day)}`;
|
|
1104
|
+
}
|
|
1105
|
+
return `${spec.start}${ordinalSuffix(spec.start)} to ${spec.end}${ordinalSuffix(spec.end)}`;
|
|
1106
|
+
}).join(", ");
|
|
1107
|
+
}
|
|
1108
|
+
function ordinalSuffix(n) {
|
|
1109
|
+
const mod100 = n % 100;
|
|
1110
|
+
if (mod100 >= 11 && mod100 <= 13) return "th";
|
|
1111
|
+
switch (n % 10) {
|
|
1112
|
+
case 1:
|
|
1113
|
+
return "st";
|
|
1114
|
+
case 2:
|
|
1115
|
+
return "nd";
|
|
1116
|
+
case 3:
|
|
1117
|
+
return "rd";
|
|
1118
|
+
default:
|
|
1119
|
+
return "th";
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function unitDisplay(interval, unit) {
|
|
1123
|
+
if (unit === "min") {
|
|
1124
|
+
return interval === 1 ? "minute" : "min";
|
|
1125
|
+
}
|
|
1126
|
+
return interval === 1 ? "hour" : "hours";
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/eval.ts
|
|
1130
|
+
var import_polyfill = require("@js-temporal/polyfill");
|
|
1131
|
+
function resolveTz(tz) {
|
|
1132
|
+
if (tz) return tz;
|
|
1133
|
+
try {
|
|
1134
|
+
return import_polyfill.Temporal.Now.timeZoneId();
|
|
1135
|
+
} catch {
|
|
1136
|
+
return "UTC";
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function toPlainTime(tod) {
|
|
1140
|
+
return import_polyfill.Temporal.PlainTime.from({ hour: tod.hour, minute: tod.minute });
|
|
1141
|
+
}
|
|
1142
|
+
function atTimeOnDate(date, time, tz) {
|
|
1143
|
+
return date.toPlainDateTime(time).toZonedDateTime(tz, {
|
|
1144
|
+
disambiguation: "compatible"
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
function weekdayNameToNumber(day) {
|
|
1148
|
+
return weekdayNumber(day);
|
|
1149
|
+
}
|
|
1150
|
+
function matchesDayFilter(date, filter) {
|
|
1151
|
+
const dow = date.dayOfWeek;
|
|
1152
|
+
switch (filter.type) {
|
|
1153
|
+
case "every":
|
|
1154
|
+
return true;
|
|
1155
|
+
case "weekday":
|
|
1156
|
+
return dow >= 1 && dow <= 5;
|
|
1157
|
+
case "weekend":
|
|
1158
|
+
return dow === 6 || dow === 7;
|
|
1159
|
+
case "days":
|
|
1160
|
+
return filter.days.some((d) => weekdayNameToNumber(d) === dow);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function lastDayOfMonth(year, month) {
|
|
1164
|
+
return import_polyfill.Temporal.PlainDate.from({ year, month, day: 1 }).add({ months: 1 }).subtract({ days: 1 });
|
|
1165
|
+
}
|
|
1166
|
+
function lastWeekdayOfMonth(year, month) {
|
|
1167
|
+
let d = lastDayOfMonth(year, month);
|
|
1168
|
+
while (d.dayOfWeek === 6 || d.dayOfWeek === 7) {
|
|
1169
|
+
d = d.subtract({ days: 1 });
|
|
1170
|
+
}
|
|
1171
|
+
return d;
|
|
1172
|
+
}
|
|
1173
|
+
function nthWeekdayOfMonth(year, month, weekday, n) {
|
|
1174
|
+
const targetDow = weekdayNameToNumber(weekday);
|
|
1175
|
+
let d = import_polyfill.Temporal.PlainDate.from({ year, month, day: 1 });
|
|
1176
|
+
while (d.dayOfWeek !== targetDow) {
|
|
1177
|
+
d = d.add({ days: 1 });
|
|
1178
|
+
}
|
|
1179
|
+
for (let i = 1; i < n; i++) {
|
|
1180
|
+
d = d.add({ days: 7 });
|
|
1181
|
+
}
|
|
1182
|
+
if (d.month !== month) return null;
|
|
1183
|
+
return d;
|
|
1184
|
+
}
|
|
1185
|
+
function lastWeekdayInMonth(year, month, weekday) {
|
|
1186
|
+
const targetDow = weekdayNameToNumber(weekday);
|
|
1187
|
+
let d = lastDayOfMonth(year, month);
|
|
1188
|
+
while (d.dayOfWeek !== targetDow) {
|
|
1189
|
+
d = d.subtract({ days: 1 });
|
|
1190
|
+
}
|
|
1191
|
+
return d;
|
|
1192
|
+
}
|
|
1193
|
+
var EPOCH_MONDAY = import_polyfill.Temporal.PlainDate.from("1970-01-05");
|
|
1194
|
+
var MIDNIGHT = import_polyfill.Temporal.PlainTime.from({ hour: 0, minute: 0 });
|
|
1195
|
+
function weeksBetween(a, b) {
|
|
1196
|
+
const days = a.until(b, { largestUnit: "days" }).days;
|
|
1197
|
+
return Math.floor(days / 7);
|
|
1198
|
+
}
|
|
1199
|
+
function isExcepted(date, exceptions) {
|
|
1200
|
+
for (const exc of exceptions) {
|
|
1201
|
+
if (exc.type === "named") {
|
|
1202
|
+
if (date.month === monthNumber(exc.month) && date.day === exc.day) {
|
|
1203
|
+
return true;
|
|
1204
|
+
}
|
|
1205
|
+
} else {
|
|
1206
|
+
const excDate = import_polyfill.Temporal.PlainDate.from(exc.date);
|
|
1207
|
+
if (import_polyfill.Temporal.PlainDate.compare(date, excDate) === 0) {
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return false;
|
|
1213
|
+
}
|
|
1214
|
+
function parseExceptions(exceptions) {
|
|
1215
|
+
const named = [];
|
|
1216
|
+
const isoDates = [];
|
|
1217
|
+
for (const exc of exceptions) {
|
|
1218
|
+
if (exc.type === "named") {
|
|
1219
|
+
named.push({ month: monthNumber(exc.month), day: exc.day });
|
|
1220
|
+
} else {
|
|
1221
|
+
isoDates.push(import_polyfill.Temporal.PlainDate.from(exc.date));
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return { named, isoDates };
|
|
1225
|
+
}
|
|
1226
|
+
function isExceptedParsed(date, parsed) {
|
|
1227
|
+
for (const n of parsed.named) {
|
|
1228
|
+
if (date.month === n.month && date.day === n.day) return true;
|
|
1229
|
+
}
|
|
1230
|
+
for (const d of parsed.isoDates) {
|
|
1231
|
+
if (import_polyfill.Temporal.PlainDate.compare(date, d) === 0) return true;
|
|
1232
|
+
}
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
function matchesDuring(date, during) {
|
|
1236
|
+
if (during.length === 0) return true;
|
|
1237
|
+
return during.some((mn) => monthNumber(mn) === date.month);
|
|
1238
|
+
}
|
|
1239
|
+
function nextDuringMonth(date, during) {
|
|
1240
|
+
const currentMonth = date.month;
|
|
1241
|
+
const months = during.map((mn) => monthNumber(mn)).sort((a, b) => a - b);
|
|
1242
|
+
for (const m of months) {
|
|
1243
|
+
if (m > currentMonth) {
|
|
1244
|
+
return import_polyfill.Temporal.PlainDate.from({ year: date.year, month: m, day: 1 });
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return import_polyfill.Temporal.PlainDate.from({ year: date.year + 1, month: months[0], day: 1 });
|
|
1248
|
+
}
|
|
1249
|
+
function resolveUntil(until, now) {
|
|
1250
|
+
if (until.type === "iso") {
|
|
1251
|
+
return import_polyfill.Temporal.PlainDate.from(until.date);
|
|
1252
|
+
}
|
|
1253
|
+
const year = now.toPlainDate().year;
|
|
1254
|
+
for (const y of [year, year + 1]) {
|
|
1255
|
+
try {
|
|
1256
|
+
const d = import_polyfill.Temporal.PlainDate.from({
|
|
1257
|
+
year: y,
|
|
1258
|
+
month: monthNumber(until.month),
|
|
1259
|
+
day: until.day
|
|
1260
|
+
}, { overflow: "reject" });
|
|
1261
|
+
if (import_polyfill.Temporal.PlainDate.compare(d, now.toPlainDate()) >= 0) {
|
|
1262
|
+
return d;
|
|
1263
|
+
}
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return import_polyfill.Temporal.PlainDate.from({
|
|
1268
|
+
year: year + 1,
|
|
1269
|
+
month: monthNumber(until.month),
|
|
1270
|
+
day: until.day
|
|
1271
|
+
}, { overflow: "reject" });
|
|
1272
|
+
}
|
|
1273
|
+
function earliestFutureAtTimes(date, times, tz, now) {
|
|
1274
|
+
let best = null;
|
|
1275
|
+
for (const tod of times) {
|
|
1276
|
+
const t = toPlainTime(tod);
|
|
1277
|
+
const candidate = atTimeOnDate(date, t, tz);
|
|
1278
|
+
if (import_polyfill.Temporal.ZonedDateTime.compare(candidate, now) > 0) {
|
|
1279
|
+
if (best === null || import_polyfill.Temporal.ZonedDateTime.compare(candidate, best) < 0) {
|
|
1280
|
+
best = candidate;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return best;
|
|
1285
|
+
}
|
|
1286
|
+
function nextFrom(schedule, now) {
|
|
1287
|
+
const tz = resolveTz(schedule.timezone);
|
|
1288
|
+
const untilDate = schedule.until ? resolveUntil(schedule.until, now) : null;
|
|
1289
|
+
const parsedExceptions = parseExceptions(schedule.except);
|
|
1290
|
+
const hasExceptions = schedule.except.length > 0;
|
|
1291
|
+
const hasDuring = schedule.during.length > 0;
|
|
1292
|
+
const needsTzConversion = untilDate !== null || hasDuring || hasExceptions;
|
|
1293
|
+
let current = now;
|
|
1294
|
+
for (let i = 0; i < 1e3; i++) {
|
|
1295
|
+
const candidate = nextExpr(schedule.expr, tz, schedule.anchor, current);
|
|
1296
|
+
if (candidate === null) return null;
|
|
1297
|
+
const cDate = needsTzConversion ? candidate.withTimeZone(tz).toPlainDate() : null;
|
|
1298
|
+
if (untilDate) {
|
|
1299
|
+
if (import_polyfill.Temporal.PlainDate.compare(cDate, untilDate) > 0) {
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
if (hasDuring && !matchesDuring(cDate, schedule.during)) {
|
|
1304
|
+
const skipTo = nextDuringMonth(cDate, schedule.during);
|
|
1305
|
+
current = atTimeOnDate(skipTo, MIDNIGHT, tz).subtract({ seconds: 1 });
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
if (hasExceptions && isExceptedParsed(cDate, parsedExceptions)) {
|
|
1309
|
+
const nextDay = cDate.add({ days: 1 });
|
|
1310
|
+
current = atTimeOnDate(
|
|
1311
|
+
nextDay,
|
|
1312
|
+
MIDNIGHT,
|
|
1313
|
+
tz
|
|
1314
|
+
).subtract({ seconds: 1 });
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
return candidate;
|
|
1318
|
+
}
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
function nextExpr(expr, tz, anchor, now) {
|
|
1322
|
+
switch (expr.type) {
|
|
1323
|
+
case "dayRepeat":
|
|
1324
|
+
return nextDayRepeat(expr.days, expr.times, tz, now);
|
|
1325
|
+
case "intervalRepeat":
|
|
1326
|
+
return nextIntervalRepeat(
|
|
1327
|
+
expr.interval,
|
|
1328
|
+
expr.unit,
|
|
1329
|
+
expr.from,
|
|
1330
|
+
expr.to,
|
|
1331
|
+
expr.dayFilter,
|
|
1332
|
+
tz,
|
|
1333
|
+
now
|
|
1334
|
+
);
|
|
1335
|
+
case "weekRepeat":
|
|
1336
|
+
return nextWeekRepeat(expr.interval, expr.days, expr.times, tz, anchor, now);
|
|
1337
|
+
case "monthRepeat":
|
|
1338
|
+
return nextMonthRepeat(expr.target, expr.times, tz, now);
|
|
1339
|
+
case "ordinalRepeat":
|
|
1340
|
+
return nextOrdinalRepeat(expr.ordinal, expr.day, expr.times, tz, now);
|
|
1341
|
+
case "singleDate":
|
|
1342
|
+
return nextSingleDate(expr.date, expr.times, tz, now);
|
|
1343
|
+
case "yearRepeat":
|
|
1344
|
+
return nextYearRepeat(expr.target, expr.times, tz, now);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function nextNFrom(schedule, now, n) {
|
|
1348
|
+
const results = [];
|
|
1349
|
+
let current = now;
|
|
1350
|
+
for (let i = 0; i < n; i++) {
|
|
1351
|
+
const next = nextFrom(schedule, current);
|
|
1352
|
+
if (next === null) break;
|
|
1353
|
+
current = next.add({ minutes: 1 });
|
|
1354
|
+
results.push(next);
|
|
1355
|
+
}
|
|
1356
|
+
return results;
|
|
1357
|
+
}
|
|
1358
|
+
function matches(schedule, datetime) {
|
|
1359
|
+
const tz = resolveTz(schedule.timezone);
|
|
1360
|
+
const zdt = datetime.withTimeZone(tz);
|
|
1361
|
+
const date = zdt.toPlainDate();
|
|
1362
|
+
if (!matchesDuring(date, schedule.during)) return false;
|
|
1363
|
+
if (isExcepted(date, schedule.except)) return false;
|
|
1364
|
+
if (schedule.until) {
|
|
1365
|
+
const untilDate = resolveUntil(schedule.until, datetime);
|
|
1366
|
+
if (import_polyfill.Temporal.PlainDate.compare(date, untilDate) > 0) return false;
|
|
1367
|
+
}
|
|
1368
|
+
const timeMatches = (times) => times.some((tod) => zdt.hour === tod.hour && zdt.minute === tod.minute);
|
|
1369
|
+
switch (schedule.expr.type) {
|
|
1370
|
+
case "dayRepeat": {
|
|
1371
|
+
if (!matchesDayFilter(date, schedule.expr.days)) return false;
|
|
1372
|
+
return timeMatches(schedule.expr.times);
|
|
1373
|
+
}
|
|
1374
|
+
case "intervalRepeat": {
|
|
1375
|
+
const { interval, unit, from, to, dayFilter } = schedule.expr;
|
|
1376
|
+
if (dayFilter && !matchesDayFilter(date, dayFilter)) return false;
|
|
1377
|
+
const fromMinutes = from.hour * 60 + from.minute;
|
|
1378
|
+
const toMinutes = to.hour * 60 + to.minute;
|
|
1379
|
+
const currentMinutes = zdt.hour * 60 + zdt.minute;
|
|
1380
|
+
if (currentMinutes < fromMinutes || currentMinutes > toMinutes) return false;
|
|
1381
|
+
const diff = currentMinutes - fromMinutes;
|
|
1382
|
+
const step = unit === "min" ? interval : interval * 60;
|
|
1383
|
+
return diff >= 0 && diff % step === 0;
|
|
1384
|
+
}
|
|
1385
|
+
case "weekRepeat": {
|
|
1386
|
+
const { interval, days, times } = schedule.expr;
|
|
1387
|
+
const dow = date.dayOfWeek;
|
|
1388
|
+
if (!days.some((d) => weekdayNameToNumber(d) === dow)) return false;
|
|
1389
|
+
if (!timeMatches(times)) return false;
|
|
1390
|
+
const anchorDate = schedule.anchor ? import_polyfill.Temporal.PlainDate.from(schedule.anchor) : EPOCH_MONDAY;
|
|
1391
|
+
const weeks = weeksBetween(anchorDate, date);
|
|
1392
|
+
return weeks >= 0 && weeks % interval === 0;
|
|
1393
|
+
}
|
|
1394
|
+
case "monthRepeat": {
|
|
1395
|
+
if (!timeMatches(schedule.expr.times)) return false;
|
|
1396
|
+
const { target } = schedule.expr;
|
|
1397
|
+
if (target.type === "days") {
|
|
1398
|
+
const expanded = expandMonthTarget(target);
|
|
1399
|
+
return expanded.includes(date.day);
|
|
1400
|
+
}
|
|
1401
|
+
if (target.type === "lastDay") {
|
|
1402
|
+
const last = lastDayOfMonth(date.year, date.month);
|
|
1403
|
+
return import_polyfill.Temporal.PlainDate.compare(date, last) === 0;
|
|
1404
|
+
}
|
|
1405
|
+
const lastWd = lastWeekdayOfMonth(date.year, date.month);
|
|
1406
|
+
return import_polyfill.Temporal.PlainDate.compare(date, lastWd) === 0;
|
|
1407
|
+
}
|
|
1408
|
+
case "ordinalRepeat": {
|
|
1409
|
+
if (!timeMatches(schedule.expr.times)) return false;
|
|
1410
|
+
const { ordinal, day } = schedule.expr;
|
|
1411
|
+
let targetDate;
|
|
1412
|
+
if (ordinal === "last") {
|
|
1413
|
+
targetDate = lastWeekdayInMonth(date.year, date.month, day);
|
|
1414
|
+
} else {
|
|
1415
|
+
targetDate = nthWeekdayOfMonth(
|
|
1416
|
+
date.year,
|
|
1417
|
+
date.month,
|
|
1418
|
+
day,
|
|
1419
|
+
ordinalToN(ordinal)
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
if (!targetDate) return false;
|
|
1423
|
+
return import_polyfill.Temporal.PlainDate.compare(date, targetDate) === 0;
|
|
1424
|
+
}
|
|
1425
|
+
case "singleDate": {
|
|
1426
|
+
if (!timeMatches(schedule.expr.times)) return false;
|
|
1427
|
+
const { date: dateSpec } = schedule.expr;
|
|
1428
|
+
if (dateSpec.type === "iso") {
|
|
1429
|
+
const target = import_polyfill.Temporal.PlainDate.from(dateSpec.date);
|
|
1430
|
+
return import_polyfill.Temporal.PlainDate.compare(date, target) === 0;
|
|
1431
|
+
}
|
|
1432
|
+
if (dateSpec.type === "named") {
|
|
1433
|
+
return date.month === monthNumber(dateSpec.month) && date.day === dateSpec.day;
|
|
1434
|
+
}
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
case "yearRepeat": {
|
|
1438
|
+
if (!timeMatches(schedule.expr.times)) return false;
|
|
1439
|
+
return matchesYearTarget(schedule.expr.target, date);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
function matchesYearTarget(target, date) {
|
|
1444
|
+
switch (target.type) {
|
|
1445
|
+
case "date":
|
|
1446
|
+
return date.month === monthNumber(target.month) && date.day === target.day;
|
|
1447
|
+
case "ordinalWeekday": {
|
|
1448
|
+
if (date.month !== monthNumber(target.month)) return false;
|
|
1449
|
+
let targetDate;
|
|
1450
|
+
if (target.ordinal === "last") {
|
|
1451
|
+
targetDate = lastWeekdayInMonth(date.year, date.month, target.weekday);
|
|
1452
|
+
} else {
|
|
1453
|
+
targetDate = nthWeekdayOfMonth(
|
|
1454
|
+
date.year,
|
|
1455
|
+
date.month,
|
|
1456
|
+
target.weekday,
|
|
1457
|
+
ordinalToN(target.ordinal)
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
if (!targetDate) return false;
|
|
1461
|
+
return import_polyfill.Temporal.PlainDate.compare(date, targetDate) === 0;
|
|
1462
|
+
}
|
|
1463
|
+
case "dayOfMonth":
|
|
1464
|
+
return date.month === monthNumber(target.month) && date.day === target.day;
|
|
1465
|
+
case "lastWeekday": {
|
|
1466
|
+
if (date.month !== monthNumber(target.month)) return false;
|
|
1467
|
+
const lwd = lastWeekdayOfMonth(date.year, date.month);
|
|
1468
|
+
return import_polyfill.Temporal.PlainDate.compare(date, lwd) === 0;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
function nextDayRepeat(days, times, tz, now) {
|
|
1473
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1474
|
+
let date = nowInTz.toPlainDate();
|
|
1475
|
+
if (matchesDayFilter(date, days)) {
|
|
1476
|
+
const candidate = earliestFutureAtTimes(date, times, tz, now);
|
|
1477
|
+
if (candidate) return candidate;
|
|
1478
|
+
}
|
|
1479
|
+
for (let i = 0; i < 8; i++) {
|
|
1480
|
+
date = date.add({ days: 1 });
|
|
1481
|
+
if (matchesDayFilter(date, days)) {
|
|
1482
|
+
const candidate = earliestFutureAtTimes(date, times, tz, now);
|
|
1483
|
+
if (candidate) return candidate;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
function nextIntervalRepeat(interval, unit, from, to, dayFilter, tz, now) {
|
|
1489
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1490
|
+
const stepMinutes = unit === "min" ? interval : interval * 60;
|
|
1491
|
+
const fromMinutes = from.hour * 60 + from.minute;
|
|
1492
|
+
const toMinutes = to.hour * 60 + to.minute;
|
|
1493
|
+
let date = nowInTz.toPlainDate();
|
|
1494
|
+
for (let d = 0; d < 400; d++) {
|
|
1495
|
+
if (dayFilter && !matchesDayFilter(date, dayFilter)) {
|
|
1496
|
+
date = date.add({ days: 1 });
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
const sameDay = import_polyfill.Temporal.PlainDate.compare(date, nowInTz.toPlainDate()) === 0;
|
|
1500
|
+
const nowMinutes = sameDay ? nowInTz.hour * 60 + nowInTz.minute : -1;
|
|
1501
|
+
let nextSlot;
|
|
1502
|
+
if (nowMinutes < fromMinutes) {
|
|
1503
|
+
nextSlot = fromMinutes;
|
|
1504
|
+
} else {
|
|
1505
|
+
const elapsed = nowMinutes - fromMinutes;
|
|
1506
|
+
nextSlot = fromMinutes + (Math.floor(elapsed / stepMinutes) + 1) * stepMinutes;
|
|
1507
|
+
}
|
|
1508
|
+
if (nextSlot <= toMinutes) {
|
|
1509
|
+
const h = Math.floor(nextSlot / 60);
|
|
1510
|
+
const m = nextSlot % 60;
|
|
1511
|
+
const t = import_polyfill.Temporal.PlainTime.from({ hour: h, minute: m });
|
|
1512
|
+
const candidate = atTimeOnDate(date, t, tz);
|
|
1513
|
+
if (import_polyfill.Temporal.ZonedDateTime.compare(candidate, now) > 0) {
|
|
1514
|
+
return candidate;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
date = date.add({ days: 1 });
|
|
1518
|
+
}
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
function nextWeekRepeat(interval, days, times, tz, anchor, now) {
|
|
1522
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1523
|
+
const anchorDate = anchor ? import_polyfill.Temporal.PlainDate.from(anchor) : EPOCH_MONDAY;
|
|
1524
|
+
const date = nowInTz.toPlainDate();
|
|
1525
|
+
const sortedDays = [...days].sort(
|
|
1526
|
+
(a, b) => weekdayNameToNumber(a) - weekdayNameToNumber(b)
|
|
1527
|
+
);
|
|
1528
|
+
const dowOffset = date.dayOfWeek - 1;
|
|
1529
|
+
let currentMonday = date.subtract({ days: dowOffset });
|
|
1530
|
+
const anchorDowOffset = anchorDate.dayOfWeek - 1;
|
|
1531
|
+
const anchorMonday = anchorDate.subtract({ days: anchorDowOffset });
|
|
1532
|
+
for (let i = 0; i < 54; i++) {
|
|
1533
|
+
const weeks = weeksBetween(anchorMonday, currentMonday);
|
|
1534
|
+
if (weeks < 0) {
|
|
1535
|
+
const skip = Math.ceil(-weeks / interval);
|
|
1536
|
+
currentMonday = currentMonday.add({ days: skip * interval * 7 });
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
if (weeks % interval === 0) {
|
|
1540
|
+
for (const wd of sortedDays) {
|
|
1541
|
+
const dayOffset = weekdayNameToNumber(wd) - 1;
|
|
1542
|
+
const targetDate = currentMonday.add({ days: dayOffset });
|
|
1543
|
+
const candidate = earliestFutureAtTimes(targetDate, times, tz, now);
|
|
1544
|
+
if (candidate) return candidate;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
const remainder = weeks % interval;
|
|
1548
|
+
const skipWeeks = remainder === 0 ? interval : interval - remainder;
|
|
1549
|
+
currentMonday = currentMonday.add({ days: skipWeeks * 7 });
|
|
1550
|
+
}
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
function nextMonthRepeat(target, times, tz, now) {
|
|
1554
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1555
|
+
let year = nowInTz.year;
|
|
1556
|
+
let month = nowInTz.month;
|
|
1557
|
+
for (let i = 0; i < 24; i++) {
|
|
1558
|
+
const dateCandidates = [];
|
|
1559
|
+
if (target.type === "days") {
|
|
1560
|
+
const expanded = expandMonthTarget(target);
|
|
1561
|
+
for (const dayNum of expanded) {
|
|
1562
|
+
const last = lastDayOfMonth(year, month);
|
|
1563
|
+
if (dayNum <= last.day) {
|
|
1564
|
+
try {
|
|
1565
|
+
dateCandidates.push(
|
|
1566
|
+
import_polyfill.Temporal.PlainDate.from({ year, month, day: dayNum })
|
|
1567
|
+
);
|
|
1568
|
+
} catch {
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
} else if (target.type === "lastDay") {
|
|
1573
|
+
dateCandidates.push(lastDayOfMonth(year, month));
|
|
1574
|
+
} else {
|
|
1575
|
+
dateCandidates.push(lastWeekdayOfMonth(year, month));
|
|
1576
|
+
}
|
|
1577
|
+
let best = null;
|
|
1578
|
+
for (const date of dateCandidates) {
|
|
1579
|
+
const candidate = earliestFutureAtTimes(date, times, tz, now);
|
|
1580
|
+
if (candidate) {
|
|
1581
|
+
if (best === null || import_polyfill.Temporal.ZonedDateTime.compare(candidate, best) < 0) {
|
|
1582
|
+
best = candidate;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (best) return best;
|
|
1587
|
+
month++;
|
|
1588
|
+
if (month > 12) {
|
|
1589
|
+
month = 1;
|
|
1590
|
+
year++;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
function nextOrdinalRepeat(ordinal, day, times, tz, now) {
|
|
1596
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1597
|
+
let year = nowInTz.year;
|
|
1598
|
+
let month = nowInTz.month;
|
|
1599
|
+
for (let i = 0; i < 24; i++) {
|
|
1600
|
+
let targetDate;
|
|
1601
|
+
if (ordinal === "last") {
|
|
1602
|
+
targetDate = lastWeekdayInMonth(year, month, day);
|
|
1603
|
+
} else {
|
|
1604
|
+
targetDate = nthWeekdayOfMonth(year, month, day, ordinalToN(ordinal));
|
|
1605
|
+
}
|
|
1606
|
+
if (targetDate) {
|
|
1607
|
+
const candidate = earliestFutureAtTimes(targetDate, times, tz, now);
|
|
1608
|
+
if (candidate) return candidate;
|
|
1609
|
+
}
|
|
1610
|
+
month++;
|
|
1611
|
+
if (month > 12) {
|
|
1612
|
+
month = 1;
|
|
1613
|
+
year++;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return null;
|
|
1617
|
+
}
|
|
1618
|
+
function nextSingleDate(dateSpec, times, tz, now) {
|
|
1619
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1620
|
+
if (dateSpec.type === "iso") {
|
|
1621
|
+
const date = import_polyfill.Temporal.PlainDate.from(dateSpec.date);
|
|
1622
|
+
return earliestFutureAtTimes(date, times, tz, now);
|
|
1623
|
+
}
|
|
1624
|
+
if (dateSpec.type === "named") {
|
|
1625
|
+
const startYear = nowInTz.year;
|
|
1626
|
+
for (let y = 0; y < 8; y++) {
|
|
1627
|
+
const year = startYear + y;
|
|
1628
|
+
try {
|
|
1629
|
+
const date = import_polyfill.Temporal.PlainDate.from({
|
|
1630
|
+
year,
|
|
1631
|
+
month: monthNumber(dateSpec.month),
|
|
1632
|
+
day: dateSpec.day
|
|
1633
|
+
}, { overflow: "reject" });
|
|
1634
|
+
const candidate = earliestFutureAtTimes(date, times, tz, now);
|
|
1635
|
+
if (candidate) return candidate;
|
|
1636
|
+
} catch {
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
return null;
|
|
1640
|
+
}
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
function nextYearRepeat(target, times, tz, now) {
|
|
1644
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1645
|
+
const startYear = nowInTz.year;
|
|
1646
|
+
for (let y = 0; y < 8; y++) {
|
|
1647
|
+
const year = startYear + y;
|
|
1648
|
+
let targetDate = null;
|
|
1649
|
+
switch (target.type) {
|
|
1650
|
+
case "date":
|
|
1651
|
+
try {
|
|
1652
|
+
targetDate = import_polyfill.Temporal.PlainDate.from({
|
|
1653
|
+
year,
|
|
1654
|
+
month: monthNumber(target.month),
|
|
1655
|
+
day: target.day
|
|
1656
|
+
}, { overflow: "reject" });
|
|
1657
|
+
} catch {
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
break;
|
|
1661
|
+
case "ordinalWeekday":
|
|
1662
|
+
if (target.ordinal === "last") {
|
|
1663
|
+
targetDate = lastWeekdayInMonth(
|
|
1664
|
+
year,
|
|
1665
|
+
monthNumber(target.month),
|
|
1666
|
+
target.weekday
|
|
1667
|
+
);
|
|
1668
|
+
} else {
|
|
1669
|
+
targetDate = nthWeekdayOfMonth(
|
|
1670
|
+
year,
|
|
1671
|
+
monthNumber(target.month),
|
|
1672
|
+
target.weekday,
|
|
1673
|
+
ordinalToN(target.ordinal)
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
break;
|
|
1677
|
+
case "dayOfMonth":
|
|
1678
|
+
try {
|
|
1679
|
+
targetDate = import_polyfill.Temporal.PlainDate.from({
|
|
1680
|
+
year,
|
|
1681
|
+
month: monthNumber(target.month),
|
|
1682
|
+
day: target.day
|
|
1683
|
+
}, { overflow: "reject" });
|
|
1684
|
+
} catch {
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
break;
|
|
1688
|
+
case "lastWeekday":
|
|
1689
|
+
targetDate = lastWeekdayOfMonth(year, monthNumber(target.month));
|
|
1690
|
+
break;
|
|
1691
|
+
}
|
|
1692
|
+
if (targetDate) {
|
|
1693
|
+
const candidate = earliestFutureAtTimes(targetDate, times, tz, now);
|
|
1694
|
+
if (candidate) return candidate;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/cron.ts
|
|
1701
|
+
function toCron(schedule) {
|
|
1702
|
+
if (schedule.except.length > 0) {
|
|
1703
|
+
throw HronError.cron(
|
|
1704
|
+
"not expressible as cron (except clauses not supported)"
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
if (schedule.until) {
|
|
1708
|
+
throw HronError.cron(
|
|
1709
|
+
"not expressible as cron (until clauses not supported)"
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
if (schedule.during.length > 0) {
|
|
1713
|
+
throw HronError.cron(
|
|
1714
|
+
"not expressible as cron (during clauses not supported)"
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
const expr = schedule.expr;
|
|
1718
|
+
switch (expr.type) {
|
|
1719
|
+
case "dayRepeat": {
|
|
1720
|
+
if (expr.times.length !== 1) {
|
|
1721
|
+
throw HronError.cron(
|
|
1722
|
+
"not expressible as cron (multiple times not supported)"
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
const time = expr.times[0];
|
|
1726
|
+
const dow = dayFilterToCronDow(expr.days);
|
|
1727
|
+
return `${time.minute} ${time.hour} * * ${dow}`;
|
|
1728
|
+
}
|
|
1729
|
+
case "intervalRepeat": {
|
|
1730
|
+
const fullDay = expr.from.hour === 0 && expr.from.minute === 0 && expr.to.hour === 23 && expr.to.minute === 59;
|
|
1731
|
+
if (!fullDay) {
|
|
1732
|
+
throw HronError.cron(
|
|
1733
|
+
"not expressible as cron (partial-day interval windows not supported)"
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
if (expr.dayFilter) {
|
|
1737
|
+
throw HronError.cron(
|
|
1738
|
+
"not expressible as cron (interval with day filter not supported)"
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
if (expr.unit === "min") {
|
|
1742
|
+
if (60 % expr.interval !== 0) {
|
|
1743
|
+
throw HronError.cron(
|
|
1744
|
+
`not expressible as cron (*/${expr.interval} breaks at hour boundaries)`
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
return `*/${expr.interval} * * * *`;
|
|
1748
|
+
}
|
|
1749
|
+
return `0 */${expr.interval} * * *`;
|
|
1750
|
+
}
|
|
1751
|
+
case "weekRepeat":
|
|
1752
|
+
throw HronError.cron(
|
|
1753
|
+
"not expressible as cron (multi-week intervals not supported)"
|
|
1754
|
+
);
|
|
1755
|
+
case "monthRepeat": {
|
|
1756
|
+
if (expr.times.length !== 1) {
|
|
1757
|
+
throw HronError.cron(
|
|
1758
|
+
"not expressible as cron (multiple times not supported)"
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
const time = expr.times[0];
|
|
1762
|
+
const { target } = expr;
|
|
1763
|
+
if (target.type === "days") {
|
|
1764
|
+
const expanded = target.specs.flatMap((s) => {
|
|
1765
|
+
if (s.type === "single") return [s.day];
|
|
1766
|
+
const r = [];
|
|
1767
|
+
for (let d = s.start; d <= s.end; d++) r.push(d);
|
|
1768
|
+
return r;
|
|
1769
|
+
});
|
|
1770
|
+
const dom = expanded.join(",");
|
|
1771
|
+
return `${time.minute} ${time.hour} ${dom} * *`;
|
|
1772
|
+
}
|
|
1773
|
+
if (target.type === "lastDay") {
|
|
1774
|
+
throw HronError.cron(
|
|
1775
|
+
"not expressible as cron (last day of month not supported)"
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
throw HronError.cron(
|
|
1779
|
+
"not expressible as cron (last weekday of month not supported)"
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
case "ordinalRepeat":
|
|
1783
|
+
throw HronError.cron(
|
|
1784
|
+
"not expressible as cron (ordinal weekday of month not supported)"
|
|
1785
|
+
);
|
|
1786
|
+
case "singleDate":
|
|
1787
|
+
throw HronError.cron(
|
|
1788
|
+
"not expressible as cron (single dates are not repeating)"
|
|
1789
|
+
);
|
|
1790
|
+
case "yearRepeat":
|
|
1791
|
+
throw HronError.cron(
|
|
1792
|
+
"not expressible as cron (yearly schedules not supported in 5-field cron)"
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function dayFilterToCronDow(filter) {
|
|
1797
|
+
switch (filter.type) {
|
|
1798
|
+
case "every":
|
|
1799
|
+
return "*";
|
|
1800
|
+
case "weekday":
|
|
1801
|
+
return "1-5";
|
|
1802
|
+
case "weekend":
|
|
1803
|
+
return "0,6";
|
|
1804
|
+
case "days": {
|
|
1805
|
+
const nums = filter.days.map((d) => cronDowNumber(d));
|
|
1806
|
+
nums.sort((a, b) => a - b);
|
|
1807
|
+
return nums.join(",");
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
function fromCron(cron) {
|
|
1812
|
+
const fields = cron.trim().split(/\s+/);
|
|
1813
|
+
if (fields.length !== 5) {
|
|
1814
|
+
throw HronError.cron(`expected 5 cron fields, got ${fields.length}`);
|
|
1815
|
+
}
|
|
1816
|
+
const [minuteField, hourField, domField, _monthField, dowField] = fields;
|
|
1817
|
+
if (minuteField.startsWith("*/")) {
|
|
1818
|
+
const interval = parseInt(minuteField.slice(2), 10);
|
|
1819
|
+
if (isNaN(interval)) throw HronError.cron("invalid minute interval");
|
|
1820
|
+
let fromHour = 0;
|
|
1821
|
+
let toHour = 23;
|
|
1822
|
+
if (hourField === "*") {
|
|
1823
|
+
} else if (hourField.includes("-")) {
|
|
1824
|
+
const [start, end] = hourField.split("-");
|
|
1825
|
+
fromHour = parseInt(start, 10);
|
|
1826
|
+
toHour = parseInt(end, 10);
|
|
1827
|
+
if (isNaN(fromHour) || isNaN(toHour))
|
|
1828
|
+
throw HronError.cron("invalid hour range");
|
|
1829
|
+
} else {
|
|
1830
|
+
const h = parseInt(hourField, 10);
|
|
1831
|
+
if (isNaN(h)) throw HronError.cron("invalid hour");
|
|
1832
|
+
fromHour = h;
|
|
1833
|
+
toHour = h;
|
|
1834
|
+
}
|
|
1835
|
+
const dayFilter = dowField === "*" ? null : parseCronDow(dowField);
|
|
1836
|
+
if (domField === "*") {
|
|
1837
|
+
const expr2 = {
|
|
1838
|
+
type: "intervalRepeat",
|
|
1839
|
+
interval,
|
|
1840
|
+
unit: "min",
|
|
1841
|
+
from: { hour: fromHour, minute: 0 },
|
|
1842
|
+
to: { hour: toHour, minute: toHour === 23 ? 59 : 0 },
|
|
1843
|
+
dayFilter
|
|
1844
|
+
};
|
|
1845
|
+
return newScheduleData(expr2);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (hourField.startsWith("*/") && minuteField === "0") {
|
|
1849
|
+
const interval = parseInt(hourField.slice(2), 10);
|
|
1850
|
+
if (isNaN(interval)) throw HronError.cron("invalid hour interval");
|
|
1851
|
+
if (domField === "*" && dowField === "*") {
|
|
1852
|
+
const expr2 = {
|
|
1853
|
+
type: "intervalRepeat",
|
|
1854
|
+
interval,
|
|
1855
|
+
unit: "hours",
|
|
1856
|
+
from: { hour: 0, minute: 0 },
|
|
1857
|
+
to: { hour: 23, minute: 59 },
|
|
1858
|
+
dayFilter: null
|
|
1859
|
+
};
|
|
1860
|
+
return newScheduleData(expr2);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
const minute = parseInt(minuteField, 10);
|
|
1864
|
+
if (isNaN(minute))
|
|
1865
|
+
throw HronError.cron(`invalid minute field: ${minuteField}`);
|
|
1866
|
+
const hour = parseInt(hourField, 10);
|
|
1867
|
+
if (isNaN(hour)) throw HronError.cron(`invalid hour field: ${hourField}`);
|
|
1868
|
+
const time = { hour, minute };
|
|
1869
|
+
if (domField !== "*" && dowField === "*") {
|
|
1870
|
+
if (domField.includes("-")) {
|
|
1871
|
+
throw HronError.cron(`DOM ranges not supported: ${domField}`);
|
|
1872
|
+
}
|
|
1873
|
+
const dayNums = domField.split(",").map((s) => {
|
|
1874
|
+
const n = parseInt(s, 10);
|
|
1875
|
+
if (isNaN(n))
|
|
1876
|
+
throw HronError.cron(`invalid DOM field: ${domField}`);
|
|
1877
|
+
return n;
|
|
1878
|
+
});
|
|
1879
|
+
const specs = dayNums.map((d) => ({
|
|
1880
|
+
type: "single",
|
|
1881
|
+
day: d
|
|
1882
|
+
}));
|
|
1883
|
+
const expr2 = {
|
|
1884
|
+
type: "monthRepeat",
|
|
1885
|
+
target: { type: "days", specs },
|
|
1886
|
+
times: [time]
|
|
1887
|
+
};
|
|
1888
|
+
return newScheduleData(expr2);
|
|
1889
|
+
}
|
|
1890
|
+
const days = parseCronDow(dowField);
|
|
1891
|
+
const expr = {
|
|
1892
|
+
type: "dayRepeat",
|
|
1893
|
+
days,
|
|
1894
|
+
times: [time]
|
|
1895
|
+
};
|
|
1896
|
+
return newScheduleData(expr);
|
|
1897
|
+
}
|
|
1898
|
+
function parseCronDow(field) {
|
|
1899
|
+
if (field === "*") return { type: "every" };
|
|
1900
|
+
if (field === "1-5") return { type: "weekday" };
|
|
1901
|
+
if (field === "0,6" || field === "6,0") return { type: "weekend" };
|
|
1902
|
+
if (field.includes("-")) {
|
|
1903
|
+
throw HronError.cron(`DOW ranges not supported: ${field}`);
|
|
1904
|
+
}
|
|
1905
|
+
const nums = field.split(",").map((s) => {
|
|
1906
|
+
const n = parseInt(s, 10);
|
|
1907
|
+
if (isNaN(n)) throw HronError.cron(`invalid DOW field: ${field}`);
|
|
1908
|
+
return n;
|
|
1909
|
+
});
|
|
1910
|
+
const days = nums.map((n) => cronDowToWeekday(n));
|
|
1911
|
+
return { type: "days", days };
|
|
1912
|
+
}
|
|
1913
|
+
function cronDowToWeekday(n) {
|
|
1914
|
+
const map = {
|
|
1915
|
+
0: "sunday",
|
|
1916
|
+
1: "monday",
|
|
1917
|
+
2: "tuesday",
|
|
1918
|
+
3: "wednesday",
|
|
1919
|
+
4: "thursday",
|
|
1920
|
+
5: "friday",
|
|
1921
|
+
6: "saturday",
|
|
1922
|
+
7: "sunday"
|
|
1923
|
+
};
|
|
1924
|
+
const result = map[n];
|
|
1925
|
+
if (!result) throw HronError.cron(`invalid DOW number: ${n}`);
|
|
1926
|
+
return result;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// src/index.ts
|
|
1930
|
+
var import_polyfill2 = require("@js-temporal/polyfill");
|
|
1931
|
+
var Schedule = class _Schedule {
|
|
1932
|
+
data;
|
|
1933
|
+
constructor(data) {
|
|
1934
|
+
this.data = data;
|
|
1935
|
+
}
|
|
1936
|
+
/** Parse an hron expression string. */
|
|
1937
|
+
static parse(input) {
|
|
1938
|
+
return new _Schedule(parse(input));
|
|
1939
|
+
}
|
|
1940
|
+
/** Convert a 5-field cron expression to a Schedule. */
|
|
1941
|
+
static fromCron(cronExpr) {
|
|
1942
|
+
return new _Schedule(fromCron(cronExpr));
|
|
1943
|
+
}
|
|
1944
|
+
/** Check if an input string is a valid hron expression. */
|
|
1945
|
+
static validate(input) {
|
|
1946
|
+
try {
|
|
1947
|
+
parse(input);
|
|
1948
|
+
return true;
|
|
1949
|
+
} catch {
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
/** Compute the next occurrence after `now`. */
|
|
1954
|
+
nextFrom(now) {
|
|
1955
|
+
return nextFrom(this.data, now);
|
|
1956
|
+
}
|
|
1957
|
+
/** Compute the next `n` occurrences after `now`. */
|
|
1958
|
+
nextNFrom(now, n) {
|
|
1959
|
+
return nextNFrom(this.data, now, n);
|
|
1960
|
+
}
|
|
1961
|
+
/** Check if a datetime matches this schedule. */
|
|
1962
|
+
matches(datetime) {
|
|
1963
|
+
return matches(this.data, datetime);
|
|
1964
|
+
}
|
|
1965
|
+
/** Convert this schedule to a 5-field cron expression. */
|
|
1966
|
+
toCron() {
|
|
1967
|
+
return toCron(this.data);
|
|
1968
|
+
}
|
|
1969
|
+
/** Render as canonical string (roundtrip-safe). */
|
|
1970
|
+
toString() {
|
|
1971
|
+
return display(this.data);
|
|
1972
|
+
}
|
|
1973
|
+
/** Get the timezone, if specified. */
|
|
1974
|
+
get timezone() {
|
|
1975
|
+
return this.data.timezone;
|
|
1976
|
+
}
|
|
1977
|
+
/** Get the underlying schedule expression. */
|
|
1978
|
+
get expression() {
|
|
1979
|
+
return this.data.expr;
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1983
|
+
0 && (module.exports = {
|
|
1984
|
+
HronError,
|
|
1985
|
+
Schedule,
|
|
1986
|
+
Temporal
|
|
1987
|
+
});
|
|
1988
|
+
//# sourceMappingURL=index.cjs.map
|