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.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
- return applyYear(render(ir, ir.plan, opts), ir, opts);
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(bounds, opts);
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
- const lead = plan.form === "range" ? minuteRangeLead(ir.pattern.minute, opts) : (
310
- // The 'list' form is a minute list, which has segments; an offset/uneven
311
- // step enumerated to that list reads as a stride.
312
- strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
313
- segmentWords(ir.analyses.segments.minute, opts),
314
- "minute",
315
- "hour",
316
- opts
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(bounds, opts);
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
- return { from: plan.from, last: plan.boundMinute ?? 0, to: plan.to };
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 "from " + getTime({ hour: window.from, minute: 0 }, opts) + through(opts) + getTime({ hour: window.to, minute: window.last }, opts);
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 singles = [];
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("from " + getTime(
454
- { hour: segment.bounds[0], minute: 0 },
496
+ windows.push(rangeWindow(
497
+ +segment.bounds[0],
498
+ +segment.bounds[1],
499
+ minute,
455
500
  opts
456
- ) + through(opts) + getTime({ hour: segment.bounds[1], minute }, opts));
457
- } else if (segment.kind === "step") {
458
- singles.push(...segment.fires);
459
- } else {
460
- singles.push(+segment.value);
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
- let phrase = rangeMinuteLead(ir, opts) + " " + joinList(windows, opts);
464
- if (singles.length) {
465
- phrase += " and at " + joinList(singles.map(function time(hour) {
466
- return getTime({ hour, minute }, opts);
467
- }), opts);
518
+ return { hours, pureStrays };
519
+ }
520
+ function outlierTail(times, pureStrays, opts) {
521
+ if (!times.length) {
522
+ return "";
468
523
  }
469
- return phrase;
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([start, last], opts);
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 confinement = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
629
- if (confinement) {
630
- return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
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 singles = [];
827
+ const outliers = collectHourOutliers(ir);
653
828
  ir.analyses.segments.hour.forEach(function classify(segment) {
654
829
  if (segment.kind === "range") {
655
- windows.push("from " + getTime(
656
- { hour: +segment.bounds[0], minute: 0 },
830
+ windows.push(rangeWindow(
831
+ +segment.bounds[0],
832
+ +segment.bounds[1],
833
+ 0,
657
834
  opts
658
- ) + through(opts) + getTime({ hour: +segment.bounds[1], minute: 0 }, opts));
659
- } else if (segment.kind === "step") {
660
- singles.push(...segment.fires);
661
- } else {
662
- singles.push(+segment.value);
835
+ ));
663
836
  }
664
837
  });
665
- let phrase = "every hour " + joinList(windows, opts);
666
- if (singles.length) {
667
- phrase += " and at " + joinList(singles.map(function time(hour) {
668
- return getTime({ hour, minute: 0 }, opts);
669
- }), opts);
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(values, opts) {
686
- const anyBig = values.some(function big(v) {
687
- return +v > 10;
688
- });
856
+ function seriesNumber() {
689
857
  return function format(n) {
690
- return anyBig ? "" + n : getNumber(n, opts);
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(seriesNumber(fires, opts));
869
+ return fires.map(listNumber(fires.length, opts));
695
870
  }
696
871
  function segmentWords(segments, opts) {
697
- const values = segments.flatMap(function collect(segment) {
872
+ const count = segments.reduce(function tally(sum, segment) {
698
873
  if (segment.kind === "range") {
699
- return segment.bounds;
874
+ return sum + 1;
700
875
  }
701
- return segment.kind === "step" ? segment.fires : [segment.value];
702
- });
703
- const num = seriesNumber(values, opts);
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 joinList(items, opts) {
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] + " and " + items[1];
965
+ return items[0] + conjunction + items[1];
788
966
  }
789
- const and = opts.style.serialComma ? ", and " : " and ";
790
- return items.slice(0, -1).join(", ") + and + items[items.length - 1];
967
+ const tail = opts.style.serialComma ? "," + conjunction : conjunction;
968
+ return items.slice(0, -1).join(", ") + tail + items[items.length - 1];
791
969
  }
792
- var trailingWords = { all: "", month: "in ", stepDate: "on ", weekday: "on " };
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 weekdays = quartzWeekdayPhrase(pattern.weekday, opts) || words.weekday + weekdayPhrase(ir, opts);
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 + monthScope(ir, opts);
1028
+ return monthScopeForRecurrence(quartzDate, ir, opts);
828
1029
  }
829
1030
  if (isOpenStep(pattern.date)) {
830
- return words.stepDate + stepDates(pattern.date) + monthScope(ir, opts);
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 + monthScope(ir, opts) + " or " + weekdayPart;
1138
+ return quartzDate;
852
1139
  }
853
1140
  if (isOpenStep(pattern.date)) {
854
- return stepDates(pattern.date) + monthScope(ir, opts) + " or " + weekdayPart;
1141
+ return stepDates(pattern.date);
855
1142
  }
856
- if (pattern.month !== "*" && monthFoldsIntoDate(ir)) {
857
- return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
1143
+ return "on the " + dateOrdinals(ir, opts);
1144
+ }
1145
+ function orMonthScope(ir, opts) {
1146
+ if (ir.pattern.month === "*") {
1147
+ return "";
858
1148
  }
859
- return "on the " + dateOrdinals(ir, opts) + " or " + weekdayPart + monthScope(ir, opts);
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
- return renderSegments(ir.analyses.segments.weekday, function name(value) {
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
- }, opts);
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
  }