cronli5 0.1.5 → 0.1.6

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.
@@ -197,14 +197,41 @@ function composeHourCadence(ir: IR, plan: PlanOf<'composeSeconds'>,
197
197
  const clockRest = plan.rest.kind === 'clockTimes' ||
198
198
  plan.rest.kind === 'compactClockTimes';
199
199
 
200
- return clockRest && ir.shapes.minute === 'single' ?
201
- hourCadence(ir, +ir.pattern.minute, opts) :
202
- null;
200
+ if (!clockRest || ir.shapes.minute !== 'single') {
201
+ return null;
202
+ }
203
+
204
+ const minute = +ir.pattern.minute;
205
+
206
+ return hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
203
207
  }
204
208
 
205
209
  // A meaningful second under minute/hour shapes the earlier strategies
206
210
  // deferred on: the second leads with its own clause and the rest of the
207
211
  // pattern follows.
212
+ // A wildcard or stepped second under a fixed minute across one or more specific
213
+ // hours. The clock-time rest collapses the pinned minute into the hour, and on
214
+ // the clock a pinned minute-0 reads as the whole hour ("9 a.m." spoken ==
215
+ // "9:00 a.m."), losing the one-minute confinement.
216
+ //
217
+ // A SINGLE minute-0 is the one-minute window at the top of each named hour: a
218
+ // duration frame ("for one minute at 9 a.m.") states the confinement outright,
219
+ // with the hour as its word so it cannot be heard as the hour itself. A minute
220
+ // LIST whose first value is 0 (e.g. */25 → :00, :25, :50) is a wall of distinct
221
+ // clock times, not one confinement, so it names each minute via the compact
222
+ // form, never collapsing to the bare hour (which once repeated it, "9 a.m.,
223
+ // 9 a.m."). A non-zero pinned minute is an unambiguous clock time the compact
224
+ // "of 9:05 a.m." form reads as the minute, never the hour.
225
+ function clockTimesConfinement(ir: IR, rest: PlanOf<'clockTimes'>,
226
+ opts: NormalizedOptions): string {
227
+ if (+rest.times[0].minute === 0 && ir.shapes.minute === 'single') {
228
+ return secondsLeadClause(ir, opts) + ' for one minute at ' +
229
+ durationHours(ir, rest, opts);
230
+ }
231
+
232
+ return secondsLeadClause(ir, opts) + ' of ' + clockTimesOf(ir, rest, opts);
233
+ }
234
+
208
235
  function renderComposeSeconds(ir: IR, plan: PlanOf<'composeSeconds'>,
209
236
  opts: NormalizedOptions): string {
210
237
  // An hour step (or arithmetic-progression hour list) under a single pinned
@@ -217,28 +244,11 @@ function renderComposeSeconds(ir: IR, plan: PlanOf<'composeSeconds'>,
217
244
  return cadence;
218
245
  }
219
246
 
220
- // A wildcard or stepped second under a minute pinned to a single value
221
- // across one or more specific hours. The clock-time rest collapses the
222
- // pinned minute into the hour, and on the clock a pinned minute-0 reads as
223
- // the whole hour ("9 a.m." spoken == "9:00 a.m."), losing the one-minute
224
- // confinement. (A second list/range/single leads with a "past the minute"
225
- // clause that an "of"/duration frame cannot follow, so it stays generic.)
247
+ // A wildcard or stepped second under a fixed minute across one or more
248
+ // specific hours confines the seconds to the clock time(s).
226
249
  if (plan.rest.kind === 'clockTimes' &&
227
250
  (ir.shapes.second === 'wildcard' || ir.shapes.second === 'step')) {
228
- const minute = plan.rest.times[0].minute;
229
-
230
- // Minute 0 is the one-minute window at the top of each named hour: a
231
- // duration frame ("for one minute at 9 a.m.") states the confinement
232
- // outright, with the hour as its word so it cannot be heard as the hour
233
- // itself. A non-zero pinned minute is an unambiguous clock time, so the
234
- // compact "of 9:05 a.m." form reads it as the minute, never the hour.
235
- if (+minute === 0) {
236
- return secondsLeadClause(ir, opts) + ' for one minute at ' +
237
- durationHours(ir, plan.rest, opts);
238
- }
239
-
240
- return secondsLeadClause(ir, opts) + ' of ' +
241
- clockTimesOf(ir, plan.rest, opts);
251
+ return clockTimesConfinement(ir, plan.rest, opts);
242
252
  }
243
253
 
244
254
  // A wildcard second under a */2 minute step with a wildcard hour binds
@@ -254,7 +264,15 @@ function renderComposeSeconds(ir: IR, plan: PlanOf<'composeSeconds'>,
254
264
  trailingQualifier(ir, opts);
255
265
  }
256
266
 
257
- return secondsLeadClause(ir, opts) + ', ' + render(ir, plan.rest, opts);
267
+ // A compact clock-time rest folds a meaningful SINGLE second into its own
268
+ // leading clause, so the composer must not prepend a second lead that would
269
+ // double it. A wildcard or stepped second is not folded there (no
270
+ // clockSecond), so it still leads its own clause here.
271
+ const restOwnsLead = plan.rest.kind === 'compactClockTimes' &&
272
+ ir.analyses.clockSecond;
273
+ const lead = restOwnsLead ? '' : secondsLeadClause(ir, opts) + ', ';
274
+
275
+ return lead + render(ir, plan.rest, opts);
258
276
  }
259
277
 
260
278
  // The bare-hour words for a minute-0 duration confinement, joined and followed
@@ -381,9 +399,15 @@ function renderMinuteFrequency(ir: IR, plan: PlanOf<'minuteFrequency'>,
381
399
  'minute', 'hour', opts);
382
400
 
383
401
  if (plan.hours.kind === 'during') {
384
- // An hour list confines the cadence to each listed hour's window.
385
- phrase += ' during the ' +
386
- hourTimesFromPlan(ir, plan.hours.times, false, opts) + ' hours';
402
+ // A uneven hour stride confines the minute cadence to its own bounded hour
403
+ // cadence ("every 15 minutes, every five hours from midnight through 8
404
+ // p.m."); an irregular hour list still names each hour's window.
405
+ const cadence = unevenHourCadence(ir, opts);
406
+
407
+ phrase += cadence ?
408
+ ', ' + cadence :
409
+ ' during the ' +
410
+ hourTimesFromPlan(ir, plan.hours.times, false, opts) + ' hours';
387
411
  }
388
412
  else if (plan.hours.kind === 'window') {
389
413
  phrase += ' ' + hourWindow(plan.hours, opts);
@@ -422,13 +446,20 @@ function renderMinuteSpanInHour(ir: IR, plan: PlanOf<'minuteSpanInHour'>,
422
446
  // during each hour.
423
447
  function renderMinutesAcrossHours(ir: IR, plan: PlanOf<'minutesAcrossHours'>,
424
448
  opts: NormalizedOptions): string {
449
+ // A uneven hour stride reads as a cadence, not a wall of hour columns: the
450
+ // minute lead, then "every N hours from X through Y".
451
+ const cadence = unevenHourCadence(ir, opts);
452
+
425
453
  if (plan.form === 'wildcard') {
454
+ if (cadence !== null) {
455
+ return 'every minute, ' + cadence + trailingQualifier(ir, opts);
456
+ }
457
+
426
458
  return 'every minute during the ' +
427
459
  hourTimesFromPlan(ir, plan.times, false, opts) + ' hours' +
428
460
  trailingQualifier(ir, opts);
429
461
  }
430
462
 
431
- const times = hourTimesFromPlan(ir, plan.times, true, opts);
432
463
  const lead = plan.form === 'range' ?
433
464
  minuteRangeLead(ir.pattern.minute, opts) :
434
465
  // The 'list' form is a minute list, which has segments; an offset/uneven
@@ -437,6 +468,12 @@ function renderMinutesAcrossHours(ir: IR, plan: PlanOf<'minutesAcrossHours'>,
437
468
  listPastThe(segmentWords(ir.analyses.segments.minute!, opts),
438
469
  'minute', 'hour', opts);
439
470
 
471
+ if (cadence !== null) {
472
+ return lead + ', ' + cadence + trailingQualifier(ir, opts);
473
+ }
474
+
475
+ const times = hourTimesFromPlan(ir, plan.times, true, opts);
476
+
440
477
  return lead + ', at ' + times + trailingQualifier(ir, opts);
441
478
  }
442
479
 
@@ -466,6 +503,9 @@ function renderMinuteSpanAcrossHourStep(ir: IR,
466
503
  // segment is a step segment.
467
504
  const segment = ir.analyses.segments.hour![0] as StepSegment;
468
505
 
506
+ // A wildcard minute over a stepped hour is reached only for a clean stride
507
+ // (a bounded or uneven step routes through minutesAcrossHours instead), so it
508
+ // confines to every Nth hour without a bounded-cadence case here.
469
509
  if (plan.form === 'wildcard') {
470
510
  return 'every minute ' + everyNthHour(segment, opts) +
471
511
  trailingQualifier(ir, opts);
@@ -478,8 +518,13 @@ function renderMinuteSpanAcrossHourStep(ir: IR,
478
518
  listPastThe(segmentWords(ir.analyses.segments.minute!, opts),
479
519
  'minute', 'hour', opts) :
480
520
  minuteRangeLead(ir.pattern.minute, opts);
521
+ // A bounded or uneven hour step reads as its endpoint-pinning cadence after
522
+ // the minute lead, not a wall of clock-time columns; an offset-clean step
523
+ // keeps its existing per-step phrasing.
524
+ const cadence = unevenHourCadence(ir, opts);
481
525
 
482
- return lead + ', ' + stepHours(segment, opts) + trailingQualifier(ir, opts);
526
+ return lead + ', ' +
527
+ (cadence ?? stepHours(segment, opts)) + trailingQualifier(ir, opts);
483
528
  }
484
529
 
485
530
  // Lead phrase for a plain minute range: "every minute from <a> through <b>
@@ -536,6 +581,15 @@ function rangeMinuteLead(ir: IR, opts: NormalizedOptions): string {
536
581
 
537
582
  function renderHourStep(ir: IR, plan: PlanOf<'hourStep'>,
538
583
  opts: NormalizedOptions): string {
584
+ // A bounded or uneven hour step reads as its endpoint-pinning cadence ("every
585
+ // two hours from 9 a.m. through 5 p.m."), the same form the compound paths
586
+ // speak; an offset-clean step keeps its bare or "from M" cadence.
587
+ const cadence = unevenHourCadence(ir, opts);
588
+
589
+ if (cadence !== null) {
590
+ return cadence + trailingQualifier(ir, opts);
591
+ }
592
+
539
593
  // An hour-step plan is selected only for a stepped hour field, whose
540
594
  // first segment is a step segment.
541
595
  return stepHours(ir.analyses.segments.hour![0] as StepSegment, opts) +
@@ -563,10 +617,13 @@ function hourWindow(window: {from: number; to: number; last: number},
563
617
  // a day-level qualifier, e.g. "every day at 9 a.m. and 9:30 a.m.".
564
618
  function renderClockTimes(ir: IR, plan: PlanOf<'clockTimes'>,
565
619
  opts: NormalizedOptions): string {
566
- // An hour step (or arithmetic-progression hour list) under a single pinned
567
- // minute reads as a cadence rather than a cross-product of clock times.
620
+ // An hour step or range (or arithmetic-progression hour list) under a
621
+ // single pinned minute reads as a cadence or window rather than a
622
+ // cross-product of clock times.
568
623
  if (ir.shapes.minute === 'single') {
569
- const cadence = hourCadence(ir, +ir.pattern.minute, opts);
624
+ const minute = +ir.pattern.minute;
625
+ const cadence = hourCadence(ir, minute, opts) ??
626
+ hourRangeCadence(ir, minute, opts);
570
627
 
571
628
  if (cadence !== null) {
572
629
  return cadence;
@@ -592,10 +649,11 @@ function renderClockTimes(ir: IR, plan: PlanOf<'clockTimes'>,
592
649
  function renderCompactClockTimes(ir: IR, plan: PlanOf<'compactClockTimes'>,
593
650
  opts: NormalizedOptions): string {
594
651
  if (plan.fold) {
595
- // An hour step (or arithmetic-progression hour list) under the single
596
- // pinned minute reads as a cadence, not a wall of clock times. (Returns
597
- // null for an irregular list or a range, which keep folding below.)
598
- const cadence = hourCadence(ir, +plan.minute, opts);
652
+ // An hour step or range (or arithmetic-progression hour list) under the
653
+ // single pinned minute reads as a cadence or window, not a wall of clock
654
+ // times. (Returns null for an irregular list, which keeps folding below.)
655
+ const cadence = hourCadence(ir, +plan.minute, opts) ??
656
+ hourRangeCadence(ir, +plan.minute, opts);
599
657
 
600
658
  if (cadence !== null) {
601
659
  return cadence;
@@ -619,12 +677,18 @@ function renderCompactClockTimes(ir: IR, plan: PlanOf<'compactClockTimes'>,
619
677
  hourSegmentTimes(ir, fold, true, opts);
620
678
  }
621
679
 
622
- const phrase =
680
+ const minuteLead =
623
681
  // The non-fold branch is a minute list, which has segments. An
624
682
  // offset/uneven step enumerated to that list reads as a stride.
625
- (strideFromSegments(ir.analyses.segments.minute!, 'minute', 'hour', opts) ??
683
+ strideFromSegments(ir.analyses.segments.minute!, 'minute', 'hour', opts) ??
626
684
  listPastThe(segmentWords(ir.analyses.segments.minute!, opts),
627
- 'minute', 'hour', opts)) +
685
+ 'minute', 'hour', opts);
686
+ // A uneven hour stride reads as a cadence after the minute lead, not a wall
687
+ // of clock-time columns.
688
+ const cadence = unevenHourCadence(ir, opts);
689
+ const phrase = cadence ?
690
+ minuteLead + ', ' + cadence + trailingQualifier(ir, opts) :
691
+ minuteLead +
628
692
  ', at ' + hourSegmentTimes(ir, {minute: 0, second: null}, true, opts) +
629
693
  trailingQualifier(ir, opts);
630
694
 
@@ -847,12 +911,74 @@ function hourStrideCadence(stride: {start: number; interval: number;
847
911
  through(opts) + getTime({hour: last, minute: 0}, opts);
848
912
  }
849
913
 
914
+ // Whether an hour stride wraps the day cleanly from within its first interval
915
+ // (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
916
+ // stride has no distinct endpoint and keeps its bare or "from M" cadence. Every
917
+ // other stride — a uneven interval, or one starting at or past its interval
918
+ // (a bounded `a-b/n`) — is a bounded set the cadence pins both endpoints of.
919
+ function offsetCleanStride(
920
+ stride: {start: number; interval: number}
921
+ ): boolean {
922
+ return stride.start < stride.interval && 24 % stride.interval === 0;
923
+ }
924
+
925
+ // The bounded cadence for an hour stride that pins both clock-time endpoints,
926
+ // or null when the hour is not such a stride. The core rewrites a uneven step
927
+ // to its fire list, so a minute window/list/step crossed with it lands in the
928
+ // enumerating list paths; there the bounded hour reads better as its cadence
929
+ // ("…, every five hours from midnight through 8 p.m.") than as a wall of
930
+ // clock-time columns. An offset-clean stride keeps its existing confinement
931
+ // form, so only the endpoint-bearing case routes here.
932
+ function unevenHourCadence(ir: IR, opts: NormalizedOptions): string | null {
933
+ const stride = hourStride(ir);
934
+
935
+ if (!stride || offsetCleanStride(stride)) {
936
+ return null;
937
+ }
938
+
939
+ return hourStrideCadence(stride, opts);
940
+ }
941
+
942
+ // An hour list's arithmetic progression, or null when its values are not a
943
+ // step the renderer should speak as a cadence. The core rewrites a uneven hour
944
+ // step (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its
945
+ // literal fire list, indistinguishable in the IR from a hand-written list; the
946
+ // renderer recovers the cadence from the values. A progression starting at
947
+ // zero is a `*/n` step however short (0,7,14,21 is `*/7`); a non-zero one is
948
+ // only a step when it is too long to be a deliberate clock-time list (e.g.
949
+ // 9,17 is two named times, not a cadence), the same length the minute/second
950
+ // list path uses. Interval one is a plain range, never a step.
951
+ function hourListStride(values: number[]):
952
+ {start: number; interval: number; last: number} | null {
953
+ if (values.length < 2) {
954
+ return null;
955
+ }
956
+
957
+ const interval = values[1] - values[0];
958
+
959
+ if (interval < 2) {
960
+ return null;
961
+ }
962
+
963
+ for (let i = 2; i < values.length; i += 1) {
964
+ if (values[i] - values[i - 1] !== interval) {
965
+ return null;
966
+ }
967
+ }
968
+
969
+ if (values[0] !== 0 && values.length < 5) {
970
+ return null;
971
+ }
972
+
973
+ return {interval, last: values[values.length - 1], start: values[0]};
974
+ }
975
+
850
976
  // The hour field's stride, or null when the hour is not a cadence: a step
851
977
  // segment yields its {start, interval, last} directly; an all-single hour
852
- // list yields one only when its values form a long-enough arithmetic
853
- // progression (so an irregular list like 9,17 keeps enumerating). The IR is
854
- // unchanged — the renderer recognizes the stride and speaks it as a cadence
855
- // instead of the clock-time cross-product.
978
+ // list yields one only when its values form a step progression (so an irregular
979
+ // list like 9,17 keeps enumerating). The IR is unchanged — the renderer
980
+ // recognizes the stride and speaks it as a cadence instead of the clock-time
981
+ // cross-product.
856
982
  function hourStride(ir: IR):
857
983
  {start: number; interval: number; last: number} | null {
858
984
  // Reached only from the clock-time paths, which run under discrete hours
@@ -861,6 +987,13 @@ function hourStride(ir: IR):
861
987
 
862
988
  if (segments.length === 1 && segments[0].kind === 'step') {
863
989
  const segment = segments[0];
990
+
991
+ // A bounded step that fires only once (e.g. `9-10/5` → just 9) is a single
992
+ // value, not a stride: it has no interval to speak and no endpoint to pin.
993
+ if (segment.fires.length < 2) {
994
+ return null;
995
+ }
996
+
864
997
  const start = segment.startToken === '*' ?
865
998
  0 :
866
999
  +segment.startToken.split('-')[0];
@@ -870,9 +1003,8 @@ function hourStride(ir: IR):
870
1003
  }
871
1004
 
872
1005
  const values = singleValues(segments);
873
- const step = values && arithmeticStep(values);
874
1006
 
875
- return step || null;
1007
+ return values && hourListStride(values);
876
1008
  }
877
1009
 
878
1010
  // The second's status against a pinned minute: a wildcard or sub-minute step
@@ -931,7 +1063,13 @@ function hourCadence(ir: IR, minute: number,
931
1063
 
932
1064
  const fires = (stride.last - stride.start) / stride.interval + 1;
933
1065
 
934
- if (ir.pattern.second === '0' && fires <= maxClockTimes) {
1066
+ // A short stride that spells out as few clock times stays an enumeration only
1067
+ // when it wraps cleanly (an offset-clean stride with no endpoint): the bare
1068
+ // or "from M" form is no shorter than the list, so the list reads fine. A
1069
+ // bounded or uneven stride has no clean wrap, so its endpoint-pinning cadence
1070
+ // ("every five hours from midnight through 8 p.m.") reads better however few.
1071
+ if (ir.pattern.second === '0' && fires <= maxClockTimes &&
1072
+ offsetCleanStride(stride)) {
935
1073
  return null;
936
1074
  }
937
1075
 
@@ -948,6 +1086,14 @@ function hourCadence(ir: IR, minute: number,
948
1086
  everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
949
1087
  }
950
1088
 
1089
+ // A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
1090
+ // lead clause to fold in, so the bounded cadence stands on its own ("every
1091
+ // five hours from midnight through 8 p.m."); only a real minute or second
1092
+ // prefixes its clause.
1093
+ if (minute === 0 && ir.pattern.second === '0') {
1094
+ return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
1095
+ }
1096
+
951
1097
  return hourCadenceLead(ir, minute, opts) + ', ' +
952
1098
  hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
953
1099
  }
@@ -970,6 +1116,94 @@ function cleanStrideSegment(ir: IR): StepSegment | null {
970
1116
  return segment;
971
1117
  }
972
1118
 
1119
+ // Whether the hour field is a range — or a list whose segments include a
1120
+ // range — and so forms a window rather than a cross-product of clock times.
1121
+ // A pure single-value list (9,17) has no range to span and still enumerates;
1122
+ // a step is handled by hourStride/hourCadence, so a field whose only segments
1123
+ // are steps and singles is left alone here.
1124
+ function hasHourWindow(ir: IR): boolean {
1125
+ // Reached only from the clock-time paths, which run under discrete hours
1126
+ // and so always carry hour segments.
1127
+ return ir.analyses.segments.hour!.some(function range(segment) {
1128
+ return segment.kind === 'range';
1129
+ });
1130
+ }
1131
+
1132
+ // The hour-range window as a cadence tail at the top of each hour: each range
1133
+ // segment is a "from X through Y" window ("every hour from 9 a.m. through
1134
+ // 5 p.m."), and any non-contiguous single hour is appended ("and at 10 p.m.").
1135
+ // The minute has already folded into the lead, so the window closes on the
1136
+ // top of its final hour. Mirrors foldedHourWindows but pinned to minute 0.
1137
+ function hourRangeWindowTail(ir: IR, opts: NormalizedOptions): string {
1138
+ const windows: string[] = [];
1139
+ const singles: number[] = [];
1140
+
1141
+ // Reached only after hasHourWindow, so hour segments exist.
1142
+ ir.analyses.segments.hour!.forEach(function classify(segment) {
1143
+ if (segment.kind === 'range') {
1144
+ windows.push('from ' + getTime({hour: +segment.bounds[0], minute: 0},
1145
+ opts) + through(opts) +
1146
+ getTime({hour: +segment.bounds[1], minute: 0}, opts));
1147
+ }
1148
+ else if (segment.kind === 'step') {
1149
+ singles.push(...segment.fires);
1150
+ }
1151
+ else {
1152
+ singles.push(+segment.value);
1153
+ }
1154
+ });
1155
+
1156
+ let phrase = 'every hour ' + joinList(windows, opts);
1157
+
1158
+ if (singles.length) {
1159
+ phrase += ' and at ' + joinList(singles.map(function time(hour) {
1160
+ return getTime({hour, minute: 0}, opts);
1161
+ }), opts);
1162
+ }
1163
+
1164
+ return phrase;
1165
+ }
1166
+
1167
+ // Render an hour range (or a list whose segments include a range) under a
1168
+ // single pinned minute and a second as the hour-range window — the lead
1169
+ // clause, then "every hour from X through Y" — instead of cross-multiplying
1170
+ // the hours into a wall of clock times. Returns null when the hour has no
1171
+ // range (a pure single-value list, a single hour, or a step, which other
1172
+ // paths own), or when a plain :00 set is short enough that enumeration is no
1173
+ // longer than the window. Renderer-only; the IR is unchanged.
1174
+ function hourRangeCadence(ir: IR, minute: number,
1175
+ opts: NormalizedOptions): string | null {
1176
+ // Scoped to minute 0: the minute folds into the lead and every hour fires
1177
+ // at the top, so the window closes cleanly on the final hour. A non-zero
1178
+ // pinned minute is a real clock minute the existing clock-time window form
1179
+ // already speaks ("9:30:15 a.m. through 8:30:15 p.m."), unchanged.
1180
+ if (minute !== 0 || !hasHourWindow(ir)) {
1181
+ return null;
1182
+ }
1183
+
1184
+ // A plain top-of-minute second (:00) carries no clause: the existing
1185
+ // hour-range and folded-window renderers already speak that window, so this
1186
+ // path only forms a window when there is a meaningful second to lead with.
1187
+ if (ir.pattern.second === '0') {
1188
+ return null;
1189
+ }
1190
+
1191
+ // A wildcard or sub-minute step second confined to minute 0 is the whole
1192
+ // minute-0 window ("every second for one minute"), confined to the hour
1193
+ // range with the "during the … hours" idiom (the same idiom an hour list
1194
+ // uses). This is kept distinct from the bare minute-0 window ("every hour
1195
+ // from 9 a.m. through 5 p.m.") so the one-minute confinement is never heard
1196
+ // as it — the hour-range analog of "for one minute during every other hour".
1197
+ if (subMinuteSecond(ir)) {
1198
+ return secondsClause(ir, 'minute', opts) + ' for one minute during the ' +
1199
+ hourSegmentTimes(ir, {minute: 0, second: null}, false, opts) +
1200
+ ' hours' + trailingQualifier(ir, opts);
1201
+ }
1202
+
1203
+ return hourCadenceLead(ir, minute, opts) + ', ' +
1204
+ hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
1205
+ }
1206
+
973
1207
  // --- List and segment phrasing. ---
974
1208
 
975
1209
  // Chicago number style for a series: if any value crosses the spell-out