cronli5 0.1.7 → 0.2.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/CHANGELOG.md +43 -0
- package/README.md +5 -5
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +363 -75
- package/dist/cronli5.js +363 -75
- package/dist/lang/en.cjs +363 -75
- package/dist/lang/en.js +363 -75
- package/package.json +1 -1
- package/src/core/ir.ts +5 -0
- package/src/lang/en/dialects.ts +6 -2
- package/src/lang/en/index.ts +735 -102
- package/types/core/ir.d.ts +1 -0
package/dist/lang/en.js
CHANGED
|
@@ -78,7 +78,8 @@ var dialects = {
|
|
|
78
78
|
pm: "p.m.",
|
|
79
79
|
sep: ":",
|
|
80
80
|
serialComma: true,
|
|
81
|
-
through: " through "
|
|
81
|
+
through: " through ",
|
|
82
|
+
untilWindow: true
|
|
82
83
|
},
|
|
83
84
|
house: {
|
|
84
85
|
am: "AM",
|
|
@@ -95,7 +96,7 @@ var dialects = {
|
|
|
95
96
|
};
|
|
96
97
|
function resolveDialect(dialect) {
|
|
97
98
|
if (typeof dialect === "object" && dialect !== null) {
|
|
98
|
-
return { ...dialects.us, ...dialect };
|
|
99
|
+
return { ...dialects.us, untilWindow: false, ...dialect };
|
|
99
100
|
}
|
|
100
101
|
const name = dialect === "uk" ? "gb" : dialect;
|
|
101
102
|
return dialects[name] || dialects.us;
|
|
@@ -167,7 +168,9 @@ function normalizeOptions(options) {
|
|
|
167
168
|
};
|
|
168
169
|
}
|
|
169
170
|
function describe(ir, opts) {
|
|
170
|
-
|
|
171
|
+
const body = confinement(ir, opts) ?? render(ir, ir.plan, opts);
|
|
172
|
+
const lead = isDayUnion(ir, opts) ? dayUnionMonthLead(ir, opts) : "";
|
|
173
|
+
return applyYear(lead + body, ir, opts);
|
|
171
174
|
}
|
|
172
175
|
function render(ir, plan, opts) {
|
|
173
176
|
const renderer = renderers[plan.kind];
|
|
@@ -260,7 +263,7 @@ function secondsClause(ir, anchor, opts) {
|
|
|
260
263
|
}
|
|
261
264
|
if (shape === "range") {
|
|
262
265
|
const bounds = secondField.split("-");
|
|
263
|
-
const num = seriesNumber(
|
|
266
|
+
const num = seriesNumber();
|
|
264
267
|
return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the " + anchor;
|
|
265
268
|
}
|
|
266
269
|
if (shape === "single") {
|
|
@@ -326,15 +329,21 @@ function renderMinutesAcrossHours(ir, plan, opts) {
|
|
|
326
329
|
}
|
|
327
330
|
return "every minute during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
|
|
328
331
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
"
|
|
336
|
-
|
|
337
|
-
)
|
|
332
|
+
if (plan.form === "range") {
|
|
333
|
+
const lead2 = minuteRangeLead(ir.pattern.minute, opts);
|
|
334
|
+
if (cadence !== null) {
|
|
335
|
+
return lead2 + ", " + cadence + trailingQualifier(ir, opts);
|
|
336
|
+
}
|
|
337
|
+
if (singleHourFire(plan.times)) {
|
|
338
|
+
return lead2 + ", at " + hourTimesFromPlan(ir, plan.times, true, opts) + trailingQualifier(ir, opts);
|
|
339
|
+
}
|
|
340
|
+
return lead2 + " during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
|
|
341
|
+
}
|
|
342
|
+
const lead = strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
|
|
343
|
+
segmentWords(ir.analyses.segments.minute, opts),
|
|
344
|
+
"minute",
|
|
345
|
+
"hour",
|
|
346
|
+
opts
|
|
338
347
|
);
|
|
339
348
|
if (cadence !== null) {
|
|
340
349
|
return lead + ", " + cadence + trailingQualifier(ir, opts);
|
|
@@ -371,7 +380,7 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
|
|
|
371
380
|
}
|
|
372
381
|
function minuteRangeLead(minuteField, opts) {
|
|
373
382
|
const bounds = minuteField.split("-");
|
|
374
|
-
const num = seriesNumber(
|
|
383
|
+
const num = seriesNumber();
|
|
375
384
|
return "every minute from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the hour";
|
|
376
385
|
}
|
|
377
386
|
function renderEveryHour(ir, plan, opts) {
|
|
@@ -414,8 +423,15 @@ function boundedWindow(plan) {
|
|
|
414
423
|
const last = plan.minuteForm === "wildcard" ? plan.boundMinute ?? 0 : 0;
|
|
415
424
|
return { from: plan.from, last, to: plan.to };
|
|
416
425
|
}
|
|
426
|
+
function rangeWindow(from, to, throughMinute, opts) {
|
|
427
|
+
const open = "from " + getTime({ hour: from, minute: 0 }, opts);
|
|
428
|
+
if (opts.style.untilWindow && !opts.short && from !== to) {
|
|
429
|
+
return open + " until " + getTime({ hour: (to + 1) % 24, minute: 0 }, opts);
|
|
430
|
+
}
|
|
431
|
+
return open + through(opts) + getTime({ hour: to, minute: throughMinute }, opts);
|
|
432
|
+
}
|
|
417
433
|
function hourWindow(window, opts) {
|
|
418
|
-
return
|
|
434
|
+
return rangeWindow(window.from, window.to, window.last, opts);
|
|
419
435
|
}
|
|
420
436
|
function renderClockTimes(ir, plan, opts) {
|
|
421
437
|
if (ir.shapes.minute === "single") {
|
|
@@ -434,7 +450,10 @@ function renderClockTimes(ir, plan, opts) {
|
|
|
434
450
|
plain
|
|
435
451
|
}, opts);
|
|
436
452
|
});
|
|
437
|
-
return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts);
|
|
453
|
+
return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts) + dayUnionTrail(ir, opts);
|
|
454
|
+
}
|
|
455
|
+
function dayUnionTrail(ir, opts) {
|
|
456
|
+
return isDayUnion(ir, opts) ? dayUnionCondition(ir, opts) : "";
|
|
438
457
|
}
|
|
439
458
|
function renderCompactClockTimes(ir, plan, opts) {
|
|
440
459
|
if (plan.fold) {
|
|
@@ -449,7 +468,7 @@ function renderCompactClockTimes(ir, plan, opts) {
|
|
|
449
468
|
return foldedHourWindows(ir, plan, opts) + trailingQualifier(ir, opts);
|
|
450
469
|
}
|
|
451
470
|
const fold = { minute: plan.minute, second: ir.analyses.clockSecond };
|
|
452
|
-
return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
|
|
471
|
+
return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts) + dayUnionTrail(ir, opts);
|
|
453
472
|
}
|
|
454
473
|
const minuteLead = (
|
|
455
474
|
// The non-fold branch is a minute list, which has segments. An
|
|
@@ -468,26 +487,161 @@ function renderCompactClockTimes(ir, plan, opts) {
|
|
|
468
487
|
function foldedHourWindows(ir, plan, opts) {
|
|
469
488
|
const minute = plan.minute;
|
|
470
489
|
const windows = [];
|
|
471
|
-
const
|
|
490
|
+
const outliers = collectHourOutliers(ir);
|
|
491
|
+
const times = outliers.hours.map(function time(hour) {
|
|
492
|
+
return getTime({ hour, minute }, opts);
|
|
493
|
+
});
|
|
472
494
|
ir.analyses.segments.hour.forEach(function classify(segment) {
|
|
473
495
|
if (segment.kind === "range") {
|
|
474
|
-
windows.push(
|
|
475
|
-
|
|
496
|
+
windows.push(rangeWindow(
|
|
497
|
+
+segment.bounds[0],
|
|
498
|
+
+segment.bounds[1],
|
|
499
|
+
minute,
|
|
476
500
|
opts
|
|
477
|
-
)
|
|
478
|
-
} else if (segment.kind === "step") {
|
|
479
|
-
singles.push(...segment.fires);
|
|
480
|
-
} else {
|
|
481
|
-
singles.push(+segment.value);
|
|
501
|
+
));
|
|
482
502
|
}
|
|
483
503
|
});
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
504
|
+
const phrase = rangeMinuteLead(ir, opts) + " " + joinList(windows, opts);
|
|
505
|
+
return phrase + outlierTail(times, outliers.pureStrays, opts);
|
|
506
|
+
}
|
|
507
|
+
function collectHourOutliers(ir) {
|
|
508
|
+
const hours = [];
|
|
509
|
+
let pureStrays = true;
|
|
510
|
+
ir.analyses.segments.hour.forEach(function classify(segment) {
|
|
511
|
+
if (segment.kind === "step") {
|
|
512
|
+
hours.push(...segment.fires);
|
|
513
|
+
pureStrays = false;
|
|
514
|
+
} else if (segment.kind !== "range") {
|
|
515
|
+
hours.push(+segment.value);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
return { hours, pureStrays };
|
|
519
|
+
}
|
|
520
|
+
function outlierTail(times, pureStrays, opts) {
|
|
521
|
+
if (!times.length) {
|
|
522
|
+
return "";
|
|
489
523
|
}
|
|
490
|
-
|
|
524
|
+
const connector = pureStrays && opts.style.untilWindow && !opts.short ? " plus " : " and at ";
|
|
525
|
+
return connector + joinList(times, opts);
|
|
526
|
+
}
|
|
527
|
+
function isCadenceField(token) {
|
|
528
|
+
return token === "*" || token.startsWith("*/") && token.indexOf("-") === -1;
|
|
529
|
+
}
|
|
530
|
+
function leadingCadence(ir, opts) {
|
|
531
|
+
const { second, minute } = ir.pattern;
|
|
532
|
+
if (isCadenceField(second)) {
|
|
533
|
+
return { secondLead: true, text: secondsClause(ir, "minute", opts) };
|
|
534
|
+
}
|
|
535
|
+
if (second === "0" && isCadenceField(minute)) {
|
|
536
|
+
const text = minute === "*" ? "every minute" : (
|
|
537
|
+
// A clean minute step's first segment is a step segment.
|
|
538
|
+
stepCycle60(
|
|
539
|
+
ir.analyses.segments.minute[0],
|
|
540
|
+
"minute",
|
|
541
|
+
"hour",
|
|
542
|
+
opts
|
|
543
|
+
)
|
|
544
|
+
);
|
|
545
|
+
return { secondLead: false, text };
|
|
546
|
+
}
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
function minuteConfinement(ir, opts) {
|
|
550
|
+
const minute = ir.pattern.minute;
|
|
551
|
+
if (minute === "*") {
|
|
552
|
+
return "";
|
|
553
|
+
}
|
|
554
|
+
if (isCadenceField(minute)) {
|
|
555
|
+
return " of every other minute";
|
|
556
|
+
}
|
|
557
|
+
const segments = ir.analyses.segments.minute;
|
|
558
|
+
if (ir.shapes.minute === "single") {
|
|
559
|
+
return " during minute :" + pad(minute);
|
|
560
|
+
}
|
|
561
|
+
if (ir.shapes.minute === "range") {
|
|
562
|
+
const bounds = minute.split("-");
|
|
563
|
+
return " during minutes :" + pad(bounds[0]) + through(opts) + ":" + pad(bounds[1]);
|
|
564
|
+
}
|
|
565
|
+
const values = segmentWords(segments, opts).map(function colon(word) {
|
|
566
|
+
return ":" + pad(word);
|
|
567
|
+
});
|
|
568
|
+
return " during minutes " + joinList(values, opts);
|
|
569
|
+
}
|
|
570
|
+
function hourConfinement(ir, opts) {
|
|
571
|
+
const hour = ir.pattern.hour;
|
|
572
|
+
if (hour === "*") {
|
|
573
|
+
const minutePinned = ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute);
|
|
574
|
+
return minutePinned ? " of every hour" : "";
|
|
575
|
+
}
|
|
576
|
+
if (isCadenceField(hour)) {
|
|
577
|
+
return hour === "*/2" ? " of every other hour" : "";
|
|
578
|
+
}
|
|
579
|
+
if (ir.shapes.hour === "single") {
|
|
580
|
+
const h = +hour;
|
|
581
|
+
if (ir.shapes.minute === "step") {
|
|
582
|
+
return " from " + getTime({ hour: h, minute: 0 }, opts) + " until " + getTime({ hour: (h + 1) % 24, minute: 0 }, opts);
|
|
583
|
+
}
|
|
584
|
+
if (ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute)) {
|
|
585
|
+
return " at " + getTime({ hour: h, minute: 0 }, opts);
|
|
586
|
+
}
|
|
587
|
+
return " of the " + getTime({ hour: h, minute: 0 }, opts) + " hour";
|
|
588
|
+
}
|
|
589
|
+
if (ir.shapes.hour === "range") {
|
|
590
|
+
const bounds = hour.split("-");
|
|
591
|
+
return " " + rangeWindow(+bounds[0], +bounds[1], 0, opts);
|
|
592
|
+
}
|
|
593
|
+
return " during the " + hourSegmentTimes(ir, { minute: 0, second: null }, false, opts) + " hours";
|
|
594
|
+
}
|
|
595
|
+
function isContiguousHourRange(ir) {
|
|
596
|
+
return ir.shapes.hour === "range";
|
|
597
|
+
}
|
|
598
|
+
function confinableHour(ir) {
|
|
599
|
+
if (ir.shapes.hour !== "step") {
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
const segment = ir.analyses.segments.hour[0];
|
|
603
|
+
return ir.pattern.hour === "*/2" || segment.startToken.indexOf("-") !== -1;
|
|
604
|
+
}
|
|
605
|
+
function isMinuteStride(ir) {
|
|
606
|
+
if (ir.shapes.minute !== "list") {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
const values = singleValues(ir.analyses.segments.minute);
|
|
610
|
+
return values !== null && arithmeticStep(values) !== null;
|
|
611
|
+
}
|
|
612
|
+
function confinementEligible(ir, lead) {
|
|
613
|
+
const { minute, hour } = ir.pattern;
|
|
614
|
+
const minuteStep = isCadenceField(minute) && minute !== "*";
|
|
615
|
+
if (!confinableHour(ir)) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
if (lead.secondLead) {
|
|
619
|
+
if (minuteStep) {
|
|
620
|
+
return minute === "*/2" && !isContiguousHourRange(ir);
|
|
621
|
+
}
|
|
622
|
+
if (isMinuteStride(ir) || ir.shapes.minute === "list" && ir.shapes.hour === "list") {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
if (hour === "*/2") {
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
return ir.shapes.hour === "single" && minute === "*/2";
|
|
631
|
+
}
|
|
632
|
+
function confinement(ir, opts) {
|
|
633
|
+
if (!opts.style.untilWindow || opts.short) {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
if (ir.pattern.minute === "*" && ir.pattern.hour === "*") {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
const lead = leadingCadence(ir, opts);
|
|
640
|
+
if (!lead || !confinementEligible(ir, lead)) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
const minutePart = lead.secondLead ? minuteConfinement(ir, opts) : "";
|
|
644
|
+
return lead.text + minutePart + hourConfinement(ir, opts) + trailingQualifier(ir, opts);
|
|
491
645
|
}
|
|
492
646
|
var renderers = {
|
|
493
647
|
clockTimes: renderClockTimes,
|
|
@@ -519,7 +673,7 @@ function renderStride(stride, opts) {
|
|
|
519
673
|
if (start < interval && tiles) {
|
|
520
674
|
return cadence + " from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
|
|
521
675
|
}
|
|
522
|
-
const num = seriesNumber(
|
|
676
|
+
const num = seriesNumber();
|
|
523
677
|
return cadence + " from " + num(start) + through(opts) + num(last) + " " + pluralize(last, unit) + " past the " + anchor;
|
|
524
678
|
}
|
|
525
679
|
function singleValues(segments) {
|
|
@@ -646,9 +800,9 @@ function hourCadence(ir, minute, opts) {
|
|
|
646
800
|
if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
|
|
647
801
|
return null;
|
|
648
802
|
}
|
|
649
|
-
const
|
|
650
|
-
if (
|
|
651
|
-
return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(
|
|
803
|
+
const minuteZeroStride = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
|
|
804
|
+
if (minuteZeroStride) {
|
|
805
|
+
return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(minuteZeroStride, opts) + trailingQualifier(ir, opts);
|
|
652
806
|
}
|
|
653
807
|
if (minute === 0 && ir.pattern.second === "0") {
|
|
654
808
|
return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
|
|
@@ -670,26 +824,22 @@ function hasHourWindow(ir) {
|
|
|
670
824
|
}
|
|
671
825
|
function hourRangeWindowTail(ir, opts) {
|
|
672
826
|
const windows = [];
|
|
673
|
-
const
|
|
827
|
+
const outliers = collectHourOutliers(ir);
|
|
674
828
|
ir.analyses.segments.hour.forEach(function classify(segment) {
|
|
675
829
|
if (segment.kind === "range") {
|
|
676
|
-
windows.push(
|
|
677
|
-
|
|
830
|
+
windows.push(rangeWindow(
|
|
831
|
+
+segment.bounds[0],
|
|
832
|
+
+segment.bounds[1],
|
|
833
|
+
0,
|
|
678
834
|
opts
|
|
679
|
-
)
|
|
680
|
-
} else if (segment.kind === "step") {
|
|
681
|
-
singles.push(...segment.fires);
|
|
682
|
-
} else {
|
|
683
|
-
singles.push(+segment.value);
|
|
835
|
+
));
|
|
684
836
|
}
|
|
685
837
|
});
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
return phrase;
|
|
838
|
+
const phrase = "every hour " + joinList(windows, opts);
|
|
839
|
+
const times = outliers.hours.map(function time(hour) {
|
|
840
|
+
return getTime({ hour, minute: 0 }, opts);
|
|
841
|
+
});
|
|
842
|
+
return phrase + outlierTail(times, outliers.pureStrays, opts);
|
|
693
843
|
}
|
|
694
844
|
function hourRangeCadence(ir, minute, opts) {
|
|
695
845
|
if (minute !== 0 || !hasHourWindow(ir)) {
|
|
@@ -703,25 +853,29 @@ function hourRangeCadence(ir, minute, opts) {
|
|
|
703
853
|
}
|
|
704
854
|
return hourCadenceLead(ir, minute, opts) + ", " + hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
|
|
705
855
|
}
|
|
706
|
-
function seriesNumber(
|
|
707
|
-
const anyBig = values.some(function big(v) {
|
|
708
|
-
return +v > 10;
|
|
709
|
-
});
|
|
856
|
+
function seriesNumber() {
|
|
710
857
|
return function format(n) {
|
|
711
|
-
return
|
|
858
|
+
return "" + n;
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function listNumber(count, opts) {
|
|
862
|
+
return count > 1 ? function asNumeral(n) {
|
|
863
|
+
return "" + n;
|
|
864
|
+
} : function spelled(n) {
|
|
865
|
+
return getNumber(n, opts);
|
|
712
866
|
};
|
|
713
867
|
}
|
|
714
868
|
function numberWords(fires, opts) {
|
|
715
|
-
return fires.map(
|
|
869
|
+
return fires.map(listNumber(fires.length, opts));
|
|
716
870
|
}
|
|
717
871
|
function segmentWords(segments, opts) {
|
|
718
|
-
const
|
|
872
|
+
const count = segments.reduce(function tally(sum, segment) {
|
|
719
873
|
if (segment.kind === "range") {
|
|
720
|
-
return
|
|
874
|
+
return sum + 1;
|
|
721
875
|
}
|
|
722
|
-
return segment.kind === "step" ? segment.fires :
|
|
723
|
-
});
|
|
724
|
-
const num =
|
|
876
|
+
return sum + (segment.kind === "step" ? segment.fires.length : 1);
|
|
877
|
+
}, 0);
|
|
878
|
+
const num = listNumber(count, opts);
|
|
725
879
|
return segments.flatMap(function word(segment) {
|
|
726
880
|
if (segment.kind === "range") {
|
|
727
881
|
return [num(segment.bounds[0]) + through(opts) + num(segment.bounds[1])];
|
|
@@ -753,6 +907,9 @@ function hourTimes(hours, opts) {
|
|
|
753
907
|
});
|
|
754
908
|
return joinList(times, opts);
|
|
755
909
|
}
|
|
910
|
+
function singleHourFire(times) {
|
|
911
|
+
return times.kind === "fires" && times.fires.length === 1;
|
|
912
|
+
}
|
|
756
913
|
function hourTimesFromPlan(ir, times, atContext, opts) {
|
|
757
914
|
if (times.kind === "fires") {
|
|
758
915
|
return hourTimes(times.fires, opts);
|
|
@@ -800,28 +957,47 @@ function disambiguateTimes(pieces, segments, atContext) {
|
|
|
800
957
|
return index === 0 ? piece : "at " + piece;
|
|
801
958
|
});
|
|
802
959
|
}
|
|
803
|
-
function
|
|
960
|
+
function joinWith(items, conjunction, opts) {
|
|
804
961
|
if (items.length <= 1) {
|
|
805
962
|
return items.join("");
|
|
806
963
|
}
|
|
807
964
|
if (items.length === 2) {
|
|
808
|
-
return items[0] +
|
|
965
|
+
return items[0] + conjunction + items[1];
|
|
809
966
|
}
|
|
810
|
-
const
|
|
811
|
-
return items.slice(0, -1).join(", ") +
|
|
967
|
+
const tail = opts.style.serialComma ? "," + conjunction : conjunction;
|
|
968
|
+
return items.slice(0, -1).join(", ") + tail + items[items.length - 1];
|
|
969
|
+
}
|
|
970
|
+
function joinList(items, opts) {
|
|
971
|
+
return joinWith(items, " and ", opts);
|
|
972
|
+
}
|
|
973
|
+
function joinOr(items, opts) {
|
|
974
|
+
return joinWith(items, " or ", opts);
|
|
812
975
|
}
|
|
813
|
-
var trailingWords = {
|
|
976
|
+
var trailingWords = {
|
|
977
|
+
all: "",
|
|
978
|
+
month: "in ",
|
|
979
|
+
recurringWeekday: true,
|
|
980
|
+
stepDate: "on ",
|
|
981
|
+
weekday: "on "
|
|
982
|
+
};
|
|
814
983
|
var leadingWords = {
|
|
815
984
|
all: "every day",
|
|
816
985
|
month: "every day in ",
|
|
986
|
+
recurringWeekday: false,
|
|
817
987
|
stepDate: "",
|
|
818
988
|
weekday: "every "
|
|
819
989
|
};
|
|
820
990
|
function trailingQualifier(ir, opts) {
|
|
991
|
+
if (isDayUnion(ir, opts)) {
|
|
992
|
+
return dayUnionCondition(ir, opts);
|
|
993
|
+
}
|
|
821
994
|
const phrase = dayQualifier(ir, trailingWords, opts);
|
|
822
995
|
return phrase && " " + phrase;
|
|
823
996
|
}
|
|
824
997
|
function interpretDayQualifier(ir, opts) {
|
|
998
|
+
if (isDayUnion(ir, opts)) {
|
|
999
|
+
return "";
|
|
1000
|
+
}
|
|
825
1001
|
return dayQualifier(ir, leadingWords, opts) + " ";
|
|
826
1002
|
}
|
|
827
1003
|
function dayQualifier(ir, words, opts) {
|
|
@@ -833,7 +1009,11 @@ function dayQualifier(ir, words, opts) {
|
|
|
833
1009
|
return datePhrase(ir, words, opts);
|
|
834
1010
|
}
|
|
835
1011
|
if (pattern.weekday !== "*") {
|
|
836
|
-
const
|
|
1012
|
+
const quartzWeekday = quartzWeekdayPhrase(pattern.weekday, opts);
|
|
1013
|
+
if (quartzWeekday) {
|
|
1014
|
+
return monthScopeForRecurrence(quartzWeekday, ir, opts);
|
|
1015
|
+
}
|
|
1016
|
+
const weekdays = words.weekday + weekdayPhrase(ir, words.recurringWeekday, opts);
|
|
837
1017
|
return weekdays + monthScope(ir, opts);
|
|
838
1018
|
}
|
|
839
1019
|
if (pattern.month !== "*") {
|
|
@@ -845,10 +1025,14 @@ function datePhrase(ir, words, opts) {
|
|
|
845
1025
|
const pattern = ir.pattern;
|
|
846
1026
|
const quartzDate = quartzDatePhrase(pattern.date, opts);
|
|
847
1027
|
if (quartzDate) {
|
|
848
|
-
return quartzDate
|
|
1028
|
+
return monthScopeForRecurrence(quartzDate, ir, opts);
|
|
849
1029
|
}
|
|
850
1030
|
if (isOpenStep(pattern.date)) {
|
|
851
|
-
return
|
|
1031
|
+
return monthScopeForRecurrence(
|
|
1032
|
+
words.stepDate + stepDates(pattern.date),
|
|
1033
|
+
ir,
|
|
1034
|
+
opts
|
|
1035
|
+
);
|
|
852
1036
|
}
|
|
853
1037
|
if (pattern.month !== "*" && !monthFoldsIntoDate(ir)) {
|
|
854
1038
|
return "on the " + dateOrdinals(ir, opts) + monthScope(ir, opts);
|
|
@@ -864,9 +1048,84 @@ function monthFoldsIntoDate(ir) {
|
|
|
864
1048
|
return segment.kind !== "range";
|
|
865
1049
|
});
|
|
866
1050
|
}
|
|
1051
|
+
function isDayUnion(ir, opts) {
|
|
1052
|
+
return ir.pattern.date !== "*" && ir.pattern.weekday !== "*" && !!opts.style.untilWindow && !opts.short;
|
|
1053
|
+
}
|
|
1054
|
+
function dayUnionCondition(ir, opts) {
|
|
1055
|
+
const pieces = [
|
|
1056
|
+
...dayUnionDatePieces(ir, opts),
|
|
1057
|
+
...dayUnionWeekdayPieces(ir, opts)
|
|
1058
|
+
];
|
|
1059
|
+
return " whenever the day is " + joinOr(pieces, opts);
|
|
1060
|
+
}
|
|
1061
|
+
function dayUnionMonthLead(ir, opts) {
|
|
1062
|
+
if (ir.pattern.month === "*") {
|
|
1063
|
+
return "";
|
|
1064
|
+
}
|
|
1065
|
+
return "in " + monthName(ir, opts) + " ";
|
|
1066
|
+
}
|
|
1067
|
+
function dayUnionDatePieces(ir, opts) {
|
|
1068
|
+
const dateField = ir.pattern.date;
|
|
1069
|
+
const quartz = quartzDatePhrase(dateField, opts);
|
|
1070
|
+
if (quartz) {
|
|
1071
|
+
return [quartz.replace(/^on /, "")];
|
|
1072
|
+
}
|
|
1073
|
+
const oddEven = oddEvenDay(dateField);
|
|
1074
|
+
if (oddEven) {
|
|
1075
|
+
return [oddEven];
|
|
1076
|
+
}
|
|
1077
|
+
const pieces = [];
|
|
1078
|
+
ir.analyses.segments.date.forEach(function expand(segment) {
|
|
1079
|
+
if (segment.kind === "range") {
|
|
1080
|
+
pieces.push("from the " + getOrdinal(segment.bounds[0]) + through(opts) + "the " + getOrdinal(segment.bounds[1]));
|
|
1081
|
+
} else if (segment.kind === "step") {
|
|
1082
|
+
segment.fires.forEach(function fire(value) {
|
|
1083
|
+
pieces.push("the " + getOrdinal(value));
|
|
1084
|
+
});
|
|
1085
|
+
} else {
|
|
1086
|
+
pieces.push("the " + getOrdinal(segment.value));
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
return pieces;
|
|
1090
|
+
}
|
|
1091
|
+
function dayUnionWeekdayPieces(ir, opts) {
|
|
1092
|
+
const weekdayField = ir.pattern.weekday;
|
|
1093
|
+
const quartz = quartzWeekdayPhrase(weekdayField, opts);
|
|
1094
|
+
if (quartz) {
|
|
1095
|
+
return [quartz.replace(/^on /, "")];
|
|
1096
|
+
}
|
|
1097
|
+
const pieces = [];
|
|
1098
|
+
ir.analyses.segments.weekday.forEach(function expand(segment) {
|
|
1099
|
+
if (segment.kind === "range" && segment.bounds[0] === "1" && segment.bounds[1] === "5") {
|
|
1100
|
+
pieces.push("a weekday");
|
|
1101
|
+
} else if (segment.kind === "range") {
|
|
1102
|
+
pieces.push("a " + getWeekday(segment.bounds[0], opts) + through(opts) + "a " + getWeekday(segment.bounds[1], opts));
|
|
1103
|
+
} else if (segment.kind === "step") {
|
|
1104
|
+
segment.fires.forEach(function fire(value) {
|
|
1105
|
+
pieces.push("a " + getWeekday(value, opts));
|
|
1106
|
+
});
|
|
1107
|
+
} else {
|
|
1108
|
+
pieces.push("a " + getWeekday(segment.value, opts));
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
return pieces;
|
|
1112
|
+
}
|
|
1113
|
+
function oddEvenDay(dateField) {
|
|
1114
|
+
if (!isOpenStep(dateField)) {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
const [start, step] = dateField.split("/");
|
|
1118
|
+
if (+step !== 2) {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
if (start === "*" || start === "1") {
|
|
1122
|
+
return "an odd-numbered day";
|
|
1123
|
+
}
|
|
1124
|
+
return start === "2" ? "an even-numbered day" : null;
|
|
1125
|
+
}
|
|
867
1126
|
function dateOrWeekday(ir, opts) {
|
|
868
1127
|
const pattern = ir.pattern;
|
|
869
|
-
const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, opts);
|
|
1128
|
+
const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, false, opts);
|
|
870
1129
|
if (pattern.month !== "*" && monthFoldsIntoDate(ir) && !quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
|
|
871
1130
|
return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
|
|
872
1131
|
}
|
|
@@ -921,6 +1180,9 @@ function monthDatePhrase(ir, opts) {
|
|
|
921
1180
|
opts.style.ordinals ? getOrdinal : cardinalDay,
|
|
922
1181
|
opts
|
|
923
1182
|
);
|
|
1183
|
+
if (opts.style.dayFirst && ir.shapes.date === "single" && ir.shapes.month !== "single") {
|
|
1184
|
+
return "the " + getOrdinal(ir.pattern.date) + " of " + month;
|
|
1185
|
+
}
|
|
924
1186
|
return opts.style.dayFirst ? days + " " + month : month + " " + days;
|
|
925
1187
|
}
|
|
926
1188
|
function cardinalDay(value) {
|
|
@@ -932,6 +1194,19 @@ function monthScope(ir, opts) {
|
|
|
932
1194
|
}
|
|
933
1195
|
return " in " + monthName(ir, opts);
|
|
934
1196
|
}
|
|
1197
|
+
function monthScopeForRecurrence(phrase, ir, opts) {
|
|
1198
|
+
if (ir.pattern.month === "*") {
|
|
1199
|
+
return phrase;
|
|
1200
|
+
}
|
|
1201
|
+
const carriesRecurrence = phrase.indexOf(" of the month") !== -1;
|
|
1202
|
+
if (carriesRecurrence && ir.shapes.month === "range") {
|
|
1203
|
+
return phrase.replace(" of the month", " of each month") + " from " + monthName(ir, opts);
|
|
1204
|
+
}
|
|
1205
|
+
if (carriesRecurrence && (ir.shapes.month === "single" || ir.shapes.month === "step")) {
|
|
1206
|
+
return phrase.replace(" of the month", "") + " in " + monthName(ir, opts);
|
|
1207
|
+
}
|
|
1208
|
+
return phrase + " in " + monthName(ir, opts);
|
|
1209
|
+
}
|
|
935
1210
|
function stepDates(dateField) {
|
|
936
1211
|
const parts = dateField.split("/");
|
|
937
1212
|
const interval = +parts[1];
|
|
@@ -968,11 +1243,21 @@ function oddEvenMonth(monthField) {
|
|
|
968
1243
|
}
|
|
969
1244
|
return start === "2" ? "every even-numbered month" : null;
|
|
970
1245
|
}
|
|
971
|
-
function weekdayPhrase(ir, opts) {
|
|
1246
|
+
function weekdayPhrase(ir, recurring, opts) {
|
|
972
1247
|
const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
|
|
973
|
-
|
|
1248
|
+
const hasRange = segments.some(function range(segment) {
|
|
1249
|
+
return segment.kind === "range";
|
|
1250
|
+
});
|
|
1251
|
+
const name = recurring && !hasRange ? function plural(value) {
|
|
1252
|
+
return pluralWeekday(value, opts);
|
|
1253
|
+
} : function singular(value) {
|
|
974
1254
|
return getWeekday(value, opts);
|
|
975
|
-
}
|
|
1255
|
+
};
|
|
1256
|
+
return renderSegments(segments, name, opts);
|
|
1257
|
+
}
|
|
1258
|
+
function pluralWeekday(value, opts) {
|
|
1259
|
+
const name = getWeekday(value, opts);
|
|
1260
|
+
return opts.short ? name : name + "s";
|
|
976
1261
|
}
|
|
977
1262
|
function renderSegments(segments, word, opts) {
|
|
978
1263
|
const pieces = [];
|
|
@@ -996,7 +1281,7 @@ function applyYear(description, ir, opts) {
|
|
|
996
1281
|
return description;
|
|
997
1282
|
}
|
|
998
1283
|
if (yearField.indexOf("/") !== -1) {
|
|
999
|
-
return description + " " + stepYears(yearField, opts);
|
|
1284
|
+
return description + ", " + stepYears(yearField, opts);
|
|
1000
1285
|
}
|
|
1001
1286
|
const label = yearLabel(yearField, opts);
|
|
1002
1287
|
if (yearField.indexOf("-") === -1 && yearField.indexOf(",") === -1 && ir.pattern.date !== "*" && description.indexOf(" at ") !== -1) {
|
|
@@ -1009,6 +1294,9 @@ function yearLabel(yearField, opts) {
|
|
|
1009
1294
|
if (yearField.indexOf(",") !== -1) {
|
|
1010
1295
|
return joinList(yearField.split(","), opts);
|
|
1011
1296
|
}
|
|
1297
|
+
if (yearField.indexOf("-") !== -1) {
|
|
1298
|
+
return yearField.split("-").join(through(opts));
|
|
1299
|
+
}
|
|
1012
1300
|
return yearField;
|
|
1013
1301
|
}
|
|
1014
1302
|
function stepYears(yearField, opts) {
|
|
@@ -1018,7 +1306,7 @@ function stepYears(yearField, opts) {
|
|
|
1018
1306
|
if (interval <= 1) {
|
|
1019
1307
|
return "every year";
|
|
1020
1308
|
}
|
|
1021
|
-
let phrase = "every " + getNumber(interval, opts) + " years";
|
|
1309
|
+
let phrase = interval === 2 ? "every other year" : "every " + getNumber(interval, opts) + " years";
|
|
1022
1310
|
if (start !== "*" && start !== "0") {
|
|
1023
1311
|
phrase += " from " + start;
|
|
1024
1312
|
}
|
package/package.json
CHANGED
package/src/core/ir.ts
CHANGED
|
@@ -132,6 +132,11 @@ export interface DialectStyle {
|
|
|
132
132
|
sep: string;
|
|
133
133
|
serialComma: boolean;
|
|
134
134
|
through: string;
|
|
135
|
+
// Whether a contiguous hour range reads as an up-to-but-not-including
|
|
136
|
+
// window ("from 9 a.m. until 6 p.m.") rather than a "through <last fire>"
|
|
137
|
+
// span. Set only on the default English dialect; other dialects and custom
|
|
138
|
+
// styles keep the "through" span.
|
|
139
|
+
untilWindow?: boolean;
|
|
135
140
|
}
|
|
136
141
|
|
|
137
142
|
/**
|