cronli5 0.1.4 → 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.
package/dist/cronli5.cjs CHANGED
@@ -95,6 +95,21 @@ function isNonNegativeInteger(value) {
95
95
  const digits = /^\d+$/;
96
96
  return digits.test(value);
97
97
  }
98
+ function arithmeticStep(values) {
99
+ if (values.length < 5) {
100
+ return null;
101
+ }
102
+ const interval = values[1] - values[0];
103
+ if (interval < 2) {
104
+ return null;
105
+ }
106
+ for (let i = 2; i < values.length; i += 1) {
107
+ if (values[i] - values[i - 1] !== interval) {
108
+ return null;
109
+ }
110
+ }
111
+ return { start: values[0], interval, last: values[values.length - 1] };
112
+ }
98
113
  function toFieldNumber(token, numberMap) {
99
114
  return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
100
115
  }
@@ -625,7 +640,7 @@ function planStandaloneSeconds(pattern, shapes) {
625
640
  }
626
641
  return { kind: "standaloneSeconds" };
627
642
  }
628
- function planMinutes(pattern, shapes, analyses, subMinuteSecond = false) {
643
+ function planMinutes(pattern, shapes, analyses, subMinuteSecond2 = false) {
629
644
  if (shapes.minute === "step") {
630
645
  return {
631
646
  hours: planFrequencyHours(pattern, shapes, analyses),
@@ -648,7 +663,7 @@ function planMinutes(pattern, shapes, analyses, subMinuteSecond = false) {
648
663
  return underStep;
649
664
  }
650
665
  if (pattern.hour === "*") {
651
- return planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond);
666
+ return planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond2);
652
667
  }
653
668
  }
654
669
  function cleanHourStride(hourField) {
@@ -670,6 +685,9 @@ function planMinuteUnderHourStep(pattern, shapes) {
670
685
  if (shapes.minute === "range") {
671
686
  return { form: "range", kind: "minuteSpanAcrossHourStep" };
672
687
  }
688
+ if (shapes.minute === "list" && cleanHourStride(pattern.hour)) {
689
+ return { form: "list", kind: "minuteSpanAcrossHourStep" };
690
+ }
673
691
  return null;
674
692
  }
675
693
  function planFrequencyHours(pattern, shapes, analyses) {
@@ -718,7 +736,7 @@ function planMinutesAcrossHours(pattern, shapes) {
718
736
  }
719
737
  return null;
720
738
  }
721
- function planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond) {
739
+ function planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond2) {
722
740
  if (shapes.minute === "range") {
723
741
  return { kind: "rangeOfMinutes" };
724
742
  }
@@ -728,16 +746,16 @@ function planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond) {
728
746
  if (pattern.minute === "*") {
729
747
  return { kind: "everyMinute" };
730
748
  }
731
- if (pattern.minute !== "0" || subMinuteSecond) {
749
+ if (pattern.minute !== "0" || subMinuteSecond2) {
732
750
  return { kind: "singleMinute" };
733
751
  }
734
752
  }
735
- function planHours(pattern, shapes, analyses, subMinuteSecond = false) {
736
- const absorbsMinuteZero = subMinuteSecond && pattern.minute === "0";
753
+ function planHours(pattern, shapes, analyses, subMinuteSecond2 = false) {
754
+ const absorbsMinuteZero = subMinuteSecond2 && pattern.minute === "0";
737
755
  if (shapes.hour === "range" && !absorbsMinuteZero) {
738
756
  return planHourRange(pattern, shapes, analyses);
739
757
  }
740
- if (shapes.hour === "step" && pattern.minute === "0" && !subMinuteSecond) {
758
+ if (shapes.hour === "step" && pattern.minute === "0" && !subMinuteSecond2) {
741
759
  return { kind: "hourStep" };
742
760
  }
743
761
  if (pattern.hour === "*" && !absorbsMinuteZero) {
@@ -952,18 +970,34 @@ function renderSecondsWithinMinute(ir, plan, opts) {
952
970
  }
953
971
  return secondsLeadClause(ir, opts) + ", " + minuteWord + " " + minuteUnit + " past the hour, every hour" + trailingQualifier(ir, opts);
954
972
  }
973
+ function composeHourCadence(ir, plan, opts) {
974
+ const clockRest = plan.rest.kind === "clockTimes" || plan.rest.kind === "compactClockTimes";
975
+ if (!clockRest || ir.shapes.minute !== "single") {
976
+ return null;
977
+ }
978
+ const minute = +ir.pattern.minute;
979
+ return hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
980
+ }
981
+ function clockTimesConfinement(ir, rest, opts) {
982
+ if (+rest.times[0].minute === 0 && ir.shapes.minute === "single") {
983
+ return secondsLeadClause(ir, opts) + " for one minute at " + durationHours(ir, rest, opts);
984
+ }
985
+ return secondsLeadClause(ir, opts) + " of " + clockTimesOf(ir, rest, opts);
986
+ }
955
987
  function renderComposeSeconds(ir, plan, opts) {
988
+ const cadence = composeHourCadence(ir, plan, opts);
989
+ if (cadence !== null) {
990
+ return cadence;
991
+ }
956
992
  if (plan.rest.kind === "clockTimes" && (ir.shapes.second === "wildcard" || ir.shapes.second === "step")) {
957
- const minute = plan.rest.times[0].minute;
958
- if (+minute === 0) {
959
- return secondsLeadClause(ir, opts) + " for one minute at " + durationHours(ir, plan.rest, opts);
960
- }
961
- return secondsLeadClause(ir, opts) + " of " + clockTimesOf(ir, plan.rest, opts);
993
+ return clockTimesConfinement(ir, plan.rest, opts);
962
994
  }
963
995
  if (ir.shapes.second === "wildcard" && plan.rest.kind === "minuteFrequency" && plan.rest.hours.kind === "none" && ir.pattern.minute === "*/2") {
964
996
  return "every second of every other minute" + trailingQualifier(ir, opts);
965
997
  }
966
- return secondsLeadClause(ir, opts) + ", " + render(ir, plan.rest, opts);
998
+ const restOwnsLead = plan.rest.kind === "compactClockTimes" && ir.analyses.clockSecond;
999
+ const lead = restOwnsLead ? "" : secondsLeadClause(ir, opts) + ", ";
1000
+ return lead + render(ir, plan.rest, opts);
967
1001
  }
968
1002
  function durationHours(ir, plan, opts) {
969
1003
  const hours = plan.times.map(function clock(time) {
@@ -985,6 +1019,9 @@ function clockTimesOf(ir, plan, opts) {
985
1019
  return joinList(times, opts) + (trail && ", " + trail);
986
1020
  }
987
1021
  function secondsLeadClause(ir, opts) {
1022
+ return secondsClause(ir, "minute", opts);
1023
+ }
1024
+ function secondsClause(ir, anchor, opts) {
988
1025
  const secondField = ir.pattern.second;
989
1026
  const shape = ir.shapes.second;
990
1027
  if (secondField === "*") {
@@ -994,22 +1031,27 @@ function secondsLeadClause(ir, opts) {
994
1031
  return stepCycle60(
995
1032
  ir.analyses.segments.second[0],
996
1033
  "second",
997
- "minute",
1034
+ anchor,
998
1035
  opts
999
1036
  );
1000
1037
  }
1001
1038
  if (shape === "range") {
1002
1039
  const bounds = secondField.split("-");
1003
1040
  const num = seriesNumber(bounds, opts);
1004
- return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the minute";
1041
+ return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the " + anchor;
1005
1042
  }
1006
1043
  if (shape === "single") {
1007
- return "at " + getNumber(secondField, opts) + " " + pluralize(secondField, "second") + " past the minute";
1044
+ return "at " + getNumber(secondField, opts) + " " + pluralize(secondField, "second") + " past the " + anchor;
1008
1045
  }
1009
- return listPastThe(
1046
+ return strideFromSegments(
1047
+ ir.analyses.segments.second,
1048
+ "second",
1049
+ anchor,
1050
+ opts
1051
+ ) ?? listPastThe(
1010
1052
  segmentWords(ir.analyses.segments.second, opts),
1011
1053
  "second",
1012
- "minute",
1054
+ anchor,
1013
1055
  opts
1014
1056
  );
1015
1057
  }
@@ -1024,12 +1066,11 @@ function renderRangeOfMinutes(ir, plan, opts) {
1024
1066
  return minuteRangeLead(ir.pattern.minute, opts) + trailingQualifier(ir, opts);
1025
1067
  }
1026
1068
  function renderMultipleMinutes(ir, plan, opts) {
1027
- return listPastThe(
1028
- segmentWords(ir.analyses.segments.minute, opts),
1029
- "minute",
1030
- "hour",
1069
+ const stride = strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts);
1070
+ return (stride ?? listPastThe(segmentWords(
1071
+ ir.analyses.segments.minute,
1031
1072
  opts
1032
- ) + trailingQualifier(ir, opts);
1073
+ ), "minute", "hour", opts)) + trailingQualifier(ir, opts);
1033
1074
  }
1034
1075
  function renderMinuteFrequency(ir, plan, opts) {
1035
1076
  let phrase = stepCycle60(
@@ -1039,7 +1080,8 @@ function renderMinuteFrequency(ir, plan, opts) {
1039
1080
  opts
1040
1081
  );
1041
1082
  if (plan.hours.kind === "during") {
1042
- phrase += " during the " + hourTimesFromPlan(ir, plan.hours.times, false, opts) + " hours";
1083
+ const cadence = unevenHourCadence(ir, opts);
1084
+ phrase += cadence ? ", " + cadence : " during the " + hourTimesFromPlan(ir, plan.hours.times, false, opts) + " hours";
1043
1085
  } else if (plan.hours.kind === "window") {
1044
1086
  phrase += " " + hourWindow(plan.hours, opts);
1045
1087
  } else if (plan.hours.kind === "step") {
@@ -1054,19 +1096,27 @@ function renderMinuteSpanInHour(ir, plan, opts) {
1054
1096
  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);
1055
1097
  }
1056
1098
  function renderMinutesAcrossHours(ir, plan, opts) {
1099
+ const cadence = unevenHourCadence(ir, opts);
1057
1100
  if (plan.form === "wildcard") {
1101
+ if (cadence !== null) {
1102
+ return "every minute, " + cadence + trailingQualifier(ir, opts);
1103
+ }
1058
1104
  return "every minute during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
1059
1105
  }
1060
- const times = hourTimesFromPlan(ir, plan.times, true, opts);
1061
1106
  const lead = plan.form === "range" ? minuteRangeLead(ir.pattern.minute, opts) : (
1062
- // The 'list' form is a minute list, which has segments.
1063
- listPastThe(
1107
+ // The 'list' form is a minute list, which has segments; an offset/uneven
1108
+ // step enumerated to that list reads as a stride.
1109
+ strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1064
1110
  segmentWords(ir.analyses.segments.minute, opts),
1065
1111
  "minute",
1066
1112
  "hour",
1067
1113
  opts
1068
1114
  )
1069
1115
  );
1116
+ if (cadence !== null) {
1117
+ return lead + ", " + cadence + trailingQualifier(ir, opts);
1118
+ }
1119
+ const times = hourTimesFromPlan(ir, plan.times, true, opts);
1070
1120
  return lead + ", at " + times + trailingQualifier(ir, opts);
1071
1121
  }
1072
1122
  var stepOrdinals = {
@@ -1087,7 +1137,14 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
1087
1137
  if (plan.form === "wildcard") {
1088
1138
  return "every minute " + everyNthHour(segment, opts) + trailingQualifier(ir, opts);
1089
1139
  }
1090
- return minuteRangeLead(ir.pattern.minute, opts) + ", " + stepHours(segment, opts) + trailingQualifier(ir, opts);
1140
+ const lead = plan.form === "list" ? strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1141
+ segmentWords(ir.analyses.segments.minute, opts),
1142
+ "minute",
1143
+ "hour",
1144
+ opts
1145
+ ) : minuteRangeLead(ir.pattern.minute, opts);
1146
+ const cadence = unevenHourCadence(ir, opts);
1147
+ return lead + ", " + (cadence ?? stepHours(segment, opts)) + trailingQualifier(ir, opts);
1091
1148
  }
1092
1149
  function minuteRangeLead(minuteField, opts) {
1093
1150
  const bounds = minuteField.split("-");
@@ -1111,7 +1168,12 @@ function rangeMinuteLead(ir, opts) {
1111
1168
  if (ir.pattern.minute === "0") {
1112
1169
  return "every hour";
1113
1170
  }
1114
- return listPastThe(
1171
+ return strideFromSegments(
1172
+ ir.analyses.segments.minute,
1173
+ "minute",
1174
+ "hour",
1175
+ opts
1176
+ ) ?? listPastThe(
1115
1177
  segmentWords(ir.analyses.segments.minute, opts),
1116
1178
  "minute",
1117
1179
  "hour",
@@ -1119,6 +1181,10 @@ function rangeMinuteLead(ir, opts) {
1119
1181
  );
1120
1182
  }
1121
1183
  function renderHourStep(ir, plan, opts) {
1184
+ const cadence = unevenHourCadence(ir, opts);
1185
+ if (cadence !== null) {
1186
+ return cadence + trailingQualifier(ir, opts);
1187
+ }
1122
1188
  return stepHours(ir.analyses.segments.hour[0], opts) + trailingQualifier(ir, opts);
1123
1189
  }
1124
1190
  function boundedWindow(plan) {
@@ -1128,6 +1194,13 @@ function hourWindow(window, opts) {
1128
1194
  return "from " + getTime({ hour: window.from, minute: 0 }, opts) + through(opts) + getTime({ hour: window.to, minute: window.last }, opts);
1129
1195
  }
1130
1196
  function renderClockTimes(ir, plan, opts) {
1197
+ if (ir.shapes.minute === "single") {
1198
+ const minute = +ir.pattern.minute;
1199
+ const cadence = hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
1200
+ if (cadence !== null) {
1201
+ return cadence;
1202
+ }
1203
+ }
1131
1204
  const plain = mixedTwelve(plan.times);
1132
1205
  const times = plan.times.map(function clock(time) {
1133
1206
  return getTime({
@@ -1141,6 +1214,10 @@ function renderClockTimes(ir, plan, opts) {
1141
1214
  }
1142
1215
  function renderCompactClockTimes(ir, plan, opts) {
1143
1216
  if (plan.fold) {
1217
+ const cadence2 = hourCadence(ir, +plan.minute, opts) ?? hourRangeCadence(ir, +plan.minute, opts);
1218
+ if (cadence2 !== null) {
1219
+ return cadence2;
1220
+ }
1144
1221
  const hasRange = ir.analyses.segments.hour.some(function range(segment) {
1145
1222
  return segment.kind === "range";
1146
1223
  });
@@ -1150,15 +1227,18 @@ function renderCompactClockTimes(ir, plan, opts) {
1150
1227
  const fold = { minute: plan.minute, second: ir.analyses.clockSecond };
1151
1228
  return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
1152
1229
  }
1153
- const phrase = (
1154
- // The non-fold branch is a minute list, which has segments.
1155
- listPastThe(
1230
+ const minuteLead = (
1231
+ // The non-fold branch is a minute list, which has segments. An
1232
+ // offset/uneven step enumerated to that list reads as a stride.
1233
+ strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1156
1234
  segmentWords(ir.analyses.segments.minute, opts),
1157
1235
  "minute",
1158
1236
  "hour",
1159
1237
  opts
1160
- ) + ", at " + hourSegmentTimes(ir, { minute: 0, second: null }, true, opts) + trailingQualifier(ir, opts)
1238
+ )
1161
1239
  );
1240
+ const cadence = unevenHourCadence(ir, opts);
1241
+ const phrase = cadence ? minuteLead + ", " + cadence + trailingQualifier(ir, opts) : minuteLead + ", at " + hourSegmentTimes(ir, { minute: 0, second: null }, true, opts) + trailingQualifier(ir, opts);
1162
1242
  return ir.analyses.clockSecond ? secondsLeadClause(ir, opts) + ", " + phrase : phrase;
1163
1243
  }
1164
1244
  function foldedHourWindows(ir, plan, opts) {
@@ -1205,24 +1285,50 @@ var renderers = {
1205
1285
  singleMinute: renderSingleMinute,
1206
1286
  standaloneSeconds: renderStandaloneSeconds
1207
1287
  };
1288
+ function renderStride(stride, opts) {
1289
+ const { interval, start, last, cycle, unit, anchor } = stride;
1290
+ const cadence = "every " + getNumber(interval, opts) + " " + unit + "s";
1291
+ const tiles = cycle % interval === 0;
1292
+ if (start === 0 && tiles) {
1293
+ return cadence;
1294
+ }
1295
+ if (start < interval && tiles) {
1296
+ return cadence + " from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
1297
+ }
1298
+ const num = seriesNumber([start, last], opts);
1299
+ return cadence + " from " + num(start) + through(opts) + num(last) + " " + pluralize(last, unit) + " past the " + anchor;
1300
+ }
1301
+ function singleValues(segments) {
1302
+ const values = [];
1303
+ for (const segment of segments) {
1304
+ if (segment.kind !== "single") {
1305
+ return null;
1306
+ }
1307
+ values.push(+segment.value);
1308
+ }
1309
+ return values;
1310
+ }
1311
+ function strideFromSegments(segments, unit, anchor, opts) {
1312
+ const values = singleValues(segments);
1313
+ const step = values && arithmeticStep(values);
1314
+ return step ? renderStride({ ...step, cycle: 60, unit, anchor }, opts) : null;
1315
+ }
1208
1316
  function stepCycle60(segment, unit, anchor, opts) {
1209
1317
  if (segment.startToken.indexOf("-") !== -1) {
1210
1318
  return listPastThe(numberWords(segment.fires, opts), unit, anchor, opts);
1211
1319
  }
1212
1320
  const start = segment.startToken === "*" ? 0 : +segment.startToken;
1213
- const interval = segment.interval;
1214
- if (start !== 0) {
1215
- if (segment.fires.length <= 3) {
1216
- return listPastThe(
1217
- numberWords(segment.fires, opts),
1218
- unit,
1219
- anchor,
1220
- opts
1221
- );
1222
- }
1223
- return "every " + getNumber(interval, opts) + " " + unit + "s from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
1321
+ if (start !== 0 && segment.fires.length <= 3) {
1322
+ return listPastThe(numberWords(segment.fires, opts), unit, anchor, opts);
1224
1323
  }
1225
- return "every " + getNumber(interval, opts) + " " + unit + "s";
1324
+ return renderStride({
1325
+ interval: segment.interval,
1326
+ start,
1327
+ last: segment.fires[segment.fires.length - 1],
1328
+ cycle: 60,
1329
+ unit,
1330
+ anchor
1331
+ }, opts);
1226
1332
  }
1227
1333
  function stepHours(segment, opts) {
1228
1334
  if (segment.startToken.indexOf("-") !== -1) {
@@ -1238,6 +1344,141 @@ function stepHours(segment, opts) {
1238
1344
  }
1239
1345
  return "every " + getNumber(interval, opts) + " hours from " + getTime({ hour: start, minute: 0 }, opts);
1240
1346
  }
1347
+ function hourStrideCadence(stride, opts) {
1348
+ const { start, interval, last } = stride;
1349
+ const cadence = "every " + getNumber(interval, opts) + " hours";
1350
+ const tiles = 24 % interval === 0;
1351
+ if (start === 0 && tiles) {
1352
+ return cadence;
1353
+ }
1354
+ if (start < interval && tiles) {
1355
+ return cadence + " from " + getTime({ hour: start, minute: 0 }, opts);
1356
+ }
1357
+ return cadence + " from " + getTime({ hour: start, minute: 0 }, opts) + through(opts) + getTime({ hour: last, minute: 0 }, opts);
1358
+ }
1359
+ function offsetCleanStride(stride) {
1360
+ return stride.start < stride.interval && 24 % stride.interval === 0;
1361
+ }
1362
+ function unevenHourCadence(ir, opts) {
1363
+ const stride = hourStride(ir);
1364
+ if (!stride || offsetCleanStride(stride)) {
1365
+ return null;
1366
+ }
1367
+ return hourStrideCadence(stride, opts);
1368
+ }
1369
+ function hourListStride(values) {
1370
+ if (values.length < 2) {
1371
+ return null;
1372
+ }
1373
+ const interval = values[1] - values[0];
1374
+ if (interval < 2) {
1375
+ return null;
1376
+ }
1377
+ for (let i = 2; i < values.length; i += 1) {
1378
+ if (values[i] - values[i - 1] !== interval) {
1379
+ return null;
1380
+ }
1381
+ }
1382
+ if (values[0] !== 0 && values.length < 5) {
1383
+ return null;
1384
+ }
1385
+ return { interval, last: values[values.length - 1], start: values[0] };
1386
+ }
1387
+ function hourStride(ir) {
1388
+ const segments = ir.analyses.segments.hour;
1389
+ if (segments.length === 1 && segments[0].kind === "step") {
1390
+ const segment = segments[0];
1391
+ if (segment.fires.length < 2) {
1392
+ return null;
1393
+ }
1394
+ const start = segment.startToken === "*" ? 0 : +segment.startToken.split("-")[0];
1395
+ return { interval: segment.interval, last: segment.fires[segment.fires.length - 1], start };
1396
+ }
1397
+ const values = singleValues(segments);
1398
+ return values && hourListStride(values);
1399
+ }
1400
+ function subMinuteSecond(ir) {
1401
+ return ir.pattern.second === "*" || ir.shapes.second === "step";
1402
+ }
1403
+ function hourCadenceLead(ir, minute, opts) {
1404
+ if (minute === 0) {
1405
+ if (subMinuteSecond(ir)) {
1406
+ return secondsClause(ir, "minute", opts) + " for one minute";
1407
+ }
1408
+ return secondsClause(ir, "hour", opts);
1409
+ }
1410
+ const minutePhrase = getNumber(minute, opts) + " " + pluralize(minute, "minute") + " past the hour";
1411
+ if (ir.pattern.second === "0") {
1412
+ return minutePhrase;
1413
+ }
1414
+ return secondsClause(ir, "minute", opts) + ", " + minutePhrase;
1415
+ }
1416
+ function hourCadence(ir, minute, opts) {
1417
+ const stride = hourStride(ir);
1418
+ if (!stride) {
1419
+ return null;
1420
+ }
1421
+ const fires = (stride.last - stride.start) / stride.interval + 1;
1422
+ if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
1423
+ return null;
1424
+ }
1425
+ const confinement = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
1426
+ if (confinement) {
1427
+ return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
1428
+ }
1429
+ if (minute === 0 && ir.pattern.second === "0") {
1430
+ return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
1431
+ }
1432
+ return hourCadenceLead(ir, minute, opts) + ", " + hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
1433
+ }
1434
+ function cleanStrideSegment(ir) {
1435
+ const segments = ir.analyses.segments.hour;
1436
+ const segment = segments.length === 1 && segments[0];
1437
+ if (!segment || segment.kind !== "step" || segment.startToken.indexOf("-") !== -1 || !(segment.interval in stepOrdinals)) {
1438
+ return null;
1439
+ }
1440
+ return segment;
1441
+ }
1442
+ function hasHourWindow(ir) {
1443
+ return ir.analyses.segments.hour.some(function range(segment) {
1444
+ return segment.kind === "range";
1445
+ });
1446
+ }
1447
+ function hourRangeWindowTail(ir, opts) {
1448
+ const windows = [];
1449
+ const singles = [];
1450
+ ir.analyses.segments.hour.forEach(function classify(segment) {
1451
+ if (segment.kind === "range") {
1452
+ windows.push("from " + getTime(
1453
+ { hour: +segment.bounds[0], minute: 0 },
1454
+ opts
1455
+ ) + through(opts) + getTime({ hour: +segment.bounds[1], minute: 0 }, opts));
1456
+ } else if (segment.kind === "step") {
1457
+ singles.push(...segment.fires);
1458
+ } else {
1459
+ singles.push(+segment.value);
1460
+ }
1461
+ });
1462
+ let phrase = "every hour " + joinList(windows, opts);
1463
+ if (singles.length) {
1464
+ phrase += " and at " + joinList(singles.map(function time(hour) {
1465
+ return getTime({ hour, minute: 0 }, opts);
1466
+ }), opts);
1467
+ }
1468
+ return phrase;
1469
+ }
1470
+ function hourRangeCadence(ir, minute, opts) {
1471
+ if (minute !== 0 || !hasHourWindow(ir)) {
1472
+ return null;
1473
+ }
1474
+ if (ir.pattern.second === "0") {
1475
+ return null;
1476
+ }
1477
+ if (subMinuteSecond(ir)) {
1478
+ return secondsClause(ir, "minute", opts) + " for one minute during the " + hourSegmentTimes(ir, { minute: 0, second: null }, false, opts) + " hours" + trailingQualifier(ir, opts);
1479
+ }
1480
+ return hourCadenceLead(ir, minute, opts) + ", " + hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
1481
+ }
1241
1482
  function seriesNumber(values, opts) {
1242
1483
  const anyBig = values.some(function big(v) {
1243
1484
  return +v > 10;