hron-ts 0.5.0 → 0.6.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/dist/index.cjs +1418 -282
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -6
- package/dist/index.d.ts +22 -6
- package/dist/index.js +1418 -282
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -141,6 +141,14 @@ function ordinalToN(ord) {
|
|
|
141
141
|
};
|
|
142
142
|
return map[ord];
|
|
143
143
|
}
|
|
144
|
+
var ALL_WEEKDAYS = [
|
|
145
|
+
"monday",
|
|
146
|
+
"tuesday",
|
|
147
|
+
"wednesday",
|
|
148
|
+
"thursday",
|
|
149
|
+
"friday"
|
|
150
|
+
];
|
|
151
|
+
var ALL_WEEKEND = ["saturday", "sunday"];
|
|
144
152
|
function newScheduleData(expr) {
|
|
145
153
|
return {
|
|
146
154
|
expr,
|
|
@@ -286,14 +294,23 @@ function toCron(schedule) {
|
|
|
286
294
|
"not expressible as cron (last day of month not supported)"
|
|
287
295
|
);
|
|
288
296
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
297
|
+
if (target.type === "lastWeekday") {
|
|
298
|
+
throw HronError.cron(
|
|
299
|
+
"not expressible as cron (last weekday of month not supported)"
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (target.type === "ordinalWeekday") {
|
|
303
|
+
throw HronError.cron(
|
|
304
|
+
"not expressible as cron (ordinal weekday of month not supported)"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (target.direction !== null) {
|
|
308
|
+
throw HronError.cron(
|
|
309
|
+
"not expressible as cron (directional nearest weekday not supported)"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return `${time.minute} ${time.hour} ${target.day}W * *`;
|
|
292
313
|
}
|
|
293
|
-
case "ordinalRepeat":
|
|
294
|
-
throw HronError.cron(
|
|
295
|
-
"not expressible as cron (ordinal weekday of month not supported)"
|
|
296
|
-
);
|
|
297
314
|
case "singleDate":
|
|
298
315
|
throw HronError.cron(
|
|
299
316
|
"not expressible as cron (single dates are not repeating)"
|
|
@@ -320,110 +337,596 @@ function dayFilterToCronDow(filter) {
|
|
|
320
337
|
}
|
|
321
338
|
}
|
|
322
339
|
function fromCron(cron) {
|
|
323
|
-
const
|
|
340
|
+
const trimmed = cron.trim();
|
|
341
|
+
if (trimmed.startsWith("@")) {
|
|
342
|
+
return parseCronShortcut(trimmed);
|
|
343
|
+
}
|
|
344
|
+
const fields = trimmed.split(/\s+/);
|
|
324
345
|
if (fields.length !== 5) {
|
|
325
346
|
throw HronError.cron(`expected 5 cron fields, got ${fields.length}`);
|
|
326
347
|
}
|
|
327
|
-
const [minuteField, hourField,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
348
|
+
const [minuteField, hourField, domFieldRaw, monthField, dowFieldRaw] = fields;
|
|
349
|
+
const domField = domFieldRaw === "?" ? "*" : domFieldRaw;
|
|
350
|
+
const dowField = dowFieldRaw === "?" ? "*" : dowFieldRaw;
|
|
351
|
+
const during = parseMonthField(monthField);
|
|
352
|
+
const nthWeekdayResult = tryParseNthWeekday(
|
|
353
|
+
minuteField,
|
|
354
|
+
hourField,
|
|
355
|
+
domField,
|
|
356
|
+
dowField,
|
|
357
|
+
during
|
|
358
|
+
);
|
|
359
|
+
if (nthWeekdayResult) return nthWeekdayResult;
|
|
360
|
+
const lastDayResult = tryParseLastDay(
|
|
361
|
+
minuteField,
|
|
362
|
+
hourField,
|
|
363
|
+
domField,
|
|
364
|
+
dowField,
|
|
365
|
+
during
|
|
366
|
+
);
|
|
367
|
+
if (lastDayResult) return lastDayResult;
|
|
368
|
+
if (domField.endsWith("W") && domField !== "LW") {
|
|
369
|
+
const nearestWeekdayResult = tryParseNearestWeekday(
|
|
370
|
+
minuteField,
|
|
371
|
+
hourField,
|
|
372
|
+
domField,
|
|
373
|
+
dowField,
|
|
374
|
+
during
|
|
375
|
+
);
|
|
376
|
+
if (nearestWeekdayResult) return nearestWeekdayResult;
|
|
377
|
+
}
|
|
378
|
+
const intervalResult = tryParseInterval(
|
|
379
|
+
minuteField,
|
|
380
|
+
hourField,
|
|
381
|
+
domField,
|
|
382
|
+
dowField,
|
|
383
|
+
during
|
|
384
|
+
);
|
|
385
|
+
if (intervalResult) return intervalResult;
|
|
386
|
+
const minute = parseSingleValue(minuteField, "minute", 0, 59);
|
|
387
|
+
const hour = parseSingleValue(hourField, "hour", 0, 23);
|
|
388
|
+
const time = { hour, minute };
|
|
389
|
+
if (domField !== "*" && dowField === "*") {
|
|
390
|
+
const target = parseDomField(domField);
|
|
391
|
+
const schedule2 = newScheduleData({
|
|
392
|
+
type: "monthRepeat",
|
|
393
|
+
interval: 1,
|
|
394
|
+
target,
|
|
395
|
+
times: [time]
|
|
396
|
+
});
|
|
397
|
+
schedule2.during = during;
|
|
398
|
+
return schedule2;
|
|
399
|
+
}
|
|
400
|
+
const days = parseCronDow(dowField);
|
|
401
|
+
const schedule = newScheduleData({
|
|
402
|
+
type: "dayRepeat",
|
|
403
|
+
interval: 1,
|
|
404
|
+
days,
|
|
405
|
+
times: [time]
|
|
406
|
+
});
|
|
407
|
+
schedule.during = during;
|
|
408
|
+
return schedule;
|
|
409
|
+
}
|
|
410
|
+
function parseCronShortcut(cron) {
|
|
411
|
+
switch (cron.toLowerCase()) {
|
|
412
|
+
case "@yearly":
|
|
413
|
+
case "@annually":
|
|
414
|
+
return newScheduleData({
|
|
415
|
+
type: "yearRepeat",
|
|
416
|
+
interval: 1,
|
|
417
|
+
target: { type: "date", month: "jan", day: 1 },
|
|
418
|
+
times: [{ hour: 0, minute: 0 }]
|
|
419
|
+
});
|
|
420
|
+
case "@monthly":
|
|
421
|
+
return newScheduleData({
|
|
422
|
+
type: "monthRepeat",
|
|
423
|
+
interval: 1,
|
|
424
|
+
target: { type: "days", specs: [{ type: "single", day: 1 }] },
|
|
425
|
+
times: [{ hour: 0, minute: 0 }]
|
|
426
|
+
});
|
|
427
|
+
case "@weekly":
|
|
428
|
+
return newScheduleData({
|
|
429
|
+
type: "dayRepeat",
|
|
430
|
+
interval: 1,
|
|
431
|
+
days: { type: "days", days: ["sunday"] },
|
|
432
|
+
times: [{ hour: 0, minute: 0 }]
|
|
433
|
+
});
|
|
434
|
+
case "@daily":
|
|
435
|
+
case "@midnight":
|
|
436
|
+
return newScheduleData({
|
|
437
|
+
type: "dayRepeat",
|
|
438
|
+
interval: 1,
|
|
439
|
+
days: { type: "every" },
|
|
440
|
+
times: [{ hour: 0, minute: 0 }]
|
|
441
|
+
});
|
|
442
|
+
case "@hourly":
|
|
443
|
+
return newScheduleData({
|
|
444
|
+
type: "intervalRepeat",
|
|
445
|
+
interval: 1,
|
|
446
|
+
unit: "hours",
|
|
447
|
+
from: { hour: 0, minute: 0 },
|
|
448
|
+
to: { hour: 23, minute: 59 },
|
|
449
|
+
dayFilter: null
|
|
450
|
+
});
|
|
451
|
+
default:
|
|
452
|
+
throw HronError.cron(`unknown @ shortcut: ${cron}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function parseMonthField(field) {
|
|
456
|
+
if (field === "*") return [];
|
|
457
|
+
const months = [];
|
|
458
|
+
for (const part of field.split(",")) {
|
|
459
|
+
if (part.includes("/")) {
|
|
460
|
+
const [rangePart, stepStr] = part.split("/");
|
|
461
|
+
let start, end;
|
|
462
|
+
if (rangePart === "*") {
|
|
463
|
+
start = 1;
|
|
464
|
+
end = 12;
|
|
465
|
+
} else if (rangePart.includes("-")) {
|
|
466
|
+
const [s, e] = rangePart.split("-");
|
|
467
|
+
start = monthNumber(parseMonthValue(s));
|
|
468
|
+
end = monthNumber(parseMonthValue(e));
|
|
469
|
+
} else {
|
|
470
|
+
throw HronError.cron(`invalid month step expression: ${part}`);
|
|
471
|
+
}
|
|
472
|
+
const step = parseInt(stepStr, 10);
|
|
473
|
+
if (Number.isNaN(step)) {
|
|
474
|
+
throw HronError.cron(`invalid month step value: ${stepStr}`);
|
|
475
|
+
}
|
|
476
|
+
if (step === 0) {
|
|
477
|
+
throw HronError.cron("step cannot be 0");
|
|
478
|
+
}
|
|
479
|
+
for (let n = start; n <= end; n += step) {
|
|
480
|
+
months.push(monthFromNumber(n));
|
|
481
|
+
}
|
|
482
|
+
} else if (part.includes("-")) {
|
|
483
|
+
const [startStr, endStr] = part.split("-");
|
|
484
|
+
const startMonth = parseMonthValue(startStr);
|
|
485
|
+
const endMonth = parseMonthValue(endStr);
|
|
486
|
+
const startNum = monthNumber(startMonth);
|
|
487
|
+
const endNum = monthNumber(endMonth);
|
|
488
|
+
if (startNum > endNum) {
|
|
489
|
+
throw HronError.cron(`invalid month range: ${startStr} > ${endStr}`);
|
|
490
|
+
}
|
|
491
|
+
for (let n = startNum; n <= endNum; n++) {
|
|
492
|
+
months.push(monthFromNumber(n));
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
months.push(parseMonthValue(part));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return months;
|
|
499
|
+
}
|
|
500
|
+
function parseMonthValue(s) {
|
|
501
|
+
const n = parseInt(s, 10);
|
|
502
|
+
if (!Number.isNaN(n)) {
|
|
503
|
+
return monthFromNumber(n);
|
|
504
|
+
}
|
|
505
|
+
const name = parseMonthName(s);
|
|
506
|
+
if (!name) {
|
|
507
|
+
throw HronError.cron(`invalid month: ${s}`);
|
|
508
|
+
}
|
|
509
|
+
return name;
|
|
510
|
+
}
|
|
511
|
+
function monthFromNumber(n) {
|
|
512
|
+
const map = {
|
|
513
|
+
1: "jan",
|
|
514
|
+
2: "feb",
|
|
515
|
+
3: "mar",
|
|
516
|
+
4: "apr",
|
|
517
|
+
5: "may",
|
|
518
|
+
6: "jun",
|
|
519
|
+
7: "jul",
|
|
520
|
+
8: "aug",
|
|
521
|
+
9: "sep",
|
|
522
|
+
10: "oct",
|
|
523
|
+
11: "nov",
|
|
524
|
+
12: "dec"
|
|
525
|
+
};
|
|
526
|
+
const result = map[n];
|
|
527
|
+
if (!result) {
|
|
528
|
+
throw HronError.cron(`invalid month number: ${n}`);
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
function tryParseNthWeekday(minuteField, hourField, domField, dowField, during) {
|
|
533
|
+
if (dowField.includes("#")) {
|
|
534
|
+
const [dowStr, nthStr] = dowField.split("#");
|
|
535
|
+
const dowNum = parseDowValue(dowStr);
|
|
536
|
+
const weekday = cronDowToWeekday(dowNum);
|
|
537
|
+
const nth = parseInt(nthStr, 10);
|
|
538
|
+
if (Number.isNaN(nth) || nth < 1 || nth > 5) {
|
|
539
|
+
throw HronError.cron(`nth must be 1-5, got ${nthStr}`);
|
|
540
|
+
}
|
|
541
|
+
if (domField !== "*" && domField !== "?") {
|
|
542
|
+
throw HronError.cron("DOM must be * when using # for nth weekday");
|
|
543
|
+
}
|
|
544
|
+
const minute = parseSingleValue(minuteField, "minute", 0, 59);
|
|
545
|
+
const hour = parseSingleValue(hourField, "hour", 0, 23);
|
|
546
|
+
const ordinalMap = {
|
|
547
|
+
1: "first",
|
|
548
|
+
2: "second",
|
|
549
|
+
3: "third",
|
|
550
|
+
4: "fourth",
|
|
551
|
+
5: "fifth"
|
|
552
|
+
};
|
|
553
|
+
const schedule = newScheduleData({
|
|
554
|
+
type: "monthRepeat",
|
|
555
|
+
interval: 1,
|
|
556
|
+
target: { type: "ordinalWeekday", ordinal: ordinalMap[nth], weekday },
|
|
557
|
+
times: [{ hour, minute }]
|
|
558
|
+
});
|
|
559
|
+
schedule.during = during;
|
|
560
|
+
return schedule;
|
|
561
|
+
}
|
|
562
|
+
if (dowField.endsWith("L") && dowField.length > 1) {
|
|
563
|
+
const dowStr = dowField.slice(0, -1);
|
|
564
|
+
const dowNum = parseDowValue(dowStr);
|
|
565
|
+
const weekday = cronDowToWeekday(dowNum);
|
|
566
|
+
if (domField !== "*" && domField !== "?") {
|
|
567
|
+
throw HronError.cron("DOM must be * when using nL for last weekday");
|
|
568
|
+
}
|
|
569
|
+
const minute = parseSingleValue(minuteField, "minute", 0, 59);
|
|
570
|
+
const hour = parseSingleValue(hourField, "hour", 0, 23);
|
|
571
|
+
const schedule = newScheduleData({
|
|
572
|
+
type: "monthRepeat",
|
|
573
|
+
interval: 1,
|
|
574
|
+
target: { type: "ordinalWeekday", ordinal: "last", weekday },
|
|
575
|
+
times: [{ hour, minute }]
|
|
576
|
+
});
|
|
577
|
+
schedule.during = during;
|
|
578
|
+
return schedule;
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
function tryParseLastDay(minuteField, hourField, domField, dowField, during) {
|
|
583
|
+
if (domField !== "L" && domField !== "LW") {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
if (dowField !== "*" && dowField !== "?") {
|
|
587
|
+
throw HronError.cron("DOW must be * when using L or LW in DOM");
|
|
588
|
+
}
|
|
589
|
+
const minute = parseSingleValue(minuteField, "minute", 0, 59);
|
|
590
|
+
const hour = parseSingleValue(hourField, "hour", 0, 23);
|
|
591
|
+
const target = domField === "LW" ? { type: "lastWeekday" } : { type: "lastDay" };
|
|
592
|
+
const schedule = newScheduleData({
|
|
593
|
+
type: "monthRepeat",
|
|
594
|
+
interval: 1,
|
|
595
|
+
target,
|
|
596
|
+
times: [{ hour, minute }]
|
|
597
|
+
});
|
|
598
|
+
schedule.during = during;
|
|
599
|
+
return schedule;
|
|
600
|
+
}
|
|
601
|
+
function tryParseNearestWeekday(minuteField, hourField, domField, dowField, during) {
|
|
602
|
+
if (!domField.endsWith("W") || domField === "LW") {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
if (dowField !== "*" && dowField !== "?") {
|
|
606
|
+
throw HronError.cron("DOW must be * when using W in DOM");
|
|
607
|
+
}
|
|
608
|
+
const dayStr = domField.slice(0, -1);
|
|
609
|
+
const day = parseInt(dayStr, 10);
|
|
610
|
+
if (Number.isNaN(day)) {
|
|
611
|
+
throw HronError.cron(`invalid W day: ${dayStr}`);
|
|
612
|
+
}
|
|
613
|
+
if (day < 1 || day > 31) {
|
|
614
|
+
throw HronError.cron(`W day must be 1-31, got ${day}`);
|
|
615
|
+
}
|
|
616
|
+
const minute = parseSingleValue(minuteField, "minute", 0, 59);
|
|
617
|
+
const hour = parseSingleValue(hourField, "hour", 0, 23);
|
|
618
|
+
const target = {
|
|
619
|
+
type: "nearestWeekday",
|
|
620
|
+
day,
|
|
621
|
+
direction: null
|
|
622
|
+
};
|
|
623
|
+
const schedule = newScheduleData({
|
|
624
|
+
type: "monthRepeat",
|
|
625
|
+
interval: 1,
|
|
626
|
+
target,
|
|
627
|
+
times: [{ hour, minute }]
|
|
628
|
+
});
|
|
629
|
+
schedule.during = during;
|
|
630
|
+
return schedule;
|
|
631
|
+
}
|
|
632
|
+
function tryParseInterval(minuteField, hourField, domField, dowField, during) {
|
|
633
|
+
if (minuteField.includes("/")) {
|
|
634
|
+
const [rangePart, stepStr] = minuteField.split("/");
|
|
635
|
+
const interval = parseInt(stepStr, 10);
|
|
636
|
+
if (Number.isNaN(interval)) {
|
|
637
|
+
throw HronError.cron("invalid minute interval value");
|
|
638
|
+
}
|
|
639
|
+
if (interval === 0) {
|
|
640
|
+
throw HronError.cron("step cannot be 0");
|
|
641
|
+
}
|
|
642
|
+
let fromMinute, toMinute;
|
|
643
|
+
if (rangePart === "*") {
|
|
644
|
+
fromMinute = 0;
|
|
645
|
+
toMinute = 59;
|
|
646
|
+
} else if (rangePart.includes("-")) {
|
|
647
|
+
const [s, e] = rangePart.split("-");
|
|
648
|
+
fromMinute = parseInt(s, 10);
|
|
649
|
+
toMinute = parseInt(e, 10);
|
|
650
|
+
if (Number.isNaN(fromMinute) || Number.isNaN(toMinute)) {
|
|
651
|
+
throw HronError.cron("invalid minute range");
|
|
652
|
+
}
|
|
653
|
+
if (fromMinute > toMinute) {
|
|
654
|
+
throw HronError.cron(
|
|
655
|
+
`range start must be <= end: ${fromMinute}-${toMinute}`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
fromMinute = parseInt(rangePart, 10);
|
|
660
|
+
if (Number.isNaN(fromMinute)) {
|
|
661
|
+
throw HronError.cron("invalid minute value");
|
|
662
|
+
}
|
|
663
|
+
toMinute = 59;
|
|
664
|
+
}
|
|
665
|
+
let fromHour, toHour;
|
|
333
666
|
if (hourField === "*") {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
667
|
+
fromHour = 0;
|
|
668
|
+
toHour = 23;
|
|
669
|
+
} else if (hourField.includes("-") && !hourField.includes("/")) {
|
|
670
|
+
const [s, e] = hourField.split("-");
|
|
671
|
+
fromHour = parseInt(s, 10);
|
|
672
|
+
toHour = parseInt(e, 10);
|
|
673
|
+
if (Number.isNaN(fromHour) || Number.isNaN(toHour)) {
|
|
339
674
|
throw HronError.cron("invalid hour range");
|
|
675
|
+
}
|
|
676
|
+
} else if (hourField.includes("/")) {
|
|
677
|
+
return null;
|
|
340
678
|
} else {
|
|
341
679
|
const h = parseInt(hourField, 10);
|
|
342
|
-
if (Number.isNaN(h))
|
|
680
|
+
if (Number.isNaN(h)) {
|
|
681
|
+
throw HronError.cron("invalid hour");
|
|
682
|
+
}
|
|
343
683
|
fromHour = h;
|
|
344
684
|
toHour = h;
|
|
345
685
|
}
|
|
346
686
|
const dayFilter = dowField === "*" ? null : parseCronDow(dowField);
|
|
347
|
-
if (domField === "*") {
|
|
348
|
-
|
|
687
|
+
if (domField === "*" || domField === "?") {
|
|
688
|
+
let endMinute;
|
|
689
|
+
if (fromMinute === 0 && toMinute === 59 && toHour === 23) {
|
|
690
|
+
endMinute = 59;
|
|
691
|
+
} else if (fromMinute === 0 && toMinute === 59) {
|
|
692
|
+
endMinute = 0;
|
|
693
|
+
} else {
|
|
694
|
+
endMinute = toMinute;
|
|
695
|
+
}
|
|
696
|
+
const schedule = newScheduleData({
|
|
349
697
|
type: "intervalRepeat",
|
|
350
698
|
interval,
|
|
351
699
|
unit: "min",
|
|
352
|
-
from: { hour: fromHour, minute:
|
|
353
|
-
to: { hour: toHour, minute:
|
|
700
|
+
from: { hour: fromHour, minute: fromMinute },
|
|
701
|
+
to: { hour: toHour, minute: endMinute },
|
|
354
702
|
dayFilter
|
|
355
|
-
};
|
|
356
|
-
|
|
703
|
+
});
|
|
704
|
+
schedule.during = during;
|
|
705
|
+
return schedule;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (hourField.includes("/") && (minuteField === "0" || minuteField === "00")) {
|
|
709
|
+
const [rangePart, stepStr] = hourField.split("/");
|
|
710
|
+
const interval = parseInt(stepStr, 10);
|
|
711
|
+
if (Number.isNaN(interval)) {
|
|
712
|
+
throw HronError.cron("invalid hour interval value");
|
|
713
|
+
}
|
|
714
|
+
if (interval === 0) {
|
|
715
|
+
throw HronError.cron("step cannot be 0");
|
|
716
|
+
}
|
|
717
|
+
let fromHour, toHour;
|
|
718
|
+
if (rangePart === "*") {
|
|
719
|
+
fromHour = 0;
|
|
720
|
+
toHour = 23;
|
|
721
|
+
} else if (rangePart.includes("-")) {
|
|
722
|
+
const [s, e] = rangePart.split("-");
|
|
723
|
+
fromHour = parseInt(s, 10);
|
|
724
|
+
toHour = parseInt(e, 10);
|
|
725
|
+
if (Number.isNaN(fromHour) || Number.isNaN(toHour)) {
|
|
726
|
+
throw HronError.cron("invalid hour range");
|
|
727
|
+
}
|
|
728
|
+
if (fromHour > toHour) {
|
|
729
|
+
throw HronError.cron(
|
|
730
|
+
`range start must be <= end: ${fromHour}-${toHour}`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
fromHour = parseInt(rangePart, 10);
|
|
735
|
+
if (Number.isNaN(fromHour)) {
|
|
736
|
+
throw HronError.cron("invalid hour value");
|
|
737
|
+
}
|
|
738
|
+
toHour = 23;
|
|
357
739
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (Number.isNaN(interval)) throw HronError.cron("invalid hour interval");
|
|
362
|
-
if (domField === "*" && dowField === "*") {
|
|
363
|
-
const expr2 = {
|
|
740
|
+
if ((domField === "*" || domField === "?") && (dowField === "*" || dowField === "?")) {
|
|
741
|
+
const endMinute = fromHour === 0 && toHour === 23 ? 59 : 0;
|
|
742
|
+
const schedule = newScheduleData({
|
|
364
743
|
type: "intervalRepeat",
|
|
365
744
|
interval,
|
|
366
745
|
unit: "hours",
|
|
367
|
-
from: { hour:
|
|
368
|
-
to: { hour:
|
|
746
|
+
from: { hour: fromHour, minute: 0 },
|
|
747
|
+
to: { hour: toHour, minute: endMinute },
|
|
369
748
|
dayFilter: null
|
|
370
|
-
};
|
|
371
|
-
|
|
749
|
+
});
|
|
750
|
+
schedule.during = during;
|
|
751
|
+
return schedule;
|
|
372
752
|
}
|
|
373
753
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
function parseDomField(field) {
|
|
757
|
+
const specs = [];
|
|
758
|
+
for (const part of field.split(",")) {
|
|
759
|
+
if (part.includes("/")) {
|
|
760
|
+
const [rangePart, stepStr] = part.split("/");
|
|
761
|
+
let start, end;
|
|
762
|
+
if (rangePart === "*") {
|
|
763
|
+
start = 1;
|
|
764
|
+
end = 31;
|
|
765
|
+
} else if (rangePart.includes("-")) {
|
|
766
|
+
const [s, e] = rangePart.split("-");
|
|
767
|
+
start = parseInt(s, 10);
|
|
768
|
+
end = parseInt(e, 10);
|
|
769
|
+
if (Number.isNaN(start)) {
|
|
770
|
+
throw HronError.cron(`invalid DOM range start: ${s}`);
|
|
771
|
+
}
|
|
772
|
+
if (Number.isNaN(end)) {
|
|
773
|
+
throw HronError.cron(`invalid DOM range end: ${e}`);
|
|
774
|
+
}
|
|
775
|
+
if (start > end) {
|
|
776
|
+
throw HronError.cron(`range start must be <= end: ${start}-${end}`);
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
start = parseInt(rangePart, 10);
|
|
780
|
+
if (Number.isNaN(start)) {
|
|
781
|
+
throw HronError.cron(`invalid DOM value: ${rangePart}`);
|
|
782
|
+
}
|
|
783
|
+
end = 31;
|
|
784
|
+
}
|
|
785
|
+
const step = parseInt(stepStr, 10);
|
|
786
|
+
if (Number.isNaN(step)) {
|
|
787
|
+
throw HronError.cron(`invalid DOM step: ${stepStr}`);
|
|
788
|
+
}
|
|
789
|
+
if (step === 0) {
|
|
790
|
+
throw HronError.cron("step cannot be 0");
|
|
791
|
+
}
|
|
792
|
+
validateDom(start);
|
|
793
|
+
validateDom(end);
|
|
794
|
+
for (let d = start; d <= end; d += step) {
|
|
795
|
+
specs.push({ type: "single", day: d });
|
|
796
|
+
}
|
|
797
|
+
} else if (part.includes("-")) {
|
|
798
|
+
const [startStr, endStr] = part.split("-");
|
|
799
|
+
const start = parseInt(startStr, 10);
|
|
800
|
+
const end = parseInt(endStr, 10);
|
|
801
|
+
if (Number.isNaN(start)) {
|
|
802
|
+
throw HronError.cron(`invalid DOM range start: ${startStr}`);
|
|
803
|
+
}
|
|
804
|
+
if (Number.isNaN(end)) {
|
|
805
|
+
throw HronError.cron(`invalid DOM range end: ${endStr}`);
|
|
806
|
+
}
|
|
807
|
+
if (start > end) {
|
|
808
|
+
throw HronError.cron(`range start must be <= end: ${start}-${end}`);
|
|
809
|
+
}
|
|
810
|
+
validateDom(start);
|
|
811
|
+
validateDom(end);
|
|
812
|
+
specs.push({ type: "range", start, end });
|
|
813
|
+
} else {
|
|
814
|
+
const day = parseInt(part, 10);
|
|
815
|
+
if (Number.isNaN(day)) {
|
|
816
|
+
throw HronError.cron(`invalid DOM value: ${part}`);
|
|
817
|
+
}
|
|
818
|
+
validateDom(day);
|
|
819
|
+
specs.push({ type: "single", day });
|
|
384
820
|
}
|
|
385
|
-
const dayNums = domField.split(",").map((s) => {
|
|
386
|
-
const n = parseInt(s, 10);
|
|
387
|
-
if (Number.isNaN(n))
|
|
388
|
-
throw HronError.cron(`invalid DOM field: ${domField}`);
|
|
389
|
-
return n;
|
|
390
|
-
});
|
|
391
|
-
const specs = dayNums.map((d) => ({
|
|
392
|
-
type: "single",
|
|
393
|
-
day: d
|
|
394
|
-
}));
|
|
395
|
-
const expr2 = {
|
|
396
|
-
type: "monthRepeat",
|
|
397
|
-
interval: 1,
|
|
398
|
-
target: { type: "days", specs },
|
|
399
|
-
times: [time]
|
|
400
|
-
};
|
|
401
|
-
return newScheduleData(expr2);
|
|
402
821
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
};
|
|
410
|
-
return newScheduleData(expr);
|
|
822
|
+
return { type: "days", specs };
|
|
823
|
+
}
|
|
824
|
+
function validateDom(day) {
|
|
825
|
+
if (day < 1 || day > 31) {
|
|
826
|
+
throw HronError.cron(`DOM must be 1-31, got ${day}`);
|
|
827
|
+
}
|
|
411
828
|
}
|
|
412
829
|
function parseCronDow(field) {
|
|
413
830
|
if (field === "*") return { type: "every" };
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
831
|
+
const days = [];
|
|
832
|
+
for (const part of field.split(",")) {
|
|
833
|
+
if (part.includes("/")) {
|
|
834
|
+
const [rangePart, stepStr] = part.split("/");
|
|
835
|
+
let start, end;
|
|
836
|
+
if (rangePart === "*") {
|
|
837
|
+
start = 0;
|
|
838
|
+
end = 6;
|
|
839
|
+
} else if (rangePart.includes("-")) {
|
|
840
|
+
const [s, e] = rangePart.split("-");
|
|
841
|
+
start = parseDowValueRaw(s);
|
|
842
|
+
end = parseDowValueRaw(e);
|
|
843
|
+
if (start > end) {
|
|
844
|
+
throw HronError.cron(`range start must be <= end: ${s}-${e}`);
|
|
845
|
+
}
|
|
846
|
+
} else {
|
|
847
|
+
start = parseDowValueRaw(rangePart);
|
|
848
|
+
end = 6;
|
|
849
|
+
}
|
|
850
|
+
const step = parseInt(stepStr, 10);
|
|
851
|
+
if (Number.isNaN(step)) {
|
|
852
|
+
throw HronError.cron(`invalid DOW step: ${stepStr}`);
|
|
853
|
+
}
|
|
854
|
+
if (step === 0) {
|
|
855
|
+
throw HronError.cron("step cannot be 0");
|
|
856
|
+
}
|
|
857
|
+
for (let d = start; d <= end; d += step) {
|
|
858
|
+
const normalized = d === 7 ? 0 : d;
|
|
859
|
+
days.push(cronDowToWeekday(normalized));
|
|
860
|
+
}
|
|
861
|
+
} else if (part.includes("-")) {
|
|
862
|
+
const [startStr, endStr] = part.split("-");
|
|
863
|
+
const start = parseDowValueRaw(startStr);
|
|
864
|
+
const end = parseDowValueRaw(endStr);
|
|
865
|
+
if (start > end) {
|
|
866
|
+
throw HronError.cron(
|
|
867
|
+
`range start must be <= end: ${startStr}-${endStr}`
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
for (let d = start; d <= end; d++) {
|
|
871
|
+
const normalized = d === 7 ? 0 : d;
|
|
872
|
+
days.push(cronDowToWeekday(normalized));
|
|
873
|
+
}
|
|
874
|
+
} else {
|
|
875
|
+
const dow = parseDowValue(part);
|
|
876
|
+
days.push(cronDowToWeekday(dow));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (days.length === 5) {
|
|
880
|
+
const sorted = [...days].sort(
|
|
881
|
+
(a, b) => weekdayNumber(a) - weekdayNumber(b)
|
|
882
|
+
);
|
|
883
|
+
const weekdays = [...ALL_WEEKDAYS].sort(
|
|
884
|
+
(a, b) => weekdayNumber(a) - weekdayNumber(b)
|
|
885
|
+
);
|
|
886
|
+
if (JSON.stringify(sorted) === JSON.stringify(weekdays)) {
|
|
887
|
+
return { type: "weekday" };
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (days.length === 2) {
|
|
891
|
+
const sorted = [...days].sort(
|
|
892
|
+
(a, b) => weekdayNumber(a) - weekdayNumber(b)
|
|
893
|
+
);
|
|
894
|
+
const weekend = [...ALL_WEEKEND].sort(
|
|
895
|
+
(a, b) => weekdayNumber(a) - weekdayNumber(b)
|
|
896
|
+
);
|
|
897
|
+
if (JSON.stringify(sorted) === JSON.stringify(weekend)) {
|
|
898
|
+
return { type: "weekend" };
|
|
899
|
+
}
|
|
900
|
+
}
|
|
425
901
|
return { type: "days", days };
|
|
426
902
|
}
|
|
903
|
+
function parseDowValue(s) {
|
|
904
|
+
const raw = parseDowValueRaw(s);
|
|
905
|
+
return raw === 7 ? 0 : raw;
|
|
906
|
+
}
|
|
907
|
+
function parseDowValueRaw(s) {
|
|
908
|
+
const n = parseInt(s, 10);
|
|
909
|
+
if (!Number.isNaN(n)) {
|
|
910
|
+
if (n > 7) {
|
|
911
|
+
throw HronError.cron(`DOW must be 0-7, got ${n}`);
|
|
912
|
+
}
|
|
913
|
+
return n;
|
|
914
|
+
}
|
|
915
|
+
const map = {
|
|
916
|
+
SUN: 0,
|
|
917
|
+
MON: 1,
|
|
918
|
+
TUE: 2,
|
|
919
|
+
WED: 3,
|
|
920
|
+
THU: 4,
|
|
921
|
+
FRI: 5,
|
|
922
|
+
SAT: 6
|
|
923
|
+
};
|
|
924
|
+
const result = map[s.toUpperCase()];
|
|
925
|
+
if (result === void 0) {
|
|
926
|
+
throw HronError.cron(`invalid DOW: ${s}`);
|
|
927
|
+
}
|
|
928
|
+
return result;
|
|
929
|
+
}
|
|
427
930
|
function cronDowToWeekday(n) {
|
|
428
931
|
const map = {
|
|
429
932
|
0: "sunday",
|
|
@@ -436,9 +939,21 @@ function cronDowToWeekday(n) {
|
|
|
436
939
|
7: "sunday"
|
|
437
940
|
};
|
|
438
941
|
const result = map[n];
|
|
439
|
-
if (!result)
|
|
942
|
+
if (!result) {
|
|
943
|
+
throw HronError.cron(`invalid DOW number: ${n}`);
|
|
944
|
+
}
|
|
440
945
|
return result;
|
|
441
946
|
}
|
|
947
|
+
function parseSingleValue(field, name, min, max) {
|
|
948
|
+
const value = parseInt(field, 10);
|
|
949
|
+
if (Number.isNaN(value)) {
|
|
950
|
+
throw HronError.cron(`invalid ${name} field: ${field}`);
|
|
951
|
+
}
|
|
952
|
+
if (value < min || value > max) {
|
|
953
|
+
throw HronError.cron(`${name} must be ${min}-${max}, got ${value}`);
|
|
954
|
+
}
|
|
955
|
+
return value;
|
|
956
|
+
}
|
|
442
957
|
|
|
443
958
|
// src/display.ts
|
|
444
959
|
function display(schedule) {
|
|
@@ -491,19 +1006,24 @@ function displayExpr(expr) {
|
|
|
491
1006
|
targetStr = formatOrdinalDaySpecs(expr.target.specs);
|
|
492
1007
|
} else if (expr.target.type === "lastDay") {
|
|
493
1008
|
targetStr = "last day";
|
|
494
|
-
} else {
|
|
1009
|
+
} else if (expr.target.type === "lastWeekday") {
|
|
495
1010
|
targetStr = "last weekday";
|
|
1011
|
+
} else if (expr.target.type === "ordinalWeekday") {
|
|
1012
|
+
targetStr = `${expr.target.ordinal} ${expr.target.weekday}`;
|
|
1013
|
+
} else if (expr.target.type === "nearestWeekday") {
|
|
1014
|
+
const { day, direction } = expr.target;
|
|
1015
|
+
const dirPrefix = direction ? `${direction} ` : "";
|
|
1016
|
+
targetStr = `${dirPrefix}nearest weekday to ${day}${ordinalSuffix(day)}`;
|
|
1017
|
+
} else {
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
`unknown month target type: ${expr.target.type}`
|
|
1020
|
+
);
|
|
496
1021
|
}
|
|
497
1022
|
if (expr.interval > 1) {
|
|
498
1023
|
return `every ${expr.interval} months on the ${targetStr} at ${formatTimeList(expr.times)}`;
|
|
499
1024
|
}
|
|
500
1025
|
return `every month on the ${targetStr} at ${formatTimeList(expr.times)}`;
|
|
501
1026
|
}
|
|
502
|
-
case "ordinalRepeat":
|
|
503
|
-
if (expr.interval > 1) {
|
|
504
|
-
return `${expr.ordinal} ${expr.day} of every ${expr.interval} months at ${formatTimeList(expr.times)}`;
|
|
505
|
-
}
|
|
506
|
-
return `${expr.ordinal} ${expr.day} of every month at ${formatTimeList(expr.times)}`;
|
|
507
1027
|
case "singleDate": {
|
|
508
1028
|
let dateStr;
|
|
509
1029
|
if (expr.date.type === "named") {
|
|
@@ -521,14 +1041,24 @@ function displayExpr(expr) {
|
|
|
521
1041
|
targetStr = `the ${expr.target.ordinal} ${expr.target.weekday} of ${expr.target.month}`;
|
|
522
1042
|
} else if (expr.target.type === "dayOfMonth") {
|
|
523
1043
|
targetStr = `the ${expr.target.day}${ordinalSuffix(expr.target.day)} of ${expr.target.month}`;
|
|
524
|
-
} else {
|
|
1044
|
+
} else if (expr.target.type === "lastWeekday") {
|
|
525
1045
|
targetStr = `the last weekday of ${expr.target.month}`;
|
|
1046
|
+
} else {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`unknown year target type: ${expr.target.type}`
|
|
1049
|
+
);
|
|
526
1050
|
}
|
|
527
1051
|
if (expr.interval > 1) {
|
|
528
1052
|
return `every ${expr.interval} years on ${targetStr} at ${formatTimeList(expr.times)}`;
|
|
529
1053
|
}
|
|
530
1054
|
return `every year on ${targetStr} at ${formatTimeList(expr.times)}`;
|
|
531
1055
|
}
|
|
1056
|
+
default: {
|
|
1057
|
+
const _exhaustive = expr;
|
|
1058
|
+
throw new Error(
|
|
1059
|
+
`unknown expression type: ${_exhaustive.type}`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
532
1062
|
}
|
|
533
1063
|
}
|
|
534
1064
|
function displayDayFilter(filter) {
|
|
@@ -541,6 +1071,12 @@ function displayDayFilter(filter) {
|
|
|
541
1071
|
return "weekend";
|
|
542
1072
|
case "days":
|
|
543
1073
|
return formatDayList(filter.days);
|
|
1074
|
+
default: {
|
|
1075
|
+
const _exhaustive = filter;
|
|
1076
|
+
throw new Error(
|
|
1077
|
+
`unknown day filter type: ${_exhaustive.type}`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
544
1080
|
}
|
|
545
1081
|
}
|
|
546
1082
|
function formatTime(t) {
|
|
@@ -620,6 +1156,40 @@ function lastWeekdayOfMonth(year, month) {
|
|
|
620
1156
|
}
|
|
621
1157
|
return d;
|
|
622
1158
|
}
|
|
1159
|
+
function nearestWeekday(year, month, targetDay, direction) {
|
|
1160
|
+
const last = lastDayOfMonth(year, month);
|
|
1161
|
+
const lastDay = last.day;
|
|
1162
|
+
if (targetDay > lastDay) {
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
const date = import_polyfill.Temporal.PlainDate.from({ year, month, day: targetDay });
|
|
1166
|
+
const dow = date.dayOfWeek;
|
|
1167
|
+
if (dow >= 1 && dow <= 5) {
|
|
1168
|
+
return date;
|
|
1169
|
+
}
|
|
1170
|
+
if (dow === 6) {
|
|
1171
|
+
if (direction === null) {
|
|
1172
|
+
if (targetDay === 1) {
|
|
1173
|
+
return date.add({ days: 2 });
|
|
1174
|
+
}
|
|
1175
|
+
return date.subtract({ days: 1 });
|
|
1176
|
+
}
|
|
1177
|
+
if (direction === "next") {
|
|
1178
|
+
return date.add({ days: 2 });
|
|
1179
|
+
}
|
|
1180
|
+
return date.subtract({ days: 1 });
|
|
1181
|
+
}
|
|
1182
|
+
if (direction === null) {
|
|
1183
|
+
if (targetDay >= lastDay) {
|
|
1184
|
+
return date.subtract({ days: 2 });
|
|
1185
|
+
}
|
|
1186
|
+
return date.add({ days: 1 });
|
|
1187
|
+
}
|
|
1188
|
+
if (direction === "next") {
|
|
1189
|
+
return date.add({ days: 1 });
|
|
1190
|
+
}
|
|
1191
|
+
return date.subtract({ days: 2 });
|
|
1192
|
+
}
|
|
623
1193
|
function nthWeekdayOfMonth(year, month, weekday, n) {
|
|
624
1194
|
const targetDow = weekdayNameToNumber(weekday);
|
|
625
1195
|
let d = import_polyfill.Temporal.PlainDate.from({ year, month, day: 1 });
|
|
@@ -762,18 +1332,24 @@ function nextFrom(schedule, now) {
|
|
|
762
1332
|
const parsedExceptions = parseExceptions(schedule.except);
|
|
763
1333
|
const hasExceptions = schedule.except.length > 0;
|
|
764
1334
|
const hasDuring = schedule.during.length > 0;
|
|
765
|
-
const
|
|
1335
|
+
const handlesDuringInternally = schedule.expr.type === "monthRepeat" && schedule.expr.target.type === "nearestWeekday" && schedule.expr.target.direction !== null;
|
|
766
1336
|
let current = now;
|
|
767
1337
|
for (let i = 0; i < 1e3; i++) {
|
|
768
|
-
const candidate = nextExpr(
|
|
1338
|
+
const candidate = nextExpr(
|
|
1339
|
+
schedule.expr,
|
|
1340
|
+
tz,
|
|
1341
|
+
schedule.anchor,
|
|
1342
|
+
current,
|
|
1343
|
+
schedule.during
|
|
1344
|
+
);
|
|
769
1345
|
if (candidate === null) return null;
|
|
770
|
-
const cDate =
|
|
1346
|
+
const cDate = candidate.withTimeZone(tz).toPlainDate();
|
|
771
1347
|
if (untilDate) {
|
|
772
1348
|
if (import_polyfill.Temporal.PlainDate.compare(cDate, untilDate) > 0) {
|
|
773
1349
|
return null;
|
|
774
1350
|
}
|
|
775
1351
|
}
|
|
776
|
-
if (hasDuring && !matchesDuring(cDate, schedule.during)) {
|
|
1352
|
+
if (hasDuring && !handlesDuringInternally && !matchesDuring(cDate, schedule.during)) {
|
|
777
1353
|
const skipTo = nextDuringMonth(cDate, schedule.during);
|
|
778
1354
|
current = atTimeOnDate(skipTo, MIDNIGHT, tz).subtract({ seconds: 1 });
|
|
779
1355
|
continue;
|
|
@@ -787,7 +1363,7 @@ function nextFrom(schedule, now) {
|
|
|
787
1363
|
}
|
|
788
1364
|
return null;
|
|
789
1365
|
}
|
|
790
|
-
function nextExpr(expr, tz, anchor, now) {
|
|
1366
|
+
function nextExpr(expr, tz, anchor, now, during) {
|
|
791
1367
|
switch (expr.type) {
|
|
792
1368
|
case "dayRepeat":
|
|
793
1369
|
return nextDayRepeat(
|
|
@@ -824,17 +1400,8 @@ function nextExpr(expr, tz, anchor, now) {
|
|
|
824
1400
|
expr.times,
|
|
825
1401
|
tz,
|
|
826
1402
|
anchor,
|
|
827
|
-
now
|
|
828
|
-
|
|
829
|
-
case "ordinalRepeat":
|
|
830
|
-
return nextOrdinalRepeat(
|
|
831
|
-
expr.interval,
|
|
832
|
-
expr.ordinal,
|
|
833
|
-
expr.day,
|
|
834
|
-
expr.times,
|
|
835
|
-
tz,
|
|
836
|
-
anchor,
|
|
837
|
-
now
|
|
1403
|
+
now,
|
|
1404
|
+
during
|
|
838
1405
|
);
|
|
839
1406
|
case "singleDate":
|
|
840
1407
|
return nextSingleDate(expr.date, expr.times, tz, now);
|
|
@@ -926,30 +1493,35 @@ function matches(schedule, datetime) {
|
|
|
926
1493
|
const last = lastDayOfMonth(date.year, date.month);
|
|
927
1494
|
return import_polyfill.Temporal.PlainDate.compare(date, last) === 0;
|
|
928
1495
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
case "ordinalRepeat": {
|
|
933
|
-
if (!timeMatchesWithDst(schedule.expr.times)) return false;
|
|
934
|
-
if (schedule.expr.interval > 1) {
|
|
935
|
-
const anchorDate = schedule.anchor ? import_polyfill.Temporal.PlainDate.from(schedule.anchor) : EPOCH_DATE;
|
|
936
|
-
const monthOffset = monthsBetweenYM(anchorDate, date);
|
|
937
|
-
if (monthOffset < 0 || monthOffset % schedule.expr.interval !== 0) {
|
|
938
|
-
return false;
|
|
939
|
-
}
|
|
1496
|
+
if (target.type === "lastWeekday") {
|
|
1497
|
+
const lastWd = lastWeekdayOfMonth(date.year, date.month);
|
|
1498
|
+
return import_polyfill.Temporal.PlainDate.compare(date, lastWd) === 0;
|
|
940
1499
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1500
|
+
if (target.type === "ordinalWeekday") {
|
|
1501
|
+
let targetDate2;
|
|
1502
|
+
if (target.ordinal === "last") {
|
|
1503
|
+
targetDate2 = lastWeekdayInMonth(
|
|
1504
|
+
date.year,
|
|
1505
|
+
date.month,
|
|
1506
|
+
target.weekday
|
|
1507
|
+
);
|
|
1508
|
+
} else {
|
|
1509
|
+
targetDate2 = nthWeekdayOfMonth(
|
|
1510
|
+
date.year,
|
|
1511
|
+
date.month,
|
|
1512
|
+
target.weekday,
|
|
1513
|
+
ordinalToN(target.ordinal)
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
if (!targetDate2) return false;
|
|
1517
|
+
return import_polyfill.Temporal.PlainDate.compare(date, targetDate2) === 0;
|
|
952
1518
|
}
|
|
1519
|
+
const targetDate = nearestWeekday(
|
|
1520
|
+
date.year,
|
|
1521
|
+
date.month,
|
|
1522
|
+
target.day,
|
|
1523
|
+
target.direction
|
|
1524
|
+
);
|
|
953
1525
|
if (!targetDate) return false;
|
|
954
1526
|
return import_polyfill.Temporal.PlainDate.compare(date, targetDate) === 0;
|
|
955
1527
|
}
|
|
@@ -1082,8 +1654,7 @@ function nextWeekRepeat(interval, days, times, tz, anchor, now) {
|
|
|
1082
1654
|
for (let i = 0; i < 54; i++) {
|
|
1083
1655
|
const weeks = weeksBetween(anchorMonday, currentMonday);
|
|
1084
1656
|
if (weeks < 0) {
|
|
1085
|
-
|
|
1086
|
-
currentMonday = currentMonday.add({ days: skip * interval * 7 });
|
|
1657
|
+
currentMonday = anchorMonday;
|
|
1087
1658
|
continue;
|
|
1088
1659
|
}
|
|
1089
1660
|
if (weeks % interval === 0) {
|
|
@@ -1100,13 +1671,22 @@ function nextWeekRepeat(interval, days, times, tz, anchor, now) {
|
|
|
1100
1671
|
}
|
|
1101
1672
|
return null;
|
|
1102
1673
|
}
|
|
1103
|
-
function nextMonthRepeat(interval, target, times, tz, anchor, now) {
|
|
1674
|
+
function nextMonthRepeat(interval, target, times, tz, anchor, now, during) {
|
|
1104
1675
|
const nowInTz = now.withTimeZone(tz);
|
|
1105
1676
|
let year = nowInTz.year;
|
|
1106
1677
|
let month = nowInTz.month;
|
|
1107
1678
|
const anchorDate = anchor ? import_polyfill.Temporal.PlainDate.from(anchor) : EPOCH_DATE;
|
|
1108
1679
|
const maxIter = interval > 1 ? 24 * interval : 24;
|
|
1680
|
+
const applyDuringFilter = during.length > 0 && target.type === "nearestWeekday" && target.direction !== null;
|
|
1109
1681
|
for (let i = 0; i < maxIter; i++) {
|
|
1682
|
+
if (applyDuringFilter && !during.some((mn) => monthNumber(mn) === month)) {
|
|
1683
|
+
month++;
|
|
1684
|
+
if (month > 12) {
|
|
1685
|
+
month = 1;
|
|
1686
|
+
year++;
|
|
1687
|
+
}
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1110
1690
|
if (interval > 1) {
|
|
1111
1691
|
const cur = import_polyfill.Temporal.PlainDate.from({ year, month, day: 1 });
|
|
1112
1692
|
const monthOffset = monthsBetweenYM(anchorDate, cur);
|
|
@@ -1135,161 +1715,567 @@ function nextMonthRepeat(interval, target, times, tz, anchor, now) {
|
|
|
1135
1715
|
}
|
|
1136
1716
|
} else if (target.type === "lastDay") {
|
|
1137
1717
|
dateCandidates.push(lastDayOfMonth(year, month));
|
|
1138
|
-
} else {
|
|
1718
|
+
} else if (target.type === "lastWeekday") {
|
|
1139
1719
|
dateCandidates.push(lastWeekdayOfMonth(year, month));
|
|
1720
|
+
} else if (target.type === "ordinalWeekday") {
|
|
1721
|
+
let owDate;
|
|
1722
|
+
if (target.ordinal === "last") {
|
|
1723
|
+
owDate = lastWeekdayInMonth(year, month, target.weekday);
|
|
1724
|
+
} else {
|
|
1725
|
+
owDate = nthWeekdayOfMonth(
|
|
1726
|
+
year,
|
|
1727
|
+
month,
|
|
1728
|
+
target.weekday,
|
|
1729
|
+
ordinalToN(target.ordinal)
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
if (owDate) {
|
|
1733
|
+
dateCandidates.push(owDate);
|
|
1734
|
+
}
|
|
1735
|
+
} else {
|
|
1736
|
+
const nwDate = nearestWeekday(year, month, target.day, target.direction);
|
|
1737
|
+
if (nwDate) {
|
|
1738
|
+
dateCandidates.push(nwDate);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
let best = null;
|
|
1742
|
+
for (const date of dateCandidates) {
|
|
1743
|
+
const candidate = earliestFutureAtTimes(date, times, tz, now);
|
|
1744
|
+
if (candidate) {
|
|
1745
|
+
if (best === null || import_polyfill.Temporal.ZonedDateTime.compare(candidate, best) < 0) {
|
|
1746
|
+
best = candidate;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (best) return best;
|
|
1751
|
+
month++;
|
|
1752
|
+
if (month > 12) {
|
|
1753
|
+
month = 1;
|
|
1754
|
+
year++;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return null;
|
|
1758
|
+
}
|
|
1759
|
+
function nextSingleDate(dateSpec, times, tz, now) {
|
|
1760
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1761
|
+
if (dateSpec.type === "iso") {
|
|
1762
|
+
const date = import_polyfill.Temporal.PlainDate.from(dateSpec.date);
|
|
1763
|
+
return earliestFutureAtTimes(date, times, tz, now);
|
|
1764
|
+
}
|
|
1765
|
+
if (dateSpec.type === "named") {
|
|
1766
|
+
const startYear = nowInTz.year;
|
|
1767
|
+
for (let y = 0; y < 8; y++) {
|
|
1768
|
+
const year = startYear + y;
|
|
1769
|
+
try {
|
|
1770
|
+
const date = import_polyfill.Temporal.PlainDate.from(
|
|
1771
|
+
{
|
|
1772
|
+
year,
|
|
1773
|
+
month: monthNumber(dateSpec.month),
|
|
1774
|
+
day: dateSpec.day
|
|
1775
|
+
},
|
|
1776
|
+
{ overflow: "reject" }
|
|
1777
|
+
);
|
|
1778
|
+
const candidate = earliestFutureAtTimes(date, times, tz, now);
|
|
1779
|
+
if (candidate) return candidate;
|
|
1780
|
+
} catch {
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return null;
|
|
1784
|
+
}
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
function nextYearRepeat(interval, target, times, tz, anchor, now) {
|
|
1788
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1789
|
+
const startYear = nowInTz.year;
|
|
1790
|
+
const anchorYear = anchor ? import_polyfill.Temporal.PlainDate.from(anchor).year : EPOCH_DATE.year;
|
|
1791
|
+
const maxIter = interval > 1 ? 8 * interval : 8;
|
|
1792
|
+
for (let y = 0; y < maxIter; y++) {
|
|
1793
|
+
const year = startYear + y;
|
|
1794
|
+
if (interval > 1) {
|
|
1795
|
+
const yearOffset = year - anchorYear;
|
|
1796
|
+
if (yearOffset < 0 || euclideanMod(yearOffset, interval) !== 0) {
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
let targetDate = null;
|
|
1801
|
+
switch (target.type) {
|
|
1802
|
+
case "date":
|
|
1803
|
+
try {
|
|
1804
|
+
targetDate = import_polyfill.Temporal.PlainDate.from(
|
|
1805
|
+
{
|
|
1806
|
+
year,
|
|
1807
|
+
month: monthNumber(target.month),
|
|
1808
|
+
day: target.day
|
|
1809
|
+
},
|
|
1810
|
+
{ overflow: "reject" }
|
|
1811
|
+
);
|
|
1812
|
+
} catch {
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
break;
|
|
1816
|
+
case "ordinalWeekday":
|
|
1817
|
+
if (target.ordinal === "last") {
|
|
1818
|
+
targetDate = lastWeekdayInMonth(
|
|
1819
|
+
year,
|
|
1820
|
+
monthNumber(target.month),
|
|
1821
|
+
target.weekday
|
|
1822
|
+
);
|
|
1823
|
+
} else {
|
|
1824
|
+
targetDate = nthWeekdayOfMonth(
|
|
1825
|
+
year,
|
|
1826
|
+
monthNumber(target.month),
|
|
1827
|
+
target.weekday,
|
|
1828
|
+
ordinalToN(target.ordinal)
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
break;
|
|
1832
|
+
case "dayOfMonth":
|
|
1833
|
+
try {
|
|
1834
|
+
targetDate = import_polyfill.Temporal.PlainDate.from(
|
|
1835
|
+
{
|
|
1836
|
+
year,
|
|
1837
|
+
month: monthNumber(target.month),
|
|
1838
|
+
day: target.day
|
|
1839
|
+
},
|
|
1840
|
+
{ overflow: "reject" }
|
|
1841
|
+
);
|
|
1842
|
+
} catch {
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
break;
|
|
1846
|
+
case "lastWeekday":
|
|
1847
|
+
targetDate = lastWeekdayOfMonth(year, monthNumber(target.month));
|
|
1848
|
+
break;
|
|
1849
|
+
}
|
|
1850
|
+
if (targetDate) {
|
|
1851
|
+
const candidate = earliestFutureAtTimes(targetDate, times, tz, now);
|
|
1852
|
+
if (candidate) return candidate;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
return null;
|
|
1856
|
+
}
|
|
1857
|
+
function previousFrom(schedule, now) {
|
|
1858
|
+
const tz = resolveTz(schedule.timezone);
|
|
1859
|
+
const anchor = schedule.anchor;
|
|
1860
|
+
const parsedExceptions = parseExceptions(schedule.except);
|
|
1861
|
+
const hasExceptions = schedule.except.length > 0;
|
|
1862
|
+
const hasDuring = schedule.during.length > 0;
|
|
1863
|
+
let current = now;
|
|
1864
|
+
for (let i = 0; i < 1e3; i++) {
|
|
1865
|
+
const candidate = prevExpr(schedule, tz, anchor, current);
|
|
1866
|
+
if (candidate === null) return null;
|
|
1867
|
+
const cDate = candidate.withTimeZone(tz).toPlainDate();
|
|
1868
|
+
if (anchor) {
|
|
1869
|
+
const anchorDate = import_polyfill.Temporal.PlainDate.from(anchor);
|
|
1870
|
+
if (import_polyfill.Temporal.PlainDate.compare(cDate, anchorDate) < 0) {
|
|
1871
|
+
return null;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (schedule.until) {
|
|
1875
|
+
const untilDate = resolveUntil(schedule.until, now);
|
|
1876
|
+
if (import_polyfill.Temporal.PlainDate.compare(cDate, untilDate) > 0) {
|
|
1877
|
+
const endOfDay = toPlainTime({ hour: 23, minute: 59 });
|
|
1878
|
+
const skipTo = atTimeOnDate(untilDate, endOfDay, tz);
|
|
1879
|
+
current = skipTo.add({ seconds: 1 });
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
if (hasDuring && !matchesDuring(cDate, schedule.during)) {
|
|
1884
|
+
const skipTo = prevDuringMonth(cDate, schedule.during);
|
|
1885
|
+
const endOfDay = toPlainTime({ hour: 23, minute: 59 });
|
|
1886
|
+
current = atTimeOnDate(skipTo, endOfDay, tz).add({
|
|
1887
|
+
seconds: 1
|
|
1888
|
+
});
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
if (hasExceptions && isExceptedParsed(cDate, parsedExceptions)) {
|
|
1892
|
+
const prevDay = cDate.subtract({ days: 1 });
|
|
1893
|
+
const endOfDay = toPlainTime({ hour: 23, minute: 59 });
|
|
1894
|
+
current = atTimeOnDate(prevDay, endOfDay, tz).add({
|
|
1895
|
+
seconds: 1
|
|
1896
|
+
});
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
return candidate;
|
|
1900
|
+
}
|
|
1901
|
+
return null;
|
|
1902
|
+
}
|
|
1903
|
+
function prevExpr(schedule, tz, anchor, now) {
|
|
1904
|
+
const expr = schedule.expr;
|
|
1905
|
+
switch (expr.type) {
|
|
1906
|
+
case "dayRepeat":
|
|
1907
|
+
return prevDayRepeat(expr, tz, anchor, now);
|
|
1908
|
+
case "intervalRepeat":
|
|
1909
|
+
return prevIntervalRepeat(expr, tz, now);
|
|
1910
|
+
case "weekRepeat":
|
|
1911
|
+
return prevWeekRepeat(expr, tz, anchor, now);
|
|
1912
|
+
case "monthRepeat":
|
|
1913
|
+
return prevMonthRepeat(expr, tz, anchor, now);
|
|
1914
|
+
case "singleDate":
|
|
1915
|
+
return prevSingleDate(expr, tz, now);
|
|
1916
|
+
case "yearRepeat":
|
|
1917
|
+
return prevYearRepeat(expr, tz, anchor, now);
|
|
1918
|
+
default:
|
|
1919
|
+
return null;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
function prevDayRepeat(expr, tz, anchor, now) {
|
|
1923
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1924
|
+
let date = nowInTz.toPlainDate();
|
|
1925
|
+
const { interval, days, times } = expr;
|
|
1926
|
+
if (interval <= 1) {
|
|
1927
|
+
if (matchesDayFilter(date, days)) {
|
|
1928
|
+
const candidate = latestPastAtTimes(date, times, tz, now);
|
|
1929
|
+
if (candidate !== null) return candidate;
|
|
1930
|
+
}
|
|
1931
|
+
for (let i = 0; i < 8; i++) {
|
|
1932
|
+
date = date.subtract({ days: 1 });
|
|
1933
|
+
if (matchesDayFilter(date, days)) {
|
|
1934
|
+
const candidate = latestAtTimes(date, times, tz);
|
|
1935
|
+
if (candidate !== null) return candidate;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
const anchorDate = anchor ? import_polyfill.Temporal.PlainDate.from(anchor) : EPOCH_DATE;
|
|
1941
|
+
const offset = daysBetween(anchorDate, date);
|
|
1942
|
+
const remainder = (offset % interval + interval) % interval;
|
|
1943
|
+
const alignedDate = remainder === 0 ? date : date.subtract({ days: remainder });
|
|
1944
|
+
for (let i = 0; i < 2; i++) {
|
|
1945
|
+
const checkDate = alignedDate.subtract({ days: i * interval });
|
|
1946
|
+
const candidate = latestPastAtTimes(checkDate, times, tz, now);
|
|
1947
|
+
if (candidate !== null) return candidate;
|
|
1948
|
+
const latest = latestAtTimes(checkDate, times, tz);
|
|
1949
|
+
if (latest !== null && import_polyfill.Temporal.ZonedDateTime.compare(latest, now) < 0) {
|
|
1950
|
+
return latest;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
function prevIntervalRepeat(expr, tz, now) {
|
|
1956
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1957
|
+
let date = nowInTz.toPlainDate();
|
|
1958
|
+
const { interval, unit, from, to, dayFilter } = expr;
|
|
1959
|
+
const stepMinutes = unit === "min" ? interval : interval * 60;
|
|
1960
|
+
const fromMinutes = from.hour * 60 + from.minute;
|
|
1961
|
+
const toMinutes = to.hour * 60 + to.minute;
|
|
1962
|
+
for (let d = 0; d < 8; d++) {
|
|
1963
|
+
if (dayFilter && !matchesDayFilter(date, dayFilter)) {
|
|
1964
|
+
date = date.subtract({ days: 1 });
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
const nowMinutes = d === 0 ? nowInTz.hour * 60 + nowInTz.minute : toMinutes + 1;
|
|
1968
|
+
const searchUntil = Math.min(nowMinutes, toMinutes);
|
|
1969
|
+
if (searchUntil >= fromMinutes) {
|
|
1970
|
+
const slotsInRange = Math.floor(
|
|
1971
|
+
(searchUntil - fromMinutes) / stepMinutes
|
|
1972
|
+
);
|
|
1973
|
+
let lastSlotMinutes = fromMinutes + slotsInRange * stepMinutes;
|
|
1974
|
+
if (d === 0 && lastSlotMinutes >= nowMinutes) {
|
|
1975
|
+
lastSlotMinutes -= stepMinutes;
|
|
1976
|
+
}
|
|
1977
|
+
if (lastSlotMinutes >= fromMinutes) {
|
|
1978
|
+
const h = Math.floor(lastSlotMinutes / 60);
|
|
1979
|
+
const m = lastSlotMinutes % 60;
|
|
1980
|
+
return atTimeOnDate(date, toPlainTime({ hour: h, minute: m }), tz);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
date = date.subtract({ days: 1 });
|
|
1984
|
+
}
|
|
1985
|
+
return null;
|
|
1986
|
+
}
|
|
1987
|
+
function prevWeekRepeat(expr, tz, anchor, now) {
|
|
1988
|
+
const nowInTz = now.withTimeZone(tz);
|
|
1989
|
+
const date = nowInTz.toPlainDate();
|
|
1990
|
+
const { interval, days, times } = expr;
|
|
1991
|
+
const dayOfWeek = date.dayOfWeek;
|
|
1992
|
+
const currentMonday = date.subtract({ days: dayOfWeek - 1 });
|
|
1993
|
+
const anchorDate = anchor ? import_polyfill.Temporal.PlainDate.from(anchor) : EPOCH_MONDAY;
|
|
1994
|
+
const anchorDayOfWeek = anchorDate.dayOfWeek;
|
|
1995
|
+
const anchorMonday = anchorDate.subtract({ days: anchorDayOfWeek - 1 });
|
|
1996
|
+
const sortedDays = [...days].sort((a, b) => dayToNumber(b) - dayToNumber(a));
|
|
1997
|
+
let checkMonday = currentMonday;
|
|
1998
|
+
for (let w = 0; w < 54; w++) {
|
|
1999
|
+
const weeks = weeksBetween(anchorMonday, checkMonday);
|
|
2000
|
+
if (weeks < 0) {
|
|
2001
|
+
return null;
|
|
1140
2002
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
if (
|
|
1146
|
-
|
|
2003
|
+
if (weeks % interval === 0) {
|
|
2004
|
+
for (const wd of sortedDays) {
|
|
2005
|
+
const dayNum = dayToNumber(wd);
|
|
2006
|
+
const targetDate = checkMonday.add({ days: dayNum - 1 });
|
|
2007
|
+
if (import_polyfill.Temporal.PlainDate.compare(targetDate, date) < 0) {
|
|
2008
|
+
const candidate = latestAtTimes(targetDate, times, tz);
|
|
2009
|
+
if (candidate !== null) return candidate;
|
|
2010
|
+
} else if (import_polyfill.Temporal.PlainDate.compare(targetDate, date) === 0) {
|
|
2011
|
+
const candidate = latestPastAtTimes(targetDate, times, tz, now);
|
|
2012
|
+
if (candidate !== null) return candidate;
|
|
1147
2013
|
}
|
|
1148
2014
|
}
|
|
1149
2015
|
}
|
|
1150
|
-
|
|
1151
|
-
month++;
|
|
1152
|
-
if (month > 12) {
|
|
1153
|
-
month = 1;
|
|
1154
|
-
year++;
|
|
1155
|
-
}
|
|
2016
|
+
checkMonday = checkMonday.subtract({ days: interval * 7 });
|
|
1156
2017
|
}
|
|
1157
2018
|
return null;
|
|
1158
2019
|
}
|
|
1159
|
-
function
|
|
2020
|
+
function prevMonthRepeat(expr, tz, anchor, now) {
|
|
1160
2021
|
const nowInTz = now.withTimeZone(tz);
|
|
1161
|
-
|
|
1162
|
-
|
|
2022
|
+
const startDate = nowInTz.toPlainDate();
|
|
2023
|
+
const { interval, target, times } = expr;
|
|
1163
2024
|
const anchorDate = anchor ? import_polyfill.Temporal.PlainDate.from(anchor) : EPOCH_DATE;
|
|
2025
|
+
let year = startDate.year;
|
|
2026
|
+
let month = startDate.month;
|
|
1164
2027
|
const maxIter = interval > 1 ? 24 * interval : 24;
|
|
1165
2028
|
for (let i = 0; i < maxIter; i++) {
|
|
1166
2029
|
if (interval > 1) {
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
year++;
|
|
1174
|
-
}
|
|
2030
|
+
const monthOffset = monthsBetweenYM(
|
|
2031
|
+
anchorDate,
|
|
2032
|
+
import_polyfill.Temporal.PlainDate.from({ year, month, day: 1 })
|
|
2033
|
+
);
|
|
2034
|
+
if (monthOffset < 0 || monthOffset % interval !== 0) {
|
|
2035
|
+
({ year, month } = prevMonth(year, month));
|
|
1175
2036
|
continue;
|
|
1176
2037
|
}
|
|
1177
2038
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
month = 1;
|
|
1191
|
-
year++;
|
|
2039
|
+
const targetDates = getMonthTargetDates(year, month, target);
|
|
2040
|
+
for (const d of targetDates.sort(
|
|
2041
|
+
(a, b) => import_polyfill.Temporal.PlainDate.compare(b, a)
|
|
2042
|
+
)) {
|
|
2043
|
+
if (import_polyfill.Temporal.PlainDate.compare(d, startDate) > 0) continue;
|
|
2044
|
+
if (import_polyfill.Temporal.PlainDate.compare(d, startDate) === 0) {
|
|
2045
|
+
const candidate = latestPastAtTimes(d, times, tz, now);
|
|
2046
|
+
if (candidate !== null) return candidate;
|
|
2047
|
+
} else {
|
|
2048
|
+
const candidate = latestAtTimes(d, times, tz);
|
|
2049
|
+
if (candidate !== null) return candidate;
|
|
2050
|
+
}
|
|
1192
2051
|
}
|
|
2052
|
+
({ year, month } = prevMonth(year, month));
|
|
1193
2053
|
}
|
|
1194
2054
|
return null;
|
|
1195
2055
|
}
|
|
1196
|
-
function
|
|
2056
|
+
function prevSingleDate(expr, tz, now) {
|
|
1197
2057
|
const nowInTz = now.withTimeZone(tz);
|
|
2058
|
+
const nowDate = nowInTz.toPlainDate();
|
|
2059
|
+
const { date: dateSpec, times } = expr;
|
|
2060
|
+
let targetDate;
|
|
1198
2061
|
if (dateSpec.type === "iso") {
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
2062
|
+
targetDate = import_polyfill.Temporal.PlainDate.from(dateSpec.date);
|
|
2063
|
+
if (import_polyfill.Temporal.PlainDate.compare(targetDate, nowDate) > 0) {
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
if (import_polyfill.Temporal.PlainDate.compare(targetDate, nowDate) === 0) {
|
|
2067
|
+
return latestPastAtTimes(targetDate, times, tz, now);
|
|
2068
|
+
}
|
|
2069
|
+
return latestAtTimes(targetDate, times, tz);
|
|
2070
|
+
} else {
|
|
2071
|
+
const { month, day } = dateSpec;
|
|
2072
|
+
const monthNum = monthNumber(month);
|
|
2073
|
+
const thisYear = import_polyfill.Temporal.PlainDate.from({
|
|
2074
|
+
year: nowDate.year,
|
|
2075
|
+
month: monthNum,
|
|
2076
|
+
day
|
|
2077
|
+
});
|
|
2078
|
+
const lastYear = import_polyfill.Temporal.PlainDate.from({
|
|
2079
|
+
year: nowDate.year - 1,
|
|
2080
|
+
month: monthNum,
|
|
2081
|
+
day
|
|
2082
|
+
});
|
|
2083
|
+
if (import_polyfill.Temporal.PlainDate.compare(thisYear, nowDate) < 0) {
|
|
2084
|
+
targetDate = thisYear;
|
|
2085
|
+
} else if (import_polyfill.Temporal.PlainDate.compare(thisYear, nowDate) === 0) {
|
|
2086
|
+
const candidate = latestPastAtTimes(thisYear, times, tz, now);
|
|
2087
|
+
if (candidate !== null) return candidate;
|
|
2088
|
+
targetDate = lastYear;
|
|
2089
|
+
} else {
|
|
2090
|
+
targetDate = lastYear;
|
|
1219
2091
|
}
|
|
1220
|
-
return
|
|
2092
|
+
return latestAtTimes(targetDate, times, tz);
|
|
1221
2093
|
}
|
|
1222
|
-
return null;
|
|
1223
2094
|
}
|
|
1224
|
-
function
|
|
2095
|
+
function prevYearRepeat(expr, tz, anchor, now) {
|
|
1225
2096
|
const nowInTz = now.withTimeZone(tz);
|
|
1226
|
-
const
|
|
2097
|
+
const startDate = nowInTz.toPlainDate();
|
|
2098
|
+
const startYear = startDate.year;
|
|
2099
|
+
const { interval, target, times } = expr;
|
|
1227
2100
|
const anchorYear = anchor ? import_polyfill.Temporal.PlainDate.from(anchor).year : EPOCH_DATE.year;
|
|
1228
2101
|
const maxIter = interval > 1 ? 8 * interval : 8;
|
|
1229
2102
|
for (let y = 0; y < maxIter; y++) {
|
|
1230
|
-
const year = startYear
|
|
2103
|
+
const year = startYear - y;
|
|
1231
2104
|
if (interval > 1) {
|
|
1232
2105
|
const yearOffset = year - anchorYear;
|
|
1233
|
-
if (yearOffset < 0 ||
|
|
2106
|
+
if (yearOffset < 0 || yearOffset % interval !== 0) {
|
|
1234
2107
|
continue;
|
|
1235
2108
|
}
|
|
1236
2109
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
2110
|
+
const targetDate = getYearTargetDate(year, target);
|
|
2111
|
+
if (targetDate !== null) {
|
|
2112
|
+
if (import_polyfill.Temporal.PlainDate.compare(targetDate, startDate) > 0) {
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
if (import_polyfill.Temporal.PlainDate.compare(targetDate, startDate) === 0) {
|
|
2116
|
+
const candidate = latestPastAtTimes(targetDate, times, tz, now);
|
|
2117
|
+
if (candidate !== null) return candidate;
|
|
2118
|
+
} else {
|
|
2119
|
+
const candidate = latestAtTimes(targetDate, times, tz);
|
|
2120
|
+
if (candidate !== null) return candidate;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
return null;
|
|
2125
|
+
}
|
|
2126
|
+
function latestPastAtTimes(date, times, tz, now) {
|
|
2127
|
+
const sortedTimes = [...times].sort(
|
|
2128
|
+
(a, b) => b.hour * 60 + b.minute - (a.hour * 60 + a.minute)
|
|
2129
|
+
);
|
|
2130
|
+
for (const tod of sortedTimes) {
|
|
2131
|
+
const t = toPlainTime(tod);
|
|
2132
|
+
const candidate = atTimeOnDate(date, t, tz);
|
|
2133
|
+
if (import_polyfill.Temporal.ZonedDateTime.compare(candidate, now) < 0) {
|
|
2134
|
+
return candidate;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return null;
|
|
2138
|
+
}
|
|
2139
|
+
function latestAtTimes(date, times, tz) {
|
|
2140
|
+
const sortedTimes = [...times].sort(
|
|
2141
|
+
(a, b) => a.hour * 60 + a.minute - (b.hour * 60 + b.minute)
|
|
2142
|
+
);
|
|
2143
|
+
if (sortedTimes.length === 0) return null;
|
|
2144
|
+
const latest = sortedTimes[sortedTimes.length - 1];
|
|
2145
|
+
return atTimeOnDate(date, toPlainTime(latest), tz);
|
|
2146
|
+
}
|
|
2147
|
+
function prevMonth(year, month) {
|
|
2148
|
+
if (month === 1) {
|
|
2149
|
+
return { year: year - 1, month: 12 };
|
|
2150
|
+
}
|
|
2151
|
+
return { year, month: month - 1 };
|
|
2152
|
+
}
|
|
2153
|
+
function prevDuringMonth(date, during) {
|
|
2154
|
+
let { year, month } = prevMonth(date.year, date.month);
|
|
2155
|
+
for (let i = 0; i < 12; i++) {
|
|
2156
|
+
const monthName = numberToMonthName(month);
|
|
2157
|
+
if (during.includes(monthName)) {
|
|
2158
|
+
return lastDayOfMonth(year, month);
|
|
2159
|
+
}
|
|
2160
|
+
({ year, month } = prevMonth(year, month));
|
|
2161
|
+
}
|
|
2162
|
+
return date.subtract({ days: 1 });
|
|
2163
|
+
}
|
|
2164
|
+
function numberToMonthName(n) {
|
|
2165
|
+
const names = [
|
|
2166
|
+
"jan",
|
|
2167
|
+
"feb",
|
|
2168
|
+
"mar",
|
|
2169
|
+
"apr",
|
|
2170
|
+
"may",
|
|
2171
|
+
"jun",
|
|
2172
|
+
"jul",
|
|
2173
|
+
"aug",
|
|
2174
|
+
"sep",
|
|
2175
|
+
"oct",
|
|
2176
|
+
"nov",
|
|
2177
|
+
"dec"
|
|
2178
|
+
];
|
|
2179
|
+
return names[n - 1];
|
|
2180
|
+
}
|
|
2181
|
+
function getMonthTargetDates(year, month, target) {
|
|
2182
|
+
switch (target.type) {
|
|
2183
|
+
case "days": {
|
|
2184
|
+
const expanded = expandMonthTarget(target);
|
|
2185
|
+
return expanded.map((d) => {
|
|
1270
2186
|
try {
|
|
1271
|
-
|
|
1272
|
-
{
|
|
1273
|
-
year,
|
|
1274
|
-
month: monthNumber(target.month),
|
|
1275
|
-
day: target.day
|
|
1276
|
-
},
|
|
1277
|
-
{ overflow: "reject" }
|
|
1278
|
-
);
|
|
2187
|
+
return import_polyfill.Temporal.PlainDate.from({ year, month, day: d });
|
|
1279
2188
|
} catch {
|
|
1280
|
-
|
|
2189
|
+
return null;
|
|
1281
2190
|
}
|
|
1282
|
-
|
|
1283
|
-
case "lastWeekday":
|
|
1284
|
-
targetDate = lastWeekdayOfMonth(year, monthNumber(target.month));
|
|
1285
|
-
break;
|
|
2191
|
+
}).filter((d) => d !== null);
|
|
1286
2192
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
2193
|
+
case "lastDay":
|
|
2194
|
+
return [lastDayOfMonth(year, month)];
|
|
2195
|
+
case "lastWeekday":
|
|
2196
|
+
return [lastWeekdayOfMonth(year, month)];
|
|
2197
|
+
case "nearestWeekday": {
|
|
2198
|
+
const d = nearestWeekday(year, month, target.day, target.direction);
|
|
2199
|
+
return d ? [d] : [];
|
|
2200
|
+
}
|
|
2201
|
+
case "ordinalWeekday": {
|
|
2202
|
+
const d = getOrdinalWeekday(year, month, target.ordinal, target.weekday);
|
|
2203
|
+
return d ? [d] : [];
|
|
1290
2204
|
}
|
|
2205
|
+
default:
|
|
2206
|
+
return [];
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
function getYearTargetDate(year, target) {
|
|
2210
|
+
switch (target.type) {
|
|
2211
|
+
case "date": {
|
|
2212
|
+
const monthNum = monthNumber(target.month);
|
|
2213
|
+
try {
|
|
2214
|
+
return import_polyfill.Temporal.PlainDate.from({
|
|
2215
|
+
year,
|
|
2216
|
+
month: monthNum,
|
|
2217
|
+
day: target.day
|
|
2218
|
+
});
|
|
2219
|
+
} catch {
|
|
2220
|
+
return null;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
case "ordinalWeekday": {
|
|
2224
|
+
const monthNum = monthNumber(target.month);
|
|
2225
|
+
return getOrdinalWeekday(year, monthNum, target.ordinal, target.weekday);
|
|
2226
|
+
}
|
|
2227
|
+
case "dayOfMonth": {
|
|
2228
|
+
const monthNum = monthNumber(target.month);
|
|
2229
|
+
try {
|
|
2230
|
+
return import_polyfill.Temporal.PlainDate.from({
|
|
2231
|
+
year,
|
|
2232
|
+
month: monthNum,
|
|
2233
|
+
day: target.day
|
|
2234
|
+
});
|
|
2235
|
+
} catch {
|
|
2236
|
+
return null;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
case "lastWeekday": {
|
|
2240
|
+
const monthNum = monthNumber(target.month);
|
|
2241
|
+
return lastWeekdayOfMonth(year, monthNum);
|
|
2242
|
+
}
|
|
2243
|
+
default:
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
function getOrdinalWeekday(year, month, ordinal, day) {
|
|
2248
|
+
if (ordinal === "last") {
|
|
2249
|
+
return lastWeekdayInMonth(year, month, day);
|
|
2250
|
+
}
|
|
2251
|
+
return nthWeekdayOfMonth(year, month, day, ordinalToN(ordinal));
|
|
2252
|
+
}
|
|
2253
|
+
function dayToNumber(day) {
|
|
2254
|
+
const map = {
|
|
2255
|
+
monday: 1,
|
|
2256
|
+
tuesday: 2,
|
|
2257
|
+
wednesday: 3,
|
|
2258
|
+
thursday: 4,
|
|
2259
|
+
friday: 5,
|
|
2260
|
+
saturday: 6,
|
|
2261
|
+
sunday: 7
|
|
2262
|
+
};
|
|
2263
|
+
return map[day];
|
|
2264
|
+
}
|
|
2265
|
+
function* occurrences(schedule, from) {
|
|
2266
|
+
let current = from;
|
|
2267
|
+
for (; ; ) {
|
|
2268
|
+
const next = nextFrom(schedule, current);
|
|
2269
|
+
if (next === null) return;
|
|
2270
|
+
current = next.add({ minutes: 1 });
|
|
2271
|
+
yield next;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
function* between(schedule, from, to) {
|
|
2275
|
+
for (const dt of occurrences(schedule, from)) {
|
|
2276
|
+
if (import_polyfill.Temporal.ZonedDateTime.compare(dt, to) > 0) return;
|
|
2277
|
+
yield dt;
|
|
1291
2278
|
}
|
|
1292
|
-
return null;
|
|
1293
2279
|
}
|
|
1294
2280
|
|
|
1295
2281
|
// src/lexer.ts
|
|
@@ -1459,6 +2445,9 @@ var KEYWORD_MAP = {
|
|
|
1459
2445
|
during: { type: "during" },
|
|
1460
2446
|
year: { type: "year" },
|
|
1461
2447
|
years: { type: "year" },
|
|
2448
|
+
nearest: { type: "nearest" },
|
|
2449
|
+
next: { type: "next" },
|
|
2450
|
+
previous: { type: "previous" },
|
|
1462
2451
|
day: { type: "day" },
|
|
1463
2452
|
days: { type: "day" },
|
|
1464
2453
|
weekday: { type: "weekday" },
|
|
@@ -1594,13 +2583,8 @@ var Parser = class {
|
|
|
1594
2583
|
} else if (kind?.type === "on") {
|
|
1595
2584
|
this.advance();
|
|
1596
2585
|
expr = this.parseOn();
|
|
1597
|
-
} else if (kind?.type === "ordinal" || kind?.type === "last") {
|
|
1598
|
-
expr = this.parseOrdinalRepeat();
|
|
1599
2586
|
} else {
|
|
1600
|
-
throw this.error(
|
|
1601
|
-
"expected 'every', 'on', or an ordinal (first, second, ...)",
|
|
1602
|
-
span
|
|
1603
|
-
);
|
|
2587
|
+
throw this.error("expected 'every' or 'on'", span);
|
|
1604
2588
|
}
|
|
1605
2589
|
return this.parseTrailingClauses(expr);
|
|
1606
2590
|
}
|
|
@@ -1618,7 +2602,9 @@ var Parser = class {
|
|
|
1618
2602
|
this.advance();
|
|
1619
2603
|
const k = this.peekKind();
|
|
1620
2604
|
if (k?.type === "isoDate") {
|
|
1621
|
-
|
|
2605
|
+
const startDate = k.date;
|
|
2606
|
+
this.validateIsoDate(startDate);
|
|
2607
|
+
schedule.anchor = startDate;
|
|
1622
2608
|
this.advance();
|
|
1623
2609
|
} else {
|
|
1624
2610
|
throw this.error(
|
|
@@ -1655,6 +2641,7 @@ var Parser = class {
|
|
|
1655
2641
|
const k = this.peekKind();
|
|
1656
2642
|
if (k?.type === "isoDate") {
|
|
1657
2643
|
const date = k.date;
|
|
2644
|
+
this.validateIsoDate(date);
|
|
1658
2645
|
this.advance();
|
|
1659
2646
|
return { type: "iso", date };
|
|
1660
2647
|
}
|
|
@@ -1662,10 +2649,13 @@ var Parser = class {
|
|
|
1662
2649
|
const month = parseMonthName(
|
|
1663
2650
|
k.name
|
|
1664
2651
|
);
|
|
2652
|
+
if (!month) throw this.error("invalid month name", this.currentSpan());
|
|
1665
2653
|
this.advance();
|
|
2654
|
+
const dayPos = this.currentSpan().start;
|
|
1666
2655
|
const day = this.parseDayNumber(
|
|
1667
2656
|
"expected day number after month name in exception"
|
|
1668
2657
|
);
|
|
2658
|
+
this.validateNamedDate(month, day, dayPos);
|
|
1669
2659
|
return { type: "named", month, day };
|
|
1670
2660
|
}
|
|
1671
2661
|
throw this.error(
|
|
@@ -1677,6 +2667,7 @@ var Parser = class {
|
|
|
1677
2667
|
const k = this.peekKind();
|
|
1678
2668
|
if (k?.type === "isoDate") {
|
|
1679
2669
|
const date = k.date;
|
|
2670
|
+
this.validateIsoDate(date);
|
|
1680
2671
|
this.advance();
|
|
1681
2672
|
return { type: "iso", date };
|
|
1682
2673
|
}
|
|
@@ -1684,10 +2675,13 @@ var Parser = class {
|
|
|
1684
2675
|
const month = parseMonthName(
|
|
1685
2676
|
k.name
|
|
1686
2677
|
);
|
|
2678
|
+
if (!month) throw this.error("invalid month name", this.currentSpan());
|
|
1687
2679
|
this.advance();
|
|
2680
|
+
const dayPos = this.currentSpan().start;
|
|
1688
2681
|
const day = this.parseDayNumber(
|
|
1689
2682
|
"expected day number after month name in until"
|
|
1690
2683
|
);
|
|
2684
|
+
this.validateNamedDate(month, day, dayPos);
|
|
1691
2685
|
return { type: "named", month, day };
|
|
1692
2686
|
}
|
|
1693
2687
|
throw this.error(
|
|
@@ -1699,11 +2693,23 @@ var Parser = class {
|
|
|
1699
2693
|
const k = this.peekKind();
|
|
1700
2694
|
if (k?.type === "number") {
|
|
1701
2695
|
const n = k.value;
|
|
2696
|
+
if (n < 1 || n > 31) {
|
|
2697
|
+
throw this.error(
|
|
2698
|
+
`invalid day number ${n} (must be 1-31)`,
|
|
2699
|
+
this.currentSpan()
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
1702
2702
|
this.advance();
|
|
1703
2703
|
return n;
|
|
1704
2704
|
}
|
|
1705
2705
|
if (k?.type === "ordinalNumber") {
|
|
1706
2706
|
const n = k.value;
|
|
2707
|
+
if (n < 1 || n > 31) {
|
|
2708
|
+
throw this.error(
|
|
2709
|
+
`invalid day number ${n} (must be 1-31)`,
|
|
2710
|
+
this.currentSpan()
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
1707
2713
|
this.advance();
|
|
1708
2714
|
return n;
|
|
1709
2715
|
}
|
|
@@ -1713,6 +2719,7 @@ var Parser = class {
|
|
|
1713
2719
|
parseEvery() {
|
|
1714
2720
|
if (!this.peek()) throw this.errorAtEnd("expected repeater");
|
|
1715
2721
|
const k = this.peekKind();
|
|
2722
|
+
if (!k) throw this.errorAtEnd("expected repeater");
|
|
1716
2723
|
if (k.type === "year") {
|
|
1717
2724
|
this.advance();
|
|
1718
2725
|
return this.parseYearRepeat(1);
|
|
@@ -1755,6 +2762,7 @@ var Parser = class {
|
|
|
1755
2762
|
parseNumberRepeat() {
|
|
1756
2763
|
const span = this.currentSpan();
|
|
1757
2764
|
const k = this.peekKind();
|
|
2765
|
+
if (!k) throw this.errorAtEnd("expected number");
|
|
1758
2766
|
const num = k.value;
|
|
1759
2767
|
if (num === 0) {
|
|
1760
2768
|
throw this.error("interval must be at least 1", span);
|
|
@@ -1786,6 +2794,7 @@ var Parser = class {
|
|
|
1786
2794
|
}
|
|
1787
2795
|
parseIntervalRepeat(interval) {
|
|
1788
2796
|
const k = this.peekKind();
|
|
2797
|
+
if (!k) throw this.errorAtEnd("expected interval unit");
|
|
1789
2798
|
const unitStr = k.unit;
|
|
1790
2799
|
this.advance();
|
|
1791
2800
|
const unit = unitStr === "min" ? "min" : "hours";
|
|
@@ -1821,18 +2830,43 @@ var Parser = class {
|
|
|
1821
2830
|
} else if (next?.type === "weekday") {
|
|
1822
2831
|
this.advance();
|
|
1823
2832
|
target = { type: "lastWeekday" };
|
|
2833
|
+
} else if (next?.type === "dayName") {
|
|
2834
|
+
const weekday = parseWeekday(
|
|
2835
|
+
next.name
|
|
2836
|
+
);
|
|
2837
|
+
if (!weekday) throw this.error("invalid weekday", this.currentSpan());
|
|
2838
|
+
this.advance();
|
|
2839
|
+
target = { type: "ordinalWeekday", ordinal: "last", weekday };
|
|
2840
|
+
} else {
|
|
2841
|
+
throw this.error(
|
|
2842
|
+
"expected 'day', 'weekday', or day name after 'last'",
|
|
2843
|
+
this.currentSpan()
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
} else if (k?.type === "ordinal") {
|
|
2847
|
+
const ordinal = this.parseOrdinalPosition();
|
|
2848
|
+
const next = this.peekKind();
|
|
2849
|
+
if (next?.type === "dayName") {
|
|
2850
|
+
const weekday = parseWeekday(
|
|
2851
|
+
next.name
|
|
2852
|
+
);
|
|
2853
|
+
if (!weekday) throw this.error("invalid weekday", this.currentSpan());
|
|
2854
|
+
this.advance();
|
|
2855
|
+
target = { type: "ordinalWeekday", ordinal, weekday };
|
|
1824
2856
|
} else {
|
|
1825
2857
|
throw this.error(
|
|
1826
|
-
"expected
|
|
2858
|
+
"expected day name after ordinal in month expression",
|
|
1827
2859
|
this.currentSpan()
|
|
1828
2860
|
);
|
|
1829
2861
|
}
|
|
1830
2862
|
} else if (k?.type === "ordinalNumber") {
|
|
1831
2863
|
const specs = this.parseOrdinalDayList();
|
|
1832
2864
|
target = { type: "days", specs };
|
|
2865
|
+
} else if (k?.type === "next" || k?.type === "previous" || k?.type === "nearest") {
|
|
2866
|
+
target = this.parseNearestWeekdayTarget();
|
|
1833
2867
|
} else {
|
|
1834
2868
|
throw this.error(
|
|
1835
|
-
"expected ordinal day (1st, 15th) or '
|
|
2869
|
+
"expected ordinal day (1st, 15th), 'last', ordinal weekday, or '[next|previous] nearest' after 'the'",
|
|
1836
2870
|
this.currentSpan()
|
|
1837
2871
|
);
|
|
1838
2872
|
}
|
|
@@ -1840,31 +2874,37 @@ var Parser = class {
|
|
|
1840
2874
|
const times = this.parseTimeList();
|
|
1841
2875
|
return { type: "monthRepeat", interval, target, times };
|
|
1842
2876
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
2877
|
+
// [next|previous] nearest weekday to <day>
|
|
2878
|
+
parseNearestWeekdayTarget() {
|
|
2879
|
+
let direction = null;
|
|
1845
2880
|
const k = this.peekKind();
|
|
1846
|
-
if (k?.type
|
|
1847
|
-
|
|
2881
|
+
if (k?.type === "next") {
|
|
2882
|
+
this.advance();
|
|
2883
|
+
direction = "next";
|
|
2884
|
+
} else if (k?.type === "previous") {
|
|
2885
|
+
this.advance();
|
|
2886
|
+
direction = "previous";
|
|
1848
2887
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
);
|
|
1852
|
-
this.
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
const
|
|
1857
|
-
if (
|
|
1858
|
-
|
|
1859
|
-
if (
|
|
1860
|
-
throw this.error(
|
|
2888
|
+
this.consumeKind("'nearest'", (k2) => k2.type === "nearest");
|
|
2889
|
+
this.consumeKind("'weekday'", (k2) => k2.type === "weekday");
|
|
2890
|
+
this.consumeKind("'to'", (k2) => k2.type === "to");
|
|
2891
|
+
const day = this.parseOrdinalDayNumber();
|
|
2892
|
+
return { type: "nearestWeekday", day, direction };
|
|
2893
|
+
}
|
|
2894
|
+
parseOrdinalDayNumber() {
|
|
2895
|
+
const k = this.peekKind();
|
|
2896
|
+
if (k?.type === "ordinalNumber") {
|
|
2897
|
+
const d = k.value;
|
|
2898
|
+
if (d < 1 || d > 31) {
|
|
2899
|
+
throw this.error(
|
|
2900
|
+
`invalid day number ${d} (must be 1-31)`,
|
|
2901
|
+
this.currentSpan()
|
|
2902
|
+
);
|
|
1861
2903
|
}
|
|
1862
2904
|
this.advance();
|
|
2905
|
+
return d;
|
|
1863
2906
|
}
|
|
1864
|
-
this.
|
|
1865
|
-
this.consumeKind("'at'", (k2) => k2.type === "at");
|
|
1866
|
-
const times = this.parseTimeList();
|
|
1867
|
-
return { type: "ordinalRepeat", interval, ordinal, day, times };
|
|
2907
|
+
throw this.error("expected ordinal day number", this.currentSpan());
|
|
1868
2908
|
}
|
|
1869
2909
|
parseYearRepeat(interval) {
|
|
1870
2910
|
this.consumeKind("'on'", (k2) => k2.type === "on");
|
|
@@ -1877,8 +2917,11 @@ var Parser = class {
|
|
|
1877
2917
|
const month = parseMonthName(
|
|
1878
2918
|
k.name
|
|
1879
2919
|
);
|
|
2920
|
+
if (!month) throw this.error("invalid month name", this.currentSpan());
|
|
1880
2921
|
this.advance();
|
|
2922
|
+
const dayPos = this.currentSpan().start;
|
|
1881
2923
|
const day = this.parseDayNumber("expected day number after month name");
|
|
2924
|
+
this.validateNamedDate(month, day, dayPos);
|
|
1882
2925
|
target = { type: "date", month, day };
|
|
1883
2926
|
} else {
|
|
1884
2927
|
throw this.error(
|
|
@@ -1905,6 +2948,7 @@ var Parser = class {
|
|
|
1905
2948
|
const weekday = parseWeekday(
|
|
1906
2949
|
next.name
|
|
1907
2950
|
);
|
|
2951
|
+
if (!weekday) throw this.error("invalid weekday", this.currentSpan());
|
|
1908
2952
|
this.advance();
|
|
1909
2953
|
this.consumeKind("'of'", (k2) => k2.type === "of");
|
|
1910
2954
|
const month = this.parseMonthNameToken();
|
|
@@ -1922,6 +2966,7 @@ var Parser = class {
|
|
|
1922
2966
|
const weekday = parseWeekday(
|
|
1923
2967
|
next.name
|
|
1924
2968
|
);
|
|
2969
|
+
if (!weekday) throw this.error("invalid weekday", this.currentSpan());
|
|
1925
2970
|
this.advance();
|
|
1926
2971
|
this.consumeKind("'of'", (k2) => k2.type === "of");
|
|
1927
2972
|
const month = this.parseMonthNameToken();
|
|
@@ -1934,9 +2979,17 @@ var Parser = class {
|
|
|
1934
2979
|
}
|
|
1935
2980
|
if (k?.type === "ordinalNumber") {
|
|
1936
2981
|
const day = k.value;
|
|
2982
|
+
if (day < 1 || day > 31) {
|
|
2983
|
+
throw this.error(
|
|
2984
|
+
`invalid day number ${day} (must be 1-31)`,
|
|
2985
|
+
this.currentSpan()
|
|
2986
|
+
);
|
|
2987
|
+
}
|
|
2988
|
+
const dayPos = this.currentSpan().start;
|
|
1937
2989
|
this.advance();
|
|
1938
2990
|
this.consumeKind("'of'", (k2) => k2.type === "of");
|
|
1939
2991
|
const month = this.parseMonthNameToken();
|
|
2992
|
+
this.validateNamedDate(month, day, dayPos);
|
|
1940
2993
|
return { type: "dayOfMonth", day, month };
|
|
1941
2994
|
}
|
|
1942
2995
|
throw this.error(
|
|
@@ -1950,6 +3003,7 @@ var Parser = class {
|
|
|
1950
3003
|
const month = parseMonthName(
|
|
1951
3004
|
k.name
|
|
1952
3005
|
);
|
|
3006
|
+
if (!month) throw this.error("invalid month name", this.currentSpan());
|
|
1953
3007
|
this.advance();
|
|
1954
3008
|
return month;
|
|
1955
3009
|
}
|
|
@@ -1978,10 +3032,50 @@ var Parser = class {
|
|
|
1978
3032
|
const times = this.parseTimeList();
|
|
1979
3033
|
return { type: "singleDate", date, times };
|
|
1980
3034
|
}
|
|
3035
|
+
validateNamedDate(month, day, pos) {
|
|
3036
|
+
const maxDays = {
|
|
3037
|
+
jan: 31,
|
|
3038
|
+
feb: 29,
|
|
3039
|
+
mar: 31,
|
|
3040
|
+
apr: 30,
|
|
3041
|
+
may: 31,
|
|
3042
|
+
jun: 30,
|
|
3043
|
+
jul: 31,
|
|
3044
|
+
aug: 31,
|
|
3045
|
+
sep: 30,
|
|
3046
|
+
oct: 31,
|
|
3047
|
+
nov: 30,
|
|
3048
|
+
dec: 31
|
|
3049
|
+
};
|
|
3050
|
+
const max = maxDays[month];
|
|
3051
|
+
if (day > max) {
|
|
3052
|
+
throw this.error(`invalid day ${day} for ${month} (max ${max})`, {
|
|
3053
|
+
start: pos,
|
|
3054
|
+
end: pos
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
validateIsoDate(dateStr) {
|
|
3059
|
+
const parts = dateStr.split("-");
|
|
3060
|
+
const year = parseInt(parts[0], 10);
|
|
3061
|
+
const month = parseInt(parts[1], 10);
|
|
3062
|
+
const day = parseInt(parts[2], 10);
|
|
3063
|
+
if (month < 1 || month > 12 || day < 1) {
|
|
3064
|
+
throw this.error(`invalid date: ${dateStr}`, this.currentSpan());
|
|
3065
|
+
}
|
|
3066
|
+
const daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
3067
|
+
if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
|
|
3068
|
+
daysInMonth[2] = 29;
|
|
3069
|
+
}
|
|
3070
|
+
if (day > daysInMonth[month]) {
|
|
3071
|
+
throw this.error(`invalid date: ${dateStr}`, this.currentSpan());
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
1981
3074
|
parseDateTarget() {
|
|
1982
3075
|
const k = this.peekKind();
|
|
1983
3076
|
if (k?.type === "isoDate") {
|
|
1984
3077
|
const date = k.date;
|
|
3078
|
+
this.validateIsoDate(date);
|
|
1985
3079
|
this.advance();
|
|
1986
3080
|
return { type: "iso", date };
|
|
1987
3081
|
}
|
|
@@ -1989,8 +3083,11 @@ var Parser = class {
|
|
|
1989
3083
|
const month = parseMonthName(
|
|
1990
3084
|
k.name
|
|
1991
3085
|
);
|
|
3086
|
+
if (!month) throw this.error("invalid month name", this.currentSpan());
|
|
1992
3087
|
this.advance();
|
|
3088
|
+
const dayPos = this.currentSpan().start;
|
|
1993
3089
|
const day = this.parseDayNumber("expected day number after month name");
|
|
3090
|
+
this.validateNamedDate(month, day, dayPos);
|
|
1994
3091
|
return { type: "named", month, day };
|
|
1995
3092
|
}
|
|
1996
3093
|
throw this.error(
|
|
@@ -2026,9 +3123,11 @@ var Parser = class {
|
|
|
2026
3123
|
if (k?.type !== "dayName") {
|
|
2027
3124
|
throw this.error("expected day name", this.currentSpan());
|
|
2028
3125
|
}
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2031
|
-
|
|
3126
|
+
const firstDay = parseWeekday(
|
|
3127
|
+
k.name
|
|
3128
|
+
);
|
|
3129
|
+
if (!firstDay) throw this.error("invalid weekday", this.currentSpan());
|
|
3130
|
+
const days = [firstDay];
|
|
2032
3131
|
this.advance();
|
|
2033
3132
|
while (this.peekKind()?.type === "comma") {
|
|
2034
3133
|
this.advance();
|
|
@@ -2036,11 +3135,11 @@ var Parser = class {
|
|
|
2036
3135
|
if (next?.type !== "dayName") {
|
|
2037
3136
|
throw this.error("expected day name after ','", this.currentSpan());
|
|
2038
3137
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
next.name
|
|
2042
|
-
)
|
|
3138
|
+
const day = parseWeekday(
|
|
3139
|
+
next.name
|
|
2043
3140
|
);
|
|
3141
|
+
if (!day) throw this.error("invalid weekday", this.currentSpan());
|
|
3142
|
+
days.push(day);
|
|
2044
3143
|
this.advance();
|
|
2045
3144
|
}
|
|
2046
3145
|
return days;
|
|
@@ -2059,6 +3158,12 @@ var Parser = class {
|
|
|
2059
3158
|
throw this.error("expected ordinal day number", this.currentSpan());
|
|
2060
3159
|
}
|
|
2061
3160
|
const start = k.value;
|
|
3161
|
+
if (start < 1 || start > 31) {
|
|
3162
|
+
throw this.error(
|
|
3163
|
+
`invalid day number ${start} (must be 1-31)`,
|
|
3164
|
+
this.currentSpan()
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
2062
3167
|
this.advance();
|
|
2063
3168
|
if (this.peekKind()?.type === "to") {
|
|
2064
3169
|
this.advance();
|
|
@@ -2070,7 +3175,19 @@ var Parser = class {
|
|
|
2070
3175
|
);
|
|
2071
3176
|
}
|
|
2072
3177
|
const end = next.value;
|
|
3178
|
+
if (end < 1 || end > 31) {
|
|
3179
|
+
throw this.error(
|
|
3180
|
+
`invalid day number ${end} (must be 1-31)`,
|
|
3181
|
+
this.currentSpan()
|
|
3182
|
+
);
|
|
3183
|
+
}
|
|
2073
3184
|
this.advance();
|
|
3185
|
+
if (start > end) {
|
|
3186
|
+
throw this.error(
|
|
3187
|
+
`invalid day range: ${start} to ${end} (start must be <= end)`,
|
|
3188
|
+
this.currentSpan()
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
2074
3191
|
return { type: "range", start, end };
|
|
2075
3192
|
}
|
|
2076
3193
|
return { type: "single", day: start };
|
|
@@ -2151,10 +3268,29 @@ var Schedule = class _Schedule {
|
|
|
2151
3268
|
nextNFrom(now, n) {
|
|
2152
3269
|
return nextNFrom(this.data, now, n);
|
|
2153
3270
|
}
|
|
3271
|
+
/** Compute the most recent occurrence strictly before `now`. */
|
|
3272
|
+
previousFrom(now) {
|
|
3273
|
+
return previousFrom(this.data, now);
|
|
3274
|
+
}
|
|
2154
3275
|
/** Check if a datetime matches this schedule. */
|
|
2155
3276
|
matches(datetime) {
|
|
2156
3277
|
return matches(this.data, datetime);
|
|
2157
3278
|
}
|
|
3279
|
+
/**
|
|
3280
|
+
* Returns a lazy iterator of occurrences starting after `from`.
|
|
3281
|
+
* The iterator is unbounded for repeating schedules (will iterate forever unless limited),
|
|
3282
|
+
* but respects the `until` clause if specified in the schedule.
|
|
3283
|
+
*/
|
|
3284
|
+
*occurrences(from) {
|
|
3285
|
+
yield* occurrences(this.data, from);
|
|
3286
|
+
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Returns a bounded iterator of occurrences where `from < occurrence <= to`.
|
|
3289
|
+
* The iterator yields occurrences strictly after `from` and up to and including `to`.
|
|
3290
|
+
*/
|
|
3291
|
+
*between(from, to) {
|
|
3292
|
+
yield* between(this.data, from, to);
|
|
3293
|
+
}
|
|
2158
3294
|
/** Convert this schedule to a 5-field cron expression. */
|
|
2159
3295
|
toCron() {
|
|
2160
3296
|
return toCron(this.data);
|