cronli5 0.8.2 → 0.8.5

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/zh.js CHANGED
@@ -547,8 +547,8 @@ function secondClause(schedule) {
547
547
  return "\u6BCF\u79D2";
548
548
  }
549
549
  const first = segs[0];
550
- if (segs.length === 1 && first.kind === "step" && first.startToken === "*") {
551
- return cadence(first.interval, UNITS.second);
550
+ if (segs.length === 1 && first.kind === "step") {
551
+ return stepClause(first, "\u79D2", "\u79D2", "\u6BCF\u5206\u949F");
552
552
  }
553
553
  return strideFromSegments(segs, "\u79D2", "\u79D2", "\u6BCF\u5206\u949F") ?? "\u7B2C" + valueText(segs) + "\u79D2";
554
554
  }
@@ -587,7 +587,7 @@ function composeSecondsOnHour(schedule, plan, opts) {
587
587
  return "\u6BCF\u5929" + restText + secTail;
588
588
  }
589
589
  if (rest.kind === "singleMinute") {
590
- return restText + "\uFF0C" + sec;
590
+ return secondIsCadence(schedule) ? restText + confinedSecondTail(sec) : restText + "\uFF0C" + sec;
591
591
  }
592
592
  return restText + secTail;
593
593
  }
@@ -644,6 +644,9 @@ function composeSecondsCadence(schedule) {
644
644
  return "\u6BCF\u5076\u6570\u5206\u949F\u7684\u6BCF\u4E00\u79D2";
645
645
  }
646
646
  }
647
+ if (!secondIsCadence(schedule) && !secondIsStride(schedule)) {
648
+ return minuteClause(schedule) + "\u7684" + sec;
649
+ }
647
650
  return sec + "\uFF0C" + minuteClause(schedule);
648
651
  }
649
652
  return hourFrame(schedule) + tail;
@@ -657,7 +660,7 @@ function composeSecondsListed(schedule) {
657
660
  return hourWord(hourFires(schedule)[0]) + minuteCad + "\u7684\u6BCF\u4E00\u79D2";
658
661
  }
659
662
  if (schedule.shapes.hour === "wildcard") {
660
- return minutes + "\uFF0C" + sec;
663
+ return secondIsStride(schedule) ? minutes + "\uFF0C" + sec : minutes + confinedSecondTail(sec);
661
664
  }
662
665
  const hourCad = unevenHourCadence(schedule);
663
666
  if (hourCad !== null) {
@@ -665,9 +668,38 @@ function composeSecondsListed(schedule) {
665
668
  }
666
669
  return hourFrame(schedule) + minutes + "\uFF0C" + sec;
667
670
  }
671
+ function isMinuteStride(schedule) {
672
+ if (schedule.shapes.minute === "step") {
673
+ return true;
674
+ }
675
+ const values = singleValues(segmentsOf(schedule, "minute"));
676
+ return values !== null && arithmeticStep(values) !== null;
677
+ }
678
+ function secondIsCadence(schedule) {
679
+ return schedule.pattern.second === "*" || schedule.shapes.second === "step";
680
+ }
681
+ function secondIsStride(schedule) {
682
+ if (schedule.shapes.second !== "list") {
683
+ return false;
684
+ }
685
+ const values = singleValues(segmentsOf(schedule, "second"));
686
+ return values !== null && arithmeticStep(values) !== null;
687
+ }
688
+ function confinedSecondTail(sec) {
689
+ return sec === "\u6BCF\u79D2" ? "\u7684\u6BCF\u4E00\u79D2" : "\u7684" + sec;
690
+ }
691
+ function isSteppedMinuteSeconds(schedule, composedClock) {
692
+ return !composedClock && schedule.shapes.hour === "wildcard" && secondIsCadence(schedule) && schedule.pattern.minute !== "*/2" && isMinuteStride(schedule);
693
+ }
694
+ function minuteStrideConfinement(schedule) {
695
+ return minuteHourClause(schedule) + confinedSecondTail(secondClause(schedule));
696
+ }
668
697
  function renderComposeSeconds(schedule, plan, opts) {
669
698
  const { rest } = plan;
670
699
  const composedClock = rest.kind === "clockTimes" || rest.kind === "compactClockTimes";
700
+ if (isSteppedMinuteSeconds(schedule, composedClock)) {
701
+ return minuteStrideConfinement(schedule);
702
+ }
671
703
  if (schedule.pattern.minute === "0" || composedClock && schedule.shapes.minute === "single") {
672
704
  return composeSecondsOnHour(schedule, plan, opts);
673
705
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cronli5",
3
- "version": "0.8.2",
3
+ "version": "0.8.5",
4
4
  "description": "Cron Like I'm Five: A Cron to English Utility",
5
5
  "repository": {
6
6
  "type": "git",
@@ -223,6 +223,16 @@ const stepOrdinals: {[interval: number]: string} = {
223
223
  12: 'zwölften'
224
224
  };
225
225
 
226
+ // Dative ordinals for "in jeder N-ten Minute" — the step intervals a minute
227
+ // cadence can take. The interval-2 step keeps its own "jeder zweiten Minute"
228
+ // idiom and never reaches the confinement helper; a lookup miss falls back to
229
+ // the cardinal "alle N Minuten" form, which still confines.
230
+ const minuteStepOrdinals: {[interval: number]: string} = {
231
+ 3: 'dritten', 4: 'vierten', 5: 'fünften', 6: 'sechsten', 7: 'siebten',
232
+ 8: 'achten', 9: 'neunten', 10: 'zehnten', 12: 'zwölften',
233
+ 15: 'fünfzehnten', 20: 'zwanzigsten', 30: 'dreißigsten'
234
+ };
235
+
226
236
  // Confine a cadence to a clean hour stride: "in jeder zweiten Stunde", with
227
237
  // the start named when it is not midnight ("…ab 1 Uhr" for an odd stride).
228
238
  function everyNthHour(segment: StepSegment): string {
@@ -658,7 +668,17 @@ function renderSecondsWithinMinute(
658
668
  schedule.pattern.second + ' jeder Stunde';
659
669
  }
660
670
 
661
- return secondsLead(schedule) + ', in Minute ' + schedule.pattern.minute +
671
+ // A second LIST or RANGE under a single minute confines that minute in the
672
+ // genitive ("in den Sekunden 5 und 10 der Minute 30 jeder Stunde"), never the
673
+ // comma juxtaposition; a STEP second is a cadence and keeps its own lead.
674
+ if (secondsConfinesMinute(schedule)) {
675
+ return secondsLead(schedule) + ' ' + confinedMinutePhrase(schedule);
676
+ }
677
+
678
+ // A cadence/stepped second leads straight into the locative "in Minute …"
679
+ // with NO comma ("alle 15 Sekunden in Minute 30 jeder Stunde"); the locative
680
+ // binds the two specs, matching the no-comma list/single confinement.
681
+ return secondsLead(schedule) + ' in Minute ' + schedule.pattern.minute +
662
682
  ' jeder Stunde';
663
683
  }
664
684
 
@@ -717,6 +737,155 @@ function isEveryOtherMinuteSeconds(
717
737
  return minuteStep.startToken === '*' && minuteStep.interval === 2;
718
738
  }
719
739
 
740
+ // The minute field's step stride for the confinement frame, or null when the
741
+ // minute is not a stepped cadence. A `step`-shaped field reads its segment; a
742
+ // `list`-shaped field the core enumerated from a uneven step (`2/7` → 2,9,…,58)
743
+ // recovers the progression from its values.
744
+ function minuteStride(
745
+ schedule: Schedule
746
+ ): {start: number; interval: number; last: number} | null {
747
+ if (schedule.shapes.minute === 'step') {
748
+ const segment = stepSegment(schedule, 'minute');
749
+ const start = segment.startToken === '*' ? 0 : +segment.startToken;
750
+
751
+ return {interval: segment.interval, last:
752
+ segment.fires[segment.fires.length - 1], start};
753
+ }
754
+
755
+ const values = singleValues(segmentsOf(schedule, 'minute'));
756
+
757
+ return values && arithmeticStep(values);
758
+ }
759
+
760
+ // A stepped minute under a wildcard/stepped second and wildcard hour: bind the
761
+ // second cadence to the minute cadence as a CONFINEMENT ("jede Sekunde in jeder
762
+ // sechsten Minute ab Minute 4 jeder Stunde"), never the comma juxtaposition
763
+ // that reads as two independent cadences. The cadence is ORDINAL ("in jeder
764
+ // sechsten Minute") — the cardinal "alle 6 Minuten" is what fuels the misread —
765
+ // and the start/bound mirror the standalone minute cadence.
766
+ function minuteStepConfinement(
767
+ schedule: Schedule,
768
+ stride: {start: number; interval: number; last: number}
769
+ ): string {
770
+ const ordinal = minuteStepOrdinals[stride.interval];
771
+ const head = ordinal ?
772
+ 'in jeder ' + ordinal + ' Minute' :
773
+ 'alle ' + stride.interval + ' Minuten';
774
+
775
+ const tail = chooseStride({...stride, cycle: 60}, {
776
+ bare: () => '',
777
+ offset: () => ' ab Minute ' + stride.start,
778
+ bounded: () => ' von Minute ' + stride.start + ' bis ' + stride.last
779
+ });
780
+
781
+ return secondsLead(schedule) + ' ' + head + tail + ' jeder Stunde';
782
+ }
783
+
784
+ // Whether a stepped minute fills a wildcard hour under a wildcard/stepped
785
+ // second — the shape the confinement frame above handles.
786
+ function isSteppedMinuteSeconds(
787
+ schedule: Schedule,
788
+ plan: Extract<PlanNode, {kind: 'composeSeconds'}>
789
+ ): boolean {
790
+ return (plan.rest.kind === 'minuteFrequency' ||
791
+ plan.rest.kind === 'multipleMinutes') &&
792
+ (schedule.shapes.second === 'wildcard' ||
793
+ schedule.shapes.second === 'step') &&
794
+ schedule.shapes.hour === 'wildcard' &&
795
+ schedule.pattern.minute !== '*/2' &&
796
+ minuteStride(schedule) !== null;
797
+ }
798
+
799
+ // The CONFINED-minute phrase in the genitive that a clock-point second attaches
800
+ // to ("jeder sechsten Minute ab Minute 4 jeder Stunde", "der Minuten 0, 15 und
801
+ // 30 jeder Stunde", "der Minute 30 jeder Stunde"). A stepped minute reuses the
802
+ // ordinal cadence; a list, range, or single names the minute(s) in the genitive
803
+ // — so the seconds clause's bare lead never stacks a redundant "jeder Minute".
804
+ function confinedMinutePhrase(schedule: Schedule): string {
805
+ const stride = minuteStride(schedule);
806
+
807
+ if (stride && schedule.pattern.minute !== '*/2') {
808
+ const ordinal = minuteStepOrdinals[stride.interval];
809
+ const head = ordinal ?
810
+ 'jeder ' + ordinal + ' Minute' :
811
+ 'alle ' + stride.interval + ' Minuten';
812
+ const tail = chooseStride({...stride, cycle: 60}, {
813
+ bare: () => '',
814
+ offset: () => ' ab Minute ' + stride.start,
815
+ bounded: () => ' von Minute ' + stride.start + ' bis ' + stride.last
816
+ });
817
+
818
+ return head + tail + ' jeder Stunde';
819
+ }
820
+
821
+ const genitive = schedule.shapes.minute === 'single' ?
822
+ 'der Minute ' + schedule.pattern.minute :
823
+ 'der Minuten ' + joinList(fieldValues(schedule, 'minute'));
824
+
825
+ return genitive + ' jeder Stunde';
826
+ }
827
+
828
+ // The minute-confinement rendering for a compose-seconds plan, or null when the
829
+ // plan is not one. A CADENCE second over a stepped minute uses the ordinal
830
+ // cadence form; a CLOCK-POINT second (list/range/single) over any restricted
831
+ // minute uses the genitive form. Both bind the second beneath the minute
832
+ // instead of juxtaposing the two behind a comma. Folded into one helper so
833
+ // `renderComposeSeconds` carries a single branch.
834
+ function minuteConfinementRender(
835
+ plan: Extract<PlanNode, {kind: 'composeSeconds'}>, schedule: Schedule
836
+ ): string | null {
837
+ if (isSteppedMinuteSeconds(schedule, plan)) {
838
+ return minuteStepConfinement(schedule, minuteStride(schedule)!);
839
+ }
840
+
841
+ const minuteRest = plan.rest.kind === 'minuteFrequency' ||
842
+ plan.rest.kind === 'multipleMinutes' ||
843
+ plan.rest.kind === 'rangeOfMinutes';
844
+
845
+ if (minuteRest && secondsConfinesMinute(schedule)) {
846
+ return secondsLead(schedule) + ' ' + confinedMinutePhrase(schedule);
847
+ }
848
+
849
+ return null;
850
+ }
851
+
852
+ // Whether a clock-point second (list, range, or single) sits under a restricted
853
+ // minute and a wildcard hour — the shape that must CONFINE the minute in the
854
+ // genitive rather than juxtapose it behind a comma (two independent schedules).
855
+ // A second LIST the core enumerated from a step (`3/2`) is really a stride
856
+ // cadence and stays out. The single-second + single-minute pair folds into one
857
+ // coherent clock point ("in Minute 5 und Sekunde 30 jeder Stunde") and is
858
+ // excluded.
859
+ function secondsConfinesMinute(schedule: Schedule): boolean {
860
+ const {second, minute, hour} = schedule.shapes;
861
+
862
+ if (second === 'list') {
863
+ const values = singleValues(segmentsOf(schedule, 'second'));
864
+
865
+ if (values && arithmeticStep(values)) {
866
+ return false;
867
+ }
868
+ }
869
+
870
+ const clockPoint = second === 'single' || second === 'range' ||
871
+ second === 'list';
872
+
873
+ return clockPoint && minute !== 'wildcard' && hour === 'wildcard' &&
874
+ !(second === 'single' && minute === 'single');
875
+ }
876
+
877
+ // Whether a compose-seconds plan is a cadence/stepped second under a minute
878
+ // LIST or SINGLE and a wildcard hour — the shape that leads into the locative
879
+ // "in …" minute phrase with no comma. A restricted/cadence hour keeps the
880
+ // comma, so it does not qualify.
881
+ function isLocativeMinuteConfinement(
882
+ schedule: Schedule,
883
+ plan: Extract<PlanNode, {kind: 'composeSeconds'}>
884
+ ): boolean {
885
+ return (plan.rest.kind === 'multipleMinutes' ||
886
+ plan.rest.kind === 'singleMinute') && schedule.shapes.hour === 'wildcard';
887
+ }
888
+
720
889
  function renderComposeSeconds(
721
890
  schedule: Schedule,
722
891
  plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
@@ -749,12 +918,32 @@ function renderComposeSeconds(
749
918
  clockMinuteGenitive(plan.rest.times, opts.style.sep);
750
919
  }
751
920
 
921
+ // A second confines the minute restriction (open hour), never the comma
922
+ // juxtaposition that reads as two independent cadences: a CADENCE second over
923
+ // a stepped minute uses the ordinal-cadence form ("jede Sekunde in jeder
924
+ // sechsten Minute …"); a CLOCK-POINT second uses the genitive form ("in den
925
+ // Sekunden 5, 10 und 15 jeder sechsten Minute …").
926
+ const confined = minuteConfinementRender(plan, schedule);
927
+
928
+ if (confined !== null) {
929
+ return confined;
930
+ }
931
+
752
932
  // A wildcard second under a minute */2 with a wildcard hour binds in the
753
933
  // genitive ("jede Sekunde jeder zweiten Minute").
754
934
  if (isEveryOtherMinuteSeconds(schedule, plan)) {
755
935
  return secondsLead(schedule) + ' jeder zweiten Minute';
756
936
  }
757
937
 
938
+ // A cadence/stepped second under a minute LIST or SINGLE and a wildcard hour
939
+ // leads straight into the locative minute phrase with NO comma ("jede Sekunde
940
+ // in den Minuten 0, 15 und 30 jeder Stunde"). The locative "in" already binds
941
+ // the two specs; the comma read as two independent specifications and is
942
+ // inconsistent with the no-comma stepped-minute and list-tier confinements.
943
+ if (isLocativeMinuteConfinement(schedule, plan)) {
944
+ return secondsLead(schedule) + ' ' + render(schedule, plan.rest, opts);
945
+ }
946
+
758
947
  // A compact clock-time rest folds a meaningful SINGLE second into its own
759
948
  // leading clause, so the composer must not prepend a second lead that would
760
949
  // double it. A wildcard or stepped second is not folded there (no
@@ -677,6 +677,67 @@ const stepOrdinals: Record<number, string> = {
677
677
  2: 'other', 3: 'third', 4: 'fourth', 6: 'sixth', 8: 'eighth', 12: 'twelfth'
678
678
  };
679
679
 
680
+ // Spelled ordinals for "every Nth minute" — the step intervals a minute
681
+ // cadence can take (2 reads idiomatically as "other"). A lookup miss falls back
682
+ // to the suffixed numeric ordinal, so an unusually large interval still reads.
683
+ const spelledOrdinals: Record<number, string> = {
684
+ 2: 'other', 3: 'third', 4: 'fourth', 5: 'fifth', 6: 'sixth', 7: 'seventh',
685
+ 8: 'eighth', 9: 'ninth', 10: 'tenth', 11: 'eleventh', 12: 'twelfth',
686
+ 15: 'fifteenth', 20: 'twentieth', 30: 'thirtieth'
687
+ };
688
+
689
+ // The ordinal word for a cadence interval ("sixth", "seventh"), spelled where
690
+ // known and suffixed-numeric ("13th") otherwise.
691
+ function ordinalWord(interval: number): string {
692
+ return spelledOrdinals[interval] ?? getOrdinal(interval);
693
+ }
694
+
695
+ // A stepped minute under a seconds lead reads as a CONFINEMENT of that cadence,
696
+ // not a juxtaposed clause (a comma there reads as two independent cadences) nor
697
+ // a wall of enumerated minutes: "during every Nth minute" plus the step's
698
+ // offset/bound. The cadence is ORDINAL ("every sixth minute"); the cardinal
699
+ // ("every six minutes") is the form that reads as a separate cadence. The
700
+ // offset/bound mirrors the standalone minute cadence: a clean stride from the
701
+ // top names no offset, an offset-clean stride names only its start ("from four
702
+ // minutes past the hour"), and an uneven one pins both endpoints ("from 2
703
+ // through 58 minutes past the hour").
704
+ function minuteStrideConfinement(stride: {start: number; interval: number;
705
+ last: number}, opts: NormalizedOptions): string {
706
+ const base = ' during every ' + ordinalWord(stride.interval) + ' minute';
707
+
708
+ return chooseStride({...stride, cycle: 60}, {
709
+ bare: () => base,
710
+ offset: () => base + ' from ' + getNumber(stride.start, opts) + ' ' +
711
+ pluralize(stride.start, 'minute') + ' past the hour',
712
+ bounded: () => {
713
+ const num = seriesNumber();
714
+
715
+ return base + ' from ' + num(stride.start) + through(opts) +
716
+ num(stride.last) + ' ' + pluralize(stride.last, 'minute') +
717
+ ' past the hour';
718
+ }
719
+ });
720
+ }
721
+
722
+ // The minute field's step stride for the confinement frame, or null when the
723
+ // minute is not a stepped cadence. A `step`-shaped field (`*/6`) reads its
724
+ // segment directly; a `list`-shaped field the core enumerated from an uneven
725
+ // step (`2/7` → 2,9,…,58) recovers the progression from its values.
726
+ function minuteStride(schedule: Schedule):
727
+ {start: number; interval: number; last: number} | null {
728
+ if (schedule.shapes.minute === 'step') {
729
+ const segment = stepSegment(schedule, 'minute');
730
+ const start = segment.startToken === '*' ? 0 : +segment.startToken;
731
+
732
+ return {interval: segment.interval, last:
733
+ segment.fires[segment.fires.length - 1], start};
734
+ }
735
+
736
+ const values = singleValues(segmentsOf(schedule, 'minute'));
737
+
738
+ return values && arithmeticStep(values);
739
+ }
740
+
680
741
  // Confine a cadence to a clean hour stride: "during every other hour", with
681
742
  // the start named when it is not midnight ("…from 1 a.m." for an odd stride).
682
743
  function everyNthHour(segment: StepSegment, opts: NormalizedOptions): string {
@@ -1041,15 +1102,69 @@ function isCadenceField(token: string): boolean {
1041
1102
  token.startsWith('*/') && token.indexOf('-') === -1;
1042
1103
  }
1043
1104
 
1105
+ // Whether the second field leads the confinement frame as a clean cadence. A
1106
+ // wildcard ("every second") and a clean `*/n` step both lead via
1107
+ // `isCadenceField`; an OPEN OFFSET step (`m/n`) is the SAME cadence, only named
1108
+ // from its offset ("every six seconds from five seconds past the minute"), so
1109
+ // it leads the SAME confinement rather than juxtaposing the minute restriction
1110
+ // behind a comma — whether the offset is clean from the top (`0/n`) or not
1111
+ // (`5/n`). A bounded step (`a-b/n`, a windowed set) is not an open cadence and
1112
+ // keeps its existing form.
1113
+ function secondLeadsCadence(schedule: Schedule): boolean {
1114
+ if (isCadenceField(schedule.pattern.second)) {
1115
+ return true;
1116
+ }
1117
+
1118
+ if (schedule.shapes.second !== 'step') {
1119
+ return false;
1120
+ }
1121
+
1122
+ // Reached only under a stepped second the `isCadenceField` guard did not
1123
+ // already admit, so its `*/n` clean-cadence forms are gone and the remaining
1124
+ // open form is the offset step `m/n` (`0/n` or non-zero). A bounded step
1125
+ // `a-b/n` is a windowed set, not a cadence, and stays out.
1126
+ return isOpenStep(schedule.pattern.second);
1127
+ }
1128
+
1129
+ // Whether the second leads the confinement frame as a CLOCK-POINT clause (a
1130
+ // list, range, or single second), as opposed to a cadence. A clock-point second
1131
+ // under a minute restriction confines that restriction exactly as the cadence
1132
+ // does ("at 5, 10, and 15 seconds past the minute during every sixth minute …")
1133
+ // rather than juxtaposing it behind a comma, which reads as two independent
1134
+ // schedules. The single-second + single-minute pair is excluded: it folds into
1135
+ // one coherent clock point ("30 minutes and 15 seconds past the hour"), not a
1136
+ // juxtaposition, so it keeps that fold. The confinement only applies where the
1137
+ // minute is the restriction and the hour is open; `confinementEligible` gates
1138
+ // the rest (a restricted hour folds into a clock time, left to that renderer).
1139
+ function secondLeadsClockPoint(schedule: Schedule): boolean {
1140
+ // Only a MEANINGFUL second leads a clause: the two seconds-bearing plans the
1141
+ // core chooses for a real second. A 5-field pattern (or an explicit `0`
1142
+ // second) carries no seconds clause — its plan is the minute's own — so it is
1143
+ // not confined here, which would otherwise prepend "at zero seconds …".
1144
+ if (schedule.plan.kind !== 'composeSeconds' &&
1145
+ schedule.plan.kind !== 'secondsWithinMinute') {
1146
+ return false;
1147
+ }
1148
+
1149
+ const {second, minute, hour} = schedule.shapes;
1150
+ const clockPoint = second === 'single' || second === 'range' ||
1151
+ second === 'list';
1152
+ const minuteRestricted = minute !== 'wildcard';
1153
+
1154
+ return clockPoint && minuteRestricted && hour === 'wildcard' &&
1155
+ !(second === 'single' && minute === 'single');
1156
+ }
1157
+
1044
1158
  // The leading cadence and whether the second is the leading field, or null when
1045
1159
  // the pattern has no cadence lead (the finest restricted field is a clock-point
1046
- // single/range/list). The seconds lead when restricted as a cadence; otherwise
1047
- // the minute leads when the second is a plain :00 and the minute is a cadence.
1160
+ // single/range/list). The seconds lead when restricted as a cadence, or as a
1161
+ // clock-point clause that confines a minute restriction; otherwise the minute
1162
+ // leads when the second is a plain :00 and the minute is a cadence.
1048
1163
  function leadingCadence(schedule: Schedule, opts: NormalizedOptions):
1049
1164
  {text: string; secondLead: boolean} | null {
1050
1165
  const {second, minute} = schedule.pattern;
1051
1166
 
1052
- if (isCadenceField(second)) {
1167
+ if (secondLeadsCadence(schedule) || secondLeadsClockPoint(schedule)) {
1053
1168
  return {secondLead: true, text: secondsClause(schedule, 'minute', opts)};
1054
1169
  }
1055
1170
 
@@ -1078,12 +1193,22 @@ function minuteConfinement(schedule: Schedule,
1078
1193
  return '';
1079
1194
  }
1080
1195
 
1081
- if (isCadenceField(minute)) {
1082
- // The gate admits only the `*/2` "every other minute" step here; other
1083
- // minute steps defer to the existing renderer.
1196
+ if (minute === '*/2') {
1197
+ // The `*/2` clean step reads idiomatically as "every other minute" with no
1198
+ // offset; other minute steps take the ordinal stride-cadence below.
1084
1199
  return ' of every other minute';
1085
1200
  }
1086
1201
 
1202
+ // A stepped minute (a clean `*/n`, an offset `m/n`, or a uneven step the core
1203
+ // enumerated to an arithmetic list) confines as "during every Nth minute"
1204
+ // plus the step's offset/bound — the ordinal cadence, not the cardinal that
1205
+ // reads as a separate cadence, nor a wall of enumerated ":NN" minutes.
1206
+ const stride = minuteStride(schedule);
1207
+
1208
+ if (stride) {
1209
+ return minuteStrideConfinement(stride, opts);
1210
+ }
1211
+
1087
1212
  // A minute single/range/list under the seconds lead. The minute reads as a
1088
1213
  // ":NN" clock-minute confinement, never "N minutes past the hour" (that is
1089
1214
  // the minute-lead clock-point form).
@@ -1119,10 +1244,11 @@ function hourConfinement(schedule: Schedule, opts: NormalizedOptions): string {
1119
1244
 
1120
1245
  if (hour === '*') {
1121
1246
  // A pinned minute confinement ("during minute :00") repeats across every
1122
- // hour, so the hour is named as the unit of recurrence; a stepped minute
1123
- // ("of every other minute") or absent minute already implies all hours.
1247
+ // hour, so the hour is named as the unit of recurrence; a minute cadence
1248
+ // ("of every other minute", "during every sixth minute …") or an absent
1249
+ // minute already implies all hours, so the hour is not restated.
1124
1250
  const minutePinned = schedule.pattern.minute !== '*' &&
1125
- !isCadenceField(schedule.pattern.minute);
1251
+ !isCadenceField(schedule.pattern.minute) && !minuteStride(schedule);
1126
1252
 
1127
1253
  return minutePinned ? ' of every hour' : '';
1128
1254
  }
@@ -1216,24 +1342,28 @@ function confinementEligible(schedule: Schedule,
1216
1342
  }
1217
1343
 
1218
1344
  if (lead.secondLead) {
1219
- // A minute STEP is supported only as the `*/2` "every other minute" idiom,
1220
- // and only where it fills the coarser field: a contiguous hour range or a
1221
- // single hour both close on the minute's real last fire, which the
1222
- // windowing renderer already speaks. The `*/2` step fills both, so it keeps
1223
- // the "of every other minute" confinement; other steps defer entirely. A
1224
- // contiguous hour range (`hour === 'range'`) is left to that windowing
1225
- // renderer rather than this confinement frame, which closes on the top of
1226
- // the next hour.
1345
+ // A minute STEP confines as an ordinal cadence ("during every sixth minute
1346
+ // from four minutes past the hour"), but only where it fills the coarser
1347
+ // field: under a WILDCARD hour the step repeats every hour, so the cadence
1348
+ // is the whole confinement. A single hour or a contiguous range closes on
1349
+ // the minute's real last fire, which the windowing renderer already speaks,
1350
+ // so those defer. The `*/2` step keeps its "of every other minute" idiom.
1227
1351
  if (minuteStep) {
1228
- return minute === '*/2' && schedule.shapes.hour !== 'range';
1352
+ return minute === '*/2' ?
1353
+ schedule.shapes.hour !== 'range' :
1354
+ schedule.pattern.hour === '*';
1355
+ }
1356
+
1357
+ // A minute list that is really an arithmetic stride confines as that same
1358
+ // ordinal cadence when it fills a wildcard hour; under a restricted hour it
1359
+ // keeps its existing cadence form. A short explicit minute list crossed
1360
+ // with a discrete hour LIST is a wall of distinct clock times ("9:00 a.m.,
1361
+ // 9:25 a.m., …"), not a single minute confinement, so it stays enumerated.
1362
+ if (isMinuteStride(schedule)) {
1363
+ return schedule.pattern.hour === '*';
1229
1364
  }
1230
1365
 
1231
- // A minute list that is really a stride keeps its cadence form; a short
1232
- // explicit minute list crossed with a discrete hour LIST is a wall of
1233
- // distinct clock times ("9:00 a.m., 9:25 a.m., …"), not a single minute
1234
- // confinement. Both stay with the enumerating renderer.
1235
- if (isMinuteStride(schedule) ||
1236
- schedule.shapes.minute === 'list' && schedule.shapes.hour === 'list') {
1366
+ if (schedule.shapes.minute === 'list' && schedule.shapes.hour === 'list') {
1237
1367
  return false;
1238
1368
  }
1239
1369