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/fi.cjs CHANGED
@@ -72,6 +72,26 @@ function arithmeticStep(values) {
72
72
  }
73
73
  return { start: values[0], interval, last: values[values.length - 1] };
74
74
  }
75
+ function weekdayDisplayKey(value) {
76
+ return value === 0 ? 7 : value;
77
+ }
78
+ function orderWeekdaysForDisplay(segments) {
79
+ const flattened = segments.flatMap(function flat(segment) {
80
+ return segment.kind === "step" ? segment.fires.map(function single(value) {
81
+ return { kind: "single", value: "" + value };
82
+ }) : [segment];
83
+ });
84
+ function key(segment) {
85
+ return segment.kind === "range" ? weekdayDisplayKey(+segment.bounds[0]) : weekdayDisplayKey(+segment.value);
86
+ }
87
+ return flattened.map(function index(segment, position) {
88
+ return [segment, position];
89
+ }).sort(function byDisplayKey(a, b) {
90
+ return key(a[0]) - key(b[0]) || a[1] - b[1];
91
+ }).map(function unwrap(pair) {
92
+ return pair[0];
93
+ });
94
+ }
75
95
  function toFieldNumber(token, numberMap) {
76
96
  return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
77
97
  }
@@ -289,7 +309,11 @@ function composeSecondsOverMinuteStep(ir, freq, opts) {
289
309
  }
290
310
  function composeHourCadence(ir, plan, opts) {
291
311
  const clockRest = plan.rest.kind === "clockTimes" || plan.rest.kind === "compactClockTimes";
292
- return clockRest && ir.shapes.minute === "single" ? hourCadence(ir, +ir.pattern.minute, opts) : null;
312
+ if (!clockRest || ir.shapes.minute !== "single") {
313
+ return null;
314
+ }
315
+ const minute = +ir.pattern.minute;
316
+ return hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
293
317
  }
294
318
  function renderComposeSeconds(ir, plan, opts) {
295
319
  const cadence = composeHourCadence(ir, plan, opts);
@@ -305,7 +329,9 @@ function renderComposeSeconds(ir, plan, opts) {
305
329
  if (isEveryOtherMinuteSeconds(ir, plan)) {
306
330
  return secondsLeadClause(ir, opts) + " joka toisena minuuttina";
307
331
  }
308
- return secondsLeadClause(ir, opts) + ", " + render(ir, plan.rest, opts);
332
+ const restOwnsLead = plan.rest.kind === "compactClockTimes" && ir.analyses.clockSecond;
333
+ const lead = restOwnsLead ? "" : secondsLeadClause(ir, opts) + ", ";
334
+ return lead + render(ir, plan.rest, opts);
309
335
  }
310
336
  function isEveryOtherMinuteSeconds(ir, plan) {
311
337
  if (plan.rest.kind !== "minuteFrequency" || ir.pattern.second !== "*" || ir.shapes.hour !== "wildcard") {
@@ -424,6 +450,10 @@ function hourSegmentTimesWithSeka(ir, minute, second, opts) {
424
450
  function renderMinuteFrequency(ir, plan, opts) {
425
451
  const seg = stepSegment(ir.analyses.segments.minute);
426
452
  if (plan.hours.kind === "during") {
453
+ const cadence = unevenHourCadence(ir, opts);
454
+ if (cadence !== null) {
455
+ return stepCycle60(seg, units.minute, opts) + ", " + cadence + trailingQualifier(ir, opts);
456
+ }
427
457
  if (minuteStepIsAnchored(seg)) {
428
458
  const bareHours = kloFromTimes(ir, plan.hours.times, opts);
429
459
  return hoursFirstMinutes(bareHours, ir, opts) + trailingQualifier(ir, opts);
@@ -449,8 +479,12 @@ function renderMinuteSpanInHour(ir, plan, opts) {
449
479
  ) + trailingQualifier(ir, opts);
450
480
  }
451
481
  function renderMinutesAcrossHours(ir, plan, opts) {
482
+ const cadence = unevenHourCadence(ir, opts);
452
483
  if (plan.form === "wildcard") {
453
- return "joka minuutti " + hourWindowsFromTimes(ir, plan.times, opts) + trailingQualifier(ir, opts);
484
+ return cadence ? "joka minuutti, " + cadence + trailingQualifier(ir, opts) : "joka minuutti " + hourWindowsFromTimes(ir, plan.times, opts) + trailingQualifier(ir, opts);
485
+ }
486
+ if (cadence !== null) {
487
+ return bareMinutes(ir, opts) + ", " + cadence + trailingQualifier(ir, opts);
454
488
  }
455
489
  if (hoursAreRangeIsolated(ir.analyses.segments.hour)) {
456
490
  return bareMinutes(ir, opts) + " " + hourSegmentTimesWithSeka(ir, 0, null, opts) + trailingQualifier(ir, opts);
@@ -460,12 +494,12 @@ function renderMinutesAcrossHours(ir, plan, opts) {
460
494
  }
461
495
  function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
462
496
  const segment = stepSegment(ir.analyses.segments.hour);
497
+ const cadence = unevenHourCadence(ir, opts);
463
498
  if (plan.form === "wildcard") {
464
499
  return "joka minuutti " + everyNthHour(segment, opts) + trailingQualifier(ir, opts);
465
500
  }
466
- if (segment.startToken.indexOf("-") !== -1) {
467
- const hoursStr = kloList(segment.fires, opts);
468
- return hoursFirstMinutes(hoursStr, ir, opts) + trailingQualifier(ir, opts);
501
+ if (cadence !== null) {
502
+ return bareMinutes(ir, opts) + ", " + cadence + trailingQualifier(ir, opts);
469
503
  }
470
504
  return bareMinutes(ir, opts) + hourStepTail(segment, opts) + trailingQualifier(ir, opts);
471
505
  }
@@ -540,6 +574,10 @@ function renderHourRange(ir, plan, opts) {
540
574
  return hoursFirstMinutes(window, ir, opts) + trailingQualifier(ir, opts);
541
575
  }
542
576
  function renderHourStep(ir, plan, opts) {
577
+ const cadence = unevenHourCadence(ir, opts);
578
+ if (cadence !== null) {
579
+ return cadence + trailingQualifier(ir, opts);
580
+ }
543
581
  return stepHours(stepSegment(ir.analyses.segments.hour), opts) + trailingQualifier(ir, opts);
544
582
  }
545
583
  function boundedWindow(plan) {
@@ -554,7 +592,8 @@ function hourWindow(window, opts) {
554
592
  }
555
593
  function renderClockTimes(ir, plan, opts) {
556
594
  if (ir.shapes.minute === "single") {
557
- const cadence = hourCadence(ir, +ir.pattern.minute, opts);
595
+ const minute = +ir.pattern.minute;
596
+ const cadence = hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
558
597
  if (cadence !== null) {
559
598
  return cadence;
560
599
  }
@@ -570,9 +609,9 @@ function renderClockTimes(ir, plan, opts) {
570
609
  }
571
610
  function renderCompactClockTimes(ir, plan, opts) {
572
611
  if (plan.fold) {
573
- const cadence = hourCadence(ir, plan.minute, opts);
574
- if (cadence !== null) {
575
- return cadence;
612
+ const cadence2 = hourCadence(ir, plan.minute, opts) ?? hourRangeCadence(ir, plan.minute, opts);
613
+ if (cadence2 !== null) {
614
+ return cadence2;
576
615
  }
577
616
  }
578
617
  const hourSegs = ir.analyses.segments.hour;
@@ -591,8 +630,12 @@ function renderCompactClockTimes(ir, plan, opts) {
591
630
  if (plan.fold) {
592
631
  return leadingQualifier(ir, opts) + hourSegmentTimes(ir, plan.minute, ir.analyses.clockSecond, opts);
593
632
  }
594
- const hoursStr = hourSegmentTimes(ir, 0, null, opts);
595
- const phrase = hoursFirstMinutes(hoursStr, ir, opts) + trailingQualifier(ir, opts);
633
+ const cadence = unevenHourCadence(ir, opts);
634
+ const phrase = cadence ? bareMinutes(ir, opts) + ", " + cadence + trailingQualifier(ir, opts) : (
635
+ // A minute list over purely enumerated hours (step fires, all singles) —
636
+ // hours-first, drop "joka tunti".
637
+ hoursFirstMinutes(hourSegmentTimes(ir, 0, null, opts), ir, opts) + trailingQualifier(ir, opts)
638
+ );
596
639
  return ir.analyses.clockSecond ? secondsLeadClause(ir, opts) + ", " + phrase : phrase;
597
640
  }
598
641
  var renderers = {
@@ -685,6 +728,27 @@ function hourStrideCadence(stride, opts) {
685
728
  }
686
729
  return cadence + " " + kloRange({ hour: start, minute: 0 }, { hour: last, minute: 0 }, opts);
687
730
  }
731
+ function hourListStride(values) {
732
+ if (values.length < 2) {
733
+ return null;
734
+ }
735
+ const interval = values[1] - values[0];
736
+ if (interval < 2) {
737
+ return null;
738
+ }
739
+ for (let i = 2; i < values.length; i += 1) {
740
+ if (values[i] - values[i - 1] !== interval) {
741
+ return null;
742
+ }
743
+ }
744
+ if (values[0] !== 0 && values.length < 5) {
745
+ return null;
746
+ }
747
+ return { interval, last: values[values.length - 1], start: values[0] };
748
+ }
749
+ function offsetCleanStride(stride) {
750
+ return stride.start < stride.interval && 24 % stride.interval === 0;
751
+ }
688
752
  function hourStride(ir) {
689
753
  const segments = ir.analyses.segments.hour;
690
754
  if (!segments) {
@@ -692,12 +756,21 @@ function hourStride(ir) {
692
756
  }
693
757
  if (segments.length === 1 && segments[0].kind === "step") {
694
758
  const segment = segments[0];
759
+ if (segment.fires.length < 2) {
760
+ return null;
761
+ }
695
762
  const start = segment.startToken === "*" ? 0 : +segment.startToken.split("-")[0];
696
763
  return { interval: segment.interval, last: segment.fires[segment.fires.length - 1], start };
697
764
  }
698
765
  const values = singleValues(segments);
699
- const step = values && arithmeticStep(values);
700
- return step || null;
766
+ return values && hourListStride(values);
767
+ }
768
+ function unevenHourCadence(ir, opts) {
769
+ const stride = hourStride(ir);
770
+ if (!stride || offsetCleanStride(stride)) {
771
+ return null;
772
+ }
773
+ return hourStrideCadence(stride, opts);
701
774
  }
702
775
  function subMinuteSecond(ir) {
703
776
  return ir.pattern.second === "*" || ir.shapes.second === "step";
@@ -721,7 +794,7 @@ function hourCadence(ir, minute, opts) {
721
794
  return null;
722
795
  }
723
796
  const fires = (stride.last - stride.start) / stride.interval + 1;
724
- if (ir.pattern.second === "0" && fires <= maxClockTimes) {
797
+ if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
725
798
  return null;
726
799
  }
727
800
  const segment = ir.analyses.segments.hour[0];
@@ -729,6 +802,9 @@ function hourCadence(ir, minute, opts) {
729
802
  if (confined) {
730
803
  return secondsLeadClause(ir, opts) + " minuutin ajan " + everyNthHour(segment, opts) + trailingQualifier(ir, opts);
731
804
  }
805
+ if (minute === 0 && ir.pattern.second === "0") {
806
+ return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
807
+ }
732
808
  return hourCadenceLead(ir, minute, opts) + ", " + hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
733
809
  }
734
810
  function cleanHourStride(segment) {
@@ -738,6 +814,23 @@ function cleanHourStride(segment) {
738
814
  const start = segment.startToken === "*" ? 0 : +segment.startToken;
739
815
  return 24 % segment.interval === 0 && start < segment.interval;
740
816
  }
817
+ function hasHourWindow(ir) {
818
+ const segments = ir.analyses.segments.hour;
819
+ return !!segments && segments.some(function range(segment) {
820
+ return segment.kind === "range";
821
+ });
822
+ }
823
+ function hourRangeWindowTail(ir, opts) {
824
+ return ir.analyses.segments.hour.length === 1 ? hourSegmentTimes(ir, 0, null, opts) : hourSegmentTimesWithSeka(ir, 0, null, opts);
825
+ }
826
+ function hourRangeCadence(ir, minute, opts) {
827
+ if (minute !== 0 || !hasHourWindow(ir) || ir.pattern.second === "0") {
828
+ return null;
829
+ }
830
+ const tail = hourRangeWindowTail(ir, opts);
831
+ const joiner = subMinuteSecond(ir) ? " " : ", ";
832
+ return hourCadenceLead(ir, minute, opts) + joiner + tail + trailingQualifier(ir, opts);
833
+ }
741
834
  function kloList(hours, opts) {
742
835
  if (hours.length === 1) {
743
836
  return timeWord(hours[0], 0, null, opts);
@@ -754,12 +847,16 @@ function kloFromTimes(ir, times, opts) {
754
847
  }
755
848
  function hourWindowsFromTimes(ir, times, opts) {
756
849
  if (times.kind === "fires") {
757
- return "klo " + joinList(times.fires.map(function window(hour) {
758
- return hourWindowDigits(hour, opts);
759
- }));
850
+ return kloList(times.fires, opts);
851
+ }
852
+ const segments = ir.analyses.segments.hour;
853
+ if (!segments.some(function ranged(segment) {
854
+ return segment.kind === "range";
855
+ })) {
856
+ return kloList(hourSegmentFires(segments), opts);
760
857
  }
761
858
  const pieces = [];
762
- ir.analyses.segments.hour.forEach(function window(segment) {
859
+ segments.forEach(function window(segment) {
763
860
  if (segment.kind === "range") {
764
861
  pieces.push(rangeDigits(
765
862
  { hour: +segment.bounds[0], minute: 0 },
@@ -776,6 +873,17 @@ function hourWindowsFromTimes(ir, times, opts) {
776
873
  });
777
874
  return "klo " + joinList(pieces);
778
875
  }
876
+ function hourSegmentFires(segments) {
877
+ const hours = [];
878
+ segments.forEach(function each(segment) {
879
+ if (segment.kind === "step") {
880
+ hours.push(...segment.fires);
881
+ } else if (segment.kind === "single") {
882
+ hours.push(+segment.value);
883
+ }
884
+ });
885
+ return hours;
886
+ }
779
887
  function hourWindowDigits(hour, opts) {
780
888
  return rangeDigits({ hour, minute: 0 }, { hour, minute: 59 }, opts);
781
889
  }
@@ -877,7 +985,7 @@ function weekdayQualifier(ir) {
877
985
  if (quartz) {
878
986
  return quartz;
879
987
  }
880
- const segments = flattenSteps(ir.analyses.segments.weekday);
988
+ const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
881
989
  return joinList(segments.map(function piece(segment) {
882
990
  if (segment.kind === "range") {
883
991
  return weekdays[weekdayNumber(segment.bounds[0])].ela + " " + weekdays[weekdayNumber(segment.bounds[1])].ill;
package/dist/lang/fi.js CHANGED
@@ -46,6 +46,26 @@ function arithmeticStep(values) {
46
46
  }
47
47
  return { start: values[0], interval, last: values[values.length - 1] };
48
48
  }
49
+ function weekdayDisplayKey(value) {
50
+ return value === 0 ? 7 : value;
51
+ }
52
+ function orderWeekdaysForDisplay(segments) {
53
+ const flattened = segments.flatMap(function flat(segment) {
54
+ return segment.kind === "step" ? segment.fires.map(function single(value) {
55
+ return { kind: "single", value: "" + value };
56
+ }) : [segment];
57
+ });
58
+ function key(segment) {
59
+ return segment.kind === "range" ? weekdayDisplayKey(+segment.bounds[0]) : weekdayDisplayKey(+segment.value);
60
+ }
61
+ return flattened.map(function index(segment, position) {
62
+ return [segment, position];
63
+ }).sort(function byDisplayKey(a, b) {
64
+ return key(a[0]) - key(b[0]) || a[1] - b[1];
65
+ }).map(function unwrap(pair) {
66
+ return pair[0];
67
+ });
68
+ }
49
69
  function toFieldNumber(token, numberMap) {
50
70
  return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
51
71
  }
@@ -263,7 +283,11 @@ function composeSecondsOverMinuteStep(ir, freq, opts) {
263
283
  }
264
284
  function composeHourCadence(ir, plan, opts) {
265
285
  const clockRest = plan.rest.kind === "clockTimes" || plan.rest.kind === "compactClockTimes";
266
- return clockRest && ir.shapes.minute === "single" ? hourCadence(ir, +ir.pattern.minute, opts) : null;
286
+ if (!clockRest || ir.shapes.minute !== "single") {
287
+ return null;
288
+ }
289
+ const minute = +ir.pattern.minute;
290
+ return hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
267
291
  }
268
292
  function renderComposeSeconds(ir, plan, opts) {
269
293
  const cadence = composeHourCadence(ir, plan, opts);
@@ -279,7 +303,9 @@ function renderComposeSeconds(ir, plan, opts) {
279
303
  if (isEveryOtherMinuteSeconds(ir, plan)) {
280
304
  return secondsLeadClause(ir, opts) + " joka toisena minuuttina";
281
305
  }
282
- return secondsLeadClause(ir, opts) + ", " + render(ir, plan.rest, opts);
306
+ const restOwnsLead = plan.rest.kind === "compactClockTimes" && ir.analyses.clockSecond;
307
+ const lead = restOwnsLead ? "" : secondsLeadClause(ir, opts) + ", ";
308
+ return lead + render(ir, plan.rest, opts);
283
309
  }
284
310
  function isEveryOtherMinuteSeconds(ir, plan) {
285
311
  if (plan.rest.kind !== "minuteFrequency" || ir.pattern.second !== "*" || ir.shapes.hour !== "wildcard") {
@@ -398,6 +424,10 @@ function hourSegmentTimesWithSeka(ir, minute, second, opts) {
398
424
  function renderMinuteFrequency(ir, plan, opts) {
399
425
  const seg = stepSegment(ir.analyses.segments.minute);
400
426
  if (plan.hours.kind === "during") {
427
+ const cadence = unevenHourCadence(ir, opts);
428
+ if (cadence !== null) {
429
+ return stepCycle60(seg, units.minute, opts) + ", " + cadence + trailingQualifier(ir, opts);
430
+ }
401
431
  if (minuteStepIsAnchored(seg)) {
402
432
  const bareHours = kloFromTimes(ir, plan.hours.times, opts);
403
433
  return hoursFirstMinutes(bareHours, ir, opts) + trailingQualifier(ir, opts);
@@ -423,8 +453,12 @@ function renderMinuteSpanInHour(ir, plan, opts) {
423
453
  ) + trailingQualifier(ir, opts);
424
454
  }
425
455
  function renderMinutesAcrossHours(ir, plan, opts) {
456
+ const cadence = unevenHourCadence(ir, opts);
426
457
  if (plan.form === "wildcard") {
427
- return "joka minuutti " + hourWindowsFromTimes(ir, plan.times, opts) + trailingQualifier(ir, opts);
458
+ return cadence ? "joka minuutti, " + cadence + trailingQualifier(ir, opts) : "joka minuutti " + hourWindowsFromTimes(ir, plan.times, opts) + trailingQualifier(ir, opts);
459
+ }
460
+ if (cadence !== null) {
461
+ return bareMinutes(ir, opts) + ", " + cadence + trailingQualifier(ir, opts);
428
462
  }
429
463
  if (hoursAreRangeIsolated(ir.analyses.segments.hour)) {
430
464
  return bareMinutes(ir, opts) + " " + hourSegmentTimesWithSeka(ir, 0, null, opts) + trailingQualifier(ir, opts);
@@ -434,12 +468,12 @@ function renderMinutesAcrossHours(ir, plan, opts) {
434
468
  }
435
469
  function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
436
470
  const segment = stepSegment(ir.analyses.segments.hour);
471
+ const cadence = unevenHourCadence(ir, opts);
437
472
  if (plan.form === "wildcard") {
438
473
  return "joka minuutti " + everyNthHour(segment, opts) + trailingQualifier(ir, opts);
439
474
  }
440
- if (segment.startToken.indexOf("-") !== -1) {
441
- const hoursStr = kloList(segment.fires, opts);
442
- return hoursFirstMinutes(hoursStr, ir, opts) + trailingQualifier(ir, opts);
475
+ if (cadence !== null) {
476
+ return bareMinutes(ir, opts) + ", " + cadence + trailingQualifier(ir, opts);
443
477
  }
444
478
  return bareMinutes(ir, opts) + hourStepTail(segment, opts) + trailingQualifier(ir, opts);
445
479
  }
@@ -514,6 +548,10 @@ function renderHourRange(ir, plan, opts) {
514
548
  return hoursFirstMinutes(window, ir, opts) + trailingQualifier(ir, opts);
515
549
  }
516
550
  function renderHourStep(ir, plan, opts) {
551
+ const cadence = unevenHourCadence(ir, opts);
552
+ if (cadence !== null) {
553
+ return cadence + trailingQualifier(ir, opts);
554
+ }
517
555
  return stepHours(stepSegment(ir.analyses.segments.hour), opts) + trailingQualifier(ir, opts);
518
556
  }
519
557
  function boundedWindow(plan) {
@@ -528,7 +566,8 @@ function hourWindow(window, opts) {
528
566
  }
529
567
  function renderClockTimes(ir, plan, opts) {
530
568
  if (ir.shapes.minute === "single") {
531
- const cadence = hourCadence(ir, +ir.pattern.minute, opts);
569
+ const minute = +ir.pattern.minute;
570
+ const cadence = hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
532
571
  if (cadence !== null) {
533
572
  return cadence;
534
573
  }
@@ -544,9 +583,9 @@ function renderClockTimes(ir, plan, opts) {
544
583
  }
545
584
  function renderCompactClockTimes(ir, plan, opts) {
546
585
  if (plan.fold) {
547
- const cadence = hourCadence(ir, plan.minute, opts);
548
- if (cadence !== null) {
549
- return cadence;
586
+ const cadence2 = hourCadence(ir, plan.minute, opts) ?? hourRangeCadence(ir, plan.minute, opts);
587
+ if (cadence2 !== null) {
588
+ return cadence2;
550
589
  }
551
590
  }
552
591
  const hourSegs = ir.analyses.segments.hour;
@@ -565,8 +604,12 @@ function renderCompactClockTimes(ir, plan, opts) {
565
604
  if (plan.fold) {
566
605
  return leadingQualifier(ir, opts) + hourSegmentTimes(ir, plan.minute, ir.analyses.clockSecond, opts);
567
606
  }
568
- const hoursStr = hourSegmentTimes(ir, 0, null, opts);
569
- const phrase = hoursFirstMinutes(hoursStr, ir, opts) + trailingQualifier(ir, opts);
607
+ const cadence = unevenHourCadence(ir, opts);
608
+ const phrase = cadence ? bareMinutes(ir, opts) + ", " + cadence + trailingQualifier(ir, opts) : (
609
+ // A minute list over purely enumerated hours (step fires, all singles) —
610
+ // hours-first, drop "joka tunti".
611
+ hoursFirstMinutes(hourSegmentTimes(ir, 0, null, opts), ir, opts) + trailingQualifier(ir, opts)
612
+ );
570
613
  return ir.analyses.clockSecond ? secondsLeadClause(ir, opts) + ", " + phrase : phrase;
571
614
  }
572
615
  var renderers = {
@@ -659,6 +702,27 @@ function hourStrideCadence(stride, opts) {
659
702
  }
660
703
  return cadence + " " + kloRange({ hour: start, minute: 0 }, { hour: last, minute: 0 }, opts);
661
704
  }
705
+ function hourListStride(values) {
706
+ if (values.length < 2) {
707
+ return null;
708
+ }
709
+ const interval = values[1] - values[0];
710
+ if (interval < 2) {
711
+ return null;
712
+ }
713
+ for (let i = 2; i < values.length; i += 1) {
714
+ if (values[i] - values[i - 1] !== interval) {
715
+ return null;
716
+ }
717
+ }
718
+ if (values[0] !== 0 && values.length < 5) {
719
+ return null;
720
+ }
721
+ return { interval, last: values[values.length - 1], start: values[0] };
722
+ }
723
+ function offsetCleanStride(stride) {
724
+ return stride.start < stride.interval && 24 % stride.interval === 0;
725
+ }
662
726
  function hourStride(ir) {
663
727
  const segments = ir.analyses.segments.hour;
664
728
  if (!segments) {
@@ -666,12 +730,21 @@ function hourStride(ir) {
666
730
  }
667
731
  if (segments.length === 1 && segments[0].kind === "step") {
668
732
  const segment = segments[0];
733
+ if (segment.fires.length < 2) {
734
+ return null;
735
+ }
669
736
  const start = segment.startToken === "*" ? 0 : +segment.startToken.split("-")[0];
670
737
  return { interval: segment.interval, last: segment.fires[segment.fires.length - 1], start };
671
738
  }
672
739
  const values = singleValues(segments);
673
- const step = values && arithmeticStep(values);
674
- return step || null;
740
+ return values && hourListStride(values);
741
+ }
742
+ function unevenHourCadence(ir, opts) {
743
+ const stride = hourStride(ir);
744
+ if (!stride || offsetCleanStride(stride)) {
745
+ return null;
746
+ }
747
+ return hourStrideCadence(stride, opts);
675
748
  }
676
749
  function subMinuteSecond(ir) {
677
750
  return ir.pattern.second === "*" || ir.shapes.second === "step";
@@ -695,7 +768,7 @@ function hourCadence(ir, minute, opts) {
695
768
  return null;
696
769
  }
697
770
  const fires = (stride.last - stride.start) / stride.interval + 1;
698
- if (ir.pattern.second === "0" && fires <= maxClockTimes) {
771
+ if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
699
772
  return null;
700
773
  }
701
774
  const segment = ir.analyses.segments.hour[0];
@@ -703,6 +776,9 @@ function hourCadence(ir, minute, opts) {
703
776
  if (confined) {
704
777
  return secondsLeadClause(ir, opts) + " minuutin ajan " + everyNthHour(segment, opts) + trailingQualifier(ir, opts);
705
778
  }
779
+ if (minute === 0 && ir.pattern.second === "0") {
780
+ return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
781
+ }
706
782
  return hourCadenceLead(ir, minute, opts) + ", " + hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
707
783
  }
708
784
  function cleanHourStride(segment) {
@@ -712,6 +788,23 @@ function cleanHourStride(segment) {
712
788
  const start = segment.startToken === "*" ? 0 : +segment.startToken;
713
789
  return 24 % segment.interval === 0 && start < segment.interval;
714
790
  }
791
+ function hasHourWindow(ir) {
792
+ const segments = ir.analyses.segments.hour;
793
+ return !!segments && segments.some(function range(segment) {
794
+ return segment.kind === "range";
795
+ });
796
+ }
797
+ function hourRangeWindowTail(ir, opts) {
798
+ return ir.analyses.segments.hour.length === 1 ? hourSegmentTimes(ir, 0, null, opts) : hourSegmentTimesWithSeka(ir, 0, null, opts);
799
+ }
800
+ function hourRangeCadence(ir, minute, opts) {
801
+ if (minute !== 0 || !hasHourWindow(ir) || ir.pattern.second === "0") {
802
+ return null;
803
+ }
804
+ const tail = hourRangeWindowTail(ir, opts);
805
+ const joiner = subMinuteSecond(ir) ? " " : ", ";
806
+ return hourCadenceLead(ir, minute, opts) + joiner + tail + trailingQualifier(ir, opts);
807
+ }
715
808
  function kloList(hours, opts) {
716
809
  if (hours.length === 1) {
717
810
  return timeWord(hours[0], 0, null, opts);
@@ -728,12 +821,16 @@ function kloFromTimes(ir, times, opts) {
728
821
  }
729
822
  function hourWindowsFromTimes(ir, times, opts) {
730
823
  if (times.kind === "fires") {
731
- return "klo " + joinList(times.fires.map(function window(hour) {
732
- return hourWindowDigits(hour, opts);
733
- }));
824
+ return kloList(times.fires, opts);
825
+ }
826
+ const segments = ir.analyses.segments.hour;
827
+ if (!segments.some(function ranged(segment) {
828
+ return segment.kind === "range";
829
+ })) {
830
+ return kloList(hourSegmentFires(segments), opts);
734
831
  }
735
832
  const pieces = [];
736
- ir.analyses.segments.hour.forEach(function window(segment) {
833
+ segments.forEach(function window(segment) {
737
834
  if (segment.kind === "range") {
738
835
  pieces.push(rangeDigits(
739
836
  { hour: +segment.bounds[0], minute: 0 },
@@ -750,6 +847,17 @@ function hourWindowsFromTimes(ir, times, opts) {
750
847
  });
751
848
  return "klo " + joinList(pieces);
752
849
  }
850
+ function hourSegmentFires(segments) {
851
+ const hours = [];
852
+ segments.forEach(function each(segment) {
853
+ if (segment.kind === "step") {
854
+ hours.push(...segment.fires);
855
+ } else if (segment.kind === "single") {
856
+ hours.push(+segment.value);
857
+ }
858
+ });
859
+ return hours;
860
+ }
753
861
  function hourWindowDigits(hour, opts) {
754
862
  return rangeDigits({ hour, minute: 0 }, { hour, minute: 59 }, opts);
755
863
  }
@@ -851,7 +959,7 @@ function weekdayQualifier(ir) {
851
959
  if (quartz) {
852
960
  return quartz;
853
961
  }
854
- const segments = flattenSteps(ir.analyses.segments.weekday);
962
+ const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
855
963
  return joinList(segments.map(function piece(segment) {
856
964
  if (segment.kind === "range") {
857
965
  return weekdays[weekdayNumber(segment.bounds[0])].ela + " " + weekdays[weekdayNumber(segment.bounds[1])].ill;