cronli5 0.1.7 → 0.2.0

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
@@ -849,7 +849,8 @@ var dialects = {
849
849
  pm: "p.m.",
850
850
  sep: ":",
851
851
  serialComma: true,
852
- through: " through "
852
+ through: " through ",
853
+ untilWindow: true
853
854
  },
854
855
  house: {
855
856
  am: "AM",
@@ -866,7 +867,7 @@ var dialects = {
866
867
  };
867
868
  function resolveDialect(dialect) {
868
869
  if (typeof dialect === "object" && dialect !== null) {
869
- return { ...dialects.us, ...dialect };
870
+ return { ...dialects.us, untilWindow: false, ...dialect };
870
871
  }
871
872
  const name = dialect === "uk" ? "gb" : dialect;
872
873
  return dialects[name] || dialects.us;
@@ -938,7 +939,9 @@ function normalizeOptions(options) {
938
939
  };
939
940
  }
940
941
  function describe(ir, opts) {
941
- return applyYear(render(ir, ir.plan, opts), ir, opts);
942
+ const body = confinement(ir, opts) ?? render(ir, ir.plan, opts);
943
+ const lead = isDayUnion(ir, opts) ? dayUnionMonthLead(ir, opts) : "";
944
+ return applyYear(lead + body, ir, opts);
942
945
  }
943
946
  function render(ir, plan, opts) {
944
947
  const renderer = renderers[plan.kind];
@@ -1031,7 +1034,7 @@ function secondsClause(ir, anchor, opts) {
1031
1034
  }
1032
1035
  if (shape === "range") {
1033
1036
  const bounds = secondField.split("-");
1034
- const num = seriesNumber(bounds, opts);
1037
+ const num = seriesNumber();
1035
1038
  return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the " + anchor;
1036
1039
  }
1037
1040
  if (shape === "single") {
@@ -1097,15 +1100,21 @@ function renderMinutesAcrossHours(ir, plan, opts) {
1097
1100
  }
1098
1101
  return "every minute during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
1099
1102
  }
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
- )
1103
+ if (plan.form === "range") {
1104
+ const lead2 = minuteRangeLead(ir.pattern.minute, opts);
1105
+ if (cadence !== null) {
1106
+ return lead2 + ", " + cadence + trailingQualifier(ir, opts);
1107
+ }
1108
+ if (singleHourFire(plan.times)) {
1109
+ return lead2 + ", at " + hourTimesFromPlan(ir, plan.times, true, opts) + trailingQualifier(ir, opts);
1110
+ }
1111
+ return lead2 + " during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
1112
+ }
1113
+ const lead = strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1114
+ segmentWords(ir.analyses.segments.minute, opts),
1115
+ "minute",
1116
+ "hour",
1117
+ opts
1109
1118
  );
1110
1119
  if (cadence !== null) {
1111
1120
  return lead + ", " + cadence + trailingQualifier(ir, opts);
@@ -1142,7 +1151,7 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
1142
1151
  }
1143
1152
  function minuteRangeLead(minuteField, opts) {
1144
1153
  const bounds = minuteField.split("-");
1145
- const num = seriesNumber(bounds, opts);
1154
+ const num = seriesNumber();
1146
1155
  return "every minute from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the hour";
1147
1156
  }
1148
1157
  function renderEveryHour(ir, plan, opts) {
@@ -1185,8 +1194,15 @@ function boundedWindow(plan) {
1185
1194
  const last = plan.minuteForm === "wildcard" ? plan.boundMinute ?? 0 : 0;
1186
1195
  return { from: plan.from, last, to: plan.to };
1187
1196
  }
1197
+ function rangeWindow(from, to, throughMinute, opts) {
1198
+ const open = "from " + getTime({ hour: from, minute: 0 }, opts);
1199
+ if (opts.style.untilWindow && !opts.short && from !== to) {
1200
+ return open + " until " + getTime({ hour: (to + 1) % 24, minute: 0 }, opts);
1201
+ }
1202
+ return open + through(opts) + getTime({ hour: to, minute: throughMinute }, opts);
1203
+ }
1188
1204
  function hourWindow(window, opts) {
1189
- return "from " + getTime({ hour: window.from, minute: 0 }, opts) + through(opts) + getTime({ hour: window.to, minute: window.last }, opts);
1205
+ return rangeWindow(window.from, window.to, window.last, opts);
1190
1206
  }
1191
1207
  function renderClockTimes(ir, plan, opts) {
1192
1208
  if (ir.shapes.minute === "single") {
@@ -1205,7 +1221,10 @@ function renderClockTimes(ir, plan, opts) {
1205
1221
  plain
1206
1222
  }, opts);
1207
1223
  });
1208
- return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts);
1224
+ return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts) + dayUnionTrail(ir, opts);
1225
+ }
1226
+ function dayUnionTrail(ir, opts) {
1227
+ return isDayUnion(ir, opts) ? dayUnionCondition(ir, opts) : "";
1209
1228
  }
1210
1229
  function renderCompactClockTimes(ir, plan, opts) {
1211
1230
  if (plan.fold) {
@@ -1220,7 +1239,7 @@ function renderCompactClockTimes(ir, plan, opts) {
1220
1239
  return foldedHourWindows(ir, plan, opts) + trailingQualifier(ir, opts);
1221
1240
  }
1222
1241
  const fold = { minute: plan.minute, second: ir.analyses.clockSecond };
1223
- return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
1242
+ return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts) + dayUnionTrail(ir, opts);
1224
1243
  }
1225
1244
  const minuteLead = (
1226
1245
  // The non-fold branch is a minute list, which has segments. An
@@ -1239,26 +1258,161 @@ function renderCompactClockTimes(ir, plan, opts) {
1239
1258
  function foldedHourWindows(ir, plan, opts) {
1240
1259
  const minute = plan.minute;
1241
1260
  const windows = [];
1242
- const singles = [];
1261
+ const outliers = collectHourOutliers(ir);
1262
+ const times = outliers.hours.map(function time(hour) {
1263
+ return getTime({ hour, minute }, opts);
1264
+ });
1243
1265
  ir.analyses.segments.hour.forEach(function classify(segment) {
1244
1266
  if (segment.kind === "range") {
1245
- windows.push("from " + getTime(
1246
- { hour: segment.bounds[0], minute: 0 },
1267
+ windows.push(rangeWindow(
1268
+ +segment.bounds[0],
1269
+ +segment.bounds[1],
1270
+ minute,
1247
1271
  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);
1272
+ ));
1253
1273
  }
1254
1274
  });
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);
1275
+ const phrase = rangeMinuteLead(ir, opts) + " " + joinList(windows, opts);
1276
+ return phrase + outlierTail(times, outliers.pureStrays, opts);
1277
+ }
1278
+ function collectHourOutliers(ir) {
1279
+ const hours = [];
1280
+ let pureStrays = true;
1281
+ ir.analyses.segments.hour.forEach(function classify(segment) {
1282
+ if (segment.kind === "step") {
1283
+ hours.push(...segment.fires);
1284
+ pureStrays = false;
1285
+ } else if (segment.kind !== "range") {
1286
+ hours.push(+segment.value);
1287
+ }
1288
+ });
1289
+ return { hours, pureStrays };
1290
+ }
1291
+ function outlierTail(times, pureStrays, opts) {
1292
+ if (!times.length) {
1293
+ return "";
1260
1294
  }
1261
- return phrase;
1295
+ const connector = pureStrays && opts.style.untilWindow && !opts.short ? " plus " : " and at ";
1296
+ return connector + joinList(times, opts);
1297
+ }
1298
+ function isCadenceField(token) {
1299
+ return token === "*" || token.startsWith("*/") && token.indexOf("-") === -1;
1300
+ }
1301
+ function leadingCadence(ir, opts) {
1302
+ const { second, minute } = ir.pattern;
1303
+ if (isCadenceField(second)) {
1304
+ return { secondLead: true, text: secondsClause(ir, "minute", opts) };
1305
+ }
1306
+ if (second === "0" && isCadenceField(minute)) {
1307
+ const text = minute === "*" ? "every minute" : (
1308
+ // A clean minute step's first segment is a step segment.
1309
+ stepCycle60(
1310
+ ir.analyses.segments.minute[0],
1311
+ "minute",
1312
+ "hour",
1313
+ opts
1314
+ )
1315
+ );
1316
+ return { secondLead: false, text };
1317
+ }
1318
+ return null;
1319
+ }
1320
+ function minuteConfinement(ir, opts) {
1321
+ const minute = ir.pattern.minute;
1322
+ if (minute === "*") {
1323
+ return "";
1324
+ }
1325
+ if (isCadenceField(minute)) {
1326
+ return " of every other minute";
1327
+ }
1328
+ const segments = ir.analyses.segments.minute;
1329
+ if (ir.shapes.minute === "single") {
1330
+ return " during minute :" + pad(minute);
1331
+ }
1332
+ if (ir.shapes.minute === "range") {
1333
+ const bounds = minute.split("-");
1334
+ return " during minutes :" + pad(bounds[0]) + through(opts) + ":" + pad(bounds[1]);
1335
+ }
1336
+ const values = segmentWords(segments, opts).map(function colon(word) {
1337
+ return ":" + pad(word);
1338
+ });
1339
+ return " during minutes " + joinList(values, opts);
1340
+ }
1341
+ function hourConfinement(ir, opts) {
1342
+ const hour = ir.pattern.hour;
1343
+ if (hour === "*") {
1344
+ const minutePinned = ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute);
1345
+ return minutePinned ? " of every hour" : "";
1346
+ }
1347
+ if (isCadenceField(hour)) {
1348
+ return hour === "*/2" ? " of every other hour" : "";
1349
+ }
1350
+ if (ir.shapes.hour === "single") {
1351
+ const h = +hour;
1352
+ if (ir.shapes.minute === "step") {
1353
+ return " from " + getTime({ hour: h, minute: 0 }, opts) + " until " + getTime({ hour: (h + 1) % 24, minute: 0 }, opts);
1354
+ }
1355
+ if (ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute)) {
1356
+ return " at " + getTime({ hour: h, minute: 0 }, opts);
1357
+ }
1358
+ return " of the " + getTime({ hour: h, minute: 0 }, opts) + " hour";
1359
+ }
1360
+ if (ir.shapes.hour === "range") {
1361
+ const bounds = hour.split("-");
1362
+ return " " + rangeWindow(+bounds[0], +bounds[1], 0, opts);
1363
+ }
1364
+ return " during the " + hourSegmentTimes(ir, { minute: 0, second: null }, false, opts) + " hours";
1365
+ }
1366
+ function isContiguousHourRange(ir) {
1367
+ return ir.shapes.hour === "range";
1368
+ }
1369
+ function confinableHour(ir) {
1370
+ if (ir.shapes.hour !== "step") {
1371
+ return true;
1372
+ }
1373
+ const segment = ir.analyses.segments.hour[0];
1374
+ return ir.pattern.hour === "*/2" || segment.startToken.indexOf("-") !== -1;
1375
+ }
1376
+ function isMinuteStride(ir) {
1377
+ if (ir.shapes.minute !== "list") {
1378
+ return false;
1379
+ }
1380
+ const values = singleValues(ir.analyses.segments.minute);
1381
+ return values !== null && arithmeticStep(values) !== null;
1382
+ }
1383
+ function confinementEligible(ir, lead) {
1384
+ const { minute, hour } = ir.pattern;
1385
+ const minuteStep = isCadenceField(minute) && minute !== "*";
1386
+ if (!confinableHour(ir)) {
1387
+ return false;
1388
+ }
1389
+ if (lead.secondLead) {
1390
+ if (minuteStep) {
1391
+ return minute === "*/2" && !isContiguousHourRange(ir);
1392
+ }
1393
+ if (isMinuteStride(ir) || ir.shapes.minute === "list" && ir.shapes.hour === "list") {
1394
+ return false;
1395
+ }
1396
+ return true;
1397
+ }
1398
+ if (hour === "*/2") {
1399
+ return true;
1400
+ }
1401
+ return ir.shapes.hour === "single" && minute === "*/2";
1402
+ }
1403
+ function confinement(ir, opts) {
1404
+ if (!opts.style.untilWindow || opts.short) {
1405
+ return null;
1406
+ }
1407
+ if (ir.pattern.minute === "*" && ir.pattern.hour === "*") {
1408
+ return null;
1409
+ }
1410
+ const lead = leadingCadence(ir, opts);
1411
+ if (!lead || !confinementEligible(ir, lead)) {
1412
+ return null;
1413
+ }
1414
+ const minutePart = lead.secondLead ? minuteConfinement(ir, opts) : "";
1415
+ return lead.text + minutePart + hourConfinement(ir, opts) + trailingQualifier(ir, opts);
1262
1416
  }
1263
1417
  var renderers = {
1264
1418
  clockTimes: renderClockTimes,
@@ -1290,7 +1444,7 @@ function renderStride(stride, opts) {
1290
1444
  if (start < interval && tiles) {
1291
1445
  return cadence + " from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
1292
1446
  }
1293
- const num = seriesNumber([start, last], opts);
1447
+ const num = seriesNumber();
1294
1448
  return cadence + " from " + num(start) + through(opts) + num(last) + " " + pluralize(last, unit) + " past the " + anchor;
1295
1449
  }
1296
1450
  function singleValues(segments) {
@@ -1417,9 +1571,9 @@ function hourCadence(ir, minute, opts) {
1417
1571
  if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
1418
1572
  return null;
1419
1573
  }
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);
1574
+ const minuteZeroStride = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
1575
+ if (minuteZeroStride) {
1576
+ return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(minuteZeroStride, opts) + trailingQualifier(ir, opts);
1423
1577
  }
1424
1578
  if (minute === 0 && ir.pattern.second === "0") {
1425
1579
  return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
@@ -1441,26 +1595,22 @@ function hasHourWindow(ir) {
1441
1595
  }
1442
1596
  function hourRangeWindowTail(ir, opts) {
1443
1597
  const windows = [];
1444
- const singles = [];
1598
+ const outliers = collectHourOutliers(ir);
1445
1599
  ir.analyses.segments.hour.forEach(function classify(segment) {
1446
1600
  if (segment.kind === "range") {
1447
- windows.push("from " + getTime(
1448
- { hour: +segment.bounds[0], minute: 0 },
1601
+ windows.push(rangeWindow(
1602
+ +segment.bounds[0],
1603
+ +segment.bounds[1],
1604
+ 0,
1449
1605
  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);
1606
+ ));
1455
1607
  }
1456
1608
  });
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;
1609
+ const phrase = "every hour " + joinList(windows, opts);
1610
+ const times = outliers.hours.map(function time(hour) {
1611
+ return getTime({ hour, minute: 0 }, opts);
1612
+ });
1613
+ return phrase + outlierTail(times, outliers.pureStrays, opts);
1464
1614
  }
1465
1615
  function hourRangeCadence(ir, minute, opts) {
1466
1616
  if (minute !== 0 || !hasHourWindow(ir)) {
@@ -1474,25 +1624,29 @@ function hourRangeCadence(ir, minute, opts) {
1474
1624
  }
1475
1625
  return hourCadenceLead(ir, minute, opts) + ", " + hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
1476
1626
  }
1477
- function seriesNumber(values, opts) {
1478
- const anyBig = values.some(function big(v) {
1479
- return +v > 10;
1480
- });
1627
+ function seriesNumber() {
1481
1628
  return function format(n) {
1482
- return anyBig ? "" + n : getNumber(n, opts);
1629
+ return "" + n;
1630
+ };
1631
+ }
1632
+ function listNumber(count, opts) {
1633
+ return count > 1 ? function asNumeral(n) {
1634
+ return "" + n;
1635
+ } : function spelled(n) {
1636
+ return getNumber(n, opts);
1483
1637
  };
1484
1638
  }
1485
1639
  function numberWords(fires, opts) {
1486
- return fires.map(seriesNumber(fires, opts));
1640
+ return fires.map(listNumber(fires.length, opts));
1487
1641
  }
1488
1642
  function segmentWords(segments, opts) {
1489
- const values = segments.flatMap(function collect(segment) {
1643
+ const count = segments.reduce(function tally(sum, segment) {
1490
1644
  if (segment.kind === "range") {
1491
- return segment.bounds;
1645
+ return sum + 1;
1492
1646
  }
1493
- return segment.kind === "step" ? segment.fires : [segment.value];
1494
- });
1495
- const num = seriesNumber(values, opts);
1647
+ return sum + (segment.kind === "step" ? segment.fires.length : 1);
1648
+ }, 0);
1649
+ const num = listNumber(count, opts);
1496
1650
  return segments.flatMap(function word(segment) {
1497
1651
  if (segment.kind === "range") {
1498
1652
  return [num(segment.bounds[0]) + through(opts) + num(segment.bounds[1])];
@@ -1524,6 +1678,9 @@ function hourTimes(hours, opts) {
1524
1678
  });
1525
1679
  return joinList(times, opts);
1526
1680
  }
1681
+ function singleHourFire(times) {
1682
+ return times.kind === "fires" && times.fires.length === 1;
1683
+ }
1527
1684
  function hourTimesFromPlan(ir, times, atContext, opts) {
1528
1685
  if (times.kind === "fires") {
1529
1686
  return hourTimes(times.fires, opts);
@@ -1571,28 +1728,47 @@ function disambiguateTimes(pieces, segments, atContext) {
1571
1728
  return index === 0 ? piece : "at " + piece;
1572
1729
  });
1573
1730
  }
1574
- function joinList(items, opts) {
1731
+ function joinWith(items, conjunction, opts) {
1575
1732
  if (items.length <= 1) {
1576
1733
  return items.join("");
1577
1734
  }
1578
1735
  if (items.length === 2) {
1579
- return items[0] + " and " + items[1];
1736
+ return items[0] + conjunction + items[1];
1580
1737
  }
1581
- const and = opts.style.serialComma ? ", and " : " and ";
1582
- return items.slice(0, -1).join(", ") + and + items[items.length - 1];
1738
+ const tail = opts.style.serialComma ? "," + conjunction : conjunction;
1739
+ return items.slice(0, -1).join(", ") + tail + items[items.length - 1];
1740
+ }
1741
+ function joinList(items, opts) {
1742
+ return joinWith(items, " and ", opts);
1743
+ }
1744
+ function joinOr(items, opts) {
1745
+ return joinWith(items, " or ", opts);
1583
1746
  }
1584
- var trailingWords = { all: "", month: "in ", stepDate: "on ", weekday: "on " };
1747
+ var trailingWords = {
1748
+ all: "",
1749
+ month: "in ",
1750
+ recurringWeekday: true,
1751
+ stepDate: "on ",
1752
+ weekday: "on "
1753
+ };
1585
1754
  var leadingWords = {
1586
1755
  all: "every day",
1587
1756
  month: "every day in ",
1757
+ recurringWeekday: false,
1588
1758
  stepDate: "",
1589
1759
  weekday: "every "
1590
1760
  };
1591
1761
  function trailingQualifier(ir, opts) {
1762
+ if (isDayUnion(ir, opts)) {
1763
+ return dayUnionCondition(ir, opts);
1764
+ }
1592
1765
  const phrase = dayQualifier(ir, trailingWords, opts);
1593
1766
  return phrase && " " + phrase;
1594
1767
  }
1595
1768
  function interpretDayQualifier(ir, opts) {
1769
+ if (isDayUnion(ir, opts)) {
1770
+ return "";
1771
+ }
1596
1772
  return dayQualifier(ir, leadingWords, opts) + " ";
1597
1773
  }
1598
1774
  function dayQualifier(ir, words, opts) {
@@ -1604,7 +1780,11 @@ function dayQualifier(ir, words, opts) {
1604
1780
  return datePhrase(ir, words, opts);
1605
1781
  }
1606
1782
  if (pattern.weekday !== "*") {
1607
- const weekdays = quartzWeekdayPhrase(pattern.weekday, opts) || words.weekday + weekdayPhrase(ir, opts);
1783
+ const quartzWeekday = quartzWeekdayPhrase(pattern.weekday, opts);
1784
+ if (quartzWeekday) {
1785
+ return monthScopeForRecurrence(quartzWeekday, ir, opts);
1786
+ }
1787
+ const weekdays = words.weekday + weekdayPhrase(ir, words.recurringWeekday, opts);
1608
1788
  return weekdays + monthScope(ir, opts);
1609
1789
  }
1610
1790
  if (pattern.month !== "*") {
@@ -1616,10 +1796,14 @@ function datePhrase(ir, words, opts) {
1616
1796
  const pattern = ir.pattern;
1617
1797
  const quartzDate = quartzDatePhrase(pattern.date, opts);
1618
1798
  if (quartzDate) {
1619
- return quartzDate + monthScope(ir, opts);
1799
+ return monthScopeForRecurrence(quartzDate, ir, opts);
1620
1800
  }
1621
1801
  if (isOpenStep(pattern.date)) {
1622
- return words.stepDate + stepDates(pattern.date) + monthScope(ir, opts);
1802
+ return monthScopeForRecurrence(
1803
+ words.stepDate + stepDates(pattern.date),
1804
+ ir,
1805
+ opts
1806
+ );
1623
1807
  }
1624
1808
  if (pattern.month !== "*" && !monthFoldsIntoDate(ir)) {
1625
1809
  return "on the " + dateOrdinals(ir, opts) + monthScope(ir, opts);
@@ -1635,9 +1819,84 @@ function monthFoldsIntoDate(ir) {
1635
1819
  return segment.kind !== "range";
1636
1820
  });
1637
1821
  }
1822
+ function isDayUnion(ir, opts) {
1823
+ return ir.pattern.date !== "*" && ir.pattern.weekday !== "*" && !!opts.style.untilWindow && !opts.short;
1824
+ }
1825
+ function dayUnionCondition(ir, opts) {
1826
+ const pieces = [
1827
+ ...dayUnionDatePieces(ir, opts),
1828
+ ...dayUnionWeekdayPieces(ir, opts)
1829
+ ];
1830
+ return " whenever the day is " + joinOr(pieces, opts);
1831
+ }
1832
+ function dayUnionMonthLead(ir, opts) {
1833
+ if (ir.pattern.month === "*") {
1834
+ return "";
1835
+ }
1836
+ return "in " + monthName(ir, opts) + " ";
1837
+ }
1838
+ function dayUnionDatePieces(ir, opts) {
1839
+ const dateField = ir.pattern.date;
1840
+ const quartz = quartzDatePhrase(dateField, opts);
1841
+ if (quartz) {
1842
+ return [quartz.replace(/^on /, "")];
1843
+ }
1844
+ const oddEven = oddEvenDay(dateField);
1845
+ if (oddEven) {
1846
+ return [oddEven];
1847
+ }
1848
+ const pieces = [];
1849
+ ir.analyses.segments.date.forEach(function expand(segment) {
1850
+ if (segment.kind === "range") {
1851
+ pieces.push("from the " + getOrdinal(segment.bounds[0]) + through(opts) + "the " + getOrdinal(segment.bounds[1]));
1852
+ } else if (segment.kind === "step") {
1853
+ segment.fires.forEach(function fire(value) {
1854
+ pieces.push("the " + getOrdinal(value));
1855
+ });
1856
+ } else {
1857
+ pieces.push("the " + getOrdinal(segment.value));
1858
+ }
1859
+ });
1860
+ return pieces;
1861
+ }
1862
+ function dayUnionWeekdayPieces(ir, opts) {
1863
+ const weekdayField = ir.pattern.weekday;
1864
+ const quartz = quartzWeekdayPhrase(weekdayField, opts);
1865
+ if (quartz) {
1866
+ return [quartz.replace(/^on /, "")];
1867
+ }
1868
+ const pieces = [];
1869
+ ir.analyses.segments.weekday.forEach(function expand(segment) {
1870
+ if (segment.kind === "range" && segment.bounds[0] === "1" && segment.bounds[1] === "5") {
1871
+ pieces.push("a weekday");
1872
+ } else if (segment.kind === "range") {
1873
+ pieces.push("a " + getWeekday(segment.bounds[0], opts) + through(opts) + "a " + getWeekday(segment.bounds[1], opts));
1874
+ } else if (segment.kind === "step") {
1875
+ segment.fires.forEach(function fire(value) {
1876
+ pieces.push("a " + getWeekday(value, opts));
1877
+ });
1878
+ } else {
1879
+ pieces.push("a " + getWeekday(segment.value, opts));
1880
+ }
1881
+ });
1882
+ return pieces;
1883
+ }
1884
+ function oddEvenDay(dateField) {
1885
+ if (!isOpenStep(dateField)) {
1886
+ return null;
1887
+ }
1888
+ const [start, step] = dateField.split("/");
1889
+ if (+step !== 2) {
1890
+ return null;
1891
+ }
1892
+ if (start === "*" || start === "1") {
1893
+ return "an odd-numbered day";
1894
+ }
1895
+ return start === "2" ? "an even-numbered day" : null;
1896
+ }
1638
1897
  function dateOrWeekday(ir, opts) {
1639
1898
  const pattern = ir.pattern;
1640
- const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, opts);
1899
+ const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, false, opts);
1641
1900
  if (pattern.month !== "*" && monthFoldsIntoDate(ir) && !quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
1642
1901
  return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
1643
1902
  }
@@ -1692,6 +1951,9 @@ function monthDatePhrase(ir, opts) {
1692
1951
  opts.style.ordinals ? getOrdinal : cardinalDay,
1693
1952
  opts
1694
1953
  );
1954
+ if (opts.style.dayFirst && ir.shapes.date === "single" && ir.shapes.month !== "single") {
1955
+ return "the " + getOrdinal(ir.pattern.date) + " of " + month;
1956
+ }
1695
1957
  return opts.style.dayFirst ? days + " " + month : month + " " + days;
1696
1958
  }
1697
1959
  function cardinalDay(value) {
@@ -1703,6 +1965,19 @@ function monthScope(ir, opts) {
1703
1965
  }
1704
1966
  return " in " + monthName(ir, opts);
1705
1967
  }
1968
+ function monthScopeForRecurrence(phrase, ir, opts) {
1969
+ if (ir.pattern.month === "*") {
1970
+ return phrase;
1971
+ }
1972
+ const carriesRecurrence = phrase.indexOf(" of the month") !== -1;
1973
+ if (carriesRecurrence && ir.shapes.month === "range") {
1974
+ return phrase.replace(" of the month", " of each month") + " from " + monthName(ir, opts);
1975
+ }
1976
+ if (carriesRecurrence && (ir.shapes.month === "single" || ir.shapes.month === "step")) {
1977
+ return phrase.replace(" of the month", "") + " in " + monthName(ir, opts);
1978
+ }
1979
+ return phrase + " in " + monthName(ir, opts);
1980
+ }
1706
1981
  function stepDates(dateField) {
1707
1982
  const parts = dateField.split("/");
1708
1983
  const interval = +parts[1];
@@ -1739,11 +2014,21 @@ function oddEvenMonth(monthField) {
1739
2014
  }
1740
2015
  return start === "2" ? "every even-numbered month" : null;
1741
2016
  }
1742
- function weekdayPhrase(ir, opts) {
2017
+ function weekdayPhrase(ir, recurring, opts) {
1743
2018
  const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
1744
- return renderSegments(segments, function name(value) {
2019
+ const hasRange = segments.some(function range(segment) {
2020
+ return segment.kind === "range";
2021
+ });
2022
+ const name = recurring && !hasRange ? function plural(value) {
2023
+ return pluralWeekday(value, opts);
2024
+ } : function singular(value) {
1745
2025
  return getWeekday(value, opts);
1746
- }, opts);
2026
+ };
2027
+ return renderSegments(segments, name, opts);
2028
+ }
2029
+ function pluralWeekday(value, opts) {
2030
+ const name = getWeekday(value, opts);
2031
+ return opts.short ? name : name + "s";
1747
2032
  }
1748
2033
  function renderSegments(segments, word, opts) {
1749
2034
  const pieces = [];
@@ -1767,7 +2052,7 @@ function applyYear(description, ir, opts) {
1767
2052
  return description;
1768
2053
  }
1769
2054
  if (yearField.indexOf("/") !== -1) {
1770
- return description + " " + stepYears(yearField, opts);
2055
+ return description + ", " + stepYears(yearField, opts);
1771
2056
  }
1772
2057
  const label = yearLabel(yearField, opts);
1773
2058
  if (yearField.indexOf("-") === -1 && yearField.indexOf(",") === -1 && ir.pattern.date !== "*" && description.indexOf(" at ") !== -1) {
@@ -1780,6 +2065,9 @@ function yearLabel(yearField, opts) {
1780
2065
  if (yearField.indexOf(",") !== -1) {
1781
2066
  return joinList(yearField.split(","), opts);
1782
2067
  }
2068
+ if (yearField.indexOf("-") !== -1) {
2069
+ return yearField.split("-").join(through(opts));
2070
+ }
1783
2071
  return yearField;
1784
2072
  }
1785
2073
  function stepYears(yearField, opts) {
@@ -1789,7 +2077,7 @@ function stepYears(yearField, opts) {
1789
2077
  if (interval <= 1) {
1790
2078
  return "every year";
1791
2079
  }
1792
- let phrase = "every " + getNumber(interval, opts) + " years";
2080
+ let phrase = interval === 2 ? "every other year" : "every " + getNumber(interval, opts) + " years";
1793
2081
  if (start !== "*" && start !== "0") {
1794
2082
  phrase += " from " + start;
1795
2083
  }