cronli5 0.1.5 → 0.1.7

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;
@@ -201,7 +221,17 @@ function renderSecondsWithinMinute(ir, plan, opts) {
201
221
  }
202
222
  function composeHourCadence(ir, plan, opts) {
203
223
  const clockRest = plan.rest.kind === "clockTimes" || plan.rest.kind === "compactClockTimes";
204
- return clockRest && ir.shapes.minute === "single" ? hourCadence(ir, +ir.pattern.minute, opts) : null;
224
+ if (!clockRest || ir.shapes.minute !== "single") {
225
+ return null;
226
+ }
227
+ const minute = +ir.pattern.minute;
228
+ return hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
229
+ }
230
+ function clockTimesConfinement(ir, rest, opts) {
231
+ if (+rest.times[0].minute === 0 && ir.shapes.minute === "single") {
232
+ return secondsLeadClause(ir, opts) + " for one minute at " + durationHours(ir, rest, opts);
233
+ }
234
+ return secondsLeadClause(ir, opts) + " of " + clockTimesOf(ir, rest, opts);
205
235
  }
206
236
  function renderComposeSeconds(ir, plan, opts) {
207
237
  const cadence = composeHourCadence(ir, plan, opts);
@@ -209,16 +239,14 @@ function renderComposeSeconds(ir, plan, opts) {
209
239
  return cadence;
210
240
  }
211
241
  if (plan.rest.kind === "clockTimes" && (ir.shapes.second === "wildcard" || ir.shapes.second === "step")) {
212
- const minute = plan.rest.times[0].minute;
213
- if (+minute === 0) {
214
- return secondsLeadClause(ir, opts) + " for one minute at " + durationHours(ir, plan.rest, opts);
215
- }
216
- return secondsLeadClause(ir, opts) + " of " + clockTimesOf(ir, plan.rest, opts);
242
+ return clockTimesConfinement(ir, plan.rest, opts);
217
243
  }
218
244
  if (ir.shapes.second === "wildcard" && plan.rest.kind === "minuteFrequency" && plan.rest.hours.kind === "none" && ir.pattern.minute === "*/2") {
219
245
  return "every second of every other minute" + trailingQualifier(ir, opts);
220
246
  }
221
- return secondsLeadClause(ir, opts) + ", " + render(ir, plan.rest, opts);
247
+ const restOwnsLead = plan.rest.kind === "compactClockTimes" && ir.analyses.clockSecond;
248
+ const lead = restOwnsLead ? "" : secondsLeadClause(ir, opts) + ", ";
249
+ return lead + render(ir, plan.rest, opts);
222
250
  }
223
251
  function durationHours(ir, plan, opts) {
224
252
  const hours = plan.times.map(function clock(time) {
@@ -301,7 +329,8 @@ function renderMinuteFrequency(ir, plan, opts) {
301
329
  opts
302
330
  );
303
331
  if (plan.hours.kind === "during") {
304
- phrase += " during the " + hourTimesFromPlan(ir, plan.hours.times, false, opts) + " hours";
332
+ const cadence = unevenHourCadence(ir, opts);
333
+ phrase += cadence ? ", " + cadence : " during the " + hourTimesFromPlan(ir, plan.hours.times, false, opts) + " hours";
305
334
  } else if (plan.hours.kind === "window") {
306
335
  phrase += " " + hourWindow(plan.hours, opts);
307
336
  } else if (plan.hours.kind === "step") {
@@ -316,10 +345,13 @@ function renderMinuteSpanInHour(ir, plan, opts) {
316
345
  return "every minute from " + getTime({ hour: plan.hour, minute: plan.span[0] }, opts) + through(opts) + getTime({ hour: plan.hour, minute: plan.span[1] }, opts) + trailingQualifier(ir, opts);
317
346
  }
318
347
  function renderMinutesAcrossHours(ir, plan, opts) {
348
+ const cadence = unevenHourCadence(ir, opts);
319
349
  if (plan.form === "wildcard") {
350
+ if (cadence !== null) {
351
+ return "every minute, " + cadence + trailingQualifier(ir, opts);
352
+ }
320
353
  return "every minute during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
321
354
  }
322
- const times = hourTimesFromPlan(ir, plan.times, true, opts);
323
355
  const lead = plan.form === "range" ? minuteRangeLead(ir.pattern.minute, opts) : (
324
356
  // The 'list' form is a minute list, which has segments; an offset/uneven
325
357
  // step enumerated to that list reads as a stride.
@@ -330,6 +362,10 @@ function renderMinutesAcrossHours(ir, plan, opts) {
330
362
  opts
331
363
  )
332
364
  );
365
+ if (cadence !== null) {
366
+ return lead + ", " + cadence + trailingQualifier(ir, opts);
367
+ }
368
+ const times = hourTimesFromPlan(ir, plan.times, true, opts);
333
369
  return lead + ", at " + times + trailingQualifier(ir, opts);
334
370
  }
335
371
  var stepOrdinals = {
@@ -356,7 +392,8 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
356
392
  "hour",
357
393
  opts
358
394
  ) : minuteRangeLead(ir.pattern.minute, opts);
359
- return lead + ", " + stepHours(segment, opts) + trailingQualifier(ir, opts);
395
+ const cadence = unevenHourCadence(ir, opts);
396
+ return lead + ", " + (cadence ?? stepHours(segment, opts)) + trailingQualifier(ir, opts);
360
397
  }
361
398
  function minuteRangeLead(minuteField, opts) {
362
399
  const bounds = minuteField.split("-");
@@ -393,17 +430,23 @@ function rangeMinuteLead(ir, opts) {
393
430
  );
394
431
  }
395
432
  function renderHourStep(ir, plan, opts) {
433
+ const cadence = unevenHourCadence(ir, opts);
434
+ if (cadence !== null) {
435
+ return cadence + trailingQualifier(ir, opts);
436
+ }
396
437
  return stepHours(ir.analyses.segments.hour[0], opts) + trailingQualifier(ir, opts);
397
438
  }
398
439
  function boundedWindow(plan) {
399
- return { from: plan.from, last: plan.boundMinute ?? 0, to: plan.to };
440
+ const last = plan.minuteForm === "wildcard" ? plan.boundMinute ?? 0 : 0;
441
+ return { from: plan.from, last, to: plan.to };
400
442
  }
401
443
  function hourWindow(window, opts) {
402
444
  return "from " + getTime({ hour: window.from, minute: 0 }, opts) + through(opts) + getTime({ hour: window.to, minute: window.last }, opts);
403
445
  }
404
446
  function renderClockTimes(ir, plan, opts) {
405
447
  if (ir.shapes.minute === "single") {
406
- const cadence = hourCadence(ir, +ir.pattern.minute, opts);
448
+ const minute = +ir.pattern.minute;
449
+ const cadence = hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
407
450
  if (cadence !== null) {
408
451
  return cadence;
409
452
  }
@@ -421,9 +464,9 @@ function renderClockTimes(ir, plan, opts) {
421
464
  }
422
465
  function renderCompactClockTimes(ir, plan, opts) {
423
466
  if (plan.fold) {
424
- const cadence = hourCadence(ir, +plan.minute, opts);
425
- if (cadence !== null) {
426
- return cadence;
467
+ const cadence2 = hourCadence(ir, +plan.minute, opts) ?? hourRangeCadence(ir, +plan.minute, opts);
468
+ if (cadence2 !== null) {
469
+ return cadence2;
427
470
  }
428
471
  const hasRange = ir.analyses.segments.hour.some(function range(segment) {
429
472
  return segment.kind === "range";
@@ -434,16 +477,18 @@ function renderCompactClockTimes(ir, plan, opts) {
434
477
  const fold = { minute: plan.minute, second: ir.analyses.clockSecond };
435
478
  return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
436
479
  }
437
- const phrase = (
480
+ const minuteLead = (
438
481
  // The non-fold branch is a minute list, which has segments. An
439
482
  // offset/uneven step enumerated to that list reads as a stride.
440
- (strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
483
+ strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
441
484
  segmentWords(ir.analyses.segments.minute, opts),
442
485
  "minute",
443
486
  "hour",
444
487
  opts
445
- )) + ", at " + hourSegmentTimes(ir, { minute: 0, second: null }, true, opts) + trailingQualifier(ir, opts)
488
+ )
446
489
  );
490
+ const cadence = unevenHourCadence(ir, opts);
491
+ const phrase = cadence ? minuteLead + ", " + cadence + trailingQualifier(ir, opts) : minuteLead + ", at " + hourSegmentTimes(ir, { minute: 0, second: null }, true, opts) + trailingQualifier(ir, opts);
447
492
  return ir.analyses.clockSecond ? secondsLeadClause(ir, opts) + ", " + phrase : phrase;
448
493
  }
449
494
  function foldedHourWindows(ir, plan, opts) {
@@ -561,16 +606,46 @@ function hourStrideCadence(stride, opts) {
561
606
  }
562
607
  return cadence + " from " + getTime({ hour: start, minute: 0 }, opts) + through(opts) + getTime({ hour: last, minute: 0 }, opts);
563
608
  }
609
+ function offsetCleanStride(stride) {
610
+ return stride.start < stride.interval && 24 % stride.interval === 0;
611
+ }
612
+ function unevenHourCadence(ir, opts) {
613
+ const stride = hourStride(ir);
614
+ if (!stride || offsetCleanStride(stride)) {
615
+ return null;
616
+ }
617
+ return hourStrideCadence(stride, opts);
618
+ }
619
+ function hourListStride(values) {
620
+ if (values.length < 2) {
621
+ return null;
622
+ }
623
+ const interval = values[1] - values[0];
624
+ if (interval < 2) {
625
+ return null;
626
+ }
627
+ for (let i = 2; i < values.length; i += 1) {
628
+ if (values[i] - values[i - 1] !== interval) {
629
+ return null;
630
+ }
631
+ }
632
+ if (values[0] !== 0 && values.length < 5) {
633
+ return null;
634
+ }
635
+ return { interval, last: values[values.length - 1], start: values[0] };
636
+ }
564
637
  function hourStride(ir) {
565
638
  const segments = ir.analyses.segments.hour;
566
639
  if (segments.length === 1 && segments[0].kind === "step") {
567
640
  const segment = segments[0];
641
+ if (segment.fires.length < 2) {
642
+ return null;
643
+ }
568
644
  const start = segment.startToken === "*" ? 0 : +segment.startToken.split("-")[0];
569
645
  return { interval: segment.interval, last: segment.fires[segment.fires.length - 1], start };
570
646
  }
571
647
  const values = singleValues(segments);
572
- const step = values && arithmeticStep(values);
573
- return step || null;
648
+ return values && hourListStride(values);
574
649
  }
575
650
  function subMinuteSecond(ir) {
576
651
  return ir.pattern.second === "*" || ir.shapes.second === "step";
@@ -594,13 +669,16 @@ function hourCadence(ir, minute, opts) {
594
669
  return null;
595
670
  }
596
671
  const fires = (stride.last - stride.start) / stride.interval + 1;
597
- if (ir.pattern.second === "0" && fires <= maxClockTimes) {
672
+ if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
598
673
  return null;
599
674
  }
600
675
  const confinement = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
601
676
  if (confinement) {
602
677
  return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
603
678
  }
679
+ if (minute === 0 && ir.pattern.second === "0") {
680
+ return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
681
+ }
604
682
  return hourCadenceLead(ir, minute, opts) + ", " + hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
605
683
  }
606
684
  function cleanStrideSegment(ir) {
@@ -611,6 +689,46 @@ function cleanStrideSegment(ir) {
611
689
  }
612
690
  return segment;
613
691
  }
692
+ function hasHourWindow(ir) {
693
+ return ir.analyses.segments.hour.some(function range(segment) {
694
+ return segment.kind === "range";
695
+ });
696
+ }
697
+ function hourRangeWindowTail(ir, opts) {
698
+ const windows = [];
699
+ const singles = [];
700
+ ir.analyses.segments.hour.forEach(function classify(segment) {
701
+ if (segment.kind === "range") {
702
+ windows.push("from " + getTime(
703
+ { hour: +segment.bounds[0], minute: 0 },
704
+ 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);
710
+ }
711
+ });
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;
719
+ }
720
+ function hourRangeCadence(ir, minute, opts) {
721
+ if (minute !== 0 || !hasHourWindow(ir)) {
722
+ return null;
723
+ }
724
+ if (ir.pattern.second === "0") {
725
+ return null;
726
+ }
727
+ if (subMinuteSecond(ir)) {
728
+ return secondsClause(ir, "minute", opts) + " for one minute during the " + hourSegmentTimes(ir, { minute: 0, second: null }, false, opts) + " hours" + trailingQualifier(ir, opts);
729
+ }
730
+ return hourCadenceLead(ir, minute, opts) + ", " + hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
731
+ }
614
732
  function seriesNumber(values, opts) {
615
733
  const anyBig = values.some(function big(v) {
616
734
  return +v > 10;
@@ -775,17 +893,27 @@ function monthFoldsIntoDate(ir) {
775
893
  function dateOrWeekday(ir, opts) {
776
894
  const pattern = ir.pattern;
777
895
  const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, opts);
896
+ if (pattern.month !== "*" && monthFoldsIntoDate(ir) && !quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
897
+ return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
898
+ }
899
+ return datePart(ir, opts) + " or " + weekdayPart + orMonthScope(ir, opts);
900
+ }
901
+ function datePart(ir, opts) {
902
+ const pattern = ir.pattern;
778
903
  const quartzDate = quartzDatePhrase(pattern.date, opts);
779
904
  if (quartzDate) {
780
- return quartzDate + monthScope(ir, opts) + " or " + weekdayPart;
905
+ return quartzDate;
781
906
  }
782
907
  if (isOpenStep(pattern.date)) {
783
- return stepDates(pattern.date) + monthScope(ir, opts) + " or " + weekdayPart;
908
+ return stepDates(pattern.date);
784
909
  }
785
- if (pattern.month !== "*" && monthFoldsIntoDate(ir)) {
786
- return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
910
+ return "on the " + dateOrdinals(ir, opts);
911
+ }
912
+ function orMonthScope(ir, opts) {
913
+ if (ir.pattern.month === "*") {
914
+ return "";
787
915
  }
788
- return "on the " + dateOrdinals(ir, opts) + " or " + weekdayPart + monthScope(ir, opts);
916
+ return ", in " + monthName(ir, opts);
789
917
  }
790
918
  function quartzDatePhrase(dateField, opts) {
791
919
  if (dateField === "L") {
@@ -867,7 +995,8 @@ function oddEvenMonth(monthField) {
867
995
  return start === "2" ? "every even-numbered month" : null;
868
996
  }
869
997
  function weekdayPhrase(ir, opts) {
870
- return renderSegments(ir.analyses.segments.weekday, function name(value) {
998
+ const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
999
+ return renderSegments(segments, function name(value) {
871
1000
  return getWeekday(value, opts);
872
1001
  }, opts);
873
1002
  }
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;
@@ -175,7 +195,17 @@ function renderSecondsWithinMinute(ir, plan, opts) {
175
195
  }
176
196
  function composeHourCadence(ir, plan, opts) {
177
197
  const clockRest = plan.rest.kind === "clockTimes" || plan.rest.kind === "compactClockTimes";
178
- return clockRest && ir.shapes.minute === "single" ? hourCadence(ir, +ir.pattern.minute, opts) : null;
198
+ if (!clockRest || ir.shapes.minute !== "single") {
199
+ return null;
200
+ }
201
+ const minute = +ir.pattern.minute;
202
+ return hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
203
+ }
204
+ function clockTimesConfinement(ir, rest, opts) {
205
+ if (+rest.times[0].minute === 0 && ir.shapes.minute === "single") {
206
+ return secondsLeadClause(ir, opts) + " for one minute at " + durationHours(ir, rest, opts);
207
+ }
208
+ return secondsLeadClause(ir, opts) + " of " + clockTimesOf(ir, rest, opts);
179
209
  }
180
210
  function renderComposeSeconds(ir, plan, opts) {
181
211
  const cadence = composeHourCadence(ir, plan, opts);
@@ -183,16 +213,14 @@ function renderComposeSeconds(ir, plan, opts) {
183
213
  return cadence;
184
214
  }
185
215
  if (plan.rest.kind === "clockTimes" && (ir.shapes.second === "wildcard" || ir.shapes.second === "step")) {
186
- const minute = plan.rest.times[0].minute;
187
- if (+minute === 0) {
188
- return secondsLeadClause(ir, opts) + " for one minute at " + durationHours(ir, plan.rest, opts);
189
- }
190
- return secondsLeadClause(ir, opts) + " of " + clockTimesOf(ir, plan.rest, opts);
216
+ return clockTimesConfinement(ir, plan.rest, opts);
191
217
  }
192
218
  if (ir.shapes.second === "wildcard" && plan.rest.kind === "minuteFrequency" && plan.rest.hours.kind === "none" && ir.pattern.minute === "*/2") {
193
219
  return "every second of every other minute" + trailingQualifier(ir, opts);
194
220
  }
195
- return secondsLeadClause(ir, opts) + ", " + render(ir, plan.rest, opts);
221
+ const restOwnsLead = plan.rest.kind === "compactClockTimes" && ir.analyses.clockSecond;
222
+ const lead = restOwnsLead ? "" : secondsLeadClause(ir, opts) + ", ";
223
+ return lead + render(ir, plan.rest, opts);
196
224
  }
197
225
  function durationHours(ir, plan, opts) {
198
226
  const hours = plan.times.map(function clock(time) {
@@ -275,7 +303,8 @@ function renderMinuteFrequency(ir, plan, opts) {
275
303
  opts
276
304
  );
277
305
  if (plan.hours.kind === "during") {
278
- phrase += " during the " + hourTimesFromPlan(ir, plan.hours.times, false, opts) + " hours";
306
+ const cadence = unevenHourCadence(ir, opts);
307
+ phrase += cadence ? ", " + cadence : " during the " + hourTimesFromPlan(ir, plan.hours.times, false, opts) + " hours";
279
308
  } else if (plan.hours.kind === "window") {
280
309
  phrase += " " + hourWindow(plan.hours, opts);
281
310
  } else if (plan.hours.kind === "step") {
@@ -290,10 +319,13 @@ function renderMinuteSpanInHour(ir, plan, opts) {
290
319
  return "every minute from " + getTime({ hour: plan.hour, minute: plan.span[0] }, opts) + through(opts) + getTime({ hour: plan.hour, minute: plan.span[1] }, opts) + trailingQualifier(ir, opts);
291
320
  }
292
321
  function renderMinutesAcrossHours(ir, plan, opts) {
322
+ const cadence = unevenHourCadence(ir, opts);
293
323
  if (plan.form === "wildcard") {
324
+ if (cadence !== null) {
325
+ return "every minute, " + cadence + trailingQualifier(ir, opts);
326
+ }
294
327
  return "every minute during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
295
328
  }
296
- const times = hourTimesFromPlan(ir, plan.times, true, opts);
297
329
  const lead = plan.form === "range" ? minuteRangeLead(ir.pattern.minute, opts) : (
298
330
  // The 'list' form is a minute list, which has segments; an offset/uneven
299
331
  // step enumerated to that list reads as a stride.
@@ -304,6 +336,10 @@ function renderMinutesAcrossHours(ir, plan, opts) {
304
336
  opts
305
337
  )
306
338
  );
339
+ if (cadence !== null) {
340
+ return lead + ", " + cadence + trailingQualifier(ir, opts);
341
+ }
342
+ const times = hourTimesFromPlan(ir, plan.times, true, opts);
307
343
  return lead + ", at " + times + trailingQualifier(ir, opts);
308
344
  }
309
345
  var stepOrdinals = {
@@ -330,7 +366,8 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
330
366
  "hour",
331
367
  opts
332
368
  ) : minuteRangeLead(ir.pattern.minute, opts);
333
- return lead + ", " + stepHours(segment, opts) + trailingQualifier(ir, opts);
369
+ const cadence = unevenHourCadence(ir, opts);
370
+ return lead + ", " + (cadence ?? stepHours(segment, opts)) + trailingQualifier(ir, opts);
334
371
  }
335
372
  function minuteRangeLead(minuteField, opts) {
336
373
  const bounds = minuteField.split("-");
@@ -367,17 +404,23 @@ function rangeMinuteLead(ir, opts) {
367
404
  );
368
405
  }
369
406
  function renderHourStep(ir, plan, opts) {
407
+ const cadence = unevenHourCadence(ir, opts);
408
+ if (cadence !== null) {
409
+ return cadence + trailingQualifier(ir, opts);
410
+ }
370
411
  return stepHours(ir.analyses.segments.hour[0], opts) + trailingQualifier(ir, opts);
371
412
  }
372
413
  function boundedWindow(plan) {
373
- return { from: plan.from, last: plan.boundMinute ?? 0, to: plan.to };
414
+ const last = plan.minuteForm === "wildcard" ? plan.boundMinute ?? 0 : 0;
415
+ return { from: plan.from, last, to: plan.to };
374
416
  }
375
417
  function hourWindow(window, opts) {
376
418
  return "from " + getTime({ hour: window.from, minute: 0 }, opts) + through(opts) + getTime({ hour: window.to, minute: window.last }, opts);
377
419
  }
378
420
  function renderClockTimes(ir, plan, opts) {
379
421
  if (ir.shapes.minute === "single") {
380
- const cadence = hourCadence(ir, +ir.pattern.minute, opts);
422
+ const minute = +ir.pattern.minute;
423
+ const cadence = hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
381
424
  if (cadence !== null) {
382
425
  return cadence;
383
426
  }
@@ -395,9 +438,9 @@ function renderClockTimes(ir, plan, opts) {
395
438
  }
396
439
  function renderCompactClockTimes(ir, plan, opts) {
397
440
  if (plan.fold) {
398
- const cadence = hourCadence(ir, +plan.minute, opts);
399
- if (cadence !== null) {
400
- return cadence;
441
+ const cadence2 = hourCadence(ir, +plan.minute, opts) ?? hourRangeCadence(ir, +plan.minute, opts);
442
+ if (cadence2 !== null) {
443
+ return cadence2;
401
444
  }
402
445
  const hasRange = ir.analyses.segments.hour.some(function range(segment) {
403
446
  return segment.kind === "range";
@@ -408,16 +451,18 @@ function renderCompactClockTimes(ir, plan, opts) {
408
451
  const fold = { minute: plan.minute, second: ir.analyses.clockSecond };
409
452
  return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
410
453
  }
411
- const phrase = (
454
+ const minuteLead = (
412
455
  // The non-fold branch is a minute list, which has segments. An
413
456
  // offset/uneven step enumerated to that list reads as a stride.
414
- (strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
457
+ strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
415
458
  segmentWords(ir.analyses.segments.minute, opts),
416
459
  "minute",
417
460
  "hour",
418
461
  opts
419
- )) + ", at " + hourSegmentTimes(ir, { minute: 0, second: null }, true, opts) + trailingQualifier(ir, opts)
462
+ )
420
463
  );
464
+ const cadence = unevenHourCadence(ir, opts);
465
+ const phrase = cadence ? minuteLead + ", " + cadence + trailingQualifier(ir, opts) : minuteLead + ", at " + hourSegmentTimes(ir, { minute: 0, second: null }, true, opts) + trailingQualifier(ir, opts);
421
466
  return ir.analyses.clockSecond ? secondsLeadClause(ir, opts) + ", " + phrase : phrase;
422
467
  }
423
468
  function foldedHourWindows(ir, plan, opts) {
@@ -535,16 +580,46 @@ function hourStrideCadence(stride, opts) {
535
580
  }
536
581
  return cadence + " from " + getTime({ hour: start, minute: 0 }, opts) + through(opts) + getTime({ hour: last, minute: 0 }, opts);
537
582
  }
583
+ function offsetCleanStride(stride) {
584
+ return stride.start < stride.interval && 24 % stride.interval === 0;
585
+ }
586
+ function unevenHourCadence(ir, opts) {
587
+ const stride = hourStride(ir);
588
+ if (!stride || offsetCleanStride(stride)) {
589
+ return null;
590
+ }
591
+ return hourStrideCadence(stride, opts);
592
+ }
593
+ function hourListStride(values) {
594
+ if (values.length < 2) {
595
+ return null;
596
+ }
597
+ const interval = values[1] - values[0];
598
+ if (interval < 2) {
599
+ return null;
600
+ }
601
+ for (let i = 2; i < values.length; i += 1) {
602
+ if (values[i] - values[i - 1] !== interval) {
603
+ return null;
604
+ }
605
+ }
606
+ if (values[0] !== 0 && values.length < 5) {
607
+ return null;
608
+ }
609
+ return { interval, last: values[values.length - 1], start: values[0] };
610
+ }
538
611
  function hourStride(ir) {
539
612
  const segments = ir.analyses.segments.hour;
540
613
  if (segments.length === 1 && segments[0].kind === "step") {
541
614
  const segment = segments[0];
615
+ if (segment.fires.length < 2) {
616
+ return null;
617
+ }
542
618
  const start = segment.startToken === "*" ? 0 : +segment.startToken.split("-")[0];
543
619
  return { interval: segment.interval, last: segment.fires[segment.fires.length - 1], start };
544
620
  }
545
621
  const values = singleValues(segments);
546
- const step = values && arithmeticStep(values);
547
- return step || null;
622
+ return values && hourListStride(values);
548
623
  }
549
624
  function subMinuteSecond(ir) {
550
625
  return ir.pattern.second === "*" || ir.shapes.second === "step";
@@ -568,13 +643,16 @@ function hourCadence(ir, minute, opts) {
568
643
  return null;
569
644
  }
570
645
  const fires = (stride.last - stride.start) / stride.interval + 1;
571
- if (ir.pattern.second === "0" && fires <= maxClockTimes) {
646
+ if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
572
647
  return null;
573
648
  }
574
649
  const confinement = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
575
650
  if (confinement) {
576
651
  return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
577
652
  }
653
+ if (minute === 0 && ir.pattern.second === "0") {
654
+ return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
655
+ }
578
656
  return hourCadenceLead(ir, minute, opts) + ", " + hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
579
657
  }
580
658
  function cleanStrideSegment(ir) {
@@ -585,6 +663,46 @@ function cleanStrideSegment(ir) {
585
663
  }
586
664
  return segment;
587
665
  }
666
+ function hasHourWindow(ir) {
667
+ return ir.analyses.segments.hour.some(function range(segment) {
668
+ return segment.kind === "range";
669
+ });
670
+ }
671
+ function hourRangeWindowTail(ir, opts) {
672
+ const windows = [];
673
+ const singles = [];
674
+ ir.analyses.segments.hour.forEach(function classify(segment) {
675
+ if (segment.kind === "range") {
676
+ windows.push("from " + getTime(
677
+ { hour: +segment.bounds[0], minute: 0 },
678
+ 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);
684
+ }
685
+ });
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;
693
+ }
694
+ function hourRangeCadence(ir, minute, opts) {
695
+ if (minute !== 0 || !hasHourWindow(ir)) {
696
+ return null;
697
+ }
698
+ if (ir.pattern.second === "0") {
699
+ return null;
700
+ }
701
+ if (subMinuteSecond(ir)) {
702
+ return secondsClause(ir, "minute", opts) + " for one minute during the " + hourSegmentTimes(ir, { minute: 0, second: null }, false, opts) + " hours" + trailingQualifier(ir, opts);
703
+ }
704
+ return hourCadenceLead(ir, minute, opts) + ", " + hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
705
+ }
588
706
  function seriesNumber(values, opts) {
589
707
  const anyBig = values.some(function big(v) {
590
708
  return +v > 10;
@@ -749,17 +867,27 @@ function monthFoldsIntoDate(ir) {
749
867
  function dateOrWeekday(ir, opts) {
750
868
  const pattern = ir.pattern;
751
869
  const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, opts);
870
+ if (pattern.month !== "*" && monthFoldsIntoDate(ir) && !quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
871
+ return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
872
+ }
873
+ return datePart(ir, opts) + " or " + weekdayPart + orMonthScope(ir, opts);
874
+ }
875
+ function datePart(ir, opts) {
876
+ const pattern = ir.pattern;
752
877
  const quartzDate = quartzDatePhrase(pattern.date, opts);
753
878
  if (quartzDate) {
754
- return quartzDate + monthScope(ir, opts) + " or " + weekdayPart;
879
+ return quartzDate;
755
880
  }
756
881
  if (isOpenStep(pattern.date)) {
757
- return stepDates(pattern.date) + monthScope(ir, opts) + " or " + weekdayPart;
882
+ return stepDates(pattern.date);
758
883
  }
759
- if (pattern.month !== "*" && monthFoldsIntoDate(ir)) {
760
- return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
884
+ return "on the " + dateOrdinals(ir, opts);
885
+ }
886
+ function orMonthScope(ir, opts) {
887
+ if (ir.pattern.month === "*") {
888
+ return "";
761
889
  }
762
- return "on the " + dateOrdinals(ir, opts) + " or " + weekdayPart + monthScope(ir, opts);
890
+ return ", in " + monthName(ir, opts);
763
891
  }
764
892
  function quartzDatePhrase(dateField, opts) {
765
893
  if (dateField === "L") {
@@ -841,7 +969,8 @@ function oddEvenMonth(monthField) {
841
969
  return start === "2" ? "every even-numbered month" : null;
842
970
  }
843
971
  function weekdayPhrase(ir, opts) {
844
- return renderSegments(ir.analyses.segments.weekday, function name(value) {
972
+ const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
973
+ return renderSegments(segments, function name(value) {
845
974
  return getWeekday(value, opts);
846
975
  }, opts);
847
976
  }