cronli5 0.1.2 → 0.1.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/cronli5.js CHANGED
@@ -69,6 +69,21 @@ function isNonNegativeInteger(value) {
69
69
  const digits = /^\d+$/;
70
70
  return digits.test(value);
71
71
  }
72
+ function arithmeticStep(values) {
73
+ if (values.length < 5) {
74
+ return null;
75
+ }
76
+ const interval = values[1] - values[0];
77
+ if (interval < 2) {
78
+ return null;
79
+ }
80
+ for (let i = 2; i < values.length; i += 1) {
81
+ if (values[i] - values[i - 1] !== interval) {
82
+ return null;
83
+ }
84
+ }
85
+ return { start: values[0], interval, last: values[values.length - 1] };
86
+ }
72
87
  function toFieldNumber(token, numberMap) {
73
88
  return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
74
89
  }
@@ -200,14 +215,20 @@ function normalizeField(value, field, spec) {
200
215
  }
201
216
  const cycle = timeFieldCycle[field];
202
217
  const segments = stringValue.split(",").map(function canonical(segment) {
203
- return enumerateNonUniformStep(
204
- collapseDegenerateRange(
205
- collapseOnceStep(collapseUnitStep(segment, spec), spec),
206
- spec
218
+ return canonicalizeTokens(collapseFullSpanRange(
219
+ enumerateNonUniformStep(
220
+ collapseFullSpanStep(
221
+ collapseDegenerateRange(
222
+ collapseOnceStep(collapseUnitStep(segment, spec), spec),
223
+ spec
224
+ ),
225
+ spec
226
+ ),
227
+ spec,
228
+ cycle
207
229
  ),
208
- spec,
209
- cycle
210
- );
230
+ spec
231
+ ), spec);
211
232
  }).join(",").split(",");
212
233
  if (segments.indexOf("*") !== -1) {
213
234
  return "*";
@@ -216,6 +237,23 @@ function normalizeField(value, field, spec) {
216
237
  return firstFire(a, spec) - firstFire(b, spec);
217
238
  }).join(",");
218
239
  }
240
+ function canonicalizeTokens(segment, spec) {
241
+ if (!spec.numbers) {
242
+ return segment;
243
+ }
244
+ const parts = segment.split("/");
245
+ const start = parts[0].split("-").map(function fold(token) {
246
+ return canonicalizeToken(token, spec);
247
+ }).join("-");
248
+ return parts.length === 2 ? start + "/" + parts[1] : start;
249
+ }
250
+ function canonicalizeToken(token, spec) {
251
+ if (token === "*") {
252
+ return token;
253
+ }
254
+ const number = toFieldNumber(token, spec.numbers);
255
+ return "" + (number > spec.top ? spec.min : number);
256
+ }
219
257
  function collapseUnitStep(segment, spec) {
220
258
  const parts = segment.split("/");
221
259
  if (!spec.cyclic || parts.length !== 2 || +parts[1] !== 1) {
@@ -258,6 +296,35 @@ function enumerateNonUniformStep(segment, spec, cycle) {
258
296
  }
259
297
  return fires.join(",");
260
298
  }
299
+ function collapseFullSpanStep(segment, spec) {
300
+ const parts = segment.split("/");
301
+ if (parts.length !== 2 || !includes(parts[0], "-")) {
302
+ return segment;
303
+ }
304
+ return collapseFullSpanRange(parts[0], spec) === "*" ? "*/" + parts[1] : segment;
305
+ }
306
+ function collapseFullSpanRange(segment, spec) {
307
+ if (typeof spec.top !== "number" || includes(segment, "/") || !includes(segment, "-")) {
308
+ return segment;
309
+ }
310
+ const bounds = segment.split("-");
311
+ const low = toFieldNumber(bounds[0], spec.numbers);
312
+ const high = toFieldNumber(bounds[1], spec.numbers);
313
+ if (low > high) {
314
+ return segment;
315
+ }
316
+ const top = spec.top;
317
+ const fired = {};
318
+ for (let value = low; value <= high; value += 1) {
319
+ fired[value > top ? spec.min : value] = true;
320
+ }
321
+ for (let value = spec.min; value <= top; value += 1) {
322
+ if (!fired[value]) {
323
+ return segment;
324
+ }
325
+ }
326
+ return "*";
327
+ }
261
328
  function collapseDegenerateRange(segment, spec) {
262
329
  const start = segment.split("/")[0];
263
330
  if (!includes(start, "-")) {
@@ -547,7 +614,7 @@ function planStandaloneSeconds(pattern, shapes) {
547
614
  }
548
615
  return { kind: "standaloneSeconds" };
549
616
  }
550
- function planMinutes(pattern, shapes, analyses, subMinuteSecond = false) {
617
+ function planMinutes(pattern, shapes, analyses, subMinuteSecond2 = false) {
551
618
  if (shapes.minute === "step") {
552
619
  return {
553
620
  hours: planFrequencyHours(pattern, shapes, analyses),
@@ -570,7 +637,7 @@ function planMinutes(pattern, shapes, analyses, subMinuteSecond = false) {
570
637
  return underStep;
571
638
  }
572
639
  if (pattern.hour === "*") {
573
- return planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond);
640
+ return planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond2);
574
641
  }
575
642
  }
576
643
  function cleanHourStride(hourField) {
@@ -592,6 +659,9 @@ function planMinuteUnderHourStep(pattern, shapes) {
592
659
  if (shapes.minute === "range") {
593
660
  return { form: "range", kind: "minuteSpanAcrossHourStep" };
594
661
  }
662
+ if (shapes.minute === "list" && cleanHourStride(pattern.hour)) {
663
+ return { form: "list", kind: "minuteSpanAcrossHourStep" };
664
+ }
595
665
  return null;
596
666
  }
597
667
  function planFrequencyHours(pattern, shapes, analyses) {
@@ -640,7 +710,7 @@ function planMinutesAcrossHours(pattern, shapes) {
640
710
  }
641
711
  return null;
642
712
  }
643
- function planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond) {
713
+ function planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond2) {
644
714
  if (shapes.minute === "range") {
645
715
  return { kind: "rangeOfMinutes" };
646
716
  }
@@ -650,16 +720,16 @@ function planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond) {
650
720
  if (pattern.minute === "*") {
651
721
  return { kind: "everyMinute" };
652
722
  }
653
- if (pattern.minute !== "0" || subMinuteSecond) {
723
+ if (pattern.minute !== "0" || subMinuteSecond2) {
654
724
  return { kind: "singleMinute" };
655
725
  }
656
726
  }
657
- function planHours(pattern, shapes, analyses, subMinuteSecond = false) {
658
- const absorbsMinuteZero = subMinuteSecond && pattern.minute === "0";
727
+ function planHours(pattern, shapes, analyses, subMinuteSecond2 = false) {
728
+ const absorbsMinuteZero = subMinuteSecond2 && pattern.minute === "0";
659
729
  if (shapes.hour === "range" && !absorbsMinuteZero) {
660
730
  return planHourRange(pattern, shapes, analyses);
661
731
  }
662
- if (shapes.hour === "step" && pattern.minute === "0" && !subMinuteSecond) {
732
+ if (shapes.hour === "step" && pattern.minute === "0" && !subMinuteSecond2) {
663
733
  return { kind: "hourStep" };
664
734
  }
665
735
  if (pattern.hour === "*" && !absorbsMinuteZero) {
@@ -826,20 +896,6 @@ var weekdayNames = [
826
896
  ["Friday", "Fri"],
827
897
  ["Saturday", "Sat"]
828
898
  ];
829
- var monthAbbreviations = {
830
- JAN: monthNames[1],
831
- FEB: monthNames[2],
832
- MAR: monthNames[3],
833
- APR: monthNames[4],
834
- MAY: monthNames[5],
835
- JUN: monthNames[6],
836
- JUL: monthNames[7],
837
- AUG: monthNames[8],
838
- SEP: monthNames[9],
839
- OCT: monthNames[10],
840
- NOV: monthNames[11],
841
- DEC: monthNames[12]
842
- };
843
899
  var weekdayAbbreviations = {
844
900
  SUN: weekdayNames[0],
845
901
  MON: weekdayNames[1],
@@ -888,10 +944,50 @@ function renderSecondsWithinMinute(ir, plan, opts) {
888
944
  }
889
945
  return secondsLeadClause(ir, opts) + ", " + minuteWord + " " + minuteUnit + " past the hour, every hour" + trailingQualifier(ir, opts);
890
946
  }
947
+ function composeHourCadence(ir, plan, opts) {
948
+ const clockRest = plan.rest.kind === "clockTimes" || plan.rest.kind === "compactClockTimes";
949
+ return clockRest && ir.shapes.minute === "single" ? hourCadence(ir, +ir.pattern.minute, opts) : null;
950
+ }
891
951
  function renderComposeSeconds(ir, plan, opts) {
952
+ const cadence = composeHourCadence(ir, plan, opts);
953
+ if (cadence !== null) {
954
+ return cadence;
955
+ }
956
+ 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);
962
+ }
963
+ if (ir.shapes.second === "wildcard" && plan.rest.kind === "minuteFrequency" && plan.rest.hours.kind === "none" && ir.pattern.minute === "*/2") {
964
+ return "every second of every other minute" + trailingQualifier(ir, opts);
965
+ }
892
966
  return secondsLeadClause(ir, opts) + ", " + render(ir, plan.rest, opts);
893
967
  }
968
+ function durationHours(ir, plan, opts) {
969
+ const hours = plan.times.map(function clock(time) {
970
+ return getTime({ hour: time.hour, minute: 0 }, opts);
971
+ });
972
+ const trail = dayQualifier(ir, leadingWords, opts);
973
+ return joinList(hours, opts) + (trail && ", " + trail);
974
+ }
975
+ function clockTimesOf(ir, plan, opts) {
976
+ const times = plan.times.map(function clock(time) {
977
+ return getTime({
978
+ hour: time.hour,
979
+ minute: time.minute,
980
+ second: time.second,
981
+ explicit: true
982
+ }, opts);
983
+ });
984
+ const trail = dayQualifier(ir, leadingWords, opts);
985
+ return joinList(times, opts) + (trail && ", " + trail);
986
+ }
894
987
  function secondsLeadClause(ir, opts) {
988
+ return secondsClause(ir, "minute", opts);
989
+ }
990
+ function secondsClause(ir, anchor, opts) {
895
991
  const secondField = ir.pattern.second;
896
992
  const shape = ir.shapes.second;
897
993
  if (secondField === "*") {
@@ -901,22 +997,27 @@ function secondsLeadClause(ir, opts) {
901
997
  return stepCycle60(
902
998
  ir.analyses.segments.second[0],
903
999
  "second",
904
- "minute",
1000
+ anchor,
905
1001
  opts
906
1002
  );
907
1003
  }
908
1004
  if (shape === "range") {
909
1005
  const bounds = secondField.split("-");
910
1006
  const num = seriesNumber(bounds, opts);
911
- return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the minute";
1007
+ return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the " + anchor;
912
1008
  }
913
1009
  if (shape === "single") {
914
- return "at " + getNumber(secondField, opts) + " " + pluralize(secondField, "second") + " past the minute";
1010
+ return "at " + getNumber(secondField, opts) + " " + pluralize(secondField, "second") + " past the " + anchor;
915
1011
  }
916
- return listPastThe(
1012
+ return strideFromSegments(
1013
+ ir.analyses.segments.second,
1014
+ "second",
1015
+ anchor,
1016
+ opts
1017
+ ) ?? listPastThe(
917
1018
  segmentWords(ir.analyses.segments.second, opts),
918
1019
  "second",
919
- "minute",
1020
+ anchor,
920
1021
  opts
921
1022
  );
922
1023
  }
@@ -931,12 +1032,11 @@ function renderRangeOfMinutes(ir, plan, opts) {
931
1032
  return minuteRangeLead(ir.pattern.minute, opts) + trailingQualifier(ir, opts);
932
1033
  }
933
1034
  function renderMultipleMinutes(ir, plan, opts) {
934
- return listPastThe(
935
- segmentWords(ir.analyses.segments.minute, opts),
936
- "minute",
937
- "hour",
1035
+ const stride = strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts);
1036
+ return (stride ?? listPastThe(segmentWords(
1037
+ ir.analyses.segments.minute,
938
1038
  opts
939
- ) + trailingQualifier(ir, opts);
1039
+ ), "minute", "hour", opts)) + trailingQualifier(ir, opts);
940
1040
  }
941
1041
  function renderMinuteFrequency(ir, plan, opts) {
942
1042
  let phrase = stepCycle60(
@@ -955,6 +1055,9 @@ function renderMinuteFrequency(ir, plan, opts) {
955
1055
  return phrase + trailingQualifier(ir, opts);
956
1056
  }
957
1057
  function renderMinuteSpanInHour(ir, plan, opts) {
1058
+ if (ir.pattern.minute === "*") {
1059
+ return "every minute of the " + getTime({ hour: plan.hour, minute: 0 }, opts) + " hour" + trailingQualifier(ir, opts);
1060
+ }
958
1061
  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);
959
1062
  }
960
1063
  function renderMinutesAcrossHours(ir, plan, opts) {
@@ -963,8 +1066,9 @@ function renderMinutesAcrossHours(ir, plan, opts) {
963
1066
  }
964
1067
  const times = hourTimesFromPlan(ir, plan.times, true, opts);
965
1068
  const lead = plan.form === "range" ? minuteRangeLead(ir.pattern.minute, opts) : (
966
- // The 'list' form is a minute list, which has segments.
967
- listPastThe(
1069
+ // The 'list' form is a minute list, which has segments; an offset/uneven
1070
+ // step enumerated to that list reads as a stride.
1071
+ strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
968
1072
  segmentWords(ir.analyses.segments.minute, opts),
969
1073
  "minute",
970
1074
  "hour",
@@ -991,7 +1095,13 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
991
1095
  if (plan.form === "wildcard") {
992
1096
  return "every minute " + everyNthHour(segment, opts) + trailingQualifier(ir, opts);
993
1097
  }
994
- return minuteRangeLead(ir.pattern.minute, opts) + ", " + stepHours(segment, opts) + trailingQualifier(ir, opts);
1098
+ const lead = plan.form === "list" ? strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1099
+ segmentWords(ir.analyses.segments.minute, opts),
1100
+ "minute",
1101
+ "hour",
1102
+ opts
1103
+ ) : minuteRangeLead(ir.pattern.minute, opts);
1104
+ return lead + ", " + stepHours(segment, opts) + trailingQualifier(ir, opts);
995
1105
  }
996
1106
  function minuteRangeLead(minuteField, opts) {
997
1107
  const bounds = minuteField.split("-");
@@ -1015,7 +1125,12 @@ function rangeMinuteLead(ir, opts) {
1015
1125
  if (ir.pattern.minute === "0") {
1016
1126
  return "every hour";
1017
1127
  }
1018
- return listPastThe(
1128
+ return strideFromSegments(
1129
+ ir.analyses.segments.minute,
1130
+ "minute",
1131
+ "hour",
1132
+ opts
1133
+ ) ?? listPastThe(
1019
1134
  segmentWords(ir.analyses.segments.minute, opts),
1020
1135
  "minute",
1021
1136
  "hour",
@@ -1032,6 +1147,12 @@ function hourWindow(window, opts) {
1032
1147
  return "from " + getTime({ hour: window.from, minute: 0 }, opts) + through(opts) + getTime({ hour: window.to, minute: window.last }, opts);
1033
1148
  }
1034
1149
  function renderClockTimes(ir, plan, opts) {
1150
+ if (ir.shapes.minute === "single") {
1151
+ const cadence = hourCadence(ir, +ir.pattern.minute, opts);
1152
+ if (cadence !== null) {
1153
+ return cadence;
1154
+ }
1155
+ }
1035
1156
  const plain = mixedTwelve(plan.times);
1036
1157
  const times = plan.times.map(function clock(time) {
1037
1158
  return getTime({
@@ -1045,6 +1166,10 @@ function renderClockTimes(ir, plan, opts) {
1045
1166
  }
1046
1167
  function renderCompactClockTimes(ir, plan, opts) {
1047
1168
  if (plan.fold) {
1169
+ const cadence = hourCadence(ir, +plan.minute, opts);
1170
+ if (cadence !== null) {
1171
+ return cadence;
1172
+ }
1048
1173
  const hasRange = ir.analyses.segments.hour.some(function range(segment) {
1049
1174
  return segment.kind === "range";
1050
1175
  });
@@ -1055,13 +1180,14 @@ function renderCompactClockTimes(ir, plan, opts) {
1055
1180
  return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
1056
1181
  }
1057
1182
  const phrase = (
1058
- // The non-fold branch is a minute list, which has segments.
1059
- listPastThe(
1183
+ // The non-fold branch is a minute list, which has segments. An
1184
+ // offset/uneven step enumerated to that list reads as a stride.
1185
+ (strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1060
1186
  segmentWords(ir.analyses.segments.minute, opts),
1061
1187
  "minute",
1062
1188
  "hour",
1063
1189
  opts
1064
- ) + ", at " + hourSegmentTimes(ir, { minute: 0, second: null }, true, opts) + trailingQualifier(ir, opts)
1190
+ )) + ", at " + hourSegmentTimes(ir, { minute: 0, second: null }, true, opts) + trailingQualifier(ir, opts)
1065
1191
  );
1066
1192
  return ir.analyses.clockSecond ? secondsLeadClause(ir, opts) + ", " + phrase : phrase;
1067
1193
  }
@@ -1109,24 +1235,50 @@ var renderers = {
1109
1235
  singleMinute: renderSingleMinute,
1110
1236
  standaloneSeconds: renderStandaloneSeconds
1111
1237
  };
1238
+ function renderStride(stride, opts) {
1239
+ const { interval, start, last, cycle, unit, anchor } = stride;
1240
+ const cadence = "every " + getNumber(interval, opts) + " " + unit + "s";
1241
+ const tiles = cycle % interval === 0;
1242
+ if (start === 0 && tiles) {
1243
+ return cadence;
1244
+ }
1245
+ if (start < interval && tiles) {
1246
+ return cadence + " from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
1247
+ }
1248
+ const num = seriesNumber([start, last], opts);
1249
+ return cadence + " from " + num(start) + through(opts) + num(last) + " " + pluralize(last, unit) + " past the " + anchor;
1250
+ }
1251
+ function singleValues(segments) {
1252
+ const values = [];
1253
+ for (const segment of segments) {
1254
+ if (segment.kind !== "single") {
1255
+ return null;
1256
+ }
1257
+ values.push(+segment.value);
1258
+ }
1259
+ return values;
1260
+ }
1261
+ function strideFromSegments(segments, unit, anchor, opts) {
1262
+ const values = singleValues(segments);
1263
+ const step = values && arithmeticStep(values);
1264
+ return step ? renderStride({ ...step, cycle: 60, unit, anchor }, opts) : null;
1265
+ }
1112
1266
  function stepCycle60(segment, unit, anchor, opts) {
1113
1267
  if (segment.startToken.indexOf("-") !== -1) {
1114
1268
  return listPastThe(numberWords(segment.fires, opts), unit, anchor, opts);
1115
1269
  }
1116
1270
  const start = segment.startToken === "*" ? 0 : +segment.startToken;
1117
- const interval = segment.interval;
1118
- if (start !== 0) {
1119
- if (segment.fires.length <= 3) {
1120
- return listPastThe(
1121
- numberWords(segment.fires, opts),
1122
- unit,
1123
- anchor,
1124
- opts
1125
- );
1126
- }
1127
- return "every " + getNumber(interval, opts) + " " + unit + "s from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
1271
+ if (start !== 0 && segment.fires.length <= 3) {
1272
+ return listPastThe(numberWords(segment.fires, opts), unit, anchor, opts);
1128
1273
  }
1129
- return "every " + getNumber(interval, opts) + " " + unit + "s";
1274
+ return renderStride({
1275
+ interval: segment.interval,
1276
+ start,
1277
+ last: segment.fires[segment.fires.length - 1],
1278
+ cycle: 60,
1279
+ unit,
1280
+ anchor
1281
+ }, opts);
1130
1282
  }
1131
1283
  function stepHours(segment, opts) {
1132
1284
  if (segment.startToken.indexOf("-") !== -1) {
@@ -1142,6 +1294,68 @@ function stepHours(segment, opts) {
1142
1294
  }
1143
1295
  return "every " + getNumber(interval, opts) + " hours from " + getTime({ hour: start, minute: 0 }, opts);
1144
1296
  }
1297
+ function hourStrideCadence(stride, opts) {
1298
+ const { start, interval, last } = stride;
1299
+ const cadence = "every " + getNumber(interval, opts) + " hours";
1300
+ const tiles = 24 % interval === 0;
1301
+ if (start === 0 && tiles) {
1302
+ return cadence;
1303
+ }
1304
+ if (start < interval && tiles) {
1305
+ return cadence + " from " + getTime({ hour: start, minute: 0 }, opts);
1306
+ }
1307
+ return cadence + " from " + getTime({ hour: start, minute: 0 }, opts) + through(opts) + getTime({ hour: last, minute: 0 }, opts);
1308
+ }
1309
+ function hourStride(ir) {
1310
+ const segments = ir.analyses.segments.hour;
1311
+ if (segments.length === 1 && segments[0].kind === "step") {
1312
+ const segment = segments[0];
1313
+ const start = segment.startToken === "*" ? 0 : +segment.startToken.split("-")[0];
1314
+ return { interval: segment.interval, last: segment.fires[segment.fires.length - 1], start };
1315
+ }
1316
+ const values = singleValues(segments);
1317
+ const step = values && arithmeticStep(values);
1318
+ return step || null;
1319
+ }
1320
+ function subMinuteSecond(ir) {
1321
+ return ir.pattern.second === "*" || ir.shapes.second === "step";
1322
+ }
1323
+ function hourCadenceLead(ir, minute, opts) {
1324
+ if (minute === 0) {
1325
+ if (subMinuteSecond(ir)) {
1326
+ return secondsClause(ir, "minute", opts) + " for one minute";
1327
+ }
1328
+ return secondsClause(ir, "hour", opts);
1329
+ }
1330
+ const minutePhrase = getNumber(minute, opts) + " " + pluralize(minute, "minute") + " past the hour";
1331
+ if (ir.pattern.second === "0") {
1332
+ return minutePhrase;
1333
+ }
1334
+ return secondsClause(ir, "minute", opts) + ", " + minutePhrase;
1335
+ }
1336
+ function hourCadence(ir, minute, opts) {
1337
+ const stride = hourStride(ir);
1338
+ if (!stride) {
1339
+ return null;
1340
+ }
1341
+ const fires = (stride.last - stride.start) / stride.interval + 1;
1342
+ if (ir.pattern.second === "0" && fires <= maxClockTimes) {
1343
+ return null;
1344
+ }
1345
+ const confinement = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
1346
+ if (confinement) {
1347
+ return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
1348
+ }
1349
+ return hourCadenceLead(ir, minute, opts) + ", " + hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
1350
+ }
1351
+ function cleanStrideSegment(ir) {
1352
+ const segments = ir.analyses.segments.hour;
1353
+ const segment = segments.length === 1 && segments[0];
1354
+ if (!segment || segment.kind !== "step" || segment.startToken.indexOf("-") !== -1 || !(segment.interval in stepOrdinals)) {
1355
+ return null;
1356
+ }
1357
+ return segment;
1358
+ }
1145
1359
  function seriesNumber(values, opts) {
1146
1360
  const anyBig = values.some(function big(v) {
1147
1361
  return +v > 10;
@@ -1453,7 +1667,7 @@ function stepYears(yearField, opts) {
1453
1667
  return phrase;
1454
1668
  }
1455
1669
  function getTime(time, opts) {
1456
- const { hour, minute, plain } = time;
1670
+ const { hour, minute, plain, explicit } = time;
1457
1671
  const second = typeof time.second === "number" && time.second > 0 ? time.second : 0;
1458
1672
  if (!opts.ampm) {
1459
1673
  return clockDigits({
@@ -1462,12 +1676,12 @@ function getTime(time, opts) {
1462
1676
  second
1463
1677
  }, { pad: true, sep: opts.style.sep });
1464
1678
  }
1465
- return twelveHourTime({ hour, minute, second, plain }, opts);
1679
+ return twelveHourTime({ hour, minute, second, plain, explicit }, opts);
1466
1680
  }
1467
1681
  function twelveHourTime(time, opts) {
1468
- const { hour, minute, second, plain } = time;
1682
+ const { hour, minute, second, plain, explicit } = time;
1469
1683
  const style = opts.style;
1470
- if (!plain && +minute === 0 && !second) {
1684
+ if (!plain && !explicit && +minute === 0 && !second) {
1471
1685
  if (+hour === 0) {
1472
1686
  return style.midnight;
1473
1687
  }
@@ -1477,7 +1691,7 @@ function twelveHourTime(time, opts) {
1477
1691
  }
1478
1692
  const digits = clockDigits(
1479
1693
  { hour: hour % 12 || 12, minute, second },
1480
- { lean: true, sep: style.sep }
1694
+ { lean: !explicit, sep: style.sep }
1481
1695
  );
1482
1696
  return digits + (style.closeUp ? "" : " ") + (hour < 12 ? style.am : style.pm);
1483
1697
  }
@@ -1500,7 +1714,7 @@ function getOrdinal(n) {
1500
1714
  return n + suffix;
1501
1715
  }
1502
1716
  function getMonth(m, opts) {
1503
- const month = monthNames[m] || monthAbbreviations[m];
1717
+ const month = monthNames[+m];
1504
1718
  return month && month[opts.short ? 1 : 0];
1505
1719
  }
1506
1720
  function getWeekday(d, opts) {
@@ -1513,7 +1727,9 @@ var en = {
1513
1727
  fallback: "an unrecognizable cron pattern",
1514
1728
  options: normalizeOptions,
1515
1729
  reboot: "at system startup",
1516
- sentence: (description) => "Runs " + description + "."
1730
+ // A description ending in an abbreviation already carries its period
1731
+ // ("…9 a.m."), so closing the sentence must not double it.
1732
+ sentence: (description) => "Runs " + description + (description.endsWith(".") ? "" : ".")
1517
1733
  };
1518
1734
  var en_default = en;
1519
1735