cronli5 0.1.6 → 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 +86 -0
- package/README.md +6 -6
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +401 -81
- package/dist/cronli5.js +401 -81
- package/dist/lang/de.cjs +63 -17
- package/dist/lang/de.js +63 -17
- package/dist/lang/en.cjs +401 -81
- package/dist/lang/en.js +401 -81
- package/dist/lang/es.cjs +68 -5
- package/dist/lang/es.js +68 -5
- package/dist/lang/fi.cjs +21 -1
- package/dist/lang/fi.js +21 -1
- package/dist/lang/zh.cjs +38 -7
- package/dist/lang/zh.js +38 -7
- package/package.json +1 -1
- package/src/core/ir.ts +5 -0
- package/src/core/util.ts +52 -1
- package/src/lang/de/index.ts +95 -25
- package/src/lang/en/dialects.ts +6 -2
- package/src/lang/en/index.ts +781 -117
- package/src/lang/es/index.ts +85 -9
- package/src/lang/fi/index.ts +6 -2
- package/src/lang/zh/index.ts +44 -18
- package/types/core/ir.d.ts +1 -0
- package/types/core/util.d.ts +10 -1
package/dist/lang/en.js
CHANGED
|
@@ -14,6 +14,26 @@ function arithmeticStep(values) {
|
|
|
14
14
|
}
|
|
15
15
|
return { start: values[0], interval, last: values[values.length - 1] };
|
|
16
16
|
}
|
|
17
|
+
function weekdayDisplayKey(value) {
|
|
18
|
+
return value === 0 ? 7 : value;
|
|
19
|
+
}
|
|
20
|
+
function orderWeekdaysForDisplay(segments) {
|
|
21
|
+
const flattened = segments.flatMap(function flat(segment) {
|
|
22
|
+
return segment.kind === "step" ? segment.fires.map(function single(value) {
|
|
23
|
+
return { kind: "single", value: "" + value };
|
|
24
|
+
}) : [segment];
|
|
25
|
+
});
|
|
26
|
+
function key(segment) {
|
|
27
|
+
return segment.kind === "range" ? weekdayDisplayKey(+segment.bounds[0]) : weekdayDisplayKey(+segment.value);
|
|
28
|
+
}
|
|
29
|
+
return flattened.map(function index(segment, position) {
|
|
30
|
+
return [segment, position];
|
|
31
|
+
}).sort(function byDisplayKey(a, b) {
|
|
32
|
+
return key(a[0]) - key(b[0]) || a[1] - b[1];
|
|
33
|
+
}).map(function unwrap(pair) {
|
|
34
|
+
return pair[0];
|
|
35
|
+
});
|
|
36
|
+
}
|
|
17
37
|
|
|
18
38
|
// src/core/specs.ts
|
|
19
39
|
var maxClockTimes = 6;
|
|
@@ -58,7 +78,8 @@ var dialects = {
|
|
|
58
78
|
pm: "p.m.",
|
|
59
79
|
sep: ":",
|
|
60
80
|
serialComma: true,
|
|
61
|
-
through: " through "
|
|
81
|
+
through: " through ",
|
|
82
|
+
untilWindow: true
|
|
62
83
|
},
|
|
63
84
|
house: {
|
|
64
85
|
am: "AM",
|
|
@@ -75,7 +96,7 @@ var dialects = {
|
|
|
75
96
|
};
|
|
76
97
|
function resolveDialect(dialect) {
|
|
77
98
|
if (typeof dialect === "object" && dialect !== null) {
|
|
78
|
-
return { ...dialects.us, ...dialect };
|
|
99
|
+
return { ...dialects.us, untilWindow: false, ...dialect };
|
|
79
100
|
}
|
|
80
101
|
const name = dialect === "uk" ? "gb" : dialect;
|
|
81
102
|
return dialects[name] || dialects.us;
|
|
@@ -147,7 +168,9 @@ function normalizeOptions(options) {
|
|
|
147
168
|
};
|
|
148
169
|
}
|
|
149
170
|
function describe(ir, opts) {
|
|
150
|
-
|
|
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);
|
|
151
174
|
}
|
|
152
175
|
function render(ir, plan, opts) {
|
|
153
176
|
const renderer = renderers[plan.kind];
|
|
@@ -240,7 +263,7 @@ function secondsClause(ir, anchor, opts) {
|
|
|
240
263
|
}
|
|
241
264
|
if (shape === "range") {
|
|
242
265
|
const bounds = secondField.split("-");
|
|
243
|
-
const num = seriesNumber(
|
|
266
|
+
const num = seriesNumber();
|
|
244
267
|
return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the " + anchor;
|
|
245
268
|
}
|
|
246
269
|
if (shape === "single") {
|
|
@@ -306,15 +329,21 @@ function renderMinutesAcrossHours(ir, plan, opts) {
|
|
|
306
329
|
}
|
|
307
330
|
return "every minute during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
|
|
308
331
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
"
|
|
316
|
-
|
|
317
|
-
)
|
|
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
|
|
318
347
|
);
|
|
319
348
|
if (cadence !== null) {
|
|
320
349
|
return lead + ", " + cadence + trailingQualifier(ir, opts);
|
|
@@ -351,7 +380,7 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
|
|
|
351
380
|
}
|
|
352
381
|
function minuteRangeLead(minuteField, opts) {
|
|
353
382
|
const bounds = minuteField.split("-");
|
|
354
|
-
const num = seriesNumber(
|
|
383
|
+
const num = seriesNumber();
|
|
355
384
|
return "every minute from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the hour";
|
|
356
385
|
}
|
|
357
386
|
function renderEveryHour(ir, plan, opts) {
|
|
@@ -391,10 +420,18 @@ function renderHourStep(ir, plan, opts) {
|
|
|
391
420
|
return stepHours(ir.analyses.segments.hour[0], opts) + trailingQualifier(ir, opts);
|
|
392
421
|
}
|
|
393
422
|
function boundedWindow(plan) {
|
|
394
|
-
|
|
423
|
+
const last = plan.minuteForm === "wildcard" ? plan.boundMinute ?? 0 : 0;
|
|
424
|
+
return { from: plan.from, last, to: plan.to };
|
|
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);
|
|
395
432
|
}
|
|
396
433
|
function hourWindow(window, opts) {
|
|
397
|
-
return
|
|
434
|
+
return rangeWindow(window.from, window.to, window.last, opts);
|
|
398
435
|
}
|
|
399
436
|
function renderClockTimes(ir, plan, opts) {
|
|
400
437
|
if (ir.shapes.minute === "single") {
|
|
@@ -413,7 +450,10 @@ function renderClockTimes(ir, plan, opts) {
|
|
|
413
450
|
plain
|
|
414
451
|
}, opts);
|
|
415
452
|
});
|
|
416
|
-
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) : "";
|
|
417
457
|
}
|
|
418
458
|
function renderCompactClockTimes(ir, plan, opts) {
|
|
419
459
|
if (plan.fold) {
|
|
@@ -428,7 +468,7 @@ function renderCompactClockTimes(ir, plan, opts) {
|
|
|
428
468
|
return foldedHourWindows(ir, plan, opts) + trailingQualifier(ir, opts);
|
|
429
469
|
}
|
|
430
470
|
const fold = { minute: plan.minute, second: ir.analyses.clockSecond };
|
|
431
|
-
return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
|
|
471
|
+
return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts) + dayUnionTrail(ir, opts);
|
|
432
472
|
}
|
|
433
473
|
const minuteLead = (
|
|
434
474
|
// The non-fold branch is a minute list, which has segments. An
|
|
@@ -447,26 +487,161 @@ function renderCompactClockTimes(ir, plan, opts) {
|
|
|
447
487
|
function foldedHourWindows(ir, plan, opts) {
|
|
448
488
|
const minute = plan.minute;
|
|
449
489
|
const windows = [];
|
|
450
|
-
const
|
|
490
|
+
const outliers = collectHourOutliers(ir);
|
|
491
|
+
const times = outliers.hours.map(function time(hour) {
|
|
492
|
+
return getTime({ hour, minute }, opts);
|
|
493
|
+
});
|
|
451
494
|
ir.analyses.segments.hour.forEach(function classify(segment) {
|
|
452
495
|
if (segment.kind === "range") {
|
|
453
|
-
windows.push(
|
|
454
|
-
|
|
496
|
+
windows.push(rangeWindow(
|
|
497
|
+
+segment.bounds[0],
|
|
498
|
+
+segment.bounds[1],
|
|
499
|
+
minute,
|
|
455
500
|
opts
|
|
456
|
-
)
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
501
|
+
));
|
|
502
|
+
}
|
|
503
|
+
});
|
|
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);
|
|
461
516
|
}
|
|
462
517
|
});
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
518
|
+
return { hours, pureStrays };
|
|
519
|
+
}
|
|
520
|
+
function outlierTail(times, pureStrays, opts) {
|
|
521
|
+
if (!times.length) {
|
|
522
|
+
return "";
|
|
468
523
|
}
|
|
469
|
-
|
|
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);
|
|
470
645
|
}
|
|
471
646
|
var renderers = {
|
|
472
647
|
clockTimes: renderClockTimes,
|
|
@@ -498,7 +673,7 @@ function renderStride(stride, opts) {
|
|
|
498
673
|
if (start < interval && tiles) {
|
|
499
674
|
return cadence + " from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
|
|
500
675
|
}
|
|
501
|
-
const num = seriesNumber(
|
|
676
|
+
const num = seriesNumber();
|
|
502
677
|
return cadence + " from " + num(start) + through(opts) + num(last) + " " + pluralize(last, unit) + " past the " + anchor;
|
|
503
678
|
}
|
|
504
679
|
function singleValues(segments) {
|
|
@@ -625,9 +800,9 @@ function hourCadence(ir, minute, opts) {
|
|
|
625
800
|
if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
|
|
626
801
|
return null;
|
|
627
802
|
}
|
|
628
|
-
const
|
|
629
|
-
if (
|
|
630
|
-
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);
|
|
631
806
|
}
|
|
632
807
|
if (minute === 0 && ir.pattern.second === "0") {
|
|
633
808
|
return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
|
|
@@ -649,26 +824,22 @@ function hasHourWindow(ir) {
|
|
|
649
824
|
}
|
|
650
825
|
function hourRangeWindowTail(ir, opts) {
|
|
651
826
|
const windows = [];
|
|
652
|
-
const
|
|
827
|
+
const outliers = collectHourOutliers(ir);
|
|
653
828
|
ir.analyses.segments.hour.forEach(function classify(segment) {
|
|
654
829
|
if (segment.kind === "range") {
|
|
655
|
-
windows.push(
|
|
656
|
-
|
|
830
|
+
windows.push(rangeWindow(
|
|
831
|
+
+segment.bounds[0],
|
|
832
|
+
+segment.bounds[1],
|
|
833
|
+
0,
|
|
657
834
|
opts
|
|
658
|
-
)
|
|
659
|
-
} else if (segment.kind === "step") {
|
|
660
|
-
singles.push(...segment.fires);
|
|
661
|
-
} else {
|
|
662
|
-
singles.push(+segment.value);
|
|
835
|
+
));
|
|
663
836
|
}
|
|
664
837
|
});
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
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);
|
|
672
843
|
}
|
|
673
844
|
function hourRangeCadence(ir, minute, opts) {
|
|
674
845
|
if (minute !== 0 || !hasHourWindow(ir)) {
|
|
@@ -682,25 +853,29 @@ function hourRangeCadence(ir, minute, opts) {
|
|
|
682
853
|
}
|
|
683
854
|
return hourCadenceLead(ir, minute, opts) + ", " + hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
|
|
684
855
|
}
|
|
685
|
-
function seriesNumber(
|
|
686
|
-
const anyBig = values.some(function big(v) {
|
|
687
|
-
return +v > 10;
|
|
688
|
-
});
|
|
856
|
+
function seriesNumber() {
|
|
689
857
|
return function format(n) {
|
|
690
|
-
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);
|
|
691
866
|
};
|
|
692
867
|
}
|
|
693
868
|
function numberWords(fires, opts) {
|
|
694
|
-
return fires.map(
|
|
869
|
+
return fires.map(listNumber(fires.length, opts));
|
|
695
870
|
}
|
|
696
871
|
function segmentWords(segments, opts) {
|
|
697
|
-
const
|
|
872
|
+
const count = segments.reduce(function tally(sum, segment) {
|
|
698
873
|
if (segment.kind === "range") {
|
|
699
|
-
return
|
|
874
|
+
return sum + 1;
|
|
700
875
|
}
|
|
701
|
-
return segment.kind === "step" ? segment.fires :
|
|
702
|
-
});
|
|
703
|
-
const num =
|
|
876
|
+
return sum + (segment.kind === "step" ? segment.fires.length : 1);
|
|
877
|
+
}, 0);
|
|
878
|
+
const num = listNumber(count, opts);
|
|
704
879
|
return segments.flatMap(function word(segment) {
|
|
705
880
|
if (segment.kind === "range") {
|
|
706
881
|
return [num(segment.bounds[0]) + through(opts) + num(segment.bounds[1])];
|
|
@@ -732,6 +907,9 @@ function hourTimes(hours, opts) {
|
|
|
732
907
|
});
|
|
733
908
|
return joinList(times, opts);
|
|
734
909
|
}
|
|
910
|
+
function singleHourFire(times) {
|
|
911
|
+
return times.kind === "fires" && times.fires.length === 1;
|
|
912
|
+
}
|
|
735
913
|
function hourTimesFromPlan(ir, times, atContext, opts) {
|
|
736
914
|
if (times.kind === "fires") {
|
|
737
915
|
return hourTimes(times.fires, opts);
|
|
@@ -779,28 +957,47 @@ function disambiguateTimes(pieces, segments, atContext) {
|
|
|
779
957
|
return index === 0 ? piece : "at " + piece;
|
|
780
958
|
});
|
|
781
959
|
}
|
|
782
|
-
function
|
|
960
|
+
function joinWith(items, conjunction, opts) {
|
|
783
961
|
if (items.length <= 1) {
|
|
784
962
|
return items.join("");
|
|
785
963
|
}
|
|
786
964
|
if (items.length === 2) {
|
|
787
|
-
return items[0] +
|
|
965
|
+
return items[0] + conjunction + items[1];
|
|
788
966
|
}
|
|
789
|
-
const
|
|
790
|
-
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];
|
|
791
969
|
}
|
|
792
|
-
|
|
970
|
+
function joinList(items, opts) {
|
|
971
|
+
return joinWith(items, " and ", opts);
|
|
972
|
+
}
|
|
973
|
+
function joinOr(items, opts) {
|
|
974
|
+
return joinWith(items, " or ", opts);
|
|
975
|
+
}
|
|
976
|
+
var trailingWords = {
|
|
977
|
+
all: "",
|
|
978
|
+
month: "in ",
|
|
979
|
+
recurringWeekday: true,
|
|
980
|
+
stepDate: "on ",
|
|
981
|
+
weekday: "on "
|
|
982
|
+
};
|
|
793
983
|
var leadingWords = {
|
|
794
984
|
all: "every day",
|
|
795
985
|
month: "every day in ",
|
|
986
|
+
recurringWeekday: false,
|
|
796
987
|
stepDate: "",
|
|
797
988
|
weekday: "every "
|
|
798
989
|
};
|
|
799
990
|
function trailingQualifier(ir, opts) {
|
|
991
|
+
if (isDayUnion(ir, opts)) {
|
|
992
|
+
return dayUnionCondition(ir, opts);
|
|
993
|
+
}
|
|
800
994
|
const phrase = dayQualifier(ir, trailingWords, opts);
|
|
801
995
|
return phrase && " " + phrase;
|
|
802
996
|
}
|
|
803
997
|
function interpretDayQualifier(ir, opts) {
|
|
998
|
+
if (isDayUnion(ir, opts)) {
|
|
999
|
+
return "";
|
|
1000
|
+
}
|
|
804
1001
|
return dayQualifier(ir, leadingWords, opts) + " ";
|
|
805
1002
|
}
|
|
806
1003
|
function dayQualifier(ir, words, opts) {
|
|
@@ -812,7 +1009,11 @@ function dayQualifier(ir, words, opts) {
|
|
|
812
1009
|
return datePhrase(ir, words, opts);
|
|
813
1010
|
}
|
|
814
1011
|
if (pattern.weekday !== "*") {
|
|
815
|
-
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);
|
|
816
1017
|
return weekdays + monthScope(ir, opts);
|
|
817
1018
|
}
|
|
818
1019
|
if (pattern.month !== "*") {
|
|
@@ -824,10 +1025,14 @@ function datePhrase(ir, words, opts) {
|
|
|
824
1025
|
const pattern = ir.pattern;
|
|
825
1026
|
const quartzDate = quartzDatePhrase(pattern.date, opts);
|
|
826
1027
|
if (quartzDate) {
|
|
827
|
-
return quartzDate
|
|
1028
|
+
return monthScopeForRecurrence(quartzDate, ir, opts);
|
|
828
1029
|
}
|
|
829
1030
|
if (isOpenStep(pattern.date)) {
|
|
830
|
-
return
|
|
1031
|
+
return monthScopeForRecurrence(
|
|
1032
|
+
words.stepDate + stepDates(pattern.date),
|
|
1033
|
+
ir,
|
|
1034
|
+
opts
|
|
1035
|
+
);
|
|
831
1036
|
}
|
|
832
1037
|
if (pattern.month !== "*" && !monthFoldsIntoDate(ir)) {
|
|
833
1038
|
return "on the " + dateOrdinals(ir, opts) + monthScope(ir, opts);
|
|
@@ -843,20 +1048,105 @@ function monthFoldsIntoDate(ir) {
|
|
|
843
1048
|
return segment.kind !== "range";
|
|
844
1049
|
});
|
|
845
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
|
+
}
|
|
846
1126
|
function dateOrWeekday(ir, opts) {
|
|
847
1127
|
const pattern = ir.pattern;
|
|
848
|
-
const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, opts);
|
|
1128
|
+
const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, false, opts);
|
|
1129
|
+
if (pattern.month !== "*" && monthFoldsIntoDate(ir) && !quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
|
|
1130
|
+
return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
|
|
1131
|
+
}
|
|
1132
|
+
return datePart(ir, opts) + " or " + weekdayPart + orMonthScope(ir, opts);
|
|
1133
|
+
}
|
|
1134
|
+
function datePart(ir, opts) {
|
|
1135
|
+
const pattern = ir.pattern;
|
|
849
1136
|
const quartzDate = quartzDatePhrase(pattern.date, opts);
|
|
850
1137
|
if (quartzDate) {
|
|
851
|
-
return quartzDate
|
|
1138
|
+
return quartzDate;
|
|
852
1139
|
}
|
|
853
1140
|
if (isOpenStep(pattern.date)) {
|
|
854
|
-
return stepDates(pattern.date)
|
|
1141
|
+
return stepDates(pattern.date);
|
|
855
1142
|
}
|
|
856
|
-
|
|
857
|
-
|
|
1143
|
+
return "on the " + dateOrdinals(ir, opts);
|
|
1144
|
+
}
|
|
1145
|
+
function orMonthScope(ir, opts) {
|
|
1146
|
+
if (ir.pattern.month === "*") {
|
|
1147
|
+
return "";
|
|
858
1148
|
}
|
|
859
|
-
return "
|
|
1149
|
+
return ", in " + monthName(ir, opts);
|
|
860
1150
|
}
|
|
861
1151
|
function quartzDatePhrase(dateField, opts) {
|
|
862
1152
|
if (dateField === "L") {
|
|
@@ -890,6 +1180,9 @@ function monthDatePhrase(ir, opts) {
|
|
|
890
1180
|
opts.style.ordinals ? getOrdinal : cardinalDay,
|
|
891
1181
|
opts
|
|
892
1182
|
);
|
|
1183
|
+
if (opts.style.dayFirst && ir.shapes.date === "single" && ir.shapes.month !== "single") {
|
|
1184
|
+
return "the " + getOrdinal(ir.pattern.date) + " of " + month;
|
|
1185
|
+
}
|
|
893
1186
|
return opts.style.dayFirst ? days + " " + month : month + " " + days;
|
|
894
1187
|
}
|
|
895
1188
|
function cardinalDay(value) {
|
|
@@ -901,6 +1194,19 @@ function monthScope(ir, opts) {
|
|
|
901
1194
|
}
|
|
902
1195
|
return " in " + monthName(ir, opts);
|
|
903
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
|
+
}
|
|
904
1210
|
function stepDates(dateField) {
|
|
905
1211
|
const parts = dateField.split("/");
|
|
906
1212
|
const interval = +parts[1];
|
|
@@ -937,10 +1243,21 @@ function oddEvenMonth(monthField) {
|
|
|
937
1243
|
}
|
|
938
1244
|
return start === "2" ? "every even-numbered month" : null;
|
|
939
1245
|
}
|
|
940
|
-
function weekdayPhrase(ir, opts) {
|
|
941
|
-
|
|
1246
|
+
function weekdayPhrase(ir, recurring, opts) {
|
|
1247
|
+
const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
|
|
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) {
|
|
942
1254
|
return getWeekday(value, opts);
|
|
943
|
-
}
|
|
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";
|
|
944
1261
|
}
|
|
945
1262
|
function renderSegments(segments, word, opts) {
|
|
946
1263
|
const pieces = [];
|
|
@@ -964,7 +1281,7 @@ function applyYear(description, ir, opts) {
|
|
|
964
1281
|
return description;
|
|
965
1282
|
}
|
|
966
1283
|
if (yearField.indexOf("/") !== -1) {
|
|
967
|
-
return description + " " + stepYears(yearField, opts);
|
|
1284
|
+
return description + ", " + stepYears(yearField, opts);
|
|
968
1285
|
}
|
|
969
1286
|
const label = yearLabel(yearField, opts);
|
|
970
1287
|
if (yearField.indexOf("-") === -1 && yearField.indexOf(",") === -1 && ir.pattern.date !== "*" && description.indexOf(" at ") !== -1) {
|
|
@@ -977,6 +1294,9 @@ function yearLabel(yearField, opts) {
|
|
|
977
1294
|
if (yearField.indexOf(",") !== -1) {
|
|
978
1295
|
return joinList(yearField.split(","), opts);
|
|
979
1296
|
}
|
|
1297
|
+
if (yearField.indexOf("-") !== -1) {
|
|
1298
|
+
return yearField.split("-").join(through(opts));
|
|
1299
|
+
}
|
|
980
1300
|
return yearField;
|
|
981
1301
|
}
|
|
982
1302
|
function stepYears(yearField, opts) {
|
|
@@ -986,7 +1306,7 @@ function stepYears(yearField, opts) {
|
|
|
986
1306
|
if (interval <= 1) {
|
|
987
1307
|
return "every year";
|
|
988
1308
|
}
|
|
989
|
-
let phrase = "every " + getNumber(interval, opts) + " years";
|
|
1309
|
+
let phrase = interval === 2 ? "every other year" : "every " + getNumber(interval, opts) + " years";
|
|
990
1310
|
if (start !== "*" && start !== "0") {
|
|
991
1311
|
phrase += " from " + start;
|
|
992
1312
|
}
|