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