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/dist/lang/en.cjs CHANGED
@@ -104,7 +104,8 @@ var dialects = {
104
104
  pm: "p.m.",
105
105
  sep: ":",
106
106
  serialComma: true,
107
- through: " through "
107
+ through: " through ",
108
+ untilWindow: true
108
109
  },
109
110
  house: {
110
111
  am: "AM",
@@ -121,7 +122,7 @@ var dialects = {
121
122
  };
122
123
  function resolveDialect(dialect) {
123
124
  if (typeof dialect === "object" && dialect !== null) {
124
- return { ...dialects.us, ...dialect };
125
+ return { ...dialects.us, untilWindow: false, ...dialect };
125
126
  }
126
127
  const name = dialect === "uk" ? "gb" : dialect;
127
128
  return dialects[name] || dialects.us;
@@ -193,7 +194,9 @@ function normalizeOptions(options) {
193
194
  };
194
195
  }
195
196
  function describe(ir, opts) {
196
- 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);
197
200
  }
198
201
  function render(ir, plan, opts) {
199
202
  const renderer = renderers[plan.kind];
@@ -286,7 +289,7 @@ function secondsClause(ir, anchor, opts) {
286
289
  }
287
290
  if (shape === "range") {
288
291
  const bounds = secondField.split("-");
289
- const num = seriesNumber(bounds, opts);
292
+ const num = seriesNumber();
290
293
  return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the " + anchor;
291
294
  }
292
295
  if (shape === "single") {
@@ -352,15 +355,21 @@ function renderMinutesAcrossHours(ir, plan, opts) {
352
355
  }
353
356
  return "every minute during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
354
357
  }
355
- const lead = plan.form === "range" ? minuteRangeLead(ir.pattern.minute, opts) : (
356
- // The 'list' form is a minute list, which has segments; an offset/uneven
357
- // step enumerated to that list reads as a stride.
358
- strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
359
- segmentWords(ir.analyses.segments.minute, opts),
360
- "minute",
361
- "hour",
362
- opts
363
- )
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
364
373
  );
365
374
  if (cadence !== null) {
366
375
  return lead + ", " + cadence + trailingQualifier(ir, opts);
@@ -397,7 +406,7 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
397
406
  }
398
407
  function minuteRangeLead(minuteField, opts) {
399
408
  const bounds = minuteField.split("-");
400
- const num = seriesNumber(bounds, opts);
409
+ const num = seriesNumber();
401
410
  return "every minute from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the hour";
402
411
  }
403
412
  function renderEveryHour(ir, plan, opts) {
@@ -440,8 +449,15 @@ function boundedWindow(plan) {
440
449
  const last = plan.minuteForm === "wildcard" ? plan.boundMinute ?? 0 : 0;
441
450
  return { from: plan.from, last, to: plan.to };
442
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);
458
+ }
443
459
  function hourWindow(window, opts) {
444
- 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);
445
461
  }
446
462
  function renderClockTimes(ir, plan, opts) {
447
463
  if (ir.shapes.minute === "single") {
@@ -460,7 +476,10 @@ function renderClockTimes(ir, plan, opts) {
460
476
  plain
461
477
  }, opts);
462
478
  });
463
- 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) : "";
464
483
  }
465
484
  function renderCompactClockTimes(ir, plan, opts) {
466
485
  if (plan.fold) {
@@ -475,7 +494,7 @@ function renderCompactClockTimes(ir, plan, opts) {
475
494
  return foldedHourWindows(ir, plan, opts) + trailingQualifier(ir, opts);
476
495
  }
477
496
  const fold = { minute: plan.minute, second: ir.analyses.clockSecond };
478
- return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
497
+ return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts) + dayUnionTrail(ir, opts);
479
498
  }
480
499
  const minuteLead = (
481
500
  // The non-fold branch is a minute list, which has segments. An
@@ -494,26 +513,161 @@ function renderCompactClockTimes(ir, plan, opts) {
494
513
  function foldedHourWindows(ir, plan, opts) {
495
514
  const minute = plan.minute;
496
515
  const windows = [];
497
- const singles = [];
516
+ const outliers = collectHourOutliers(ir);
517
+ const times = outliers.hours.map(function time(hour) {
518
+ return getTime({ hour, minute }, opts);
519
+ });
498
520
  ir.analyses.segments.hour.forEach(function classify(segment) {
499
521
  if (segment.kind === "range") {
500
- windows.push("from " + getTime(
501
- { hour: segment.bounds[0], minute: 0 },
522
+ windows.push(rangeWindow(
523
+ +segment.bounds[0],
524
+ +segment.bounds[1],
525
+ minute,
502
526
  opts
503
- ) + through(opts) + getTime({ hour: segment.bounds[1], minute }, opts));
504
- } else if (segment.kind === "step") {
505
- singles.push(...segment.fires);
506
- } else {
507
- singles.push(+segment.value);
527
+ ));
508
528
  }
509
529
  });
510
- let phrase = rangeMinuteLead(ir, opts) + " " + joinList(windows, opts);
511
- if (singles.length) {
512
- phrase += " and at " + joinList(singles.map(function time(hour) {
513
- return getTime({ hour, minute }, opts);
514
- }), opts);
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);
542
+ }
543
+ });
544
+ return { hours, pureStrays };
545
+ }
546
+ function outlierTail(times, pureStrays, opts) {
547
+ if (!times.length) {
548
+ return "";
515
549
  }
516
- 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);
517
671
  }
518
672
  var renderers = {
519
673
  clockTimes: renderClockTimes,
@@ -545,7 +699,7 @@ function renderStride(stride, opts) {
545
699
  if (start < interval && tiles) {
546
700
  return cadence + " from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
547
701
  }
548
- const num = seriesNumber([start, last], opts);
702
+ const num = seriesNumber();
549
703
  return cadence + " from " + num(start) + through(opts) + num(last) + " " + pluralize(last, unit) + " past the " + anchor;
550
704
  }
551
705
  function singleValues(segments) {
@@ -672,9 +826,9 @@ function hourCadence(ir, minute, opts) {
672
826
  if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
673
827
  return null;
674
828
  }
675
- const confinement = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
676
- if (confinement) {
677
- 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);
678
832
  }
679
833
  if (minute === 0 && ir.pattern.second === "0") {
680
834
  return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
@@ -696,26 +850,22 @@ function hasHourWindow(ir) {
696
850
  }
697
851
  function hourRangeWindowTail(ir, opts) {
698
852
  const windows = [];
699
- const singles = [];
853
+ const outliers = collectHourOutliers(ir);
700
854
  ir.analyses.segments.hour.forEach(function classify(segment) {
701
855
  if (segment.kind === "range") {
702
- windows.push("from " + getTime(
703
- { hour: +segment.bounds[0], minute: 0 },
856
+ windows.push(rangeWindow(
857
+ +segment.bounds[0],
858
+ +segment.bounds[1],
859
+ 0,
704
860
  opts
705
- ) + through(opts) + getTime({ hour: +segment.bounds[1], minute: 0 }, opts));
706
- } else if (segment.kind === "step") {
707
- singles.push(...segment.fires);
708
- } else {
709
- singles.push(+segment.value);
861
+ ));
710
862
  }
711
863
  });
712
- let phrase = "every hour " + joinList(windows, opts);
713
- if (singles.length) {
714
- phrase += " and at " + joinList(singles.map(function time(hour) {
715
- return getTime({ hour, minute: 0 }, opts);
716
- }), opts);
717
- }
718
- 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);
719
869
  }
720
870
  function hourRangeCadence(ir, minute, opts) {
721
871
  if (minute !== 0 || !hasHourWindow(ir)) {
@@ -729,25 +879,29 @@ function hourRangeCadence(ir, minute, opts) {
729
879
  }
730
880
  return hourCadenceLead(ir, minute, opts) + ", " + hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
731
881
  }
732
- function seriesNumber(values, opts) {
733
- const anyBig = values.some(function big(v) {
734
- return +v > 10;
735
- });
882
+ function seriesNumber() {
736
883
  return function format(n) {
737
- 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);
738
892
  };
739
893
  }
740
894
  function numberWords(fires, opts) {
741
- return fires.map(seriesNumber(fires, opts));
895
+ return fires.map(listNumber(fires.length, opts));
742
896
  }
743
897
  function segmentWords(segments, opts) {
744
- const values = segments.flatMap(function collect(segment) {
898
+ const count = segments.reduce(function tally(sum, segment) {
745
899
  if (segment.kind === "range") {
746
- return segment.bounds;
900
+ return sum + 1;
747
901
  }
748
- return segment.kind === "step" ? segment.fires : [segment.value];
749
- });
750
- const num = seriesNumber(values, opts);
902
+ return sum + (segment.kind === "step" ? segment.fires.length : 1);
903
+ }, 0);
904
+ const num = listNumber(count, opts);
751
905
  return segments.flatMap(function word(segment) {
752
906
  if (segment.kind === "range") {
753
907
  return [num(segment.bounds[0]) + through(opts) + num(segment.bounds[1])];
@@ -779,6 +933,9 @@ function hourTimes(hours, opts) {
779
933
  });
780
934
  return joinList(times, opts);
781
935
  }
936
+ function singleHourFire(times) {
937
+ return times.kind === "fires" && times.fires.length === 1;
938
+ }
782
939
  function hourTimesFromPlan(ir, times, atContext, opts) {
783
940
  if (times.kind === "fires") {
784
941
  return hourTimes(times.fires, opts);
@@ -826,28 +983,47 @@ function disambiguateTimes(pieces, segments, atContext) {
826
983
  return index === 0 ? piece : "at " + piece;
827
984
  });
828
985
  }
829
- function joinList(items, opts) {
986
+ function joinWith(items, conjunction, opts) {
830
987
  if (items.length <= 1) {
831
988
  return items.join("");
832
989
  }
833
990
  if (items.length === 2) {
834
- return items[0] + " and " + items[1];
991
+ return items[0] + conjunction + items[1];
835
992
  }
836
- const and = opts.style.serialComma ? ", and " : " and ";
837
- 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];
995
+ }
996
+ function joinList(items, opts) {
997
+ return joinWith(items, " and ", opts);
998
+ }
999
+ function joinOr(items, opts) {
1000
+ return joinWith(items, " or ", opts);
838
1001
  }
839
- var trailingWords = { all: "", month: "in ", stepDate: "on ", weekday: "on " };
1002
+ var trailingWords = {
1003
+ all: "",
1004
+ month: "in ",
1005
+ recurringWeekday: true,
1006
+ stepDate: "on ",
1007
+ weekday: "on "
1008
+ };
840
1009
  var leadingWords = {
841
1010
  all: "every day",
842
1011
  month: "every day in ",
1012
+ recurringWeekday: false,
843
1013
  stepDate: "",
844
1014
  weekday: "every "
845
1015
  };
846
1016
  function trailingQualifier(ir, opts) {
1017
+ if (isDayUnion(ir, opts)) {
1018
+ return dayUnionCondition(ir, opts);
1019
+ }
847
1020
  const phrase = dayQualifier(ir, trailingWords, opts);
848
1021
  return phrase && " " + phrase;
849
1022
  }
850
1023
  function interpretDayQualifier(ir, opts) {
1024
+ if (isDayUnion(ir, opts)) {
1025
+ return "";
1026
+ }
851
1027
  return dayQualifier(ir, leadingWords, opts) + " ";
852
1028
  }
853
1029
  function dayQualifier(ir, words, opts) {
@@ -859,7 +1035,11 @@ function dayQualifier(ir, words, opts) {
859
1035
  return datePhrase(ir, words, opts);
860
1036
  }
861
1037
  if (pattern.weekday !== "*") {
862
- 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);
863
1043
  return weekdays + monthScope(ir, opts);
864
1044
  }
865
1045
  if (pattern.month !== "*") {
@@ -871,10 +1051,14 @@ function datePhrase(ir, words, opts) {
871
1051
  const pattern = ir.pattern;
872
1052
  const quartzDate = quartzDatePhrase(pattern.date, opts);
873
1053
  if (quartzDate) {
874
- return quartzDate + monthScope(ir, opts);
1054
+ return monthScopeForRecurrence(quartzDate, ir, opts);
875
1055
  }
876
1056
  if (isOpenStep(pattern.date)) {
877
- return words.stepDate + stepDates(pattern.date) + monthScope(ir, opts);
1057
+ return monthScopeForRecurrence(
1058
+ words.stepDate + stepDates(pattern.date),
1059
+ ir,
1060
+ opts
1061
+ );
878
1062
  }
879
1063
  if (pattern.month !== "*" && !monthFoldsIntoDate(ir)) {
880
1064
  return "on the " + dateOrdinals(ir, opts) + monthScope(ir, opts);
@@ -890,9 +1074,84 @@ function monthFoldsIntoDate(ir) {
890
1074
  return segment.kind !== "range";
891
1075
  });
892
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
+ }
893
1152
  function dateOrWeekday(ir, opts) {
894
1153
  const pattern = ir.pattern;
895
- const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, opts);
1154
+ const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, false, opts);
896
1155
  if (pattern.month !== "*" && monthFoldsIntoDate(ir) && !quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
897
1156
  return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
898
1157
  }
@@ -947,6 +1206,9 @@ function monthDatePhrase(ir, opts) {
947
1206
  opts.style.ordinals ? getOrdinal : cardinalDay,
948
1207
  opts
949
1208
  );
1209
+ if (opts.style.dayFirst && ir.shapes.date === "single" && ir.shapes.month !== "single") {
1210
+ return "the " + getOrdinal(ir.pattern.date) + " of " + month;
1211
+ }
950
1212
  return opts.style.dayFirst ? days + " " + month : month + " " + days;
951
1213
  }
952
1214
  function cardinalDay(value) {
@@ -958,6 +1220,19 @@ function monthScope(ir, opts) {
958
1220
  }
959
1221
  return " in " + monthName(ir, opts);
960
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
+ }
961
1236
  function stepDates(dateField) {
962
1237
  const parts = dateField.split("/");
963
1238
  const interval = +parts[1];
@@ -994,11 +1269,21 @@ function oddEvenMonth(monthField) {
994
1269
  }
995
1270
  return start === "2" ? "every even-numbered month" : null;
996
1271
  }
997
- function weekdayPhrase(ir, opts) {
1272
+ function weekdayPhrase(ir, recurring, opts) {
998
1273
  const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
999
- return renderSegments(segments, function name(value) {
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) {
1000
1280
  return getWeekday(value, opts);
1001
- }, 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";
1002
1287
  }
1003
1288
  function renderSegments(segments, word, opts) {
1004
1289
  const pieces = [];
@@ -1022,7 +1307,7 @@ function applyYear(description, ir, opts) {
1022
1307
  return description;
1023
1308
  }
1024
1309
  if (yearField.indexOf("/") !== -1) {
1025
- return description + " " + stepYears(yearField, opts);
1310
+ return description + ", " + stepYears(yearField, opts);
1026
1311
  }
1027
1312
  const label = yearLabel(yearField, opts);
1028
1313
  if (yearField.indexOf("-") === -1 && yearField.indexOf(",") === -1 && ir.pattern.date !== "*" && description.indexOf(" at ") !== -1) {
@@ -1035,6 +1320,9 @@ function yearLabel(yearField, opts) {
1035
1320
  if (yearField.indexOf(",") !== -1) {
1036
1321
  return joinList(yearField.split(","), opts);
1037
1322
  }
1323
+ if (yearField.indexOf("-") !== -1) {
1324
+ return yearField.split("-").join(through(opts));
1325
+ }
1038
1326
  return yearField;
1039
1327
  }
1040
1328
  function stepYears(yearField, opts) {
@@ -1044,7 +1332,7 @@ function stepYears(yearField, opts) {
1044
1332
  if (interval <= 1) {
1045
1333
  return "every year";
1046
1334
  }
1047
- let phrase = "every " + getNumber(interval, opts) + " years";
1335
+ let phrase = interval === 2 ? "every other year" : "every " + getNumber(interval, opts) + " years";
1048
1336
  if (start !== "*" && start !== "0") {
1049
1337
  phrase += " from " + start;
1050
1338
  }