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.js CHANGED
@@ -107,6 +107,43 @@ function orderWeekdaysForDisplay(segments) {
107
107
  function toFieldNumber(token, numberMap) {
108
108
  return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
109
109
  }
110
+ function segmentsOf(ir, field) {
111
+ return ir.analyses.segments[field] ?? [];
112
+ }
113
+ function stepSegment(ir, field) {
114
+ return segmentsOf(ir, field)[0];
115
+ }
116
+ function singleValues(segments) {
117
+ const values = [];
118
+ for (const segment of segments) {
119
+ if (segment.kind !== "single") {
120
+ return null;
121
+ }
122
+ values.push(+segment.value);
123
+ }
124
+ return values;
125
+ }
126
+ function offsetCleanStride(stride) {
127
+ return stride.start < stride.interval && 24 % stride.interval === 0;
128
+ }
129
+ function hourListStride(values) {
130
+ if (values.length < 2) {
131
+ return null;
132
+ }
133
+ const interval = values[1] - values[0];
134
+ if (interval < 2) {
135
+ return null;
136
+ }
137
+ for (let i = 2; i < values.length; i += 1) {
138
+ if (values[i] - values[i - 1] !== interval) {
139
+ return null;
140
+ }
141
+ }
142
+ if (values[0] !== 0 && values.length < 5) {
143
+ return null;
144
+ }
145
+ return { interval, last: values[values.length - 1], start: values[0] };
146
+ }
110
147
 
111
148
  // src/core/validate.ts
112
149
  function validateCronPattern(cronPattern) {
@@ -455,6 +492,9 @@ function isDiscreteList(field) {
455
492
  function isDiscreteHours(hourField) {
456
493
  return hourField !== "*" && !isPlainRange(hourField) && !isPlainStep(hourField);
457
494
  }
495
+ function isOpenStep(field) {
496
+ return field.indexOf("/") !== -1 && field.indexOf("-") === -1 && field.indexOf(",") === -1;
497
+ }
458
498
 
459
499
  // src/core/analyze.ts
460
500
  function getOccurrences(start, interval, max) {
@@ -591,9 +631,9 @@ function analyze(pattern) {
591
631
  segments
592
632
  };
593
633
  const content = { analyses, pattern, shapes };
594
- return { ...content, plan: selectStrategy(content) };
634
+ return { ...content, plan: selectPlan(content) };
595
635
  }
596
- function selectStrategy(content) {
636
+ function selectPlan(content) {
597
637
  const { analyses, pattern, shapes } = content;
598
638
  if (pattern.second !== "0") {
599
639
  const seconds = planSeconds(pattern, shapes, analyses);
@@ -849,7 +889,8 @@ var dialects = {
849
889
  pm: "p.m.",
850
890
  sep: ":",
851
891
  serialComma: true,
852
- through: " through "
892
+ through: " through ",
893
+ untilWindow: true
853
894
  },
854
895
  house: {
855
896
  am: "AM",
@@ -866,7 +907,7 @@ var dialects = {
866
907
  };
867
908
  function resolveDialect(dialect) {
868
909
  if (typeof dialect === "object" && dialect !== null) {
869
- return { ...dialects.us, ...dialect };
910
+ return { ...dialects.us, untilWindow: false, ...dialect };
870
911
  }
871
912
  const name = dialect === "uk" ? "gb" : dialect;
872
913
  return dialects[name] || dialects.us;
@@ -938,7 +979,9 @@ function normalizeOptions(options) {
938
979
  };
939
980
  }
940
981
  function describe(ir, opts) {
941
- return applyYear(render(ir, ir.plan, opts), ir, opts);
982
+ const body = confinement(ir, opts) ?? render(ir, ir.plan, opts);
983
+ const lead = isDayUnion(ir, opts) ? dayUnionMonthLead(ir, opts) : "";
984
+ return applyYear(lead + body, ir, opts);
942
985
  }
943
986
  function render(ir, plan, opts) {
944
987
  const renderer = renderers[plan.kind];
@@ -1023,7 +1066,7 @@ function secondsClause(ir, anchor, opts) {
1023
1066
  }
1024
1067
  if (shape === "step") {
1025
1068
  return stepCycle60(
1026
- ir.analyses.segments.second[0],
1069
+ stepSegment(ir, "second"),
1027
1070
  "second",
1028
1071
  anchor,
1029
1072
  opts
@@ -1031,19 +1074,19 @@ function secondsClause(ir, anchor, opts) {
1031
1074
  }
1032
1075
  if (shape === "range") {
1033
1076
  const bounds = secondField.split("-");
1034
- const num = seriesNumber(bounds, opts);
1077
+ const num = seriesNumber();
1035
1078
  return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the " + anchor;
1036
1079
  }
1037
1080
  if (shape === "single") {
1038
1081
  return "at " + getNumber(secondField, opts) + " " + pluralize(secondField, "second") + " past the " + anchor;
1039
1082
  }
1040
1083
  return strideFromSegments(
1041
- ir.analyses.segments.second,
1084
+ segmentsOf(ir, "second"),
1042
1085
  "second",
1043
1086
  anchor,
1044
1087
  opts
1045
1088
  ) ?? listPastThe(
1046
- segmentWords(ir.analyses.segments.second, opts),
1089
+ segmentWords(segmentsOf(ir, "second"), opts),
1047
1090
  "second",
1048
1091
  anchor,
1049
1092
  opts
@@ -1060,15 +1103,15 @@ function renderRangeOfMinutes(ir, plan, opts) {
1060
1103
  return minuteRangeLead(ir.pattern.minute, opts) + trailingQualifier(ir, opts);
1061
1104
  }
1062
1105
  function renderMultipleMinutes(ir, plan, opts) {
1063
- const stride = strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts);
1106
+ const stride = strideFromSegments(segmentsOf(ir, "minute"), "minute", "hour", opts);
1064
1107
  return (stride ?? listPastThe(segmentWords(
1065
- ir.analyses.segments.minute,
1108
+ segmentsOf(ir, "minute"),
1066
1109
  opts
1067
1110
  ), "minute", "hour", opts)) + trailingQualifier(ir, opts);
1068
1111
  }
1069
1112
  function renderMinuteFrequency(ir, plan, opts) {
1070
1113
  let phrase = stepCycle60(
1071
- ir.analyses.segments.minute[0],
1114
+ stepSegment(ir, "minute"),
1072
1115
  "minute",
1073
1116
  "hour",
1074
1117
  opts
@@ -1077,9 +1120,14 @@ function renderMinuteFrequency(ir, plan, opts) {
1077
1120
  const cadence = unevenHourCadence(ir, opts);
1078
1121
  phrase += cadence ? ", " + cadence : " during the " + hourTimesFromPlan(ir, plan.hours.times, false, opts) + " hours";
1079
1122
  } else if (plan.hours.kind === "window") {
1080
- phrase += " " + hourWindow(plan.hours, opts);
1123
+ phrase += " " + rangeWindow({
1124
+ continuous: false,
1125
+ from: plan.hours.from,
1126
+ throughMinute: plan.hours.last,
1127
+ to: plan.hours.to
1128
+ }, opts);
1081
1129
  } else if (plan.hours.kind === "step") {
1082
- phrase += " " + everyNthHour(ir.analyses.segments.hour[0], opts);
1130
+ phrase += " " + everyNthHour(stepSegment(ir, "hour"), opts);
1083
1131
  }
1084
1132
  return phrase + trailingQualifier(ir, opts);
1085
1133
  }
@@ -1097,15 +1145,21 @@ function renderMinutesAcrossHours(ir, plan, opts) {
1097
1145
  }
1098
1146
  return "every minute during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
1099
1147
  }
1100
- const lead = plan.form === "range" ? minuteRangeLead(ir.pattern.minute, opts) : (
1101
- // The 'list' form is a minute list, which has segments; an offset/uneven
1102
- // step enumerated to that list reads as a stride.
1103
- strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1104
- segmentWords(ir.analyses.segments.minute, opts),
1105
- "minute",
1106
- "hour",
1107
- opts
1108
- )
1148
+ if (plan.form === "range") {
1149
+ const lead2 = minuteRangeLead(ir.pattern.minute, opts);
1150
+ if (cadence !== null) {
1151
+ return lead2 + ", " + cadence + trailingQualifier(ir, opts);
1152
+ }
1153
+ if (singleHourFire(plan.times)) {
1154
+ return lead2 + ", at " + hourTimesFromPlan(ir, plan.times, true, opts) + trailingQualifier(ir, opts);
1155
+ }
1156
+ return lead2 + " during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
1157
+ }
1158
+ const lead = strideFromSegments(segmentsOf(ir, "minute"), "minute", "hour", opts) ?? listPastThe(
1159
+ segmentWords(segmentsOf(ir, "minute"), opts),
1160
+ "minute",
1161
+ "hour",
1162
+ opts
1109
1163
  );
1110
1164
  if (cadence !== null) {
1111
1165
  return lead + ", " + cadence + trailingQualifier(ir, opts);
@@ -1127,12 +1181,12 @@ function everyNthHour(segment, opts) {
1127
1181
  return start === 0 ? base : base + " starting at " + getTime({ hour: start, minute: 0 }, opts);
1128
1182
  }
1129
1183
  function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
1130
- const segment = ir.analyses.segments.hour[0];
1184
+ const segment = stepSegment(ir, "hour");
1131
1185
  if (plan.form === "wildcard") {
1132
1186
  return "every minute " + everyNthHour(segment, opts) + trailingQualifier(ir, opts);
1133
1187
  }
1134
- const lead = plan.form === "list" ? strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1135
- segmentWords(ir.analyses.segments.minute, opts),
1188
+ const lead = plan.form === "list" ? strideFromSegments(segmentsOf(ir, "minute"), "minute", "hour", opts) ?? listPastThe(
1189
+ segmentWords(segmentsOf(ir, "minute"), opts),
1136
1190
  "minute",
1137
1191
  "hour",
1138
1192
  opts
@@ -1142,7 +1196,7 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
1142
1196
  }
1143
1197
  function minuteRangeLead(minuteField, opts) {
1144
1198
  const bounds = minuteField.split("-");
1145
- const num = seriesNumber(bounds, opts);
1199
+ const num = seriesNumber();
1146
1200
  return "every minute from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the hour";
1147
1201
  }
1148
1202
  function renderEveryHour(ir, plan, opts) {
@@ -1163,12 +1217,12 @@ function rangeMinuteLead(ir, opts) {
1163
1217
  return "every hour";
1164
1218
  }
1165
1219
  return strideFromSegments(
1166
- ir.analyses.segments.minute,
1220
+ segmentsOf(ir, "minute"),
1167
1221
  "minute",
1168
1222
  "hour",
1169
1223
  opts
1170
1224
  ) ?? listPastThe(
1171
- segmentWords(ir.analyses.segments.minute, opts),
1225
+ segmentWords(segmentsOf(ir, "minute"), opts),
1172
1226
  "minute",
1173
1227
  "hour",
1174
1228
  opts
@@ -1179,14 +1233,28 @@ function renderHourStep(ir, plan, opts) {
1179
1233
  if (cadence !== null) {
1180
1234
  return cadence + trailingQualifier(ir, opts);
1181
1235
  }
1182
- return stepHours(ir.analyses.segments.hour[0], opts) + trailingQualifier(ir, opts);
1236
+ return stepHours(stepSegment(ir, "hour"), opts) + trailingQualifier(ir, opts);
1183
1237
  }
1184
1238
  function boundedWindow(plan) {
1185
- const last = plan.minuteForm === "wildcard" ? plan.boundMinute ?? 0 : 0;
1186
- return { from: plan.from, last, to: plan.to };
1239
+ const continuous = plan.minuteForm === "wildcard";
1240
+ const closeMinute = continuous ? plan.boundMinute ?? 0 : 0;
1241
+ return { from: plan.from, closeMinute, to: plan.to, continuous };
1242
+ }
1243
+ function rangeWindow(window, opts) {
1244
+ const { from, to, throughMinute, continuous } = window;
1245
+ const open = "from " + getTime({ hour: from, minute: 0 }, opts);
1246
+ if (opts.style.untilWindow && !opts.short && from !== to) {
1247
+ return continuous ? open + " until " + getTime({ hour: (to + 1) % 24, minute: 0 }, opts) : open + through(opts) + getTime({ hour: to, minute: 0 }, opts);
1248
+ }
1249
+ return open + through(opts) + getTime({ hour: to, minute: throughMinute }, opts);
1187
1250
  }
1188
1251
  function hourWindow(window, opts) {
1189
- return "from " + getTime({ hour: window.from, minute: 0 }, opts) + through(opts) + getTime({ hour: window.to, minute: window.last }, opts);
1252
+ return rangeWindow({
1253
+ continuous: window.continuous,
1254
+ from: window.from,
1255
+ throughMinute: window.closeMinute,
1256
+ to: window.to
1257
+ }, opts);
1190
1258
  }
1191
1259
  function renderClockTimes(ir, plan, opts) {
1192
1260
  if (ir.shapes.minute === "single") {
@@ -1205,7 +1273,10 @@ function renderClockTimes(ir, plan, opts) {
1205
1273
  plain
1206
1274
  }, opts);
1207
1275
  });
1208
- return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts);
1276
+ return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts) + dayUnionTrail(ir, opts);
1277
+ }
1278
+ function dayUnionTrail(ir, opts) {
1279
+ return isDayUnion(ir, opts) ? dayUnionCondition(ir, opts) : "";
1209
1280
  }
1210
1281
  function renderCompactClockTimes(ir, plan, opts) {
1211
1282
  if (plan.fold) {
@@ -1213,20 +1284,20 @@ function renderCompactClockTimes(ir, plan, opts) {
1213
1284
  if (cadence2 !== null) {
1214
1285
  return cadence2;
1215
1286
  }
1216
- const hasRange = ir.analyses.segments.hour.some(function range(segment) {
1287
+ const hasRange = segmentsOf(ir, "hour").some(function range(segment) {
1217
1288
  return segment.kind === "range";
1218
1289
  });
1219
1290
  if (hasRange && !ir.analyses.clockSecond) {
1220
1291
  return foldedHourWindows(ir, plan, opts) + trailingQualifier(ir, opts);
1221
1292
  }
1222
1293
  const fold = { minute: plan.minute, second: ir.analyses.clockSecond };
1223
- return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
1294
+ return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts) + dayUnionTrail(ir, opts);
1224
1295
  }
1225
1296
  const minuteLead = (
1226
1297
  // The non-fold branch is a minute list, which has segments. An
1227
1298
  // offset/uneven step enumerated to that list reads as a stride.
1228
- strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1229
- segmentWords(ir.analyses.segments.minute, opts),
1299
+ strideFromSegments(segmentsOf(ir, "minute"), "minute", "hour", opts) ?? listPastThe(
1300
+ segmentWords(segmentsOf(ir, "minute"), opts),
1230
1301
  "minute",
1231
1302
  "hour",
1232
1303
  opts
@@ -1239,26 +1310,162 @@ function renderCompactClockTimes(ir, plan, opts) {
1239
1310
  function foldedHourWindows(ir, plan, opts) {
1240
1311
  const minute = plan.minute;
1241
1312
  const windows = [];
1242
- const singles = [];
1243
- ir.analyses.segments.hour.forEach(function classify(segment) {
1313
+ const times = collectHourOutliers(ir).map(function time(hour) {
1314
+ return getTime({ hour, minute }, opts);
1315
+ });
1316
+ segmentsOf(ir, "hour").forEach(function classify(segment) {
1244
1317
  if (segment.kind === "range") {
1245
- windows.push("from " + getTime(
1246
- { hour: segment.bounds[0], minute: 0 },
1247
- opts
1248
- ) + through(opts) + getTime({ hour: segment.bounds[1], minute }, opts));
1249
- } else if (segment.kind === "step") {
1250
- singles.push(...segment.fires);
1251
- } else {
1252
- singles.push(+segment.value);
1318
+ windows.push(rangeWindow({
1319
+ continuous: false,
1320
+ from: +segment.bounds[0],
1321
+ throughMinute: minute,
1322
+ to: +segment.bounds[1]
1323
+ }, opts));
1324
+ }
1325
+ });
1326
+ const phrase = rangeMinuteLead(ir, opts) + " " + joinList(windows, opts);
1327
+ return phrase + outlierTail(times, opts);
1328
+ }
1329
+ function collectHourOutliers(ir) {
1330
+ const hours = [];
1331
+ segmentsOf(ir, "hour").forEach(function classify(segment) {
1332
+ if (segment.kind === "step") {
1333
+ hours.push(...segment.fires);
1334
+ } else if (segment.kind !== "range") {
1335
+ hours.push(+segment.value);
1253
1336
  }
1254
1337
  });
1255
- let phrase = rangeMinuteLead(ir, opts) + " " + joinList(windows, opts);
1256
- if (singles.length) {
1257
- phrase += " and at " + joinList(singles.map(function time(hour) {
1258
- return getTime({ hour, minute }, opts);
1259
- }), opts);
1338
+ return hours;
1339
+ }
1340
+ function outlierTail(times, opts) {
1341
+ if (!times.length) {
1342
+ return "";
1260
1343
  }
1261
- return phrase;
1344
+ return " and at " + joinList(times, opts);
1345
+ }
1346
+ function isCadenceField(token) {
1347
+ return token === "*" || token.startsWith("*/") && token.indexOf("-") === -1;
1348
+ }
1349
+ function leadingCadence(ir, opts) {
1350
+ const { second, minute } = ir.pattern;
1351
+ if (isCadenceField(second)) {
1352
+ return { secondLead: true, text: secondsClause(ir, "minute", opts) };
1353
+ }
1354
+ if (second === "0" && isCadenceField(minute)) {
1355
+ const text = minute === "*" ? "every minute" : (
1356
+ // A clean minute step's first segment is a step segment.
1357
+ stepCycle60(
1358
+ stepSegment(ir, "minute"),
1359
+ "minute",
1360
+ "hour",
1361
+ opts
1362
+ )
1363
+ );
1364
+ return { secondLead: false, text };
1365
+ }
1366
+ return null;
1367
+ }
1368
+ function minuteConfinement(ir, opts) {
1369
+ const minute = ir.pattern.minute;
1370
+ if (minute === "*") {
1371
+ return "";
1372
+ }
1373
+ if (isCadenceField(minute)) {
1374
+ return " of every other minute";
1375
+ }
1376
+ const segments = segmentsOf(ir, "minute");
1377
+ if (ir.shapes.minute === "single") {
1378
+ return " during minute :" + pad(minute);
1379
+ }
1380
+ if (ir.shapes.minute === "range") {
1381
+ const bounds = minute.split("-");
1382
+ return " during minutes :" + pad(bounds[0]) + through(opts) + ":" + pad(bounds[1]);
1383
+ }
1384
+ const values = segmentWords(segments, opts).map(function colon(word) {
1385
+ return ":" + pad(word);
1386
+ });
1387
+ return " during minutes " + joinList(values, opts);
1388
+ }
1389
+ function hourConfinement(ir, opts) {
1390
+ const hour = ir.pattern.hour;
1391
+ if (hour === "*") {
1392
+ const minutePinned = ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute);
1393
+ return minutePinned ? " of every hour" : "";
1394
+ }
1395
+ if (isCadenceField(hour)) {
1396
+ return hour === "*/2" ? " of every other hour" : "";
1397
+ }
1398
+ if (ir.shapes.hour === "single") {
1399
+ const h = +hour;
1400
+ if (ir.shapes.minute === "step") {
1401
+ return " from " + getTime({ hour: h, minute: 0 }, opts) + " until " + getTime({ hour: (h + 1) % 24, minute: 0 }, opts);
1402
+ }
1403
+ if (ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute)) {
1404
+ return " at " + getTime({ hour: h, minute: 0 }, opts);
1405
+ }
1406
+ return " of the " + getTime({ hour: h, minute: 0 }, opts) + " hour";
1407
+ }
1408
+ if (ir.shapes.hour === "range") {
1409
+ const bounds = hour.split("-");
1410
+ return " " + rangeWindow({
1411
+ continuous: ir.pattern.minute === "*",
1412
+ from: +bounds[0],
1413
+ throughMinute: 0,
1414
+ to: +bounds[1]
1415
+ }, opts);
1416
+ }
1417
+ return " during the " + hourSegmentTimes(ir, { minute: 0, second: null }, false, opts) + " hours";
1418
+ }
1419
+ function isContiguousHourRange(ir) {
1420
+ return ir.shapes.hour === "range";
1421
+ }
1422
+ function confinableHour(ir) {
1423
+ if (ir.shapes.hour !== "step") {
1424
+ return true;
1425
+ }
1426
+ const segment = stepSegment(ir, "hour");
1427
+ return ir.pattern.hour === "*/2" || segment.startToken.indexOf("-") !== -1;
1428
+ }
1429
+ function isMinuteStride(ir) {
1430
+ if (ir.shapes.minute !== "list") {
1431
+ return false;
1432
+ }
1433
+ const values = singleValues(segmentsOf(ir, "minute"));
1434
+ return values !== null && arithmeticStep(values) !== null;
1435
+ }
1436
+ function confinementEligible(ir, lead) {
1437
+ const { minute, hour } = ir.pattern;
1438
+ const minuteStep = isCadenceField(minute) && minute !== "*";
1439
+ if (!confinableHour(ir)) {
1440
+ return false;
1441
+ }
1442
+ if (lead.secondLead) {
1443
+ if (minuteStep) {
1444
+ return minute === "*/2" && !isContiguousHourRange(ir);
1445
+ }
1446
+ if (isMinuteStride(ir) || ir.shapes.minute === "list" && ir.shapes.hour === "list") {
1447
+ return false;
1448
+ }
1449
+ return true;
1450
+ }
1451
+ if (hour === "*/2") {
1452
+ return true;
1453
+ }
1454
+ return ir.shapes.hour === "single" && minute === "*/2";
1455
+ }
1456
+ function confinement(ir, opts) {
1457
+ if (!opts.style.untilWindow || opts.short) {
1458
+ return null;
1459
+ }
1460
+ if (ir.pattern.minute === "*" && ir.pattern.hour === "*") {
1461
+ return null;
1462
+ }
1463
+ const lead = leadingCadence(ir, opts);
1464
+ if (!lead || !confinementEligible(ir, lead)) {
1465
+ return null;
1466
+ }
1467
+ const minutePart = lead.secondLead ? minuteConfinement(ir, opts) : "";
1468
+ return lead.text + minutePart + hourConfinement(ir, opts) + trailingQualifier(ir, opts);
1262
1469
  }
1263
1470
  var renderers = {
1264
1471
  clockTimes: renderClockTimes,
@@ -1290,19 +1497,9 @@ function renderStride(stride, opts) {
1290
1497
  if (start < interval && tiles) {
1291
1498
  return cadence + " from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
1292
1499
  }
1293
- const num = seriesNumber([start, last], opts);
1500
+ const num = seriesNumber();
1294
1501
  return cadence + " from " + num(start) + through(opts) + num(last) + " " + pluralize(last, unit) + " past the " + anchor;
1295
1502
  }
1296
- function singleValues(segments) {
1297
- const values = [];
1298
- for (const segment of segments) {
1299
- if (segment.kind !== "single") {
1300
- return null;
1301
- }
1302
- values.push(+segment.value);
1303
- }
1304
- return values;
1305
- }
1306
1503
  function strideFromSegments(segments, unit, anchor, opts) {
1307
1504
  const values = singleValues(segments);
1308
1505
  const step = values && arithmeticStep(values);
@@ -1351,9 +1548,6 @@ function hourStrideCadence(stride, opts) {
1351
1548
  }
1352
1549
  return cadence + " from " + getTime({ hour: start, minute: 0 }, opts) + through(opts) + getTime({ hour: last, minute: 0 }, opts);
1353
1550
  }
1354
- function offsetCleanStride(stride) {
1355
- return stride.start < stride.interval && 24 % stride.interval === 0;
1356
- }
1357
1551
  function unevenHourCadence(ir, opts) {
1358
1552
  const stride = hourStride(ir);
1359
1553
  if (!stride || offsetCleanStride(stride)) {
@@ -1361,26 +1555,8 @@ function unevenHourCadence(ir, opts) {
1361
1555
  }
1362
1556
  return hourStrideCadence(stride, opts);
1363
1557
  }
1364
- function hourListStride(values) {
1365
- if (values.length < 2) {
1366
- return null;
1367
- }
1368
- const interval = values[1] - values[0];
1369
- if (interval < 2) {
1370
- return null;
1371
- }
1372
- for (let i = 2; i < values.length; i += 1) {
1373
- if (values[i] - values[i - 1] !== interval) {
1374
- return null;
1375
- }
1376
- }
1377
- if (values[0] !== 0 && values.length < 5) {
1378
- return null;
1379
- }
1380
- return { interval, last: values[values.length - 1], start: values[0] };
1381
- }
1382
1558
  function hourStride(ir) {
1383
- const segments = ir.analyses.segments.hour;
1559
+ const segments = segmentsOf(ir, "hour");
1384
1560
  if (segments.length === 1 && segments[0].kind === "step") {
1385
1561
  const segment = segments[0];
1386
1562
  if (segment.fires.length < 2) {
@@ -1417,9 +1593,9 @@ function hourCadence(ir, minute, opts) {
1417
1593
  if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
1418
1594
  return null;
1419
1595
  }
1420
- const confinement = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
1421
- if (confinement) {
1422
- return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
1596
+ const minuteZeroStride = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
1597
+ if (minuteZeroStride) {
1598
+ return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(minuteZeroStride, opts) + trailingQualifier(ir, opts);
1423
1599
  }
1424
1600
  if (minute === 0 && ir.pattern.second === "0") {
1425
1601
  return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
@@ -1427,7 +1603,7 @@ function hourCadence(ir, minute, opts) {
1427
1603
  return hourCadenceLead(ir, minute, opts) + ", " + hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
1428
1604
  }
1429
1605
  function cleanStrideSegment(ir) {
1430
- const segments = ir.analyses.segments.hour;
1606
+ const segments = segmentsOf(ir, "hour");
1431
1607
  const segment = segments.length === 1 && segments[0];
1432
1608
  if (!segment || segment.kind !== "step" || segment.startToken.indexOf("-") !== -1 || !(segment.interval in stepOrdinals)) {
1433
1609
  return null;
@@ -1435,32 +1611,28 @@ function cleanStrideSegment(ir) {
1435
1611
  return segment;
1436
1612
  }
1437
1613
  function hasHourWindow(ir) {
1438
- return ir.analyses.segments.hour.some(function range(segment) {
1614
+ return segmentsOf(ir, "hour").some(function range(segment) {
1439
1615
  return segment.kind === "range";
1440
1616
  });
1441
1617
  }
1442
1618
  function hourRangeWindowTail(ir, opts) {
1443
1619
  const windows = [];
1444
- const singles = [];
1445
- ir.analyses.segments.hour.forEach(function classify(segment) {
1620
+ const outlierHours = collectHourOutliers(ir);
1621
+ segmentsOf(ir, "hour").forEach(function classify(segment) {
1446
1622
  if (segment.kind === "range") {
1447
- windows.push("from " + getTime(
1448
- { hour: +segment.bounds[0], minute: 0 },
1449
- opts
1450
- ) + through(opts) + getTime({ hour: +segment.bounds[1], minute: 0 }, opts));
1451
- } else if (segment.kind === "step") {
1452
- singles.push(...segment.fires);
1453
- } else {
1454
- singles.push(+segment.value);
1623
+ windows.push(rangeWindow({
1624
+ continuous: false,
1625
+ from: +segment.bounds[0],
1626
+ throughMinute: 0,
1627
+ to: +segment.bounds[1]
1628
+ }, opts));
1455
1629
  }
1456
1630
  });
1457
- let phrase = "every hour " + joinList(windows, opts);
1458
- if (singles.length) {
1459
- phrase += " and at " + joinList(singles.map(function time(hour) {
1460
- return getTime({ hour, minute: 0 }, opts);
1461
- }), opts);
1462
- }
1463
- return phrase;
1631
+ const phrase = "every hour " + joinList(windows, opts);
1632
+ const times = outlierHours.map(function time(hour) {
1633
+ return getTime({ hour, minute: 0 }, opts);
1634
+ });
1635
+ return phrase + outlierTail(times, opts);
1464
1636
  }
1465
1637
  function hourRangeCadence(ir, minute, opts) {
1466
1638
  if (minute !== 0 || !hasHourWindow(ir)) {
@@ -1474,25 +1646,29 @@ function hourRangeCadence(ir, minute, opts) {
1474
1646
  }
1475
1647
  return hourCadenceLead(ir, minute, opts) + ", " + hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
1476
1648
  }
1477
- function seriesNumber(values, opts) {
1478
- const anyBig = values.some(function big(v) {
1479
- return +v > 10;
1480
- });
1649
+ function seriesNumber() {
1481
1650
  return function format(n) {
1482
- return anyBig ? "" + n : getNumber(n, opts);
1651
+ return "" + n;
1652
+ };
1653
+ }
1654
+ function listNumber(count, opts) {
1655
+ return count > 1 ? function asNumeral(n) {
1656
+ return "" + n;
1657
+ } : function spelled(n) {
1658
+ return getNumber(n, opts);
1483
1659
  };
1484
1660
  }
1485
1661
  function numberWords(fires, opts) {
1486
- return fires.map(seriesNumber(fires, opts));
1662
+ return fires.map(listNumber(fires.length, opts));
1487
1663
  }
1488
1664
  function segmentWords(segments, opts) {
1489
- const values = segments.flatMap(function collect(segment) {
1665
+ const count = segments.reduce(function tally(sum, segment) {
1490
1666
  if (segment.kind === "range") {
1491
- return segment.bounds;
1667
+ return sum + 1;
1492
1668
  }
1493
- return segment.kind === "step" ? segment.fires : [segment.value];
1494
- });
1495
- const num = seriesNumber(values, opts);
1669
+ return sum + (segment.kind === "step" ? segment.fires.length : 1);
1670
+ }, 0);
1671
+ const num = listNumber(count, opts);
1496
1672
  return segments.flatMap(function word(segment) {
1497
1673
  if (segment.kind === "range") {
1498
1674
  return [num(segment.bounds[0]) + through(opts) + num(segment.bounds[1])];
@@ -1524,6 +1700,9 @@ function hourTimes(hours, opts) {
1524
1700
  });
1525
1701
  return joinList(times, opts);
1526
1702
  }
1703
+ function singleHourFire(times) {
1704
+ return times.kind === "fires" && times.fires.length === 1;
1705
+ }
1527
1706
  function hourTimesFromPlan(ir, times, atContext, opts) {
1528
1707
  if (times.kind === "fires") {
1529
1708
  return hourTimes(times.fires, opts);
@@ -1538,7 +1717,7 @@ function segmentHours(segment) {
1538
1717
  }
1539
1718
  function hourSegmentTimes(ir, fold, atContext, opts) {
1540
1719
  const { minute, second } = fold;
1541
- const segments = ir.analyses.segments.hour;
1720
+ const segments = segmentsOf(ir, "hour");
1542
1721
  const plain = mixedTwelve(segments.flatMap(function entries(segment) {
1543
1722
  return segmentHours(segment).map(function entry(hour) {
1544
1723
  return { hour: +hour, minute, second };
@@ -1571,28 +1750,47 @@ function disambiguateTimes(pieces, segments, atContext) {
1571
1750
  return index === 0 ? piece : "at " + piece;
1572
1751
  });
1573
1752
  }
1574
- function joinList(items, opts) {
1753
+ function joinWith(items, conjunction, opts) {
1575
1754
  if (items.length <= 1) {
1576
1755
  return items.join("");
1577
1756
  }
1578
1757
  if (items.length === 2) {
1579
- return items[0] + " and " + items[1];
1758
+ return items[0] + conjunction + items[1];
1580
1759
  }
1581
- const and = opts.style.serialComma ? ", and " : " and ";
1582
- return items.slice(0, -1).join(", ") + and + items[items.length - 1];
1760
+ const tail = opts.style.serialComma ? "," + conjunction : conjunction;
1761
+ return items.slice(0, -1).join(", ") + tail + items[items.length - 1];
1762
+ }
1763
+ function joinList(items, opts) {
1764
+ return joinWith(items, " and ", opts);
1583
1765
  }
1584
- var trailingWords = { all: "", month: "in ", stepDate: "on ", weekday: "on " };
1766
+ function joinOr(items, opts) {
1767
+ return joinWith(items, " or ", opts);
1768
+ }
1769
+ var trailingWords = {
1770
+ all: "",
1771
+ month: "in ",
1772
+ recurringWeekday: true,
1773
+ stepDate: "on ",
1774
+ weekday: "on "
1775
+ };
1585
1776
  var leadingWords = {
1586
1777
  all: "every day",
1587
1778
  month: "every day in ",
1779
+ recurringWeekday: false,
1588
1780
  stepDate: "",
1589
1781
  weekday: "every "
1590
1782
  };
1591
1783
  function trailingQualifier(ir, opts) {
1784
+ if (isDayUnion(ir, opts)) {
1785
+ return dayUnionCondition(ir, opts);
1786
+ }
1592
1787
  const phrase = dayQualifier(ir, trailingWords, opts);
1593
1788
  return phrase && " " + phrase;
1594
1789
  }
1595
1790
  function interpretDayQualifier(ir, opts) {
1791
+ if (isDayUnion(ir, opts)) {
1792
+ return "";
1793
+ }
1596
1794
  return dayQualifier(ir, leadingWords, opts) + " ";
1597
1795
  }
1598
1796
  function dayQualifier(ir, words, opts) {
@@ -1604,7 +1802,11 @@ function dayQualifier(ir, words, opts) {
1604
1802
  return datePhrase(ir, words, opts);
1605
1803
  }
1606
1804
  if (pattern.weekday !== "*") {
1607
- const weekdays = quartzWeekdayPhrase(pattern.weekday, opts) || words.weekday + weekdayPhrase(ir, opts);
1805
+ const quartzWeekday = quartzWeekdayPhrase(pattern.weekday, opts);
1806
+ if (quartzWeekday) {
1807
+ return monthScopeForRecurrence(quartzWeekday, ir, opts);
1808
+ }
1809
+ const weekdays = words.weekday + weekdayPhrase(ir, words.recurringWeekday, opts);
1608
1810
  return weekdays + monthScope(ir, opts);
1609
1811
  }
1610
1812
  if (pattern.month !== "*") {
@@ -1616,10 +1818,14 @@ function datePhrase(ir, words, opts) {
1616
1818
  const pattern = ir.pattern;
1617
1819
  const quartzDate = quartzDatePhrase(pattern.date, opts);
1618
1820
  if (quartzDate) {
1619
- return quartzDate + monthScope(ir, opts);
1821
+ return monthScopeForRecurrence(quartzDate, ir, opts);
1620
1822
  }
1621
1823
  if (isOpenStep(pattern.date)) {
1622
- return words.stepDate + stepDates(pattern.date) + monthScope(ir, opts);
1824
+ return monthScopeForRecurrence(
1825
+ words.stepDate + stepDates(pattern.date),
1826
+ ir,
1827
+ opts
1828
+ );
1623
1829
  }
1624
1830
  if (pattern.month !== "*" && !monthFoldsIntoDate(ir)) {
1625
1831
  return "on the " + dateOrdinals(ir, opts) + monthScope(ir, opts);
@@ -1631,13 +1837,88 @@ function datePhrase(ir, words, opts) {
1631
1837
  }
1632
1838
  function monthFoldsIntoDate(ir) {
1633
1839
  return !oddEvenMonth(ir.pattern.month) && // Reached only with a restricted month, which has segments.
1634
- ir.analyses.segments.month.every(function flat(segment) {
1840
+ segmentsOf(ir, "month").every(function flat(segment) {
1635
1841
  return segment.kind !== "range";
1636
1842
  });
1637
1843
  }
1844
+ function isDayUnion(ir, opts) {
1845
+ return ir.pattern.date !== "*" && ir.pattern.weekday !== "*" && !!opts.style.untilWindow && !opts.short;
1846
+ }
1847
+ function dayUnionCondition(ir, opts) {
1848
+ const pieces = [
1849
+ ...dayUnionDatePieces(ir, opts),
1850
+ ...dayUnionWeekdayPieces(ir, opts)
1851
+ ];
1852
+ return " whenever the day is " + joinOr(pieces, opts);
1853
+ }
1854
+ function dayUnionMonthLead(ir, opts) {
1855
+ if (ir.pattern.month === "*") {
1856
+ return "";
1857
+ }
1858
+ return "in " + monthName(ir, opts) + " ";
1859
+ }
1860
+ function dayUnionDatePieces(ir, opts) {
1861
+ const dateField = ir.pattern.date;
1862
+ const quartz = quartzDatePhrase(dateField, opts);
1863
+ if (quartz) {
1864
+ return [quartz.replace(/^on /, "")];
1865
+ }
1866
+ const oddEven = oddEvenDay(dateField);
1867
+ if (oddEven) {
1868
+ return [oddEven];
1869
+ }
1870
+ const pieces = [];
1871
+ segmentsOf(ir, "date").forEach(function expand(segment) {
1872
+ if (segment.kind === "range") {
1873
+ pieces.push("from the " + getOrdinal(segment.bounds[0]) + through(opts) + "the " + getOrdinal(segment.bounds[1]));
1874
+ } else if (segment.kind === "step") {
1875
+ segment.fires.forEach(function fire(value) {
1876
+ pieces.push("the " + getOrdinal(value));
1877
+ });
1878
+ } else {
1879
+ pieces.push("the " + getOrdinal(segment.value));
1880
+ }
1881
+ });
1882
+ return pieces;
1883
+ }
1884
+ function dayUnionWeekdayPieces(ir, opts) {
1885
+ const weekdayField = ir.pattern.weekday;
1886
+ const quartz = quartzWeekdayPhrase(weekdayField, opts);
1887
+ if (quartz) {
1888
+ return [quartz.replace(/^on /, "")];
1889
+ }
1890
+ const pieces = [];
1891
+ segmentsOf(ir, "weekday").forEach(function expand(segment) {
1892
+ if (segment.kind === "range" && segment.bounds[0] === "1" && segment.bounds[1] === "5") {
1893
+ pieces.push("a weekday");
1894
+ } else if (segment.kind === "range") {
1895
+ pieces.push("a " + getWeekday(segment.bounds[0], opts) + through(opts) + "a " + getWeekday(segment.bounds[1], opts));
1896
+ } else if (segment.kind === "step") {
1897
+ segment.fires.forEach(function fire(value) {
1898
+ pieces.push("a " + getWeekday(value, opts));
1899
+ });
1900
+ } else {
1901
+ pieces.push("a " + getWeekday(segment.value, opts));
1902
+ }
1903
+ });
1904
+ return pieces;
1905
+ }
1906
+ function oddEvenDay(dateField) {
1907
+ if (!isOpenStep(dateField)) {
1908
+ return null;
1909
+ }
1910
+ const [start, step] = dateField.split("/");
1911
+ if (+step !== 2) {
1912
+ return null;
1913
+ }
1914
+ if (start === "*" || start === "1") {
1915
+ return "an odd-numbered day";
1916
+ }
1917
+ return start === "2" ? "an even-numbered day" : null;
1918
+ }
1638
1919
  function dateOrWeekday(ir, opts) {
1639
1920
  const pattern = ir.pattern;
1640
- const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, opts);
1921
+ const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, false, opts);
1641
1922
  if (pattern.month !== "*" && monthFoldsIntoDate(ir) && !quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
1642
1923
  return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
1643
1924
  }
@@ -1688,10 +1969,13 @@ function quartzWeekdayPhrase(weekdayField, opts) {
1688
1969
  function monthDatePhrase(ir, opts) {
1689
1970
  const month = monthName(ir, opts);
1690
1971
  const days = renderSegments(
1691
- ir.analyses.segments.date,
1972
+ segmentsOf(ir, "date"),
1692
1973
  opts.style.ordinals ? getOrdinal : cardinalDay,
1693
1974
  opts
1694
1975
  );
1976
+ if (opts.style.dayFirst && ir.shapes.date === "single" && ir.shapes.month !== "single") {
1977
+ return "the " + getOrdinal(ir.pattern.date) + " of " + month;
1978
+ }
1695
1979
  return opts.style.dayFirst ? days + " " + month : month + " " + days;
1696
1980
  }
1697
1981
  function cardinalDay(value) {
@@ -1703,6 +1987,19 @@ function monthScope(ir, opts) {
1703
1987
  }
1704
1988
  return " in " + monthName(ir, opts);
1705
1989
  }
1990
+ function monthScopeForRecurrence(phrase, ir, opts) {
1991
+ if (ir.pattern.month === "*") {
1992
+ return phrase;
1993
+ }
1994
+ const carriesRecurrence = phrase.indexOf(" of the month") !== -1;
1995
+ if (carriesRecurrence && ir.shapes.month === "range") {
1996
+ return phrase.replace(" of the month", " of each month") + " from " + monthName(ir, opts);
1997
+ }
1998
+ if (carriesRecurrence && (ir.shapes.month === "single" || ir.shapes.month === "step")) {
1999
+ return phrase.replace(" of the month", "") + " in " + monthName(ir, opts);
2000
+ }
2001
+ return phrase + " in " + monthName(ir, opts);
2002
+ }
1706
2003
  function stepDates(dateField) {
1707
2004
  const parts = dateField.split("/");
1708
2005
  const interval = +parts[1];
@@ -1715,14 +2012,14 @@ function stepDates(dateField) {
1715
2012
  return phrase;
1716
2013
  }
1717
2014
  function dateOrdinals(ir, opts) {
1718
- return renderSegments(ir.analyses.segments.date, getOrdinal, opts);
2015
+ return renderSegments(segmentsOf(ir, "date"), getOrdinal, opts);
1719
2016
  }
1720
2017
  function monthName(ir, opts) {
1721
2018
  const oddEven = oddEvenMonth(ir.pattern.month);
1722
2019
  if (oddEven) {
1723
2020
  return oddEven;
1724
2021
  }
1725
- return renderSegments(ir.analyses.segments.month, function name(value) {
2022
+ return renderSegments(segmentsOf(ir, "month"), function name(value) {
1726
2023
  return getMonth(value, opts);
1727
2024
  }, opts);
1728
2025
  }
@@ -1739,11 +2036,21 @@ function oddEvenMonth(monthField) {
1739
2036
  }
1740
2037
  return start === "2" ? "every even-numbered month" : null;
1741
2038
  }
1742
- function weekdayPhrase(ir, opts) {
1743
- const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
1744
- return renderSegments(segments, function name(value) {
2039
+ function weekdayPhrase(ir, recurring, opts) {
2040
+ const segments = orderWeekdaysForDisplay(segmentsOf(ir, "weekday"));
2041
+ const hasRange = segments.some(function range(segment) {
2042
+ return segment.kind === "range";
2043
+ });
2044
+ const name = recurring && !hasRange ? function plural(value) {
2045
+ return pluralWeekday(value, opts);
2046
+ } : function singular(value) {
1745
2047
  return getWeekday(value, opts);
1746
- }, opts);
2048
+ };
2049
+ return renderSegments(segments, name, opts);
2050
+ }
2051
+ function pluralWeekday(value, opts) {
2052
+ const name = getWeekday(value, opts);
2053
+ return opts.short ? name : name + "s";
1747
2054
  }
1748
2055
  function renderSegments(segments, word, opts) {
1749
2056
  const pieces = [];
@@ -1758,16 +2065,13 @@ function renderSegments(segments, word, opts) {
1758
2065
  });
1759
2066
  return joinList(pieces, opts);
1760
2067
  }
1761
- function isOpenStep(field) {
1762
- return field.indexOf("/") !== -1 && field.indexOf("-") === -1 && field.indexOf(",") === -1;
1763
- }
1764
2068
  function applyYear(description, ir, opts) {
1765
2069
  const yearField = ir.pattern.year;
1766
2070
  if (yearField === "*") {
1767
2071
  return description;
1768
2072
  }
1769
2073
  if (yearField.indexOf("/") !== -1) {
1770
- return description + " " + stepYears(yearField, opts);
2074
+ return description + ", " + stepYears(yearField, opts);
1771
2075
  }
1772
2076
  const label = yearLabel(yearField, opts);
1773
2077
  if (yearField.indexOf("-") === -1 && yearField.indexOf(",") === -1 && ir.pattern.date !== "*" && description.indexOf(" at ") !== -1) {
@@ -1780,6 +2084,9 @@ function yearLabel(yearField, opts) {
1780
2084
  if (yearField.indexOf(",") !== -1) {
1781
2085
  return joinList(yearField.split(","), opts);
1782
2086
  }
2087
+ if (yearField.indexOf("-") !== -1) {
2088
+ return yearField.split("-").join(through(opts));
2089
+ }
1783
2090
  return yearField;
1784
2091
  }
1785
2092
  function stepYears(yearField, opts) {
@@ -1789,7 +2096,7 @@ function stepYears(yearField, opts) {
1789
2096
  if (interval <= 1) {
1790
2097
  return "every year";
1791
2098
  }
1792
- let phrase = "every " + getNumber(interval, opts) + " years";
2099
+ let phrase = interval === 2 ? "every other year" : "every " + getNumber(interval, opts) + " years";
1793
2100
  if (start !== "*" && start !== "0") {
1794
2101
  phrase += " from " + start;
1795
2102
  }
@@ -1891,7 +2198,7 @@ function interpretCronPattern(cronPattern, lang, opts) {
1891
2198
  return lang.reboot;
1892
2199
  }
1893
2200
  const ir = analyze(prepare(cronPattern, opts));
1894
- const plan = lang.strategy ? lang.strategy(ir, ir.plan) : ir.plan;
2201
+ const plan = lang.plan ? lang.plan(ir, ir.plan) : ir.plan;
1895
2202
  return lang.describe({ ...ir, plan }, opts);
1896
2203
  }
1897
2204
  var cronli5_default = cronli5;