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.cjs CHANGED
@@ -875,7 +875,8 @@ var dialects = {
875
875
  pm: "p.m.",
876
876
  sep: ":",
877
877
  serialComma: true,
878
- through: " through "
878
+ through: " through ",
879
+ untilWindow: true
879
880
  },
880
881
  house: {
881
882
  am: "AM",
@@ -892,7 +893,7 @@ var dialects = {
892
893
  };
893
894
  function resolveDialect(dialect) {
894
895
  if (typeof dialect === "object" && dialect !== null) {
895
- return { ...dialects.us, ...dialect };
896
+ return { ...dialects.us, untilWindow: false, ...dialect };
896
897
  }
897
898
  const name = dialect === "uk" ? "gb" : dialect;
898
899
  return dialects[name] || dialects.us;
@@ -964,7 +965,9 @@ function normalizeOptions(options) {
964
965
  };
965
966
  }
966
967
  function describe(ir, opts) {
967
- return applyYear(render(ir, ir.plan, opts), ir, opts);
968
+ const body = confinement(ir, opts) ?? render(ir, ir.plan, opts);
969
+ const lead = isDayUnion(ir, opts) ? dayUnionMonthLead(ir, opts) : "";
970
+ return applyYear(lead + body, ir, opts);
968
971
  }
969
972
  function render(ir, plan, opts) {
970
973
  const renderer = renderers[plan.kind];
@@ -1057,7 +1060,7 @@ function secondsClause(ir, anchor, opts) {
1057
1060
  }
1058
1061
  if (shape === "range") {
1059
1062
  const bounds = secondField.split("-");
1060
- const num = seriesNumber(bounds, opts);
1063
+ const num = seriesNumber();
1061
1064
  return "every second from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the " + anchor;
1062
1065
  }
1063
1066
  if (shape === "single") {
@@ -1123,15 +1126,21 @@ function renderMinutesAcrossHours(ir, plan, opts) {
1123
1126
  }
1124
1127
  return "every minute during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
1125
1128
  }
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
- )
1129
+ if (plan.form === "range") {
1130
+ const lead2 = minuteRangeLead(ir.pattern.minute, opts);
1131
+ if (cadence !== null) {
1132
+ return lead2 + ", " + cadence + trailingQualifier(ir, opts);
1133
+ }
1134
+ if (singleHourFire(plan.times)) {
1135
+ return lead2 + ", at " + hourTimesFromPlan(ir, plan.times, true, opts) + trailingQualifier(ir, opts);
1136
+ }
1137
+ return lead2 + " during the " + hourTimesFromPlan(ir, plan.times, false, opts) + " hours" + trailingQualifier(ir, opts);
1138
+ }
1139
+ const lead = strideFromSegments(ir.analyses.segments.minute, "minute", "hour", opts) ?? listPastThe(
1140
+ segmentWords(ir.analyses.segments.minute, opts),
1141
+ "minute",
1142
+ "hour",
1143
+ opts
1135
1144
  );
1136
1145
  if (cadence !== null) {
1137
1146
  return lead + ", " + cadence + trailingQualifier(ir, opts);
@@ -1168,7 +1177,7 @@ function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
1168
1177
  }
1169
1178
  function minuteRangeLead(minuteField, opts) {
1170
1179
  const bounds = minuteField.split("-");
1171
- const num = seriesNumber(bounds, opts);
1180
+ const num = seriesNumber();
1172
1181
  return "every minute from " + num(bounds[0]) + through(opts) + num(bounds[1]) + " past the hour";
1173
1182
  }
1174
1183
  function renderEveryHour(ir, plan, opts) {
@@ -1211,8 +1220,15 @@ function boundedWindow(plan) {
1211
1220
  const last = plan.minuteForm === "wildcard" ? plan.boundMinute ?? 0 : 0;
1212
1221
  return { from: plan.from, last, to: plan.to };
1213
1222
  }
1223
+ function rangeWindow(from, to, throughMinute, opts) {
1224
+ const open = "from " + getTime({ hour: from, minute: 0 }, opts);
1225
+ if (opts.style.untilWindow && !opts.short && from !== to) {
1226
+ return open + " until " + getTime({ hour: (to + 1) % 24, minute: 0 }, opts);
1227
+ }
1228
+ return open + through(opts) + getTime({ hour: to, minute: throughMinute }, opts);
1229
+ }
1214
1230
  function hourWindow(window, opts) {
1215
- return "from " + getTime({ hour: window.from, minute: 0 }, opts) + through(opts) + getTime({ hour: window.to, minute: window.last }, opts);
1231
+ return rangeWindow(window.from, window.to, window.last, opts);
1216
1232
  }
1217
1233
  function renderClockTimes(ir, plan, opts) {
1218
1234
  if (ir.shapes.minute === "single") {
@@ -1231,7 +1247,10 @@ function renderClockTimes(ir, plan, opts) {
1231
1247
  plain
1232
1248
  }, opts);
1233
1249
  });
1234
- return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts);
1250
+ return interpretDayQualifier(ir, opts) + "at " + joinList(times, opts) + dayUnionTrail(ir, opts);
1251
+ }
1252
+ function dayUnionTrail(ir, opts) {
1253
+ return isDayUnion(ir, opts) ? dayUnionCondition(ir, opts) : "";
1235
1254
  }
1236
1255
  function renderCompactClockTimes(ir, plan, opts) {
1237
1256
  if (plan.fold) {
@@ -1246,7 +1265,7 @@ function renderCompactClockTimes(ir, plan, opts) {
1246
1265
  return foldedHourWindows(ir, plan, opts) + trailingQualifier(ir, opts);
1247
1266
  }
1248
1267
  const fold = { minute: plan.minute, second: ir.analyses.clockSecond };
1249
- return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts);
1268
+ return interpretDayQualifier(ir, opts) + "at " + hourSegmentTimes(ir, fold, true, opts) + dayUnionTrail(ir, opts);
1250
1269
  }
1251
1270
  const minuteLead = (
1252
1271
  // The non-fold branch is a minute list, which has segments. An
@@ -1265,26 +1284,161 @@ function renderCompactClockTimes(ir, plan, opts) {
1265
1284
  function foldedHourWindows(ir, plan, opts) {
1266
1285
  const minute = plan.minute;
1267
1286
  const windows = [];
1268
- const singles = [];
1287
+ const outliers = collectHourOutliers(ir);
1288
+ const times = outliers.hours.map(function time(hour) {
1289
+ return getTime({ hour, minute }, opts);
1290
+ });
1269
1291
  ir.analyses.segments.hour.forEach(function classify(segment) {
1270
1292
  if (segment.kind === "range") {
1271
- windows.push("from " + getTime(
1272
- { hour: segment.bounds[0], minute: 0 },
1293
+ windows.push(rangeWindow(
1294
+ +segment.bounds[0],
1295
+ +segment.bounds[1],
1296
+ minute,
1273
1297
  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);
1298
+ ));
1279
1299
  }
1280
1300
  });
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);
1301
+ const phrase = rangeMinuteLead(ir, opts) + " " + joinList(windows, opts);
1302
+ return phrase + outlierTail(times, outliers.pureStrays, opts);
1303
+ }
1304
+ function collectHourOutliers(ir) {
1305
+ const hours = [];
1306
+ let pureStrays = true;
1307
+ ir.analyses.segments.hour.forEach(function classify(segment) {
1308
+ if (segment.kind === "step") {
1309
+ hours.push(...segment.fires);
1310
+ pureStrays = false;
1311
+ } else if (segment.kind !== "range") {
1312
+ hours.push(+segment.value);
1313
+ }
1314
+ });
1315
+ return { hours, pureStrays };
1316
+ }
1317
+ function outlierTail(times, pureStrays, opts) {
1318
+ if (!times.length) {
1319
+ return "";
1286
1320
  }
1287
- return phrase;
1321
+ const connector = pureStrays && opts.style.untilWindow && !opts.short ? " plus " : " and at ";
1322
+ return connector + joinList(times, opts);
1323
+ }
1324
+ function isCadenceField(token) {
1325
+ return token === "*" || token.startsWith("*/") && token.indexOf("-") === -1;
1326
+ }
1327
+ function leadingCadence(ir, opts) {
1328
+ const { second, minute } = ir.pattern;
1329
+ if (isCadenceField(second)) {
1330
+ return { secondLead: true, text: secondsClause(ir, "minute", opts) };
1331
+ }
1332
+ if (second === "0" && isCadenceField(minute)) {
1333
+ const text = minute === "*" ? "every minute" : (
1334
+ // A clean minute step's first segment is a step segment.
1335
+ stepCycle60(
1336
+ ir.analyses.segments.minute[0],
1337
+ "minute",
1338
+ "hour",
1339
+ opts
1340
+ )
1341
+ );
1342
+ return { secondLead: false, text };
1343
+ }
1344
+ return null;
1345
+ }
1346
+ function minuteConfinement(ir, opts) {
1347
+ const minute = ir.pattern.minute;
1348
+ if (minute === "*") {
1349
+ return "";
1350
+ }
1351
+ if (isCadenceField(minute)) {
1352
+ return " of every other minute";
1353
+ }
1354
+ const segments = ir.analyses.segments.minute;
1355
+ if (ir.shapes.minute === "single") {
1356
+ return " during minute :" + pad(minute);
1357
+ }
1358
+ if (ir.shapes.minute === "range") {
1359
+ const bounds = minute.split("-");
1360
+ return " during minutes :" + pad(bounds[0]) + through(opts) + ":" + pad(bounds[1]);
1361
+ }
1362
+ const values = segmentWords(segments, opts).map(function colon(word) {
1363
+ return ":" + pad(word);
1364
+ });
1365
+ return " during minutes " + joinList(values, opts);
1366
+ }
1367
+ function hourConfinement(ir, opts) {
1368
+ const hour = ir.pattern.hour;
1369
+ if (hour === "*") {
1370
+ const minutePinned = ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute);
1371
+ return minutePinned ? " of every hour" : "";
1372
+ }
1373
+ if (isCadenceField(hour)) {
1374
+ return hour === "*/2" ? " of every other hour" : "";
1375
+ }
1376
+ if (ir.shapes.hour === "single") {
1377
+ const h = +hour;
1378
+ if (ir.shapes.minute === "step") {
1379
+ return " from " + getTime({ hour: h, minute: 0 }, opts) + " until " + getTime({ hour: (h + 1) % 24, minute: 0 }, opts);
1380
+ }
1381
+ if (ir.pattern.minute !== "*" && !isCadenceField(ir.pattern.minute)) {
1382
+ return " at " + getTime({ hour: h, minute: 0 }, opts);
1383
+ }
1384
+ return " of the " + getTime({ hour: h, minute: 0 }, opts) + " hour";
1385
+ }
1386
+ if (ir.shapes.hour === "range") {
1387
+ const bounds = hour.split("-");
1388
+ return " " + rangeWindow(+bounds[0], +bounds[1], 0, opts);
1389
+ }
1390
+ return " during the " + hourSegmentTimes(ir, { minute: 0, second: null }, false, opts) + " hours";
1391
+ }
1392
+ function isContiguousHourRange(ir) {
1393
+ return ir.shapes.hour === "range";
1394
+ }
1395
+ function confinableHour(ir) {
1396
+ if (ir.shapes.hour !== "step") {
1397
+ return true;
1398
+ }
1399
+ const segment = ir.analyses.segments.hour[0];
1400
+ return ir.pattern.hour === "*/2" || segment.startToken.indexOf("-") !== -1;
1401
+ }
1402
+ function isMinuteStride(ir) {
1403
+ if (ir.shapes.minute !== "list") {
1404
+ return false;
1405
+ }
1406
+ const values = singleValues(ir.analyses.segments.minute);
1407
+ return values !== null && arithmeticStep(values) !== null;
1408
+ }
1409
+ function confinementEligible(ir, lead) {
1410
+ const { minute, hour } = ir.pattern;
1411
+ const minuteStep = isCadenceField(minute) && minute !== "*";
1412
+ if (!confinableHour(ir)) {
1413
+ return false;
1414
+ }
1415
+ if (lead.secondLead) {
1416
+ if (minuteStep) {
1417
+ return minute === "*/2" && !isContiguousHourRange(ir);
1418
+ }
1419
+ if (isMinuteStride(ir) || ir.shapes.minute === "list" && ir.shapes.hour === "list") {
1420
+ return false;
1421
+ }
1422
+ return true;
1423
+ }
1424
+ if (hour === "*/2") {
1425
+ return true;
1426
+ }
1427
+ return ir.shapes.hour === "single" && minute === "*/2";
1428
+ }
1429
+ function confinement(ir, opts) {
1430
+ if (!opts.style.untilWindow || opts.short) {
1431
+ return null;
1432
+ }
1433
+ if (ir.pattern.minute === "*" && ir.pattern.hour === "*") {
1434
+ return null;
1435
+ }
1436
+ const lead = leadingCadence(ir, opts);
1437
+ if (!lead || !confinementEligible(ir, lead)) {
1438
+ return null;
1439
+ }
1440
+ const minutePart = lead.secondLead ? minuteConfinement(ir, opts) : "";
1441
+ return lead.text + minutePart + hourConfinement(ir, opts) + trailingQualifier(ir, opts);
1288
1442
  }
1289
1443
  var renderers = {
1290
1444
  clockTimes: renderClockTimes,
@@ -1316,7 +1470,7 @@ function renderStride(stride, opts) {
1316
1470
  if (start < interval && tiles) {
1317
1471
  return cadence + " from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
1318
1472
  }
1319
- const num = seriesNumber([start, last], opts);
1473
+ const num = seriesNumber();
1320
1474
  return cadence + " from " + num(start) + through(opts) + num(last) + " " + pluralize(last, unit) + " past the " + anchor;
1321
1475
  }
1322
1476
  function singleValues(segments) {
@@ -1443,9 +1597,9 @@ function hourCadence(ir, minute, opts) {
1443
1597
  if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
1444
1598
  return null;
1445
1599
  }
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);
1600
+ const minuteZeroStride = minute === 0 && subMinuteSecond(ir) && cleanStrideSegment(ir);
1601
+ if (minuteZeroStride) {
1602
+ return secondsClause(ir, "minute", opts) + " for one minute " + everyNthHour(minuteZeroStride, opts) + trailingQualifier(ir, opts);
1449
1603
  }
1450
1604
  if (minute === 0 && ir.pattern.second === "0") {
1451
1605
  return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
@@ -1467,26 +1621,22 @@ function hasHourWindow(ir) {
1467
1621
  }
1468
1622
  function hourRangeWindowTail(ir, opts) {
1469
1623
  const windows = [];
1470
- const singles = [];
1624
+ const outliers = collectHourOutliers(ir);
1471
1625
  ir.analyses.segments.hour.forEach(function classify(segment) {
1472
1626
  if (segment.kind === "range") {
1473
- windows.push("from " + getTime(
1474
- { hour: +segment.bounds[0], minute: 0 },
1627
+ windows.push(rangeWindow(
1628
+ +segment.bounds[0],
1629
+ +segment.bounds[1],
1630
+ 0,
1475
1631
  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);
1632
+ ));
1481
1633
  }
1482
1634
  });
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;
1635
+ const phrase = "every hour " + joinList(windows, opts);
1636
+ const times = outliers.hours.map(function time(hour) {
1637
+ return getTime({ hour, minute: 0 }, opts);
1638
+ });
1639
+ return phrase + outlierTail(times, outliers.pureStrays, opts);
1490
1640
  }
1491
1641
  function hourRangeCadence(ir, minute, opts) {
1492
1642
  if (minute !== 0 || !hasHourWindow(ir)) {
@@ -1500,25 +1650,29 @@ function hourRangeCadence(ir, minute, opts) {
1500
1650
  }
1501
1651
  return hourCadenceLead(ir, minute, opts) + ", " + hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
1502
1652
  }
1503
- function seriesNumber(values, opts) {
1504
- const anyBig = values.some(function big(v) {
1505
- return +v > 10;
1506
- });
1653
+ function seriesNumber() {
1507
1654
  return function format(n) {
1508
- return anyBig ? "" + n : getNumber(n, opts);
1655
+ return "" + n;
1656
+ };
1657
+ }
1658
+ function listNumber(count, opts) {
1659
+ return count > 1 ? function asNumeral(n) {
1660
+ return "" + n;
1661
+ } : function spelled(n) {
1662
+ return getNumber(n, opts);
1509
1663
  };
1510
1664
  }
1511
1665
  function numberWords(fires, opts) {
1512
- return fires.map(seriesNumber(fires, opts));
1666
+ return fires.map(listNumber(fires.length, opts));
1513
1667
  }
1514
1668
  function segmentWords(segments, opts) {
1515
- const values = segments.flatMap(function collect(segment) {
1669
+ const count = segments.reduce(function tally(sum, segment) {
1516
1670
  if (segment.kind === "range") {
1517
- return segment.bounds;
1671
+ return sum + 1;
1518
1672
  }
1519
- return segment.kind === "step" ? segment.fires : [segment.value];
1520
- });
1521
- const num = seriesNumber(values, opts);
1673
+ return sum + (segment.kind === "step" ? segment.fires.length : 1);
1674
+ }, 0);
1675
+ const num = listNumber(count, opts);
1522
1676
  return segments.flatMap(function word(segment) {
1523
1677
  if (segment.kind === "range") {
1524
1678
  return [num(segment.bounds[0]) + through(opts) + num(segment.bounds[1])];
@@ -1550,6 +1704,9 @@ function hourTimes(hours, opts) {
1550
1704
  });
1551
1705
  return joinList(times, opts);
1552
1706
  }
1707
+ function singleHourFire(times) {
1708
+ return times.kind === "fires" && times.fires.length === 1;
1709
+ }
1553
1710
  function hourTimesFromPlan(ir, times, atContext, opts) {
1554
1711
  if (times.kind === "fires") {
1555
1712
  return hourTimes(times.fires, opts);
@@ -1597,28 +1754,47 @@ function disambiguateTimes(pieces, segments, atContext) {
1597
1754
  return index === 0 ? piece : "at " + piece;
1598
1755
  });
1599
1756
  }
1600
- function joinList(items, opts) {
1757
+ function joinWith(items, conjunction, opts) {
1601
1758
  if (items.length <= 1) {
1602
1759
  return items.join("");
1603
1760
  }
1604
1761
  if (items.length === 2) {
1605
- return items[0] + " and " + items[1];
1762
+ return items[0] + conjunction + items[1];
1606
1763
  }
1607
- const and = opts.style.serialComma ? ", and " : " and ";
1608
- return items.slice(0, -1).join(", ") + and + items[items.length - 1];
1764
+ const tail = opts.style.serialComma ? "," + conjunction : conjunction;
1765
+ return items.slice(0, -1).join(", ") + tail + items[items.length - 1];
1766
+ }
1767
+ function joinList(items, opts) {
1768
+ return joinWith(items, " and ", opts);
1769
+ }
1770
+ function joinOr(items, opts) {
1771
+ return joinWith(items, " or ", opts);
1609
1772
  }
1610
- var trailingWords = { all: "", month: "in ", stepDate: "on ", weekday: "on " };
1773
+ var trailingWords = {
1774
+ all: "",
1775
+ month: "in ",
1776
+ recurringWeekday: true,
1777
+ stepDate: "on ",
1778
+ weekday: "on "
1779
+ };
1611
1780
  var leadingWords = {
1612
1781
  all: "every day",
1613
1782
  month: "every day in ",
1783
+ recurringWeekday: false,
1614
1784
  stepDate: "",
1615
1785
  weekday: "every "
1616
1786
  };
1617
1787
  function trailingQualifier(ir, opts) {
1788
+ if (isDayUnion(ir, opts)) {
1789
+ return dayUnionCondition(ir, opts);
1790
+ }
1618
1791
  const phrase = dayQualifier(ir, trailingWords, opts);
1619
1792
  return phrase && " " + phrase;
1620
1793
  }
1621
1794
  function interpretDayQualifier(ir, opts) {
1795
+ if (isDayUnion(ir, opts)) {
1796
+ return "";
1797
+ }
1622
1798
  return dayQualifier(ir, leadingWords, opts) + " ";
1623
1799
  }
1624
1800
  function dayQualifier(ir, words, opts) {
@@ -1630,7 +1806,11 @@ function dayQualifier(ir, words, opts) {
1630
1806
  return datePhrase(ir, words, opts);
1631
1807
  }
1632
1808
  if (pattern.weekday !== "*") {
1633
- const weekdays = quartzWeekdayPhrase(pattern.weekday, opts) || words.weekday + weekdayPhrase(ir, opts);
1809
+ const quartzWeekday = quartzWeekdayPhrase(pattern.weekday, opts);
1810
+ if (quartzWeekday) {
1811
+ return monthScopeForRecurrence(quartzWeekday, ir, opts);
1812
+ }
1813
+ const weekdays = words.weekday + weekdayPhrase(ir, words.recurringWeekday, opts);
1634
1814
  return weekdays + monthScope(ir, opts);
1635
1815
  }
1636
1816
  if (pattern.month !== "*") {
@@ -1642,10 +1822,14 @@ function datePhrase(ir, words, opts) {
1642
1822
  const pattern = ir.pattern;
1643
1823
  const quartzDate = quartzDatePhrase(pattern.date, opts);
1644
1824
  if (quartzDate) {
1645
- return quartzDate + monthScope(ir, opts);
1825
+ return monthScopeForRecurrence(quartzDate, ir, opts);
1646
1826
  }
1647
1827
  if (isOpenStep(pattern.date)) {
1648
- return words.stepDate + stepDates(pattern.date) + monthScope(ir, opts);
1828
+ return monthScopeForRecurrence(
1829
+ words.stepDate + stepDates(pattern.date),
1830
+ ir,
1831
+ opts
1832
+ );
1649
1833
  }
1650
1834
  if (pattern.month !== "*" && !monthFoldsIntoDate(ir)) {
1651
1835
  return "on the " + dateOrdinals(ir, opts) + monthScope(ir, opts);
@@ -1661,9 +1845,84 @@ function monthFoldsIntoDate(ir) {
1661
1845
  return segment.kind !== "range";
1662
1846
  });
1663
1847
  }
1848
+ function isDayUnion(ir, opts) {
1849
+ return ir.pattern.date !== "*" && ir.pattern.weekday !== "*" && !!opts.style.untilWindow && !opts.short;
1850
+ }
1851
+ function dayUnionCondition(ir, opts) {
1852
+ const pieces = [
1853
+ ...dayUnionDatePieces(ir, opts),
1854
+ ...dayUnionWeekdayPieces(ir, opts)
1855
+ ];
1856
+ return " whenever the day is " + joinOr(pieces, opts);
1857
+ }
1858
+ function dayUnionMonthLead(ir, opts) {
1859
+ if (ir.pattern.month === "*") {
1860
+ return "";
1861
+ }
1862
+ return "in " + monthName(ir, opts) + " ";
1863
+ }
1864
+ function dayUnionDatePieces(ir, opts) {
1865
+ const dateField = ir.pattern.date;
1866
+ const quartz = quartzDatePhrase(dateField, opts);
1867
+ if (quartz) {
1868
+ return [quartz.replace(/^on /, "")];
1869
+ }
1870
+ const oddEven = oddEvenDay(dateField);
1871
+ if (oddEven) {
1872
+ return [oddEven];
1873
+ }
1874
+ const pieces = [];
1875
+ ir.analyses.segments.date.forEach(function expand(segment) {
1876
+ if (segment.kind === "range") {
1877
+ pieces.push("from the " + getOrdinal(segment.bounds[0]) + through(opts) + "the " + getOrdinal(segment.bounds[1]));
1878
+ } else if (segment.kind === "step") {
1879
+ segment.fires.forEach(function fire(value) {
1880
+ pieces.push("the " + getOrdinal(value));
1881
+ });
1882
+ } else {
1883
+ pieces.push("the " + getOrdinal(segment.value));
1884
+ }
1885
+ });
1886
+ return pieces;
1887
+ }
1888
+ function dayUnionWeekdayPieces(ir, opts) {
1889
+ const weekdayField = ir.pattern.weekday;
1890
+ const quartz = quartzWeekdayPhrase(weekdayField, opts);
1891
+ if (quartz) {
1892
+ return [quartz.replace(/^on /, "")];
1893
+ }
1894
+ const pieces = [];
1895
+ ir.analyses.segments.weekday.forEach(function expand(segment) {
1896
+ if (segment.kind === "range" && segment.bounds[0] === "1" && segment.bounds[1] === "5") {
1897
+ pieces.push("a weekday");
1898
+ } else if (segment.kind === "range") {
1899
+ pieces.push("a " + getWeekday(segment.bounds[0], opts) + through(opts) + "a " + getWeekday(segment.bounds[1], opts));
1900
+ } else if (segment.kind === "step") {
1901
+ segment.fires.forEach(function fire(value) {
1902
+ pieces.push("a " + getWeekday(value, opts));
1903
+ });
1904
+ } else {
1905
+ pieces.push("a " + getWeekday(segment.value, opts));
1906
+ }
1907
+ });
1908
+ return pieces;
1909
+ }
1910
+ function oddEvenDay(dateField) {
1911
+ if (!isOpenStep(dateField)) {
1912
+ return null;
1913
+ }
1914
+ const [start, step] = dateField.split("/");
1915
+ if (+step !== 2) {
1916
+ return null;
1917
+ }
1918
+ if (start === "*" || start === "1") {
1919
+ return "an odd-numbered day";
1920
+ }
1921
+ return start === "2" ? "an even-numbered day" : null;
1922
+ }
1664
1923
  function dateOrWeekday(ir, opts) {
1665
1924
  const pattern = ir.pattern;
1666
- const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, opts);
1925
+ const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) || "on " + weekdayPhrase(ir, false, opts);
1667
1926
  if (pattern.month !== "*" && monthFoldsIntoDate(ir) && !quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
1668
1927
  return "on " + monthDatePhrase(ir, opts) + " or " + weekdayPart + " in " + monthName(ir, opts);
1669
1928
  }
@@ -1718,6 +1977,9 @@ function monthDatePhrase(ir, opts) {
1718
1977
  opts.style.ordinals ? getOrdinal : cardinalDay,
1719
1978
  opts
1720
1979
  );
1980
+ if (opts.style.dayFirst && ir.shapes.date === "single" && ir.shapes.month !== "single") {
1981
+ return "the " + getOrdinal(ir.pattern.date) + " of " + month;
1982
+ }
1721
1983
  return opts.style.dayFirst ? days + " " + month : month + " " + days;
1722
1984
  }
1723
1985
  function cardinalDay(value) {
@@ -1729,6 +1991,19 @@ function monthScope(ir, opts) {
1729
1991
  }
1730
1992
  return " in " + monthName(ir, opts);
1731
1993
  }
1994
+ function monthScopeForRecurrence(phrase, ir, opts) {
1995
+ if (ir.pattern.month === "*") {
1996
+ return phrase;
1997
+ }
1998
+ const carriesRecurrence = phrase.indexOf(" of the month") !== -1;
1999
+ if (carriesRecurrence && ir.shapes.month === "range") {
2000
+ return phrase.replace(" of the month", " of each month") + " from " + monthName(ir, opts);
2001
+ }
2002
+ if (carriesRecurrence && (ir.shapes.month === "single" || ir.shapes.month === "step")) {
2003
+ return phrase.replace(" of the month", "") + " in " + monthName(ir, opts);
2004
+ }
2005
+ return phrase + " in " + monthName(ir, opts);
2006
+ }
1732
2007
  function stepDates(dateField) {
1733
2008
  const parts = dateField.split("/");
1734
2009
  const interval = +parts[1];
@@ -1765,11 +2040,21 @@ function oddEvenMonth(monthField) {
1765
2040
  }
1766
2041
  return start === "2" ? "every even-numbered month" : null;
1767
2042
  }
1768
- function weekdayPhrase(ir, opts) {
2043
+ function weekdayPhrase(ir, recurring, opts) {
1769
2044
  const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday);
1770
- return renderSegments(segments, function name(value) {
2045
+ const hasRange = segments.some(function range(segment) {
2046
+ return segment.kind === "range";
2047
+ });
2048
+ const name = recurring && !hasRange ? function plural(value) {
2049
+ return pluralWeekday(value, opts);
2050
+ } : function singular(value) {
1771
2051
  return getWeekday(value, opts);
1772
- }, opts);
2052
+ };
2053
+ return renderSegments(segments, name, opts);
2054
+ }
2055
+ function pluralWeekday(value, opts) {
2056
+ const name = getWeekday(value, opts);
2057
+ return opts.short ? name : name + "s";
1773
2058
  }
1774
2059
  function renderSegments(segments, word, opts) {
1775
2060
  const pieces = [];
@@ -1793,7 +2078,7 @@ function applyYear(description, ir, opts) {
1793
2078
  return description;
1794
2079
  }
1795
2080
  if (yearField.indexOf("/") !== -1) {
1796
- return description + " " + stepYears(yearField, opts);
2081
+ return description + ", " + stepYears(yearField, opts);
1797
2082
  }
1798
2083
  const label = yearLabel(yearField, opts);
1799
2084
  if (yearField.indexOf("-") === -1 && yearField.indexOf(",") === -1 && ir.pattern.date !== "*" && description.indexOf(" at ") !== -1) {
@@ -1806,6 +2091,9 @@ function yearLabel(yearField, opts) {
1806
2091
  if (yearField.indexOf(",") !== -1) {
1807
2092
  return joinList(yearField.split(","), opts);
1808
2093
  }
2094
+ if (yearField.indexOf("-") !== -1) {
2095
+ return yearField.split("-").join(through(opts));
2096
+ }
1809
2097
  return yearField;
1810
2098
  }
1811
2099
  function stepYears(yearField, opts) {
@@ -1815,7 +2103,7 @@ function stepYears(yearField, opts) {
1815
2103
  if (interval <= 1) {
1816
2104
  return "every year";
1817
2105
  }
1818
- let phrase = "every " + getNumber(interval, opts) + " years";
2106
+ let phrase = interval === 2 ? "every other year" : "every " + getNumber(interval, opts) + " years";
1819
2107
  if (start !== "*" && start !== "0") {
1820
2108
  phrase += " from " + start;
1821
2109
  }