cronli5 0.1.7 → 0.2.1

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
@@ -133,6 +133,43 @@ function orderWeekdaysForDisplay(segments) {
133
133
  function toFieldNumber(token, numberMap) {
134
134
  return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
135
135
  }
136
+ function segmentsOf(ir, field) {
137
+ return ir.analyses.segments[field] ?? [];
138
+ }
139
+ function stepSegment(ir, field) {
140
+ return segmentsOf(ir, field)[0];
141
+ }
142
+ function singleValues(segments) {
143
+ const values = [];
144
+ for (const segment of segments) {
145
+ if (segment.kind !== "single") {
146
+ return null;
147
+ }
148
+ values.push(+segment.value);
149
+ }
150
+ return values;
151
+ }
152
+ function offsetCleanStride(stride) {
153
+ return stride.start < stride.interval && 24 % stride.interval === 0;
154
+ }
155
+ function hourListStride(values) {
156
+ if (values.length < 2) {
157
+ return null;
158
+ }
159
+ const interval = values[1] - values[0];
160
+ if (interval < 2) {
161
+ return null;
162
+ }
163
+ for (let i = 2; i < values.length; i += 1) {
164
+ if (values[i] - values[i - 1] !== interval) {
165
+ return null;
166
+ }
167
+ }
168
+ if (values[0] !== 0 && values.length < 5) {
169
+ return null;
170
+ }
171
+ return { interval, last: values[values.length - 1], start: values[0] };
172
+ }
136
173
 
137
174
  // src/core/validate.ts
138
175
  function validateCronPattern(cronPattern) {
@@ -481,6 +518,9 @@ function isDiscreteList(field) {
481
518
  function isDiscreteHours(hourField) {
482
519
  return hourField !== "*" && !isPlainRange(hourField) && !isPlainStep(hourField);
483
520
  }
521
+ function isOpenStep(field) {
522
+ return field.indexOf("/") !== -1 && field.indexOf("-") === -1 && field.indexOf(",") === -1;
523
+ }
484
524
 
485
525
  // src/core/analyze.ts
486
526
  function getOccurrences(start, interval, max) {
@@ -617,9 +657,9 @@ function analyze(pattern) {
617
657
  segments
618
658
  };
619
659
  const content = { analyses, pattern, shapes };
620
- return { ...content, plan: selectStrategy(content) };
660
+ return { ...content, plan: selectPlan(content) };
621
661
  }
622
- function selectStrategy(content) {
662
+ function selectPlan(content) {
623
663
  const { analyses, pattern, shapes } = content;
624
664
  if (pattern.second !== "0") {
625
665
  const seconds = planSeconds(pattern, shapes, analyses);
@@ -875,7 +915,8 @@ var dialects = {
875
915
  pm: "p.m.",
876
916
  sep: ":",
877
917
  serialComma: true,
878
- through: " through "
918
+ through: " through ",
919
+ untilWindow: true
879
920
  },
880
921
  house: {
881
922
  am: "AM",
@@ -892,7 +933,7 @@ var dialects = {
892
933
  };
893
934
  function resolveDialect(dialect) {
894
935
  if (typeof dialect === "object" && dialect !== null) {
895
- return { ...dialects.us, ...dialect };
936
+ return { ...dialects.us, untilWindow: false, ...dialect };
896
937
  }
897
938
  const name = dialect === "uk" ? "gb" : dialect;
898
939
  return dialects[name] || dialects.us;
@@ -964,7 +1005,9 @@ function normalizeOptions(options) {
964
1005
  };
965
1006
  }
966
1007
  function describe(ir, opts) {
967
- return applyYear(render(ir, ir.plan, opts), ir, opts);
1008
+ const body = confinement(ir, opts) ?? render(ir, ir.plan, opts);
1009
+ const lead = isDayUnion(ir, opts) ? dayUnionMonthLead(ir, opts) : "";
1010
+ return applyYear(lead + body, ir, opts);
968
1011
  }
969
1012
  function render(ir, plan, opts) {
970
1013
  const renderer = renderers[plan.kind];
@@ -1049,7 +1092,7 @@ function secondsClause(ir, anchor, opts) {
1049
1092
  }
1050
1093
  if (shape === "step") {
1051
1094
  return stepCycle60(
1052
- ir.analyses.segments.second[0],
1095
+ stepSegment(ir, "second"),
1053
1096
  "second",
1054
1097
  anchor,
1055
1098
  opts
@@ -1057,19 +1100,19 @@ function secondsClause(ir, anchor, opts) {
1057
1100
  }
1058
1101
  if (shape === "range") {
1059
1102
  const bounds = secondField.split("-");
1060
- const num = seriesNumber(bounds, opts);
1103
+ const num = seriesNumber();
1061
1104
  return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the " + anchor;
1062
1105
  }
1063
1106
  if (shape === "single") {
1064
1107
  return "at " + getNumber(secondField, opts) + " " + pluralize(secondField, "second") + " past the " + anchor;
1065
1108
  }
1066
1109
  return strideFromSegments(
1067
- ir.analyses.segments.second,
1110
+ segmentsOf(ir, "second"),
1068
1111
  "second",
1069
1112
  anchor,
1070
1113
  opts
1071
1114
  ) ?? listPastThe(
1072
- segmentWords(ir.analyses.segments.second, opts),
1115
+ segmentWords(segmentsOf(ir, "second"), opts),
1073
1116
  "second",
1074
1117
  anchor,
1075
1118
  opts
@@ -1086,15 +1129,15 @@ function renderRangeOfMinutes(ir, plan, opts) {
1086
1129
  return minuteRangeLead(ir.pattern.minute, opts) + trailingQualifier(ir, opts);
1087
1130
  }
1088
1131
  function renderMultipleMinutes(ir, plan, opts) {
1089
- const stride = strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts);
1132
+ const stride = strideFromSegments(segmentsOf(ir, "minute"), "minute", "hour", opts);
1090
1133
  return (stride ?? listPastThe(segmentWords(
1091
- ir.analyses.segments.minute,
1134
+ segmentsOf(ir, "minute"),
1092
1135
  opts
1093
1136
  ), "minute", "hour", opts)) + trailingQualifier(ir, opts);
1094
1137
  }
1095
1138
  function renderMinuteFrequency(ir, plan, opts) {
1096
1139
  let phrase = stepCycle60(
1097
- ir.analyses.segments.minute[0],
1140
+ stepSegment(ir, "minute"),
1098
1141
  "minute",
1099
1142
  "hour",
1100
1143
  opts
@@ -1103,9 +1146,14 @@ function renderMinuteFrequency(ir, plan, opts) {
1103
1146
  const cadence = unevenHourCadence(ir, opts);
1104
1147
  phrase += cadence ? ", " + cadence : " during the " + hourTimesFromPlan(ir, plan.hours.times, false, opts) + " hours";
1105
1148
  } else if (plan.hours.kind === "window") {
1106
- phrase += " " + hourWindow(plan.hours, opts);
1149
+ phrase += " " + rangeWindow({
1150
+ continuous: false,
1151
+ from: plan.hours.from,
1152
+ throughMinute: plan.hours.last,
1153
+ to: plan.hours.to
1154
+ }, opts);
1107
1155
  } else if (plan.hours.kind === "step") {
1108
- phrase += " " + everyNthHour(ir.analyses.segments.hour[0], opts);
1156
+ phrase += " " + everyNthHour(stepSegment(ir, "hour"), opts);
1109
1157
  }
1110
1158
  return phrase + trailingQualifier(ir, opts);
1111
1159
  }
@@ -1123,15 +1171,21 @@ function renderMinutesAcrossHours(ir, plan, opts) {
1123
1171
  }
1124
1172
  return "every minute during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
1125
1173
  }
1126
- const lead = plan.form === "range" ? minuteRangeLead(ir.pattern.minute, opts) : (
1127
- // The 'list' form is a minute list, which has segments; an offset/uneven
1128
- // step enumerated to that list reads as a stride.
1129
- strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1130
- segmentWords(ir.analyses.segments.minute, opts),
1131
- "minute",
1132
- "hour",
1133
- opts
1134
- )
1174
+ if (plan.form === "range") {
1175
+ const lead2 = minuteRangeLead(ir.pattern.minute, opts);
1176
+ if (cadence !== null) {
1177
+ return lead2 + ", " + cadence + trailingQualifier(ir, opts);
1178
+ }
1179
+ if (singleHourFire(plan.times)) {
1180
+ return lead2 + ", at " + hourTimesFromPlan(ir, plan.times, true, opts) + trailingQualifier(ir, opts);
1181
+ }
1182
+ return lead2 + " during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
1183
+ }
1184
+ const lead = strideFromSegments(segmentsOf(ir, "minute"), "minute", "hour", opts) ?? listPastThe(
1185
+ segmentWords(segmentsOf(ir, "minute"), opts),
1186
+ "minute",
1187
+ "hour",
1188
+ opts
1135
1189
  );
1136
1190
  if (cadence !== null) {
1137
1191
  return lead + ", " + cadence + trailingQualifier(ir, opts);
@@ -1153,12 +1207,12 @@ function everyNthHour(segment, opts) {
1153
1207
  return start === 0 ? base : base + " starting at " + getTime({ hour: start, minute: 0 }, opts);
1154
1208
  }
1155
1209
  function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
1156
- const segment = ir.analyses.segments.hour[0];
1210
+ const segment = stepSegment(ir, "hour");
1157
1211
  if (plan.form === "wildcard") {
1158
1212
  return "every minute " + everyNthHour(segment, opts) + trailingQualifier(ir, opts);
1159
1213
  }
1160
- const lead = plan.form === "list" ? strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1161
- segmentWords(ir.analyses.segments.minute, opts),
1214
+ const lead = plan.form === "list" ? strideFromSegments(segmentsOf(ir, "minute"), "minute", "hour", opts) ?? listPastThe(
1215
+ segmentWords(segmentsOf(ir, "minute"), opts),
1162
1216
  "minute",
1163
1217
  "hour",
1164
1218
  opts
@@ -1168,7 +1222,7 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
1168
1222
  }
1169
1223
  function minuteRangeLead(minuteField, opts) {
1170
1224
  const bounds = minuteField.split("-");
1171
- const num = seriesNumber(bounds, opts);
1225
+ const num = seriesNumber();
1172
1226
  return "every minute from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the hour";
1173
1227
  }
1174
1228
  function renderEveryHour(ir, plan, opts) {
@@ -1189,12 +1243,12 @@ function rangeMinuteLead(ir, opts) {
1189
1243
  return "every hour";
1190
1244
  }
1191
1245
  return strideFromSegments(
1192
- ir.analyses.segments.minute,
1246
+ segmentsOf(ir, "minute"),
1193
1247
  "minute",
1194
1248
  "hour",
1195
1249
  opts
1196
1250
  ) ?? listPastThe(
1197
- segmentWords(ir.analyses.segments.minute, opts),
1251
+ segmentWords(segmentsOf(ir, "minute"), opts),
1198
1252
  "minute",
1199
1253
  "hour",
1200
1254
  opts
@@ -1205,14 +1259,28 @@ function renderHourStep(ir, plan, opts) {
1205
1259
  if (cadence !== null) {
1206
1260
  return cadence + trailingQualifier(ir, opts);
1207
1261
  }
1208
- return stepHours(ir.analyses.segments.hour[0], opts) + trailingQualifier(ir, opts);
1262
+ return stepHours(stepSegment(ir, "hour"), opts) + trailingQualifier(ir, opts);
1209
1263
  }
1210
1264
  function boundedWindow(plan) {
1211
- const last = plan.minuteForm === "wildcard" ? plan.boundMinute ?? 0 : 0;
1212
- return { from: plan.from, last, to: plan.to };
1265
+ const continuous = plan.minuteForm === "wildcard";
1266
+ const closeMinute = continuous ? plan.boundMinute ?? 0 : 0;
1267
+ return { from: plan.from, closeMinute, to: plan.to, continuous };
1268
+ }
1269
+ function rangeWindow(window, opts) {
1270
+ const { from, to, throughMinute, continuous } = window;
1271
+ const open = "from " + getTime({ hour: from, minute: 0 }, opts);
1272
+ if (opts.style.untilWindow && !opts.short && from !== to) {
1273
+ return continuous ? open + " until " + getTime({ hour: (to + 1) % 24, minute: 0 }, opts) : open + through(opts) + getTime({ hour: to, minute: 0 }, opts);
1274
+ }
1275
+ return open + through(opts) + getTime({ hour: to, minute: throughMinute }, opts);
1213
1276
  }
1214
1277
  function hourWindow(window, opts) {
1215
- return "from " + getTime({ hour: window.from, minute: 0 }, opts) + through(opts) + getTime({ hour: window.to, minute: window.last }, opts);
1278
+ return rangeWindow({
1279
+ continuous: window.continuous,
1280
+ from: window.from,
1281
+ throughMinute: window.closeMinute,
1282
+ to: window.to
1283
+ }, opts);
1216
1284
  }
1217
1285
  function renderClockTimes(ir, plan, opts) {
1218
1286
  if (ir.shapes.minute === "single") {
@@ -1231,7 +1299,10 @@ function renderClockTimes(ir, plan, opts) {
1231
1299
  plain
1232
1300
  }, opts);
1233
1301
  });
1234
- return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts);
1302
+ return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts) + dayUnionTrail(ir, opts);
1303
+ }
1304
+ function dayUnionTrail(ir, opts) {
1305
+ return isDayUnion(ir, opts) ? dayUnionCondition(ir, opts) : "";
1235
1306
  }
1236
1307
  function renderCompactClockTimes(ir, plan, opts) {
1237
1308
  if (plan.fold) {
@@ -1239,20 +1310,20 @@ function renderCompactClockTimes(ir, plan, opts) {
1239
1310
  if (cadence2 !== null) {
1240
1311
  return cadence2;
1241
1312
  }
1242
- const hasRange = ir.analyses.segments.hour.some(function range(segment) {
1313
+ const hasRange = segmentsOf(ir, "hour").some(function range(segment) {
1243
1314
  return segment.kind === "range";
1244
1315
  });
1245
1316
  if (hasRange && !ir.analyses.clockSecond) {
1246
1317
  return foldedHourWindows(ir, plan, opts) + trailingQualifier(ir, opts);
1247
1318
  }
1248
1319
  const fold = { minute: plan.minute, second: ir.analyses.clockSecond };
1249
- return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
1320
+ return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts) + dayUnionTrail(ir, opts);
1250
1321
  }
1251
1322
  const minuteLead = (
1252
1323
  // The non-fold branch is a minute list, which has segments. An
1253
1324
  // offset/uneven step enumerated to that list reads as a stride.
1254
- strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1255
- segmentWords(ir.analyses.segments.minute, opts),
1325
+ strideFromSegments(segmentsOf(ir, "minute"), "minute", "hour", opts) ?? listPastThe(
1326
+ segmentWords(segmentsOf(ir, "minute"), opts),
1256
1327
  "minute",
1257
1328
  "hour",
1258
1329
  opts
@@ -1265,26 +1336,162 @@ function renderCompactClockTimes(ir, plan, opts) {
1265
1336
  function foldedHourWindows(ir, plan, opts) {
1266
1337
  const minute = plan.minute;
1267
1338
  const windows = [];
1268
- const singles = [];
1269
- ir.analyses.segments.hour.forEach(function classify(segment) {
1339
+ const times = collectHourOutliers(ir).map(function time(hour) {
1340
+ return getTime({ hour, minute }, opts);
1341
+ });
1342
+ segmentsOf(ir, "hour").forEach(function classify(segment) {
1270
1343
  if (segment.kind === "range") {
1271
- windows.push("from " + getTime(
1272
- { hour: segment.bounds[0], minute: 0 },
1273
- opts
1274
- ) + through(opts) + getTime({ hour: segment.bounds[1], minute }, opts));
1275
- } else if (segment.kind === "step") {
1276
- singles.push(...segment.fires);
1277
- } else {
1278
- singles.push(+segment.value);
1344
+ windows.push(rangeWindow({
1345
+ continuous: false,
1346
+ from: +segment.bounds[0],
1347
+ throughMinute: minute,
1348
+ to: +segment.bounds[1]
1349
+ }, opts));
1350
+ }
1351
+ });
1352
+ const phrase = rangeMinuteLead(ir, opts) + " " + joinList(windows, opts);
1353
+ return phrase + outlierTail(times, opts);
1354
+ }
1355
+ function collectHourOutliers(ir) {
1356
+ const hours = [];
1357
+ segmentsOf(ir, "hour").forEach(function classify(segment) {
1358
+ if (segment.kind === "step") {
1359
+ hours.push(...segment.fires);
1360
+ } else if (segment.kind !== "range") {
1361
+ hours.push(+segment.value);
1279
1362
  }
1280
1363
  });
1281
- let phrase = rangeMinuteLead(ir, opts) + " " + joinList(windows, opts);
1282
- if (singles.length) {
1283
- phrase += " and at " + joinList(singles.map(function time(hour) {
1284
- return getTime({ hour, minute }, opts);
1285
- }), opts);
1364
+ return hours;
1365
+ }
1366
+ function outlierTail(times, opts) {
1367
+ if (!times.length) {
1368
+ return "";
1286
1369
  }
1287
- return phrase;
1370
+ return " and at " + joinList(times, opts);
1371
+ }
1372
+ function isCadenceField(token) {
1373
+ return token === "*" || token.startsWith("*/") && token.indexOf("-") === -1;
1374
+ }
1375
+ function leadingCadence(ir, opts) {
1376
+ const { second, minute } = ir.pattern;
1377
+ if (isCadenceField(second)) {
1378
+ return { secondLead: true, text: secondsClause(ir, "minute", opts) };
1379
+ }
1380
+ if (second === "0" && isCadenceField(minute)) {
1381
+ const text = minute === "*" ? "every minute" : (
1382
+ // A clean minute step's first segment is a step segment.
1383
+ stepCycle60(
1384
+ stepSegment(ir, "minute"),
1385
+ "minute",
1386
+ "hour",
1387
+ opts
1388
+ )
1389
+ );
1390
+ return { secondLead: false, text };
1391
+ }
1392
+ return null;
1393
+ }
1394
+ function minuteConfinement(ir, opts) {
1395
+ const minute = ir.pattern.minute;
1396
+ if (minute === "*") {
1397
+ return "";
1398
+ }
1399
+ if (isCadenceField(minute)) {
1400
+ return " of every other minute";
1401
+ }
1402
+ const segments = segmentsOf(ir, "minute");
1403
+ if (ir.shapes.minute === "single") {
1404
+ return " during minute :" + pad(minute);
1405
+ }
1406
+ if (ir.shapes.minute === "range") {
1407
+ const bounds = minute.split("-");
1408
+ return " during minutes :" + pad(bounds[0]) + through(opts) + ":" + pad(bounds[1]);
1409
+ }
1410
+ const values = segmentWords(segments, opts).map(function colon(word) {
1411
+ return ":" + pad(word);
1412
+ });
1413
+ return " during minutes " + joinList(values, opts);
1414
+ }
1415
+ function hourConfinement(ir, opts) {
1416
+ const hour = ir.pattern.hour;
1417
+ if (hour === "*") {
1418
+ const minutePinned = ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute);
1419
+ return minutePinned ? " of every hour" : "";
1420
+ }
1421
+ if (isCadenceField(hour)) {
1422
+ return hour === "*/2" ? " of every other hour" : "";
1423
+ }
1424
+ if (ir.shapes.hour === "single") {
1425
+ const h = +hour;
1426
+ if (ir.shapes.minute === "step") {
1427
+ return " from " + getTime({ hour: h, minute: 0 }, opts) + " until " + getTime({ hour: (h + 1) % 24, minute: 0 }, opts);
1428
+ }
1429
+ if (ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute)) {
1430
+ return " at " + getTime({ hour: h, minute: 0 }, opts);
1431
+ }
1432
+ return " of the " + getTime({ hour: h, minute: 0 }, opts) + " hour";
1433
+ }
1434
+ if (ir.shapes.hour === "range") {
1435
+ const bounds = hour.split("-");
1436
+ return " " + rangeWindow({
1437
+ continuous: ir.pattern.minute === "*",
1438
+ from: +bounds[0],
1439
+ throughMinute: 0,
1440
+ to: +bounds[1]
1441
+ }, opts);
1442
+ }
1443
+ return " during the " + hourSegmentTimes(ir, { minute: 0, second: null }, false, opts) + " hours";
1444
+ }
1445
+ function isContiguousHourRange(ir) {
1446
+ return ir.shapes.hour === "range";
1447
+ }
1448
+ function confinableHour(ir) {
1449
+ if (ir.shapes.hour !== "step") {
1450
+ return true;
1451
+ }
1452
+ const segment = stepSegment(ir, "hour");
1453
+ return ir.pattern.hour === "*/2" || segment.startToken.indexOf("-") !== -1;
1454
+ }
1455
+ function isMinuteStride(ir) {
1456
+ if (ir.shapes.minute !== "list") {
1457
+ return false;
1458
+ }
1459
+ const values = singleValues(segmentsOf(ir, "minute"));
1460
+ return values !== null && arithmeticStep(values) !== null;
1461
+ }
1462
+ function confinementEligible(ir, lead) {
1463
+ const { minute, hour } = ir.pattern;
1464
+ const minuteStep = isCadenceField(minute) && minute !== "*";
1465
+ if (!confinableHour(ir)) {
1466
+ return false;
1467
+ }
1468
+ if (lead.secondLead) {
1469
+ if (minuteStep) {
1470
+ return minute === "*/2" && !isContiguousHourRange(ir);
1471
+ }
1472
+ if (isMinuteStride(ir) || ir.shapes.minute === "list" && ir.shapes.hour === "list") {
1473
+ return false;
1474
+ }
1475
+ return true;
1476
+ }
1477
+ if (hour === "*/2") {
1478
+ return true;
1479
+ }
1480
+ return ir.shapes.hour === "single" && minute === "*/2";
1481
+ }
1482
+ function confinement(ir, opts) {
1483
+ if (!opts.style.untilWindow || opts.short) {
1484
+ return null;
1485
+ }
1486
+ if (ir.pattern.minute === "*" && ir.pattern.hour === "*") {
1487
+ return null;
1488
+ }
1489
+ const lead = leadingCadence(ir, opts);
1490
+ if (!lead || !confinementEligible(ir, lead)) {
1491
+ return null;
1492
+ }
1493
+ const minutePart = lead.secondLead ? minuteConfinement(ir, opts) : "";
1494
+ return lead.text + minutePart + hourConfinement(ir, opts) + trailingQualifier(ir, opts);
1288
1495
  }
1289
1496
  var renderers = {
1290
1497
  clockTimes: renderClockTimes,
@@ -1316,19 +1523,9 @@ function renderStride(stride, opts) {
1316
1523
  if (start < interval && tiles) {
1317
1524
  return cadence + " from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
1318
1525
  }
1319
- const num = seriesNumber([start, last], opts);
1526
+ const num = seriesNumber();
1320
1527
  return cadence + " from " + num(start) + through(opts) + num(last) + " " + pluralize(last, unit) + " past the " + anchor;
1321
1528
  }
1322
- function singleValues(segments) {
1323
- const values = [];
1324
- for (const segment of segments) {
1325
- if (segment.kind !== "single") {
1326
- return null;
1327
- }
1328
- values.push(+segment.value);
1329
- }
1330
- return values;
1331
- }
1332
1529
  function strideFromSegments(segments, unit, anchor, opts) {
1333
1530
  const values = singleValues(segments);
1334
1531
  const step = values && arithmeticStep(values);
@@ -1377,9 +1574,6 @@ function hourStrideCadence(stride, opts) {
1377
1574
  }
1378
1575
  return cadence + " from " + getTime({ hour: start, minute: 0 }, opts) + through(opts) + getTime({ hour: last, minute: 0 }, opts);
1379
1576
  }
1380
- function offsetCleanStride(stride) {
1381
- return stride.start < stride.interval && 24 % stride.interval === 0;
1382
- }
1383
1577
  function unevenHourCadence(ir, opts) {
1384
1578
  const stride = hourStride(ir);
1385
1579
  if (!stride || offsetCleanStride(stride)) {
@@ -1387,26 +1581,8 @@ function unevenHourCadence(ir, opts) {
1387
1581
  }
1388
1582
  return hourStrideCadence(stride, opts);
1389
1583
  }
1390
- function hourListStride(values) {
1391
- if (values.length < 2) {
1392
- return null;
1393
- }
1394
- const interval = values[1] - values[0];
1395
- if (interval < 2) {
1396
- return null;
1397
- }
1398
- for (let i = 2; i < values.length; i += 1) {
1399
- if (values[i] - values[i - 1] !== interval) {
1400
- return null;
1401
- }
1402
- }
1403
- if (values[0] !== 0 && values.length < 5) {
1404
- return null;
1405
- }
1406
- return { interval, last: values[values.length - 1], start: values[0] };
1407
- }
1408
1584
  function hourStride(ir) {
1409
- const segments = ir.analyses.segments.hour;
1585
+ const segments = segmentsOf(ir, "hour");
1410
1586
  if (segments.length === 1 && segments[0].kind === "step") {
1411
1587
  const segment = segments[0];
1412
1588
  if (segment.fires.length < 2) {
@@ -1443,9 +1619,9 @@ function hourCadence(ir, minute, opts) {
1443
1619
  if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
1444
1620
  return null;
1445
1621
  }
1446
- const confinement = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
1447
- if (confinement) {
1448
- return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
1622
+ const minuteZeroStride = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
1623
+ if (minuteZeroStride) {
1624
+ return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(minuteZeroStride, opts) + trailingQualifier(ir, opts);
1449
1625
  }
1450
1626
  if (minute === 0 && ir.pattern.second === "0") {
1451
1627
  return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
@@ -1453,7 +1629,7 @@ function hourCadence(ir, minute, opts) {
1453
1629
  return hourCadenceLead(ir, minute, opts) + ", " + hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
1454
1630
  }
1455
1631
  function cleanStrideSegment(ir) {
1456
- const segments = ir.analyses.segments.hour;
1632
+ const segments = segmentsOf(ir, "hour");
1457
1633
  const segment = segments.length === 1 && segments[0];
1458
1634
  if (!segment || segment.kind !== "step" || segment.startToken.indexOf("-") !== -1 || !(segment.interval in stepOrdinals)) {
1459
1635
  return null;
@@ -1461,32 +1637,28 @@ function cleanStrideSegment(ir) {
1461
1637
  return segment;
1462
1638
  }
1463
1639
  function hasHourWindow(ir) {
1464
- return ir.analyses.segments.hour.some(function range(segment) {
1640
+ return segmentsOf(ir, "hour").some(function range(segment) {
1465
1641
  return segment.kind === "range";
1466
1642
  });
1467
1643
  }
1468
1644
  function hourRangeWindowTail(ir, opts) {
1469
1645
  const windows = [];
1470
- const singles = [];
1471
- ir.analyses.segments.hour.forEach(function classify(segment) {
1646
+ const outlierHours = collectHourOutliers(ir);
1647
+ segmentsOf(ir, "hour").forEach(function classify(segment) {
1472
1648
  if (segment.kind === "range") {
1473
- windows.push("from " + getTime(
1474
- { hour: +segment.bounds[0], minute: 0 },
1475
- opts
1476
- ) + through(opts) + getTime({ hour: +segment.bounds[1], minute: 0 }, opts));
1477
- } else if (segment.kind === "step") {
1478
- singles.push(...segment.fires);
1479
- } else {
1480
- singles.push(+segment.value);
1649
+ windows.push(rangeWindow({
1650
+ continuous: false,
1651
+ from: +segment.bounds[0],
1652
+ throughMinute: 0,
1653
+ to: +segment.bounds[1]
1654
+ }, opts));
1481
1655
  }
1482
1656
  });
1483
- let phrase = "every hour " + joinList(windows, opts);
1484
- if (singles.length) {
1485
- phrase += " and at " + joinList(singles.map(function time(hour) {
1486
- return getTime({ hour, minute: 0 }, opts);
1487
- }), opts);
1488
- }
1489
- return phrase;
1657
+ const phrase = "every hour " + joinList(windows, opts);
1658
+ const times = outlierHours.map(function time(hour) {
1659
+ return getTime({ hour, minute: 0 }, opts);
1660
+ });
1661
+ return phrase + outlierTail(times, opts);
1490
1662
  }
1491
1663
  function hourRangeCadence(ir, minute, opts) {
1492
1664
  if (minute !== 0 || !hasHourWindow(ir)) {
@@ -1500,25 +1672,29 @@ function hourRangeCadence(ir, minute, opts) {
1500
1672
  }
1501
1673
  return hourCadenceLead(ir, minute, opts) + ", " + hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
1502
1674
  }
1503
- function seriesNumber(values, opts) {
1504
- const anyBig = values.some(function big(v) {
1505
- return +v > 10;
1506
- });
1675
+ function seriesNumber() {
1507
1676
  return function format(n) {
1508
- return anyBig ? "" + n : getNumber(n, opts);
1677
+ return "" + n;
1678
+ };
1679
+ }
1680
+ function listNumber(count, opts) {
1681
+ return count > 1 ? function asNumeral(n) {
1682
+ return "" + n;
1683
+ } : function spelled(n) {
1684
+ return getNumber(n, opts);
1509
1685
  };
1510
1686
  }
1511
1687
  function numberWords(fires, opts) {
1512
- return fires.map(seriesNumber(fires, opts));
1688
+ return fires.map(listNumber(fires.length, opts));
1513
1689
  }
1514
1690
  function segmentWords(segments, opts) {
1515
- const values = segments.flatMap(function collect(segment) {
1691
+ const count = segments.reduce(function tally(sum, segment) {
1516
1692
  if (segment.kind === "range") {
1517
- return segment.bounds;
1693
+ return sum + 1;
1518
1694
  }
1519
- return segment.kind === "step" ? segment.fires : [segment.value];
1520
- });
1521
- const num = seriesNumber(values, opts);
1695
+ return sum + (segment.kind === "step" ? segment.fires.length : 1);
1696
+ }, 0);
1697
+ const num = listNumber(count, opts);
1522
1698
  return segments.flatMap(function word(segment) {
1523
1699
  if (segment.kind === "range") {
1524
1700
  return [num(segment.bounds[0]) + through(opts) + num(segment.bounds[1])];
@@ -1550,6 +1726,9 @@ function hourTimes(hours, opts) {
1550
1726
  });
1551
1727
  return joinList(times, opts);
1552
1728
  }
1729
+ function singleHourFire(times) {
1730
+ return times.kind === "fires" && times.fires.length === 1;
1731
+ }
1553
1732
  function hourTimesFromPlan(ir, times, atContext, opts) {
1554
1733
  if (times.kind === "fires") {
1555
1734
  return hourTimes(times.fires, opts);
@@ -1564,7 +1743,7 @@ function segmentHours(segment) {
1564
1743
  }
1565
1744
  function hourSegmentTimes(ir, fold, atContext, opts) {
1566
1745
  const { minute, second } = fold;
1567
- const segments = ir.analyses.segments.hour;
1746
+ const segments = segmentsOf(ir, "hour");
1568
1747
  const plain = mixedTwelve(segments.flatMap(function entries(segment) {
1569
1748
  return segmentHours(segment).map(function entry(hour) {
1570
1749
  return { hour: +hour, minute, second };
@@ -1597,28 +1776,47 @@ function disambiguateTimes(pieces, segments, atContext) {
1597
1776
  return index === 0 ? piece : "at " + piece;
1598
1777
  });
1599
1778
  }
1600
- function joinList(items, opts) {
1779
+ function joinWith(items, conjunction, opts) {
1601
1780
  if (items.length <= 1) {
1602
1781
  return items.join("");
1603
1782
  }
1604
1783
  if (items.length === 2) {
1605
- return items[0] + " and " + items[1];
1784
+ return items[0] + conjunction + items[1];
1606
1785
  }
1607
- const and = opts.style.serialComma ? ", and " : " and ";
1608
- return items.slice(0, -1).join(", ") + and + items[items.length - 1];
1786
+ const tail = opts.style.serialComma ? "," + conjunction : conjunction;
1787
+ return items.slice(0, -1).join(", ") + tail + items[items.length - 1];
1788
+ }
1789
+ function joinList(items, opts) {
1790
+ return joinWith(items, " and ", opts);
1609
1791
  }
1610
- var trailingWords = { all: "", month: "in ", stepDate: "on ", weekday: "on " };
1792
+ function joinOr(items, opts) {
1793
+ return joinWith(items, " or ", opts);
1794
+ }
1795
+ var trailingWords = {
1796
+ all: "",
1797
+ month: "in ",
1798
+ recurringWeekday: true,
1799
+ stepDate: "on ",
1800
+ weekday: "on "
1801
+ };
1611
1802
  var leadingWords = {
1612
1803
  all: "every day",
1613
1804
  month: "every day in ",
1805
+ recurringWeekday: false,
1614
1806
  stepDate: "",
1615
1807
  weekday: "every "
1616
1808
  };
1617
1809
  function trailingQualifier(ir, opts) {
1810
+ if (isDayUnion(ir, opts)) {
1811
+ return dayUnionCondition(ir, opts);
1812
+ }
1618
1813
  const phrase = dayQualifier(ir, trailingWords, opts);
1619
1814
  return phrase && " " + phrase;
1620
1815
  }
1621
1816
  function interpretDayQualifier(ir, opts) {
1817
+ if (isDayUnion(ir, opts)) {
1818
+ return "";
1819
+ }
1622
1820
  return dayQualifier(ir, leadingWords, opts) + " ";
1623
1821
  }
1624
1822
  function dayQualifier(ir, words, opts) {
@@ -1630,7 +1828,11 @@ function dayQualifier(ir, words, opts) {
1630
1828
  return datePhrase(ir, words, opts);
1631
1829
  }
1632
1830
  if (pattern.weekday !== "*") {
1633
- const weekdays = quartzWeekdayPhrase(pattern.weekday, opts) || words.weekday + weekdayPhrase(ir, opts);
1831
+ const quartzWeekday = quartzWeekdayPhrase(pattern.weekday, opts);
1832
+ if (quartzWeekday) {
1833
+ return monthScopeForRecurrence(quartzWeekday, ir, opts);
1834
+ }
1835
+ const weekdays = words.weekday + weekdayPhrase(ir, words.recurringWeekday, opts);
1634
1836
  return weekdays + monthScope(ir, opts);
1635
1837
  }
1636
1838
  if (pattern.month !== "*") {
@@ -1642,10 +1844,14 @@ function datePhrase(ir, words, opts) {
1642
1844
  const pattern = ir.pattern;
1643
1845
  const quartzDate = quartzDatePhrase(pattern.date, opts);
1644
1846
  if (quartzDate) {
1645
- return quartzDate + monthScope(ir, opts);
1847
+ return monthScopeForRecurrence(quartzDate, ir, opts);
1646
1848
  }
1647
1849
  if (isOpenStep(pattern.date)) {
1648
- return words.stepDate + stepDates(pattern.date) + monthScope(ir, opts);
1850
+ return monthScopeForRecurrence(
1851
+ words.stepDate + stepDates(pattern.date),
1852
+ ir,
1853
+ opts
1854
+ );
1649
1855
  }
1650
1856
  if (pattern.month !== "*" && !monthFoldsIntoDate(ir)) {
1651
1857
  return "on the " + dateOrdinals(ir, opts) + monthScope(ir, opts);
@@ -1657,13 +1863,88 @@ function datePhrase(ir, words, opts) {
1657
1863
  }
1658
1864
  function monthFoldsIntoDate(ir) {
1659
1865
  return !oddEvenMonth(ir.pattern.month) && // Reached only with a restricted month, which has segments.
1660
- ir.analyses.segments.month.every(function flat(segment) {
1866
+ segmentsOf(ir, "month").every(function flat(segment) {
1661
1867
  return segment.kind !== "range";
1662
1868
  });
1663
1869
  }
1870
+ function isDayUnion(ir, opts) {
1871
+ return ir.pattern.date !== "*" && ir.pattern.weekday !== "*" && !!opts.style.untilWindow && !opts.short;
1872
+ }
1873
+ function dayUnionCondition(ir, opts) {
1874
+ const pieces = [
1875
+ ...dayUnionDatePieces(ir, opts),
1876
+ ...dayUnionWeekdayPieces(ir, opts)
1877
+ ];
1878
+ return " whenever the day is " + joinOr(pieces, opts);
1879
+ }
1880
+ function dayUnionMonthLead(ir, opts) {
1881
+ if (ir.pattern.month === "*") {
1882
+ return "";
1883
+ }
1884
+ return "in " + monthName(ir, opts) + " ";
1885
+ }
1886
+ function dayUnionDatePieces(ir, opts) {
1887
+ const dateField = ir.pattern.date;
1888
+ const quartz = quartzDatePhrase(dateField, opts);
1889
+ if (quartz) {
1890
+ return [quartz.replace(/^on /, "")];
1891
+ }
1892
+ const oddEven = oddEvenDay(dateField);
1893
+ if (oddEven) {
1894
+ return [oddEven];
1895
+ }
1896
+ const pieces = [];
1897
+ segmentsOf(ir, "date").forEach(function expand(segment) {
1898
+ if (segment.kind === "range") {
1899
+ pieces.push("from the " + getOrdinal(segment.bounds[0]) + through(opts) + "the " + getOrdinal(segment.bounds[1]));
1900
+ } else if (segment.kind === "step") {
1901
+ segment.fires.forEach(function fire(value) {
1902
+ pieces.push("the " + getOrdinal(value));
1903
+ });
1904
+ } else {
1905
+ pieces.push("the " + getOrdinal(segment.value));
1906
+ }
1907
+ });
1908
+ return pieces;
1909
+ }
1910
+ function dayUnionWeekdayPieces(ir, opts) {
1911
+ const weekdayField = ir.pattern.weekday;
1912
+ const quartz = quartzWeekdayPhrase(weekdayField, opts);
1913
+ if (quartz) {
1914
+ return [quartz.replace(/^on /, "")];
1915
+ }
1916
+ const pieces = [];
1917
+ segmentsOf(ir, "weekday").forEach(function expand(segment) {
1918
+ if (segment.kind === "range" && segment.bounds[0] === "1" && segment.bounds[1] === "5") {
1919
+ pieces.push("a weekday");
1920
+ } else if (segment.kind === "range") {
1921
+ pieces.push("a " + getWeekday(segment.bounds[0], opts) + through(opts) + "a " + getWeekday(segment.bounds[1], opts));
1922
+ } else if (segment.kind === "step") {
1923
+ segment.fires.forEach(function fire(value) {
1924
+ pieces.push("a " + getWeekday(value, opts));
1925
+ });
1926
+ } else {
1927
+ pieces.push("a " + getWeekday(segment.value, opts));
1928
+ }
1929
+ });
1930
+ return pieces;
1931
+ }
1932
+ function oddEvenDay(dateField) {
1933
+ if (!isOpenStep(dateField)) {
1934
+ return null;
1935
+ }
1936
+ const [start, step] = dateField.split("/");
1937
+ if (+step !== 2) {
1938
+ return null;
1939
+ }
1940
+ if (start === "*" || start === "1") {
1941
+ return "an odd-numbered day";
1942
+ }
1943
+ return start === "2" ? "an even-numbered day" : null;
1944
+ }
1664
1945
  function dateOrWeekday(ir, opts) {
1665
1946
  const pattern = ir.pattern;
1666
- const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, opts);
1947
+ const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, false, opts);
1667
1948
  if (pattern.month !== "*" && monthFoldsIntoDate(ir) && !quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
1668
1949
  return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
1669
1950
  }
@@ -1714,10 +1995,13 @@ function quartzWeekdayPhrase(weekdayField, opts) {
1714
1995
  function monthDatePhrase(ir, opts) {
1715
1996
  const month = monthName(ir, opts);
1716
1997
  const days = renderSegments(
1717
- ir.analyses.segments.date,
1998
+ segmentsOf(ir, "date"),
1718
1999
  opts.style.ordinals ? getOrdinal : cardinalDay,
1719
2000
  opts
1720
2001
  );
2002
+ if (opts.style.dayFirst && ir.shapes.date === "single" && ir.shapes.month !== "single") {
2003
+ return "the " + getOrdinal(ir.pattern.date) + " of " + month;
2004
+ }
1721
2005
  return opts.style.dayFirst ? days + " " + month : month + " " + days;
1722
2006
  }
1723
2007
  function cardinalDay(value) {
@@ -1729,6 +2013,19 @@ function monthScope(ir, opts) {
1729
2013
  }
1730
2014
  return " in " + monthName(ir, opts);
1731
2015
  }
2016
+ function monthScopeForRecurrence(phrase, ir, opts) {
2017
+ if (ir.pattern.month === "*") {
2018
+ return phrase;
2019
+ }
2020
+ const carriesRecurrence = phrase.indexOf(" of the month") !== -1;
2021
+ if (carriesRecurrence && ir.shapes.month === "range") {
2022
+ return phrase.replace(" of the month", " of each month") + " from " + monthName(ir, opts);
2023
+ }
2024
+ if (carriesRecurrence && (ir.shapes.month === "single" || ir.shapes.month === "step")) {
2025
+ return phrase.replace(" of the month", "") + " in " + monthName(ir, opts);
2026
+ }
2027
+ return phrase + " in " + monthName(ir, opts);
2028
+ }
1732
2029
  function stepDates(dateField) {
1733
2030
  const parts = dateField.split("/");
1734
2031
  const interval = +parts[1];
@@ -1741,14 +2038,14 @@ function stepDates(dateField) {
1741
2038
  return phrase;
1742
2039
  }
1743
2040
  function dateOrdinals(ir, opts) {
1744
- return renderSegments(ir.analyses.segments.date, getOrdinal, opts);
2041
+ return renderSegments(segmentsOf(ir, "date"), getOrdinal, opts);
1745
2042
  }
1746
2043
  function monthName(ir, opts) {
1747
2044
  const oddEven = oddEvenMonth(ir.pattern.month);
1748
2045
  if (oddEven) {
1749
2046
  return oddEven;
1750
2047
  }
1751
- return renderSegments(ir.analyses.segments.month, function name(value) {
2048
+ return renderSegments(segmentsOf(ir, "month"), function name(value) {
1752
2049
  return getMonth(value, opts);
1753
2050
  }, opts);
1754
2051
  }
@@ -1765,11 +2062,21 @@ function oddEvenMonth(monthField) {
1765
2062
  }
1766
2063
  return start === "2" ? "every even-numbered month" : null;
1767
2064
  }
1768
- function weekdayPhrase(ir, opts) {
1769
- const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
1770
- return renderSegments(segments, function name(value) {
2065
+ function weekdayPhrase(ir, recurring, opts) {
2066
+ const segments = orderWeekdaysForDisplay(segmentsOf(ir, "weekday"));
2067
+ const hasRange = segments.some(function range(segment) {
2068
+ return segment.kind === "range";
2069
+ });
2070
+ const name = recurring && !hasRange ? function plural(value) {
2071
+ return pluralWeekday(value, opts);
2072
+ } : function singular(value) {
1771
2073
  return getWeekday(value, opts);
1772
- }, opts);
2074
+ };
2075
+ return renderSegments(segments, name, opts);
2076
+ }
2077
+ function pluralWeekday(value, opts) {
2078
+ const name = getWeekday(value, opts);
2079
+ return opts.short ? name : name + "s";
1773
2080
  }
1774
2081
  function renderSegments(segments, word, opts) {
1775
2082
  const pieces = [];
@@ -1784,16 +2091,13 @@ function renderSegments(segments, word, opts) {
1784
2091
  });
1785
2092
  return joinList(pieces, opts);
1786
2093
  }
1787
- function isOpenStep(field) {
1788
- return field.indexOf("/") !== -1 && field.indexOf("-") === -1 && field.indexOf(",") === -1;
1789
- }
1790
2094
  function applyYear(description, ir, opts) {
1791
2095
  const yearField = ir.pattern.year;
1792
2096
  if (yearField === "*") {
1793
2097
  return description;
1794
2098
  }
1795
2099
  if (yearField.indexOf("/") !== -1) {
1796
- return description + " " + stepYears(yearField, opts);
2100
+ return description + ", " + stepYears(yearField, opts);
1797
2101
  }
1798
2102
  const label = yearLabel(yearField, opts);
1799
2103
  if (yearField.indexOf("-") === -1 && yearField.indexOf(",") === -1 && ir.pattern.date !== "*" && description.indexOf(" at ") !== -1) {
@@ -1806,6 +2110,9 @@ function yearLabel(yearField, opts) {
1806
2110
  if (yearField.indexOf(",") !== -1) {
1807
2111
  return joinList(yearField.split(","), opts);
1808
2112
  }
2113
+ if (yearField.indexOf("-") !== -1) {
2114
+ return yearField.split("-").join(through(opts));
2115
+ }
1809
2116
  return yearField;
1810
2117
  }
1811
2118
  function stepYears(yearField, opts) {
@@ -1815,7 +2122,7 @@ function stepYears(yearField, opts) {
1815
2122
  if (interval <= 1) {
1816
2123
  return "every year";
1817
2124
  }
1818
- let phrase = "every " + getNumber(interval, opts) + " years";
2125
+ let phrase = interval === 2 ? "every other year" : "every " + getNumber(interval, opts) + " years";
1819
2126
  if (start !== "*" && start !== "0") {
1820
2127
  phrase += " from " + start;
1821
2128
  }
@@ -1917,7 +2224,7 @@ function interpretCronPattern(cronPattern, lang, opts) {
1917
2224
  return lang.reboot;
1918
2225
  }
1919
2226
  const ir = analyze(prepare(cronPattern, opts));
1920
- const plan = lang.strategy ? lang.strategy(ir, ir.plan) : ir.plan;
2227
+ const plan = lang.plan ? lang.plan(ir, ir.plan) : ir.plan;
1921
2228
  return lang.describe({ ...ir, plan }, opts);
1922
2229
  }
1923
2230
  var cronli5_default = cronli5;