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/dist/lang/en.cjs CHANGED
@@ -40,6 +40,26 @@ function arithmeticStep(values) {
40
40
  }
41
41
  return { start: values[0], interval, last: values[values.length - 1] };
42
42
  }
43
+ function weekdayDisplayKey(value) {
44
+ return value === 0 ? 7 : value;
45
+ }
46
+ function orderWeekdaysForDisplay(segments) {
47
+ const flattened = segments.flatMap(function flat(segment) {
48
+ return segment.kind === "step" ? segment.fires.map(function single(value) {
49
+ return { kind: "single", value: "" + value };
50
+ }) : [segment];
51
+ });
52
+ function key(segment) {
53
+ return segment.kind === "range" ? weekdayDisplayKey(+segment.bounds[0]) : weekdayDisplayKey(+segment.value);
54
+ }
55
+ return flattened.map(function index(segment, position) {
56
+ return [segment, position];
57
+ }).sort(function byDisplayKey(a, b) {
58
+ return key(a[0]) - key(b[0]) || a[1] - b[1];
59
+ }).map(function unwrap(pair) {
60
+ return pair[0];
61
+ });
62
+ }
43
63
 
44
64
  // src/core/specs.ts
45
65
  var maxClockTimes = 6;
@@ -84,7 +104,8 @@ var dialects = {
84
104
  pm: "p.m.",
85
105
  sep: ":",
86
106
  serialComma: true,
87
- through: " through "
107
+ through: " through ",
108
+ untilWindow: true
88
109
  },
89
110
  house: {
90
111
  am: "AM",
@@ -101,7 +122,7 @@ var dialects = {
101
122
  };
102
123
  function resolveDialect(dialect) {
103
124
  if (typeof dialect === "object" && dialect !== null) {
104
- return { ...dialects.us, ...dialect };
125
+ return { ...dialects.us, untilWindow: false, ...dialect };
105
126
  }
106
127
  const name = dialect === "uk" ? "gb" : dialect;
107
128
  return dialects[name] || dialects.us;
@@ -173,7 +194,9 @@ function normalizeOptions(options) {
173
194
  };
174
195
  }
175
196
  function describe(ir, opts) {
176
- return applyYear(render(ir, ir.plan, opts), ir, opts);
197
+ const body = confinement(ir, opts) ?? render(ir, ir.plan, opts);
198
+ const lead = isDayUnion(ir, opts) ? dayUnionMonthLead(ir, opts) : "";
199
+ return applyYear(lead + body, ir, opts);
177
200
  }
178
201
  function render(ir, plan, opts) {
179
202
  const renderer = renderers[plan.kind];
@@ -266,7 +289,7 @@ function secondsClause(ir, anchor, opts) {
266
289
  }
267
290
  if (shape === "range") {
268
291
  const bounds = secondField.split("-");
269
- const num = seriesNumber(bounds, opts);
292
+ const num = seriesNumber();
270
293
  return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the " + anchor;
271
294
  }
272
295
  if (shape === "single") {
@@ -332,15 +355,21 @@ function renderMinutesAcrossHours(ir, plan, opts) {
332
355
  }
333
356
  return "every minute during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
334
357
  }
335
- const lead = plan.form === "range" ? minuteRangeLead(ir.pattern.minute, opts) : (
336
- // The 'list' form is a minute list, which has segments; an offset/uneven
337
- // step enumerated to that list reads as a stride.
338
- strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
339
- segmentWords(ir.analyses.segments.minute, opts),
340
- "minute",
341
- "hour",
342
- opts
343
- )
358
+ if (plan.form === "range") {
359
+ const lead2 = minuteRangeLead(ir.pattern.minute, opts);
360
+ if (cadence !== null) {
361
+ return lead2 + ", " + cadence + trailingQualifier(ir, opts);
362
+ }
363
+ if (singleHourFire(plan.times)) {
364
+ return lead2 + ", at " + hourTimesFromPlan(ir, plan.times, true, opts) + trailingQualifier(ir, opts);
365
+ }
366
+ return lead2 + " during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
367
+ }
368
+ const lead = strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
369
+ segmentWords(ir.analyses.segments.minute, opts),
370
+ "minute",
371
+ "hour",
372
+ opts
344
373
  );
345
374
  if (cadence !== null) {
346
375
  return lead + ", " + cadence + trailingQualifier(ir, opts);
@@ -377,7 +406,7 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
377
406
  }
378
407
  function minuteRangeLead(minuteField, opts) {
379
408
  const bounds = minuteField.split("-");
380
- const num = seriesNumber(bounds, opts);
409
+ const num = seriesNumber();
381
410
  return "every minute from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the hour";
382
411
  }
383
412
  function renderEveryHour(ir, plan, opts) {
@@ -417,10 +446,18 @@ function renderHourStep(ir, plan, opts) {
417
446
  return stepHours(ir.analyses.segments.hour[0], opts) + trailingQualifier(ir, opts);
418
447
  }
419
448
  function boundedWindow(plan) {
420
- return { from: plan.from, last: plan.boundMinute ?? 0, to: plan.to };
449
+ const last = plan.minuteForm === "wildcard" ? plan.boundMinute ?? 0 : 0;
450
+ return { from: plan.from, last, to: plan.to };
451
+ }
452
+ function rangeWindow(from, to, throughMinute, opts) {
453
+ const open = "from " + getTime({ hour: from, minute: 0 }, opts);
454
+ if (opts.style.untilWindow && !opts.short && from !== to) {
455
+ return open + " until " + getTime({ hour: (to + 1) % 24, minute: 0 }, opts);
456
+ }
457
+ return open + through(opts) + getTime({ hour: to, minute: throughMinute }, opts);
421
458
  }
422
459
  function hourWindow(window, opts) {
423
- return "from " + getTime({ hour: window.from, minute: 0 }, opts) + through(opts) + getTime({ hour: window.to, minute: window.last }, opts);
460
+ return rangeWindow(window.from, window.to, window.last, opts);
424
461
  }
425
462
  function renderClockTimes(ir, plan, opts) {
426
463
  if (ir.shapes.minute === "single") {
@@ -439,7 +476,10 @@ function renderClockTimes(ir, plan, opts) {
439
476
  plain
440
477
  }, opts);
441
478
  });
442
- return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts);
479
+ return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts) + dayUnionTrail(ir, opts);
480
+ }
481
+ function dayUnionTrail(ir, opts) {
482
+ return isDayUnion(ir, opts) ? dayUnionCondition(ir, opts) : "";
443
483
  }
444
484
  function renderCompactClockTimes(ir, plan, opts) {
445
485
  if (plan.fold) {
@@ -454,7 +494,7 @@ function renderCompactClockTimes(ir, plan, opts) {
454
494
  return foldedHourWindows(ir, plan, opts) + trailingQualifier(ir, opts);
455
495
  }
456
496
  const fold = { minute: plan.minute, second: ir.analyses.clockSecond };
457
- return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
497
+ return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts) + dayUnionTrail(ir, opts);
458
498
  }
459
499
  const minuteLead = (
460
500
  // The non-fold branch is a minute list, which has segments. An
@@ -473,26 +513,161 @@ function renderCompactClockTimes(ir, plan, opts) {
473
513
  function foldedHourWindows(ir, plan, opts) {
474
514
  const minute = plan.minute;
475
515
  const windows = [];
476
- const singles = [];
516
+ const outliers = collectHourOutliers(ir);
517
+ const times = outliers.hours.map(function time(hour) {
518
+ return getTime({ hour, minute }, opts);
519
+ });
477
520
  ir.analyses.segments.hour.forEach(function classify(segment) {
478
521
  if (segment.kind === "range") {
479
- windows.push("from " + getTime(
480
- { hour: segment.bounds[0], minute: 0 },
522
+ windows.push(rangeWindow(
523
+ +segment.bounds[0],
524
+ +segment.bounds[1],
525
+ minute,
481
526
  opts
482
- ) + through(opts) + getTime({ hour: segment.bounds[1], minute }, opts));
483
- } else if (segment.kind === "step") {
484
- singles.push(...segment.fires);
485
- } else {
486
- singles.push(+segment.value);
527
+ ));
528
+ }
529
+ });
530
+ const phrase = rangeMinuteLead(ir, opts) + " " + joinList(windows, opts);
531
+ return phrase + outlierTail(times, outliers.pureStrays, opts);
532
+ }
533
+ function collectHourOutliers(ir) {
534
+ const hours = [];
535
+ let pureStrays = true;
536
+ ir.analyses.segments.hour.forEach(function classify(segment) {
537
+ if (segment.kind === "step") {
538
+ hours.push(...segment.fires);
539
+ pureStrays = false;
540
+ } else if (segment.kind !== "range") {
541
+ hours.push(+segment.value);
487
542
  }
488
543
  });
489
- let phrase = rangeMinuteLead(ir, opts) + " " + joinList(windows, opts);
490
- if (singles.length) {
491
- phrase += " and at " + joinList(singles.map(function time(hour) {
492
- return getTime({ hour, minute }, opts);
493
- }), opts);
544
+ return { hours, pureStrays };
545
+ }
546
+ function outlierTail(times, pureStrays, opts) {
547
+ if (!times.length) {
548
+ return "";
494
549
  }
495
- return phrase;
550
+ const connector = pureStrays && opts.style.untilWindow && !opts.short ? " plus " : " and at ";
551
+ return connector + joinList(times, opts);
552
+ }
553
+ function isCadenceField(token) {
554
+ return token === "*" || token.startsWith("*/") && token.indexOf("-") === -1;
555
+ }
556
+ function leadingCadence(ir, opts) {
557
+ const { second, minute } = ir.pattern;
558
+ if (isCadenceField(second)) {
559
+ return { secondLead: true, text: secondsClause(ir, "minute", opts) };
560
+ }
561
+ if (second === "0" && isCadenceField(minute)) {
562
+ const text = minute === "*" ? "every minute" : (
563
+ // A clean minute step's first segment is a step segment.
564
+ stepCycle60(
565
+ ir.analyses.segments.minute[0],
566
+ "minute",
567
+ "hour",
568
+ opts
569
+ )
570
+ );
571
+ return { secondLead: false, text };
572
+ }
573
+ return null;
574
+ }
575
+ function minuteConfinement(ir, opts) {
576
+ const minute = ir.pattern.minute;
577
+ if (minute === "*") {
578
+ return "";
579
+ }
580
+ if (isCadenceField(minute)) {
581
+ return " of every other minute";
582
+ }
583
+ const segments = ir.analyses.segments.minute;
584
+ if (ir.shapes.minute === "single") {
585
+ return " during minute :" + pad(minute);
586
+ }
587
+ if (ir.shapes.minute === "range") {
588
+ const bounds = minute.split("-");
589
+ return " during minutes :" + pad(bounds[0]) + through(opts) + ":" + pad(bounds[1]);
590
+ }
591
+ const values = segmentWords(segments, opts).map(function colon(word) {
592
+ return ":" + pad(word);
593
+ });
594
+ return " during minutes " + joinList(values, opts);
595
+ }
596
+ function hourConfinement(ir, opts) {
597
+ const hour = ir.pattern.hour;
598
+ if (hour === "*") {
599
+ const minutePinned = ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute);
600
+ return minutePinned ? " of every hour" : "";
601
+ }
602
+ if (isCadenceField(hour)) {
603
+ return hour === "*/2" ? " of every other hour" : "";
604
+ }
605
+ if (ir.shapes.hour === "single") {
606
+ const h = +hour;
607
+ if (ir.shapes.minute === "step") {
608
+ return " from " + getTime({ hour: h, minute: 0 }, opts) + " until " + getTime({ hour: (h + 1) % 24, minute: 0 }, opts);
609
+ }
610
+ if (ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute)) {
611
+ return " at " + getTime({ hour: h, minute: 0 }, opts);
612
+ }
613
+ return " of the " + getTime({ hour: h, minute: 0 }, opts) + " hour";
614
+ }
615
+ if (ir.shapes.hour === "range") {
616
+ const bounds = hour.split("-");
617
+ return " " + rangeWindow(+bounds[0], +bounds[1], 0, opts);
618
+ }
619
+ return " during the " + hourSegmentTimes(ir, { minute: 0, second: null }, false, opts) + " hours";
620
+ }
621
+ function isContiguousHourRange(ir) {
622
+ return ir.shapes.hour === "range";
623
+ }
624
+ function confinableHour(ir) {
625
+ if (ir.shapes.hour !== "step") {
626
+ return true;
627
+ }
628
+ const segment = ir.analyses.segments.hour[0];
629
+ return ir.pattern.hour === "*/2" || segment.startToken.indexOf("-") !== -1;
630
+ }
631
+ function isMinuteStride(ir) {
632
+ if (ir.shapes.minute !== "list") {
633
+ return false;
634
+ }
635
+ const values = singleValues(ir.analyses.segments.minute);
636
+ return values !== null && arithmeticStep(values) !== null;
637
+ }
638
+ function confinementEligible(ir, lead) {
639
+ const { minute, hour } = ir.pattern;
640
+ const minuteStep = isCadenceField(minute) && minute !== "*";
641
+ if (!confinableHour(ir)) {
642
+ return false;
643
+ }
644
+ if (lead.secondLead) {
645
+ if (minuteStep) {
646
+ return minute === "*/2" && !isContiguousHourRange(ir);
647
+ }
648
+ if (isMinuteStride(ir) || ir.shapes.minute === "list" && ir.shapes.hour === "list") {
649
+ return false;
650
+ }
651
+ return true;
652
+ }
653
+ if (hour === "*/2") {
654
+ return true;
655
+ }
656
+ return ir.shapes.hour === "single" && minute === "*/2";
657
+ }
658
+ function confinement(ir, opts) {
659
+ if (!opts.style.untilWindow || opts.short) {
660
+ return null;
661
+ }
662
+ if (ir.pattern.minute === "*" && ir.pattern.hour === "*") {
663
+ return null;
664
+ }
665
+ const lead = leadingCadence(ir, opts);
666
+ if (!lead || !confinementEligible(ir, lead)) {
667
+ return null;
668
+ }
669
+ const minutePart = lead.secondLead ? minuteConfinement(ir, opts) : "";
670
+ return lead.text + minutePart + hourConfinement(ir, opts) + trailingQualifier(ir, opts);
496
671
  }
497
672
  var renderers = {
498
673
  clockTimes: renderClockTimes,
@@ -524,7 +699,7 @@ function renderStride(stride, opts) {
524
699
  if (start < interval && tiles) {
525
700
  return cadence + " from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
526
701
  }
527
- const num = seriesNumber([start, last], opts);
702
+ const num = seriesNumber();
528
703
  return cadence + " from " + num(start) + through(opts) + num(last) + " " + pluralize(last, unit) + " past the " + anchor;
529
704
  }
530
705
  function singleValues(segments) {
@@ -651,9 +826,9 @@ function hourCadence(ir, minute, opts) {
651
826
  if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
652
827
  return null;
653
828
  }
654
- const confinement = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
655
- if (confinement) {
656
- return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
829
+ const minuteZeroStride = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
830
+ if (minuteZeroStride) {
831
+ return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(minuteZeroStride, opts) + trailingQualifier(ir, opts);
657
832
  }
658
833
  if (minute === 0 && ir.pattern.second === "0") {
659
834
  return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
@@ -675,26 +850,22 @@ function hasHourWindow(ir) {
675
850
  }
676
851
  function hourRangeWindowTail(ir, opts) {
677
852
  const windows = [];
678
- const singles = [];
853
+ const outliers = collectHourOutliers(ir);
679
854
  ir.analyses.segments.hour.forEach(function classify(segment) {
680
855
  if (segment.kind === "range") {
681
- windows.push("from " + getTime(
682
- { hour: +segment.bounds[0], minute: 0 },
856
+ windows.push(rangeWindow(
857
+ +segment.bounds[0],
858
+ +segment.bounds[1],
859
+ 0,
683
860
  opts
684
- ) + through(opts) + getTime({ hour: +segment.bounds[1], minute: 0 }, opts));
685
- } else if (segment.kind === "step") {
686
- singles.push(...segment.fires);
687
- } else {
688
- singles.push(+segment.value);
861
+ ));
689
862
  }
690
863
  });
691
- let phrase = "every hour " + joinList(windows, opts);
692
- if (singles.length) {
693
- phrase += " and at " + joinList(singles.map(function time(hour) {
694
- return getTime({ hour, minute: 0 }, opts);
695
- }), opts);
696
- }
697
- return phrase;
864
+ const phrase = "every hour " + joinList(windows, opts);
865
+ const times = outliers.hours.map(function time(hour) {
866
+ return getTime({ hour, minute: 0 }, opts);
867
+ });
868
+ return phrase + outlierTail(times, outliers.pureStrays, opts);
698
869
  }
699
870
  function hourRangeCadence(ir, minute, opts) {
700
871
  if (minute !== 0 || !hasHourWindow(ir)) {
@@ -708,25 +879,29 @@ function hourRangeCadence(ir, minute, opts) {
708
879
  }
709
880
  return hourCadenceLead(ir, minute, opts) + ", " + hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
710
881
  }
711
- function seriesNumber(values, opts) {
712
- const anyBig = values.some(function big(v) {
713
- return +v > 10;
714
- });
882
+ function seriesNumber() {
715
883
  return function format(n) {
716
- return anyBig ? "" + n : getNumber(n, opts);
884
+ return "" + n;
885
+ };
886
+ }
887
+ function listNumber(count, opts) {
888
+ return count > 1 ? function asNumeral(n) {
889
+ return "" + n;
890
+ } : function spelled(n) {
891
+ return getNumber(n, opts);
717
892
  };
718
893
  }
719
894
  function numberWords(fires, opts) {
720
- return fires.map(seriesNumber(fires, opts));
895
+ return fires.map(listNumber(fires.length, opts));
721
896
  }
722
897
  function segmentWords(segments, opts) {
723
- const values = segments.flatMap(function collect(segment) {
898
+ const count = segments.reduce(function tally(sum, segment) {
724
899
  if (segment.kind === "range") {
725
- return segment.bounds;
900
+ return sum + 1;
726
901
  }
727
- return segment.kind === "step" ? segment.fires : [segment.value];
728
- });
729
- const num = seriesNumber(values, opts);
902
+ return sum + (segment.kind === "step" ? segment.fires.length : 1);
903
+ }, 0);
904
+ const num = listNumber(count, opts);
730
905
  return segments.flatMap(function word(segment) {
731
906
  if (segment.kind === "range") {
732
907
  return [num(segment.bounds[0]) + through(opts) + num(segment.bounds[1])];
@@ -758,6 +933,9 @@ function hourTimes(hours, opts) {
758
933
  });
759
934
  return joinList(times, opts);
760
935
  }
936
+ function singleHourFire(times) {
937
+ return times.kind === "fires" && times.fires.length === 1;
938
+ }
761
939
  function hourTimesFromPlan(ir, times, atContext, opts) {
762
940
  if (times.kind === "fires") {
763
941
  return hourTimes(times.fires, opts);
@@ -805,28 +983,47 @@ function disambiguateTimes(pieces, segments, atContext) {
805
983
  return index === 0 ? piece : "at " + piece;
806
984
  });
807
985
  }
808
- function joinList(items, opts) {
986
+ function joinWith(items, conjunction, opts) {
809
987
  if (items.length <= 1) {
810
988
  return items.join("");
811
989
  }
812
990
  if (items.length === 2) {
813
- return items[0] + " and " + items[1];
991
+ return items[0] + conjunction + items[1];
814
992
  }
815
- const and = opts.style.serialComma ? ", and " : " and ";
816
- return items.slice(0, -1).join(", ") + and + items[items.length - 1];
993
+ const tail = opts.style.serialComma ? "," + conjunction : conjunction;
994
+ return items.slice(0, -1).join(", ") + tail + items[items.length - 1];
817
995
  }
818
- var trailingWords = { all: "", month: "in ", stepDate: "on ", weekday: "on " };
996
+ function joinList(items, opts) {
997
+ return joinWith(items, " and ", opts);
998
+ }
999
+ function joinOr(items, opts) {
1000
+ return joinWith(items, " or ", opts);
1001
+ }
1002
+ var trailingWords = {
1003
+ all: "",
1004
+ month: "in ",
1005
+ recurringWeekday: true,
1006
+ stepDate: "on ",
1007
+ weekday: "on "
1008
+ };
819
1009
  var leadingWords = {
820
1010
  all: "every day",
821
1011
  month: "every day in ",
1012
+ recurringWeekday: false,
822
1013
  stepDate: "",
823
1014
  weekday: "every "
824
1015
  };
825
1016
  function trailingQualifier(ir, opts) {
1017
+ if (isDayUnion(ir, opts)) {
1018
+ return dayUnionCondition(ir, opts);
1019
+ }
826
1020
  const phrase = dayQualifier(ir, trailingWords, opts);
827
1021
  return phrase && " " + phrase;
828
1022
  }
829
1023
  function interpretDayQualifier(ir, opts) {
1024
+ if (isDayUnion(ir, opts)) {
1025
+ return "";
1026
+ }
830
1027
  return dayQualifier(ir, leadingWords, opts) + " ";
831
1028
  }
832
1029
  function dayQualifier(ir, words, opts) {
@@ -838,7 +1035,11 @@ function dayQualifier(ir, words, opts) {
838
1035
  return datePhrase(ir, words, opts);
839
1036
  }
840
1037
  if (pattern.weekday !== "*") {
841
- const weekdays = quartzWeekdayPhrase(pattern.weekday, opts) || words.weekday + weekdayPhrase(ir, opts);
1038
+ const quartzWeekday = quartzWeekdayPhrase(pattern.weekday, opts);
1039
+ if (quartzWeekday) {
1040
+ return monthScopeForRecurrence(quartzWeekday, ir, opts);
1041
+ }
1042
+ const weekdays = words.weekday + weekdayPhrase(ir, words.recurringWeekday, opts);
842
1043
  return weekdays + monthScope(ir, opts);
843
1044
  }
844
1045
  if (pattern.month !== "*") {
@@ -850,10 +1051,14 @@ function datePhrase(ir, words, opts) {
850
1051
  const pattern = ir.pattern;
851
1052
  const quartzDate = quartzDatePhrase(pattern.date, opts);
852
1053
  if (quartzDate) {
853
- return quartzDate + monthScope(ir, opts);
1054
+ return monthScopeForRecurrence(quartzDate, ir, opts);
854
1055
  }
855
1056
  if (isOpenStep(pattern.date)) {
856
- return words.stepDate + stepDates(pattern.date) + monthScope(ir, opts);
1057
+ return monthScopeForRecurrence(
1058
+ words.stepDate + stepDates(pattern.date),
1059
+ ir,
1060
+ opts
1061
+ );
857
1062
  }
858
1063
  if (pattern.month !== "*" && !monthFoldsIntoDate(ir)) {
859
1064
  return "on the " + dateOrdinals(ir, opts) + monthScope(ir, opts);
@@ -869,20 +1074,105 @@ function monthFoldsIntoDate(ir) {
869
1074
  return segment.kind !== "range";
870
1075
  });
871
1076
  }
1077
+ function isDayUnion(ir, opts) {
1078
+ return ir.pattern.date !== "*" && ir.pattern.weekday !== "*" && !!opts.style.untilWindow && !opts.short;
1079
+ }
1080
+ function dayUnionCondition(ir, opts) {
1081
+ const pieces = [
1082
+ ...dayUnionDatePieces(ir, opts),
1083
+ ...dayUnionWeekdayPieces(ir, opts)
1084
+ ];
1085
+ return " whenever the day is " + joinOr(pieces, opts);
1086
+ }
1087
+ function dayUnionMonthLead(ir, opts) {
1088
+ if (ir.pattern.month === "*") {
1089
+ return "";
1090
+ }
1091
+ return "in " + monthName(ir, opts) + " ";
1092
+ }
1093
+ function dayUnionDatePieces(ir, opts) {
1094
+ const dateField = ir.pattern.date;
1095
+ const quartz = quartzDatePhrase(dateField, opts);
1096
+ if (quartz) {
1097
+ return [quartz.replace(/^on /, "")];
1098
+ }
1099
+ const oddEven = oddEvenDay(dateField);
1100
+ if (oddEven) {
1101
+ return [oddEven];
1102
+ }
1103
+ const pieces = [];
1104
+ ir.analyses.segments.date.forEach(function expand(segment) {
1105
+ if (segment.kind === "range") {
1106
+ pieces.push("from the " + getOrdinal(segment.bounds[0]) + through(opts) + "the " + getOrdinal(segment.bounds[1]));
1107
+ } else if (segment.kind === "step") {
1108
+ segment.fires.forEach(function fire(value) {
1109
+ pieces.push("the " + getOrdinal(value));
1110
+ });
1111
+ } else {
1112
+ pieces.push("the " + getOrdinal(segment.value));
1113
+ }
1114
+ });
1115
+ return pieces;
1116
+ }
1117
+ function dayUnionWeekdayPieces(ir, opts) {
1118
+ const weekdayField = ir.pattern.weekday;
1119
+ const quartz = quartzWeekdayPhrase(weekdayField, opts);
1120
+ if (quartz) {
1121
+ return [quartz.replace(/^on /, "")];
1122
+ }
1123
+ const pieces = [];
1124
+ ir.analyses.segments.weekday.forEach(function expand(segment) {
1125
+ if (segment.kind === "range" && segment.bounds[0] === "1" && segment.bounds[1] === "5") {
1126
+ pieces.push("a weekday");
1127
+ } else if (segment.kind === "range") {
1128
+ pieces.push("a " + getWeekday(segment.bounds[0], opts) + through(opts) + "a " + getWeekday(segment.bounds[1], opts));
1129
+ } else if (segment.kind === "step") {
1130
+ segment.fires.forEach(function fire(value) {
1131
+ pieces.push("a " + getWeekday(value, opts));
1132
+ });
1133
+ } else {
1134
+ pieces.push("a " + getWeekday(segment.value, opts));
1135
+ }
1136
+ });
1137
+ return pieces;
1138
+ }
1139
+ function oddEvenDay(dateField) {
1140
+ if (!isOpenStep(dateField)) {
1141
+ return null;
1142
+ }
1143
+ const [start, step] = dateField.split("/");
1144
+ if (+step !== 2) {
1145
+ return null;
1146
+ }
1147
+ if (start === "*" || start === "1") {
1148
+ return "an odd-numbered day";
1149
+ }
1150
+ return start === "2" ? "an even-numbered day" : null;
1151
+ }
872
1152
  function dateOrWeekday(ir, opts) {
873
1153
  const pattern = ir.pattern;
874
- const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, opts);
1154
+ const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, false, opts);
1155
+ if (pattern.month !== "*" && monthFoldsIntoDate(ir) && !quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
1156
+ return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
1157
+ }
1158
+ return datePart(ir, opts) + " or " + weekdayPart + orMonthScope(ir, opts);
1159
+ }
1160
+ function datePart(ir, opts) {
1161
+ const pattern = ir.pattern;
875
1162
  const quartzDate = quartzDatePhrase(pattern.date, opts);
876
1163
  if (quartzDate) {
877
- return quartzDate + monthScope(ir, opts) + " or " + weekdayPart;
1164
+ return quartzDate;
878
1165
  }
879
1166
  if (isOpenStep(pattern.date)) {
880
- return stepDates(pattern.date) + monthScope(ir, opts) + " or " + weekdayPart;
1167
+ return stepDates(pattern.date);
881
1168
  }
882
- if (pattern.month !== "*" && monthFoldsIntoDate(ir)) {
883
- return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
1169
+ return "on the " + dateOrdinals(ir, opts);
1170
+ }
1171
+ function orMonthScope(ir, opts) {
1172
+ if (ir.pattern.month === "*") {
1173
+ return "";
884
1174
  }
885
- return "on the " + dateOrdinals(ir, opts) + " or " + weekdayPart + monthScope(ir, opts);
1175
+ return ", in " + monthName(ir, opts);
886
1176
  }
887
1177
  function quartzDatePhrase(dateField, opts) {
888
1178
  if (dateField === "L") {
@@ -916,6 +1206,9 @@ function monthDatePhrase(ir, opts) {
916
1206
  opts.style.ordinals ? getOrdinal : cardinalDay,
917
1207
  opts
918
1208
  );
1209
+ if (opts.style.dayFirst && ir.shapes.date === "single" && ir.shapes.month !== "single") {
1210
+ return "the " + getOrdinal(ir.pattern.date) + " of " + month;
1211
+ }
919
1212
  return opts.style.dayFirst ? days + " " + month : month + " " + days;
920
1213
  }
921
1214
  function cardinalDay(value) {
@@ -927,6 +1220,19 @@ function monthScope(ir, opts) {
927
1220
  }
928
1221
  return " in " + monthName(ir, opts);
929
1222
  }
1223
+ function monthScopeForRecurrence(phrase, ir, opts) {
1224
+ if (ir.pattern.month === "*") {
1225
+ return phrase;
1226
+ }
1227
+ const carriesRecurrence = phrase.indexOf(" of the month") !== -1;
1228
+ if (carriesRecurrence && ir.shapes.month === "range") {
1229
+ return phrase.replace(" of the month", " of each month") + " from " + monthName(ir, opts);
1230
+ }
1231
+ if (carriesRecurrence && (ir.shapes.month === "single" || ir.shapes.month === "step")) {
1232
+ return phrase.replace(" of the month", "") + " in " + monthName(ir, opts);
1233
+ }
1234
+ return phrase + " in " + monthName(ir, opts);
1235
+ }
930
1236
  function stepDates(dateField) {
931
1237
  const parts = dateField.split("/");
932
1238
  const interval = +parts[1];
@@ -963,10 +1269,21 @@ function oddEvenMonth(monthField) {
963
1269
  }
964
1270
  return start === "2" ? "every even-numbered month" : null;
965
1271
  }
966
- function weekdayPhrase(ir, opts) {
967
- return renderSegments(ir.analyses.segments.weekday, function name(value) {
1272
+ function weekdayPhrase(ir, recurring, opts) {
1273
+ const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
1274
+ const hasRange = segments.some(function range(segment) {
1275
+ return segment.kind === "range";
1276
+ });
1277
+ const name = recurring && !hasRange ? function plural(value) {
1278
+ return pluralWeekday(value, opts);
1279
+ } : function singular(value) {
968
1280
  return getWeekday(value, opts);
969
- }, opts);
1281
+ };
1282
+ return renderSegments(segments, name, opts);
1283
+ }
1284
+ function pluralWeekday(value, opts) {
1285
+ const name = getWeekday(value, opts);
1286
+ return opts.short ? name : name + "s";
970
1287
  }
971
1288
  function renderSegments(segments, word, opts) {
972
1289
  const pieces = [];
@@ -990,7 +1307,7 @@ function applyYear(description, ir, opts) {
990
1307
  return description;
991
1308
  }
992
1309
  if (yearField.indexOf("/") !== -1) {
993
- return description + " " + stepYears(yearField, opts);
1310
+ return description + ", " + stepYears(yearField, opts);
994
1311
  }
995
1312
  const label = yearLabel(yearField, opts);
996
1313
  if (yearField.indexOf("-") === -1 && yearField.indexOf(",") === -1 && ir.pattern.date !== "*" && description.indexOf(" at ") !== -1) {
@@ -1003,6 +1320,9 @@ function yearLabel(yearField, opts) {
1003
1320
  if (yearField.indexOf(",") !== -1) {
1004
1321
  return joinList(yearField.split(","), opts);
1005
1322
  }
1323
+ if (yearField.indexOf("-") !== -1) {
1324
+ return yearField.split("-").join(through(opts));
1325
+ }
1006
1326
  return yearField;
1007
1327
  }
1008
1328
  function stepYears(yearField, opts) {
@@ -1012,7 +1332,7 @@ function stepYears(yearField, opts) {
1012
1332
  if (interval <= 1) {
1013
1333
  return "every year";
1014
1334
  }
1015
- let phrase = "every " + getNumber(interval, opts) + " years";
1335
+ let phrase = interval === 2 ? "every other year" : "every " + getNumber(interval, opts) + " years";
1016
1336
  if (start !== "*" && start !== "0") {
1017
1337
  phrase += " from " + start;
1018
1338
  }