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.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
  }
@@ -226,14 +241,20 @@ function normalizeField(value, field, spec) {
226
241
  }
227
242
  const cycle = timeFieldCycle[field];
228
243
  const segments = stringValue.split(",").map(function canonical(segment) {
229
- return enumerateNonUniformStep(
230
- collapseDegenerateRange(
231
- collapseOnceStep(collapseUnitStep(segment, spec), spec),
232
- spec
244
+ return canonicalizeTokens(collapseFullSpanRange(
245
+ enumerateNonUniformStep(
246
+ collapseFullSpanStep(
247
+ collapseDegenerateRange(
248
+ collapseOnceStep(collapseUnitStep(segment, spec), spec),
249
+ spec
250
+ ),
251
+ spec
252
+ ),
253
+ spec,
254
+ cycle
233
255
  ),
234
- spec,
235
- cycle
236
- );
256
+ spec
257
+ ), spec);
237
258
  }).join(",").split(",");
238
259
  if (segments.indexOf("*") !== -1) {
239
260
  return "*";
@@ -242,6 +263,23 @@ function normalizeField(value, field, spec) {
242
263
  return firstFire(a, spec) - firstFire(b, spec);
243
264
  }).join(",");
244
265
  }
266
+ function canonicalizeTokens(segment, spec) {
267
+ if (!spec.numbers) {
268
+ return segment;
269
+ }
270
+ const parts = segment.split("/");
271
+ const start = parts[0].split("-").map(function fold(token) {
272
+ return canonicalizeToken(token, spec);
273
+ }).join("-");
274
+ return parts.length === 2 ? start + "/" + parts[1] : start;
275
+ }
276
+ function canonicalizeToken(token, spec) {
277
+ if (token === "*") {
278
+ return token;
279
+ }
280
+ const number = toFieldNumber(token, spec.numbers);
281
+ return "" + (number > spec.top ? spec.min : number);
282
+ }
245
283
  function collapseUnitStep(segment, spec) {
246
284
  const parts = segment.split("/");
247
285
  if (!spec.cyclic || parts.length !== 2 || +parts[1] !== 1) {
@@ -284,6 +322,35 @@ function enumerateNonUniformStep(segment, spec, cycle) {
284
322
  }
285
323
  return fires.join(",");
286
324
  }
325
+ function collapseFullSpanStep(segment, spec) {
326
+ const parts = segment.split("/");
327
+ if (parts.length !== 2 || !includes(parts[0], "-")) {
328
+ return segment;
329
+ }
330
+ return collapseFullSpanRange(parts[0], spec) === "*" ? "*/" + parts[1] : segment;
331
+ }
332
+ function collapseFullSpanRange(segment, spec) {
333
+ if (typeof spec.top !== "number" || includes(segment, "/") || !includes(segment, "-")) {
334
+ return segment;
335
+ }
336
+ const bounds = segment.split("-");
337
+ const low = toFieldNumber(bounds[0], spec.numbers);
338
+ const high = toFieldNumber(bounds[1], spec.numbers);
339
+ if (low > high) {
340
+ return segment;
341
+ }
342
+ const top = spec.top;
343
+ const fired = {};
344
+ for (let value = low; value <= high; value += 1) {
345
+ fired[value > top ? spec.min : value] = true;
346
+ }
347
+ for (let value = spec.min; value <= top; value += 1) {
348
+ if (!fired[value]) {
349
+ return segment;
350
+ }
351
+ }
352
+ return "*";
353
+ }
287
354
  function collapseDegenerateRange(segment, spec) {
288
355
  const start = segment.split("/")[0];
289
356
  if (!includes(start, "-")) {
@@ -573,7 +640,7 @@ function planStandaloneSeconds(pattern, shapes) {
573
640
  }
574
641
  return { kind: "standaloneSeconds" };
575
642
  }
576
- function planMinutes(pattern, shapes, analyses, subMinuteSecond = false) {
643
+ function planMinutes(pattern, shapes, analyses, subMinuteSecond2 = false) {
577
644
  if (shapes.minute === "step") {
578
645
  return {
579
646
  hours: planFrequencyHours(pattern, shapes, analyses),
@@ -596,7 +663,7 @@ function planMinutes(pattern, shapes, analyses, subMinuteSecond = false) {
596
663
  return underStep;
597
664
  }
598
665
  if (pattern.hour === "*") {
599
- return planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond);
666
+ return planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond2);
600
667
  }
601
668
  }
602
669
  function cleanHourStride(hourField) {
@@ -618,6 +685,9 @@ function planMinuteUnderHourStep(pattern, shapes) {
618
685
  if (shapes.minute === "range") {
619
686
  return { form: "range", kind: "minuteSpanAcrossHourStep" };
620
687
  }
688
+ if (shapes.minute === "list" && cleanHourStride(pattern.hour)) {
689
+ return { form: "list", kind: "minuteSpanAcrossHourStep" };
690
+ }
621
691
  return null;
622
692
  }
623
693
  function planFrequencyHours(pattern, shapes, analyses) {
@@ -666,7 +736,7 @@ function planMinutesAcrossHours(pattern, shapes) {
666
736
  }
667
737
  return null;
668
738
  }
669
- function planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond) {
739
+ function planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond2) {
670
740
  if (shapes.minute === "range") {
671
741
  return { kind: "rangeOfMinutes" };
672
742
  }
@@ -676,16 +746,16 @@ function planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond) {
676
746
  if (pattern.minute === "*") {
677
747
  return { kind: "everyMinute" };
678
748
  }
679
- if (pattern.minute !== "0" || subMinuteSecond) {
749
+ if (pattern.minute !== "0" || subMinuteSecond2) {
680
750
  return { kind: "singleMinute" };
681
751
  }
682
752
  }
683
- function planHours(pattern, shapes, analyses, subMinuteSecond = false) {
684
- const absorbsMinuteZero = subMinuteSecond && pattern.minute === "0";
753
+ function planHours(pattern, shapes, analyses, subMinuteSecond2 = false) {
754
+ const absorbsMinuteZero = subMinuteSecond2 && pattern.minute === "0";
685
755
  if (shapes.hour === "range" && !absorbsMinuteZero) {
686
756
  return planHourRange(pattern, shapes, analyses);
687
757
  }
688
- if (shapes.hour === "step" && pattern.minute === "0" && !subMinuteSecond) {
758
+ if (shapes.hour === "step" && pattern.minute === "0" && !subMinuteSecond2) {
689
759
  return { kind: "hourStep" };
690
760
  }
691
761
  if (pattern.hour === "*" && !absorbsMinuteZero) {
@@ -852,20 +922,6 @@ var weekdayNames = [
852
922
  ["Friday", "Fri"],
853
923
  ["Saturday", "Sat"]
854
924
  ];
855
- var monthAbbreviations = {
856
- JAN: monthNames[1],
857
- FEB: monthNames[2],
858
- MAR: monthNames[3],
859
- APR: monthNames[4],
860
- MAY: monthNames[5],
861
- JUN: monthNames[6],
862
- JUL: monthNames[7],
863
- AUG: monthNames[8],
864
- SEP: monthNames[9],
865
- OCT: monthNames[10],
866
- NOV: monthNames[11],
867
- DEC: monthNames[12]
868
- };
869
925
  var weekdayAbbreviations = {
870
926
  SUN: weekdayNames[0],
871
927
  MON: weekdayNames[1],
@@ -914,10 +970,50 @@ function renderSecondsWithinMinute(ir, plan, opts) {
914
970
  }
915
971
  return secondsLeadClause(ir, opts) + ", " + minuteWord + " " + minuteUnit + " past the hour, every hour" + trailingQualifier(ir, opts);
916
972
  }
973
+ function composeHourCadence(ir, plan, opts) {
974
+ const clockRest = plan.rest.kind === "clockTimes" || plan.rest.kind === "compactClockTimes";
975
+ return clockRest && ir.shapes.minute === "single" ? hourCadence(ir, +ir.pattern.minute, opts) : null;
976
+ }
917
977
  function renderComposeSeconds(ir, plan, opts) {
978
+ const cadence = composeHourCadence(ir, plan, opts);
979
+ if (cadence !== null) {
980
+ return cadence;
981
+ }
982
+ if (plan.rest.kind === "clockTimes" && (ir.shapes.second === "wildcard" || ir.shapes.second === "step")) {
983
+ const minute = plan.rest.times[0].minute;
984
+ if (+minute === 0) {
985
+ return secondsLeadClause(ir, opts) + " for one minute at " + durationHours(ir, plan.rest, opts);
986
+ }
987
+ return secondsLeadClause(ir, opts) + " of " + clockTimesOf(ir, plan.rest, opts);
988
+ }
989
+ if (ir.shapes.second === "wildcard" && plan.rest.kind === "minuteFrequency" && plan.rest.hours.kind === "none" && ir.pattern.minute === "*/2") {
990
+ return "every second of every other minute" + trailingQualifier(ir, opts);
991
+ }
918
992
  return secondsLeadClause(ir, opts) + ", " + render(ir, plan.rest, opts);
919
993
  }
994
+ function durationHours(ir, plan, opts) {
995
+ const hours = plan.times.map(function clock(time) {
996
+ return getTime({ hour: time.hour, minute: 0 }, opts);
997
+ });
998
+ const trail = dayQualifier(ir, leadingWords, opts);
999
+ return joinList(hours, opts) + (trail && ", " + trail);
1000
+ }
1001
+ function clockTimesOf(ir, plan, opts) {
1002
+ const times = plan.times.map(function clock(time) {
1003
+ return getTime({
1004
+ hour: time.hour,
1005
+ minute: time.minute,
1006
+ second: time.second,
1007
+ explicit: true
1008
+ }, opts);
1009
+ });
1010
+ const trail = dayQualifier(ir, leadingWords, opts);
1011
+ return joinList(times, opts) + (trail && ", " + trail);
1012
+ }
920
1013
  function secondsLeadClause(ir, opts) {
1014
+ return secondsClause(ir, "minute", opts);
1015
+ }
1016
+ function secondsClause(ir, anchor, opts) {
921
1017
  const secondField = ir.pattern.second;
922
1018
  const shape = ir.shapes.second;
923
1019
  if (secondField === "*") {
@@ -927,22 +1023,27 @@ function secondsLeadClause(ir, opts) {
927
1023
  return stepCycle60(
928
1024
  ir.analyses.segments.second[0],
929
1025
  "second",
930
- "minute",
1026
+ anchor,
931
1027
  opts
932
1028
  );
933
1029
  }
934
1030
  if (shape === "range") {
935
1031
  const bounds = secondField.split("-");
936
1032
  const num = seriesNumber(bounds, opts);
937
- return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the minute";
1033
+ return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the " + anchor;
938
1034
  }
939
1035
  if (shape === "single") {
940
- return "at " + getNumber(secondField, opts) + " " + pluralize(secondField, "second") + " past the minute";
1036
+ return "at " + getNumber(secondField, opts) + " " + pluralize(secondField, "second") + " past the " + anchor;
941
1037
  }
942
- return listPastThe(
1038
+ return strideFromSegments(
1039
+ ir.analyses.segments.second,
1040
+ "second",
1041
+ anchor,
1042
+ opts
1043
+ ) ?? listPastThe(
943
1044
  segmentWords(ir.analyses.segments.second, opts),
944
1045
  "second",
945
- "minute",
1046
+ anchor,
946
1047
  opts
947
1048
  );
948
1049
  }
@@ -957,12 +1058,11 @@ function renderRangeOfMinutes(ir, plan, opts) {
957
1058
  return minuteRangeLead(ir.pattern.minute, opts) + trailingQualifier(ir, opts);
958
1059
  }
959
1060
  function renderMultipleMinutes(ir, plan, opts) {
960
- return listPastThe(
961
- segmentWords(ir.analyses.segments.minute, opts),
962
- "minute",
963
- "hour",
1061
+ const stride = strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts);
1062
+ return (stride ?? listPastThe(segmentWords(
1063
+ ir.analyses.segments.minute,
964
1064
  opts
965
- ) + trailingQualifier(ir, opts);
1065
+ ), "minute", "hour", opts)) + trailingQualifier(ir, opts);
966
1066
  }
967
1067
  function renderMinuteFrequency(ir, plan, opts) {
968
1068
  let phrase = stepCycle60(
@@ -981,6 +1081,9 @@ function renderMinuteFrequency(ir, plan, opts) {
981
1081
  return phrase + trailingQualifier(ir, opts);
982
1082
  }
983
1083
  function renderMinuteSpanInHour(ir, plan, opts) {
1084
+ if (ir.pattern.minute === "*") {
1085
+ return "every minute of the " + getTime({ hour: plan.hour, minute: 0 }, opts) + " hour" + trailingQualifier(ir, opts);
1086
+ }
984
1087
  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);
985
1088
  }
986
1089
  function renderMinutesAcrossHours(ir, plan, opts) {
@@ -989,8 +1092,9 @@ function renderMinutesAcrossHours(ir, plan, opts) {
989
1092
  }
990
1093
  const times = hourTimesFromPlan(ir, plan.times, true, opts);
991
1094
  const lead = plan.form === "range" ? minuteRangeLead(ir.pattern.minute, opts) : (
992
- // The 'list' form is a minute list, which has segments.
993
- listPastThe(
1095
+ // The 'list' form is a minute list, which has segments; an offset/uneven
1096
+ // step enumerated to that list reads as a stride.
1097
+ strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
994
1098
  segmentWords(ir.analyses.segments.minute, opts),
995
1099
  "minute",
996
1100
  "hour",
@@ -1017,7 +1121,13 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
1017
1121
  if (plan.form === "wildcard") {
1018
1122
  return "every minute " + everyNthHour(segment, opts) + trailingQualifier(ir, opts);
1019
1123
  }
1020
- return minuteRangeLead(ir.pattern.minute, opts) + ", " + stepHours(segment, opts) + trailingQualifier(ir, opts);
1124
+ const lead = plan.form === "list" ? strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1125
+ segmentWords(ir.analyses.segments.minute, opts),
1126
+ "minute",
1127
+ "hour",
1128
+ opts
1129
+ ) : minuteRangeLead(ir.pattern.minute, opts);
1130
+ return lead + ", " + stepHours(segment, opts) + trailingQualifier(ir, opts);
1021
1131
  }
1022
1132
  function minuteRangeLead(minuteField, opts) {
1023
1133
  const bounds = minuteField.split("-");
@@ -1041,7 +1151,12 @@ function rangeMinuteLead(ir, opts) {
1041
1151
  if (ir.pattern.minute === "0") {
1042
1152
  return "every hour";
1043
1153
  }
1044
- return listPastThe(
1154
+ return strideFromSegments(
1155
+ ir.analyses.segments.minute,
1156
+ "minute",
1157
+ "hour",
1158
+ opts
1159
+ ) ?? listPastThe(
1045
1160
  segmentWords(ir.analyses.segments.minute, opts),
1046
1161
  "minute",
1047
1162
  "hour",
@@ -1058,6 +1173,12 @@ function hourWindow(window, opts) {
1058
1173
  return "from " + getTime({ hour: window.from, minute: 0 }, opts) + through(opts) + getTime({ hour: window.to, minute: window.last }, opts);
1059
1174
  }
1060
1175
  function renderClockTimes(ir, plan, opts) {
1176
+ if (ir.shapes.minute === "single") {
1177
+ const cadence = hourCadence(ir, +ir.pattern.minute, opts);
1178
+ if (cadence !== null) {
1179
+ return cadence;
1180
+ }
1181
+ }
1061
1182
  const plain = mixedTwelve(plan.times);
1062
1183
  const times = plan.times.map(function clock(time) {
1063
1184
  return getTime({
@@ -1071,6 +1192,10 @@ function renderClockTimes(ir, plan, opts) {
1071
1192
  }
1072
1193
  function renderCompactClockTimes(ir, plan, opts) {
1073
1194
  if (plan.fold) {
1195
+ const cadence = hourCadence(ir, +plan.minute, opts);
1196
+ if (cadence !== null) {
1197
+ return cadence;
1198
+ }
1074
1199
  const hasRange = ir.analyses.segments.hour.some(function range(segment) {
1075
1200
  return segment.kind === "range";
1076
1201
  });
@@ -1081,13 +1206,14 @@ function renderCompactClockTimes(ir, plan, opts) {
1081
1206
  return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
1082
1207
  }
1083
1208
  const phrase = (
1084
- // The non-fold branch is a minute list, which has segments.
1085
- listPastThe(
1209
+ // The non-fold branch is a minute list, which has segments. An
1210
+ // offset/uneven step enumerated to that list reads as a stride.
1211
+ (strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1086
1212
  segmentWords(ir.analyses.segments.minute, opts),
1087
1213
  "minute",
1088
1214
  "hour",
1089
1215
  opts
1090
- ) + ", at " + hourSegmentTimes(ir, { minute: 0, second: null }, true, opts) + trailingQualifier(ir, opts)
1216
+ )) + ", at " + hourSegmentTimes(ir, { minute: 0, second: null }, true, opts) + trailingQualifier(ir, opts)
1091
1217
  );
1092
1218
  return ir.analyses.clockSecond ? secondsLeadClause(ir, opts) + ", " + phrase : phrase;
1093
1219
  }
@@ -1135,24 +1261,50 @@ var renderers = {
1135
1261
  singleMinute: renderSingleMinute,
1136
1262
  standaloneSeconds: renderStandaloneSeconds
1137
1263
  };
1264
+ function renderStride(stride, opts) {
1265
+ const { interval, start, last, cycle, unit, anchor } = stride;
1266
+ const cadence = "every " + getNumber(interval, opts) + " " + unit + "s";
1267
+ const tiles = cycle % interval === 0;
1268
+ if (start === 0 && tiles) {
1269
+ return cadence;
1270
+ }
1271
+ if (start < interval && tiles) {
1272
+ return cadence + " from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
1273
+ }
1274
+ const num = seriesNumber([start, last], opts);
1275
+ return cadence + " from " + num(start) + through(opts) + num(last) + " " + pluralize(last, unit) + " past the " + anchor;
1276
+ }
1277
+ function singleValues(segments) {
1278
+ const values = [];
1279
+ for (const segment of segments) {
1280
+ if (segment.kind !== "single") {
1281
+ return null;
1282
+ }
1283
+ values.push(+segment.value);
1284
+ }
1285
+ return values;
1286
+ }
1287
+ function strideFromSegments(segments, unit, anchor, opts) {
1288
+ const values = singleValues(segments);
1289
+ const step = values && arithmeticStep(values);
1290
+ return step ? renderStride({ ...step, cycle: 60, unit, anchor }, opts) : null;
1291
+ }
1138
1292
  function stepCycle60(segment, unit, anchor, opts) {
1139
1293
  if (segment.startToken.indexOf("-") !== -1) {
1140
1294
  return listPastThe(numberWords(segment.fires, opts), unit, anchor, opts);
1141
1295
  }
1142
1296
  const start = segment.startToken === "*" ? 0 : +segment.startToken;
1143
- const interval = segment.interval;
1144
- if (start !== 0) {
1145
- if (segment.fires.length <= 3) {
1146
- return listPastThe(
1147
- numberWords(segment.fires, opts),
1148
- unit,
1149
- anchor,
1150
- opts
1151
- );
1152
- }
1153
- return "every " + getNumber(interval, opts) + " " + unit + "s from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
1297
+ if (start !== 0 && segment.fires.length <= 3) {
1298
+ return listPastThe(numberWords(segment.fires, opts), unit, anchor, opts);
1154
1299
  }
1155
- return "every " + getNumber(interval, opts) + " " + unit + "s";
1300
+ return renderStride({
1301
+ interval: segment.interval,
1302
+ start,
1303
+ last: segment.fires[segment.fires.length - 1],
1304
+ cycle: 60,
1305
+ unit,
1306
+ anchor
1307
+ }, opts);
1156
1308
  }
1157
1309
  function stepHours(segment, opts) {
1158
1310
  if (segment.startToken.indexOf("-") !== -1) {
@@ -1168,6 +1320,68 @@ function stepHours(segment, opts) {
1168
1320
  }
1169
1321
  return "every " + getNumber(interval, opts) + " hours from " + getTime({ hour: start, minute: 0 }, opts);
1170
1322
  }
1323
+ function hourStrideCadence(stride, opts) {
1324
+ const { start, interval, last } = stride;
1325
+ const cadence = "every " + getNumber(interval, opts) + " hours";
1326
+ const tiles = 24 % interval === 0;
1327
+ if (start === 0 && tiles) {
1328
+ return cadence;
1329
+ }
1330
+ if (start < interval && tiles) {
1331
+ return cadence + " from " + getTime({ hour: start, minute: 0 }, opts);
1332
+ }
1333
+ return cadence + " from " + getTime({ hour: start, minute: 0 }, opts) + through(opts) + getTime({ hour: last, minute: 0 }, opts);
1334
+ }
1335
+ function hourStride(ir) {
1336
+ const segments = ir.analyses.segments.hour;
1337
+ if (segments.length === 1 && segments[0].kind === "step") {
1338
+ const segment = segments[0];
1339
+ const start = segment.startToken === "*" ? 0 : +segment.startToken.split("-")[0];
1340
+ return { interval: segment.interval, last: segment.fires[segment.fires.length - 1], start };
1341
+ }
1342
+ const values = singleValues(segments);
1343
+ const step = values && arithmeticStep(values);
1344
+ return step || null;
1345
+ }
1346
+ function subMinuteSecond(ir) {
1347
+ return ir.pattern.second === "*" || ir.shapes.second === "step";
1348
+ }
1349
+ function hourCadenceLead(ir, minute, opts) {
1350
+ if (minute === 0) {
1351
+ if (subMinuteSecond(ir)) {
1352
+ return secondsClause(ir, "minute", opts) + " for one minute";
1353
+ }
1354
+ return secondsClause(ir, "hour", opts);
1355
+ }
1356
+ const minutePhrase = getNumber(minute, opts) + " " + pluralize(minute, "minute") + " past the hour";
1357
+ if (ir.pattern.second === "0") {
1358
+ return minutePhrase;
1359
+ }
1360
+ return secondsClause(ir, "minute", opts) + ", " + minutePhrase;
1361
+ }
1362
+ function hourCadence(ir, minute, opts) {
1363
+ const stride = hourStride(ir);
1364
+ if (!stride) {
1365
+ return null;
1366
+ }
1367
+ const fires = (stride.last - stride.start) / stride.interval + 1;
1368
+ if (ir.pattern.second === "0" && fires <= maxClockTimes) {
1369
+ return null;
1370
+ }
1371
+ const confinement = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
1372
+ if (confinement) {
1373
+ return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
1374
+ }
1375
+ return hourCadenceLead(ir, minute, opts) + ", " + hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
1376
+ }
1377
+ function cleanStrideSegment(ir) {
1378
+ const segments = ir.analyses.segments.hour;
1379
+ const segment = segments.length === 1 && segments[0];
1380
+ if (!segment || segment.kind !== "step" || segment.startToken.indexOf("-") !== -1 || !(segment.interval in stepOrdinals)) {
1381
+ return null;
1382
+ }
1383
+ return segment;
1384
+ }
1171
1385
  function seriesNumber(values, opts) {
1172
1386
  const anyBig = values.some(function big(v) {
1173
1387
  return +v > 10;
@@ -1479,7 +1693,7 @@ function stepYears(yearField, opts) {
1479
1693
  return phrase;
1480
1694
  }
1481
1695
  function getTime(time, opts) {
1482
- const { hour, minute, plain } = time;
1696
+ const { hour, minute, plain, explicit } = time;
1483
1697
  const second = typeof time.second === "number" && time.second > 0 ? time.second : 0;
1484
1698
  if (!opts.ampm) {
1485
1699
  return clockDigits({
@@ -1488,12 +1702,12 @@ function getTime(time, opts) {
1488
1702
  second
1489
1703
  }, { pad: true, sep: opts.style.sep });
1490
1704
  }
1491
- return twelveHourTime({ hour, minute, second, plain }, opts);
1705
+ return twelveHourTime({ hour, minute, second, plain, explicit }, opts);
1492
1706
  }
1493
1707
  function twelveHourTime(time, opts) {
1494
- const { hour, minute, second, plain } = time;
1708
+ const { hour, minute, second, plain, explicit } = time;
1495
1709
  const style = opts.style;
1496
- if (!plain && +minute === 0 && !second) {
1710
+ if (!plain && !explicit && +minute === 0 && !second) {
1497
1711
  if (+hour === 0) {
1498
1712
  return style.midnight;
1499
1713
  }
@@ -1503,7 +1717,7 @@ function twelveHourTime(time, opts) {
1503
1717
  }
1504
1718
  const digits = clockDigits(
1505
1719
  { hour: hour % 12 || 12, minute, second },
1506
- { lean: true, sep: style.sep }
1720
+ { lean: !explicit, sep: style.sep }
1507
1721
  );
1508
1722
  return digits + (style.closeUp ? "" : " ") + (hour < 12 ? style.am : style.pm);
1509
1723
  }
@@ -1526,7 +1740,7 @@ function getOrdinal(n) {
1526
1740
  return n + suffix;
1527
1741
  }
1528
1742
  function getMonth(m, opts) {
1529
- const month = monthNames[m] || monthAbbreviations[m];
1743
+ const month = monthNames[+m];
1530
1744
  return month && month[opts.short ? 1 : 0];
1531
1745
  }
1532
1746
  function getWeekday(d, opts) {
@@ -1539,7 +1753,9 @@ var en = {
1539
1753
  fallback: "an unrecognizable cron pattern",
1540
1754
  options: normalizeOptions,
1541
1755
  reboot: "at system startup",
1542
- sentence: (description) => "Runs " + description + "."
1756
+ // A description ending in an abbreviation already carries its period
1757
+ // ("…9 a.m."), so closing the sentence must not double it.
1758
+ sentence: (description) => "Runs " + description + (description.endsWith(".") ? "" : ".")
1543
1759
  };
1544
1760
  var en_default = en;
1545
1761