cronli5 0.1.1 → 0.1.4

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
@@ -168,6 +168,11 @@ function throwInvalidField(value, field) {
168
168
  }
169
169
 
170
170
  // src/core/normalize.ts
171
+ var timeFieldCycle = {
172
+ hour: 24,
173
+ minute: 60,
174
+ second: 60
175
+ };
171
176
  function applyQuartzAliases(cronPattern) {
172
177
  fieldOrder.forEach(function apply(field) {
173
178
  const aliases = fieldSpecs[field].aliases;
@@ -184,21 +189,32 @@ function normalizeCronPattern(cronPattern) {
184
189
  cronPattern[field] = value;
185
190
  return;
186
191
  }
187
- cronPattern[field] = normalizeField(value, fieldSpecs[field]);
192
+ cronPattern[field] = normalizeField(value, field, fieldSpecs[field]);
188
193
  });
189
194
  return cronPattern;
190
195
  }
191
- function normalizeField(value, spec) {
196
+ function normalizeField(value, field, spec) {
192
197
  const stringValue = "" + value;
193
198
  if (stringValue === "*") {
194
199
  return stringValue;
195
200
  }
201
+ const cycle = timeFieldCycle[field];
196
202
  const segments = stringValue.split(",").map(function canonical(segment) {
197
- return collapseDegenerateRange(
198
- collapseOnceStep(collapseUnitStep(segment, spec), spec),
203
+ return canonicalizeTokens(collapseFullSpanRange(
204
+ enumerateNonUniformStep(
205
+ collapseFullSpanStep(
206
+ collapseDegenerateRange(
207
+ collapseOnceStep(collapseUnitStep(segment, spec), spec),
208
+ spec
209
+ ),
210
+ spec
211
+ ),
212
+ spec,
213
+ cycle
214
+ ),
199
215
  spec
200
- );
201
- });
216
+ ), spec);
217
+ }).join(",").split(",");
202
218
  if (segments.indexOf("*") !== -1) {
203
219
  return "*";
204
220
  }
@@ -206,6 +222,23 @@ function normalizeField(value, spec) {
206
222
  return firstFire(a, spec) - firstFire(b, spec);
207
223
  }).join(",");
208
224
  }
225
+ function canonicalizeTokens(segment, spec) {
226
+ if (!spec.numbers) {
227
+ return segment;
228
+ }
229
+ const parts = segment.split("/");
230
+ const start = parts[0].split("-").map(function fold(token) {
231
+ return canonicalizeToken(token, spec);
232
+ }).join("-");
233
+ return parts.length === 2 ? start + "/" + parts[1] : start;
234
+ }
235
+ function canonicalizeToken(token, spec) {
236
+ if (token === "*") {
237
+ return token;
238
+ }
239
+ const number = toFieldNumber(token, spec.numbers);
240
+ return "" + (number > spec.top ? spec.min : number);
241
+ }
209
242
  function collapseUnitStep(segment, spec) {
210
243
  const parts = segment.split("/");
211
244
  if (!spec.cyclic || parts.length !== 2 || +parts[1] !== 1) {
@@ -232,6 +265,51 @@ function collapseOnceStep(segment, spec) {
232
265
  }
233
266
  return start === "*" ? "" + spec.min : start;
234
267
  }
268
+ function enumerateNonUniformStep(segment, spec, cycle) {
269
+ const parts = segment.split("/");
270
+ if (typeof cycle !== "number" || parts.length !== 2 || includes(parts[0], "-")) {
271
+ return segment;
272
+ }
273
+ const interval = +parts[1];
274
+ const start = parts[0] === "*" ? spec.min : toFieldNumber(parts[0]);
275
+ if (cycle % interval === 0 && start < interval) {
276
+ return segment;
277
+ }
278
+ const fires = [];
279
+ for (let value = start; value <= spec.top; value += interval) {
280
+ fires.push(value);
281
+ }
282
+ return fires.join(",");
283
+ }
284
+ function collapseFullSpanStep(segment, spec) {
285
+ const parts = segment.split("/");
286
+ if (parts.length !== 2 || !includes(parts[0], "-")) {
287
+ return segment;
288
+ }
289
+ return collapseFullSpanRange(parts[0], spec) === "*" ? "*/" + parts[1] : segment;
290
+ }
291
+ function collapseFullSpanRange(segment, spec) {
292
+ if (typeof spec.top !== "number" || includes(segment, "/") || !includes(segment, "-")) {
293
+ return segment;
294
+ }
295
+ const bounds = segment.split("-");
296
+ const low = toFieldNumber(bounds[0], spec.numbers);
297
+ const high = toFieldNumber(bounds[1], spec.numbers);
298
+ if (low > high) {
299
+ return segment;
300
+ }
301
+ const top = spec.top;
302
+ const fired = {};
303
+ for (let value = low; value <= high; value += 1) {
304
+ fired[value > top ? spec.min : value] = true;
305
+ }
306
+ for (let value = spec.min; value <= top; value += 1) {
307
+ if (!fired[value]) {
308
+ return segment;
309
+ }
310
+ }
311
+ return "*";
312
+ }
235
313
  function collapseDegenerateRange(segment, spec) {
236
314
  const start = segment.split("/")[0];
237
315
  if (!includes(start, "-")) {
@@ -800,20 +878,6 @@ var weekdayNames = [
800
878
  ["Friday", "Fri"],
801
879
  ["Saturday", "Sat"]
802
880
  ];
803
- var monthAbbreviations = {
804
- JAN: monthNames[1],
805
- FEB: monthNames[2],
806
- MAR: monthNames[3],
807
- APR: monthNames[4],
808
- MAY: monthNames[5],
809
- JUN: monthNames[6],
810
- JUL: monthNames[7],
811
- AUG: monthNames[8],
812
- SEP: monthNames[9],
813
- OCT: monthNames[10],
814
- NOV: monthNames[11],
815
- DEC: monthNames[12]
816
- };
817
881
  var weekdayAbbreviations = {
818
882
  SUN: weekdayNames[0],
819
883
  MON: weekdayNames[1],
@@ -863,8 +927,37 @@ function renderSecondsWithinMinute(ir, plan, opts) {
863
927
  return secondsLeadClause(ir, opts) + ", " + minuteWord + " " + minuteUnit + " past the hour, every hour" + trailingQualifier(ir, opts);
864
928
  }
865
929
  function renderComposeSeconds(ir, plan, opts) {
930
+ if (plan.rest.kind === "clockTimes" && (ir.shapes.second === "wildcard" || ir.shapes.second === "step")) {
931
+ const minute = plan.rest.times[0].minute;
932
+ if (+minute === 0) {
933
+ return secondsLeadClause(ir, opts) + " for one minute at " + durationHours(ir, plan.rest, opts);
934
+ }
935
+ return secondsLeadClause(ir, opts) + " of " + clockTimesOf(ir, plan.rest, opts);
936
+ }
937
+ if (ir.shapes.second === "wildcard" && plan.rest.kind === "minuteFrequency" && plan.rest.hours.kind === "none" && ir.pattern.minute === "*/2") {
938
+ return "every second of every other minute" + trailingQualifier(ir, opts);
939
+ }
866
940
  return secondsLeadClause(ir, opts) + ", " + render(ir, plan.rest, opts);
867
941
  }
942
+ function durationHours(ir, plan, opts) {
943
+ const hours = plan.times.map(function clock(time) {
944
+ return getTime({ hour: time.hour, minute: 0 }, opts);
945
+ });
946
+ const trail = dayQualifier(ir, leadingWords, opts);
947
+ return joinList(hours, opts) + (trail && ", " + trail);
948
+ }
949
+ function clockTimesOf(ir, plan, opts) {
950
+ const times = plan.times.map(function clock(time) {
951
+ return getTime({
952
+ hour: time.hour,
953
+ minute: time.minute,
954
+ second: time.second,
955
+ explicit: true
956
+ }, opts);
957
+ });
958
+ const trail = dayQualifier(ir, leadingWords, opts);
959
+ return joinList(times, opts) + (trail && ", " + trail);
960
+ }
868
961
  function secondsLeadClause(ir, opts) {
869
962
  const secondField = ir.pattern.second;
870
963
  const shape = ir.shapes.second;
@@ -929,6 +1022,9 @@ function renderMinuteFrequency(ir, plan, opts) {
929
1022
  return phrase + trailingQualifier(ir, opts);
930
1023
  }
931
1024
  function renderMinuteSpanInHour(ir, plan, opts) {
1025
+ if (ir.pattern.minute === "*") {
1026
+ return "every minute of the " + getTime({ hour: plan.hour, minute: 0 }, opts) + " hour" + trailingQualifier(ir, opts);
1027
+ }
932
1028
  return "every minute from " + getTime({ hour: plan.hour, minute: plan.span[0] }, opts) + through(opts) + getTime({ hour: plan.hour, minute: plan.span[1] }, opts) + trailingQualifier(ir, opts);
933
1029
  }
934
1030
  function renderMinutesAcrossHours(ir, plan, opts) {
@@ -1100,13 +1196,7 @@ function stepCycle60(segment, unit, anchor, opts) {
1100
1196
  }
1101
1197
  return "every " + getNumber(interval, opts) + " " + unit + "s from " + getNumber(start, opts) + " " + pluralize(start, unit) + " past the " + anchor;
1102
1198
  }
1103
- if (60 % interval === 0) {
1104
- return "every " + getNumber(interval, opts) + " " + unit + "s";
1105
- }
1106
- if (segment.fires.length <= 2) {
1107
- return listPastThe(numberWords(segment.fires, opts), unit, anchor, opts);
1108
- }
1109
- return "every " + getNumber(interval, opts) + " " + unit + "s past the " + anchor;
1199
+ return "every " + getNumber(interval, opts) + " " + unit + "s";
1110
1200
  }
1111
1201
  function stepHours(segment, opts) {
1112
1202
  if (segment.startToken.indexOf("-") !== -1) {
@@ -1114,15 +1204,12 @@ function stepHours(segment, opts) {
1114
1204
  }
1115
1205
  const start = segment.startToken === "*" ? 0 : +segment.startToken;
1116
1206
  const interval = segment.interval;
1117
- if (start === 0 && 24 % interval === 0) {
1207
+ if (start === 0) {
1118
1208
  return "every " + getNumber(interval, opts) + " hours";
1119
1209
  }
1120
1210
  if (segment.fires.length <= 3) {
1121
1211
  return "at " + hourTimes(segment.fires, opts);
1122
1212
  }
1123
- if (start === 0) {
1124
- return "every " + getNumber(interval, opts) + " hours from midnight";
1125
- }
1126
1213
  return "every " + getNumber(interval, opts) + " hours from " + getTime({ hour: start, minute: 0 }, opts);
1127
1214
  }
1128
1215
  function seriesNumber(values, opts) {
@@ -1436,7 +1523,7 @@ function stepYears(yearField, opts) {
1436
1523
  return phrase;
1437
1524
  }
1438
1525
  function getTime(time, opts) {
1439
- const { hour, minute, plain } = time;
1526
+ const { hour, minute, plain, explicit } = time;
1440
1527
  const second = typeof time.second === "number" && time.second > 0 ? time.second : 0;
1441
1528
  if (!opts.ampm) {
1442
1529
  return clockDigits({
@@ -1445,12 +1532,12 @@ function getTime(time, opts) {
1445
1532
  second
1446
1533
  }, { pad: true, sep: opts.style.sep });
1447
1534
  }
1448
- return twelveHourTime({ hour, minute, second, plain }, opts);
1535
+ return twelveHourTime({ hour, minute, second, plain, explicit }, opts);
1449
1536
  }
1450
1537
  function twelveHourTime(time, opts) {
1451
- const { hour, minute, second, plain } = time;
1538
+ const { hour, minute, second, plain, explicit } = time;
1452
1539
  const style = opts.style;
1453
- if (!plain && +minute === 0 && !second) {
1540
+ if (!plain && !explicit && +minute === 0 && !second) {
1454
1541
  if (+hour === 0) {
1455
1542
  return style.midnight;
1456
1543
  }
@@ -1460,7 +1547,7 @@ function twelveHourTime(time, opts) {
1460
1547
  }
1461
1548
  const digits = clockDigits(
1462
1549
  { hour: hour % 12 || 12, minute, second },
1463
- { lean: true, sep: style.sep }
1550
+ { lean: !explicit, sep: style.sep }
1464
1551
  );
1465
1552
  return digits + (style.closeUp ? "" : " ") + (hour < 12 ? style.am : style.pm);
1466
1553
  }
@@ -1483,7 +1570,7 @@ function getOrdinal(n) {
1483
1570
  return n + suffix;
1484
1571
  }
1485
1572
  function getMonth(m, opts) {
1486
- const month = monthNames[m] || monthAbbreviations[m];
1573
+ const month = monthNames[+m];
1487
1574
  return month && month[opts.short ? 1 : 0];
1488
1575
  }
1489
1576
  function getWeekday(d, opts) {
@@ -1496,7 +1583,9 @@ var en = {
1496
1583
  fallback: "an unrecognizable cron pattern",
1497
1584
  options: normalizeOptions,
1498
1585
  reboot: "at system startup",
1499
- sentence: (description) => "Runs " + description + "."
1586
+ // A description ending in an abbreviation already carries its period
1587
+ // ("…9 a.m."), so closing the sentence must not double it.
1588
+ sentence: (description) => "Runs " + description + (description.endsWith(".") ? "" : ".")
1500
1589
  };
1501
1590
  var en_default = en;
1502
1591
 
package/dist/lang/de.cjs CHANGED
@@ -30,6 +30,26 @@ function pad(n) {
30
30
  return n.length < 2 ? "0" + n : n;
31
31
  }
32
32
 
33
+ // src/core/specs.ts
34
+ var weekdayNumbers = {
35
+ SUN: 0,
36
+ MON: 1,
37
+ TUE: 2,
38
+ WED: 3,
39
+ THU: 4,
40
+ FRI: 5,
41
+ SAT: 6
42
+ };
43
+
44
+ // src/core/util.ts
45
+ function isNonNegativeInteger(value) {
46
+ const digits = /^\d+$/;
47
+ return digits.test(value);
48
+ }
49
+ function toFieldNumber(token, numberMap) {
50
+ return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
51
+ }
52
+
33
53
  // src/lang/de/dialects.ts
34
54
  var months = [
35
55
  null,
@@ -101,15 +121,6 @@ var weekdayNames = [
101
121
  "freitags",
102
122
  "samstags"
103
123
  ];
104
- var weekdayTokens = {
105
- SUN: 0,
106
- MON: 1,
107
- TUE: 2,
108
- WED: 3,
109
- THU: 4,
110
- FRI: 5,
111
- SAT: 6
112
- };
113
124
  function fieldSegments(ir, field) {
114
125
  return ir.analyses.segments[field];
115
126
  }
@@ -130,10 +141,7 @@ function joinList(items) {
130
141
  return items.slice(0, -1).join(", ") + " und " + items[items.length - 1];
131
142
  }
132
143
  function weekdayName(token) {
133
- if (token === "7" || token === 7) {
134
- return weekdayNames[0];
135
- }
136
- return weekdayNames[token] || weekdayNames[weekdayTokens[token]];
144
+ return weekdayNames[+token];
137
145
  }
138
146
  function weekdayRange(bounds) {
139
147
  return weekdayName(bounds[0]) + " bis " + weekdayName(bounds[1]);
@@ -171,10 +179,7 @@ function everyNthHour(segment) {
171
179
  return start === 0 ? base : base + " ab " + start + " Uhr";
172
180
  }
173
181
  function weekdayNoun(token) {
174
- if (token === "7") {
175
- return weekdayNouns[0];
176
- }
177
- return weekdayNouns[token in weekdayTokens ? weekdayTokens[token] : +token];
182
+ return weekdayNouns[toFieldNumber(token, weekdayNumbers)];
178
183
  }
179
184
  function quartzWeekday(field) {
180
185
  if (field.indexOf("#") !== -1) {
@@ -198,22 +203,8 @@ function quartzDate(field) {
198
203
  }
199
204
  return null;
200
205
  }
201
- var monthTokens = {
202
- JAN: 1,
203
- FEB: 2,
204
- MAR: 3,
205
- APR: 4,
206
- MAY: 5,
207
- JUN: 6,
208
- JUL: 7,
209
- AUG: 8,
210
- SEP: 9,
211
- OCT: 10,
212
- NOV: 11,
213
- DEC: 12
214
- };
215
206
  function monthName(token, months2) {
216
- return months2[token] || months2[monthTokens[token]];
207
+ return months2[+token];
217
208
  }
218
209
  function monthRange(bounds, months2) {
219
210
  return "von " + monthName(bounds[0], months2) + " bis " + monthName(bounds[1], months2);
@@ -352,7 +343,7 @@ function duringHours(ir, times, sep) {
352
343
  if (windows.length <= 3 || times.kind !== "fires") {
353
344
  return joinList(windows);
354
345
  }
355
- return "in den Stunden von " + joinList(times.fires.map(String)) + " Uhr";
346
+ return "in den Stunden " + joinList(times.fires.map(String)) + " Uhr";
356
347
  }
357
348
  function renderEverySecond() {
358
349
  return everyUnit(UNITS.second);
@@ -375,13 +366,37 @@ function renderSecondsWithinMinute(ir, plan) {
375
366
  }
376
367
  return secondsLead(ir) + ", in Minute " + ir.pattern.minute + " jeder Stunde";
377
368
  }
369
+ function wholeHour(hour) {
370
+ if (hour === 0) {
371
+ return "der Mitternachtsstunde";
372
+ }
373
+ if (hour === 12) {
374
+ return "der Mittagsstunde";
375
+ }
376
+ return "der " + hour + "-Uhr-Stunde";
377
+ }
378
378
  function renderMinuteSpanInHour(ir, plan, opts) {
379
+ if (ir.pattern.minute === "*") {
380
+ return "jede Minute " + wholeHour(plan.hour);
381
+ }
379
382
  const sep = opts.style.sep;
380
383
  return "jede Minute von " + spanTime(plan.hour, plan.span[0], sep) + " bis " + spanTime(plan.hour, plan.span[1], sep) + " Uhr";
381
384
  }
382
385
  function renderComposeSeconds(ir, plan, opts) {
386
+ if (composeMinuteZero(ir, plan)) {
387
+ return secondsLead(ir) + " " + clockMinuteGenitive(plan.rest.times, opts.style.sep);
388
+ }
383
389
  return secondsLead(ir) + ", " + render(ir, plan.rest, opts);
384
390
  }
391
+ function composeMinuteZero(ir, plan) {
392
+ return plan.rest.kind === "clockTimes" && plan.rest.times.every((time) => +time.minute === 0);
393
+ }
394
+ function clockMinuteGenitive(times, sep) {
395
+ const clocks = times.map(function clock(time) {
396
+ return time.hour + sep + pad(time.minute);
397
+ });
398
+ return clocks.length === 1 ? "der Minute " + clocks[0] : "der Minuten " + joinList(clocks);
399
+ }
385
400
  function renderMinutesAcrossHours(ir, plan, opts) {
386
401
  const sep = opts.style.sep;
387
402
  if (plan.form === "wildcard") {
@@ -490,8 +505,14 @@ function qualifier(ir, months2) {
490
505
  return "";
491
506
  }
492
507
  var LEADING_PLANS = /* @__PURE__ */ new Set(["clockTimes"]);
508
+ function leadsQualifier(ir) {
509
+ return LEADING_PLANS.has(ir.plan.kind) || isComposeMinuteZero(ir);
510
+ }
511
+ function isComposeMinuteZero(ir) {
512
+ return ir.plan.kind === "composeSeconds" && composeMinuteZero(ir, ir.plan);
513
+ }
493
514
  function needsDailyFrame(ir) {
494
- if (ir.plan.kind === "clockTimes") {
515
+ if (ir.plan.kind === "clockTimes" || isComposeMinuteZero(ir)) {
495
516
  return true;
496
517
  }
497
518
  return ir.plan.kind === "hourStep" && !cleanStep(stepSegment(ir.analyses.segments.hour), 24);
@@ -534,7 +555,7 @@ function describe(ir, opts) {
534
555
  const qual = qualifier(ir, opts.style.months);
535
556
  let base = core;
536
557
  if (qual) {
537
- base = LEADING_PLANS.has(ir.plan.kind) ? qual + " " + core : core + " " + qual;
558
+ base = leadsQualifier(ir) ? qual + " " + core : core + " " + qual;
538
559
  } else if (needsDailyFrame(ir)) {
539
560
  base = "t\xE4glich " + core;
540
561
  }
@@ -545,7 +566,9 @@ var de2 = {
545
566
  fallback: "ein unlesbares Cron-Muster",
546
567
  options: normalizeOptions,
547
568
  reboot: "beim Systemstart",
548
- sentence: (description) => "L\xE4uft " + description + "."
569
+ // A description ending in a German ordinal already carries its period
570
+ // ("…am 8."), so closing the sentence must not double it.
571
+ sentence: (description) => "L\xE4uft " + description + (description.endsWith(".") ? "" : ".")
549
572
  };
550
573
  var index_default = de2;
551
574
  module.exports = module.exports.default;
package/dist/lang/de.js CHANGED
@@ -4,6 +4,26 @@ function pad(n) {
4
4
  return n.length < 2 ? "0" + n : n;
5
5
  }
6
6
 
7
+ // src/core/specs.ts
8
+ var weekdayNumbers = {
9
+ SUN: 0,
10
+ MON: 1,
11
+ TUE: 2,
12
+ WED: 3,
13
+ THU: 4,
14
+ FRI: 5,
15
+ SAT: 6
16
+ };
17
+
18
+ // src/core/util.ts
19
+ function isNonNegativeInteger(value) {
20
+ const digits = /^\d+$/;
21
+ return digits.test(value);
22
+ }
23
+ function toFieldNumber(token, numberMap) {
24
+ return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
25
+ }
26
+
7
27
  // src/lang/de/dialects.ts
8
28
  var months = [
9
29
  null,
@@ -75,15 +95,6 @@ var weekdayNames = [
75
95
  "freitags",
76
96
  "samstags"
77
97
  ];
78
- var weekdayTokens = {
79
- SUN: 0,
80
- MON: 1,
81
- TUE: 2,
82
- WED: 3,
83
- THU: 4,
84
- FRI: 5,
85
- SAT: 6
86
- };
87
98
  function fieldSegments(ir, field) {
88
99
  return ir.analyses.segments[field];
89
100
  }
@@ -104,10 +115,7 @@ function joinList(items) {
104
115
  return items.slice(0, -1).join(", ") + " und " + items[items.length - 1];
105
116
  }
106
117
  function weekdayName(token) {
107
- if (token === "7" || token === 7) {
108
- return weekdayNames[0];
109
- }
110
- return weekdayNames[token] || weekdayNames[weekdayTokens[token]];
118
+ return weekdayNames[+token];
111
119
  }
112
120
  function weekdayRange(bounds) {
113
121
  return weekdayName(bounds[0]) + " bis " + weekdayName(bounds[1]);
@@ -145,10 +153,7 @@ function everyNthHour(segment) {
145
153
  return start === 0 ? base : base + " ab " + start + " Uhr";
146
154
  }
147
155
  function weekdayNoun(token) {
148
- if (token === "7") {
149
- return weekdayNouns[0];
150
- }
151
- return weekdayNouns[token in weekdayTokens ? weekdayTokens[token] : +token];
156
+ return weekdayNouns[toFieldNumber(token, weekdayNumbers)];
152
157
  }
153
158
  function quartzWeekday(field) {
154
159
  if (field.indexOf("#") !== -1) {
@@ -172,22 +177,8 @@ function quartzDate(field) {
172
177
  }
173
178
  return null;
174
179
  }
175
- var monthTokens = {
176
- JAN: 1,
177
- FEB: 2,
178
- MAR: 3,
179
- APR: 4,
180
- MAY: 5,
181
- JUN: 6,
182
- JUL: 7,
183
- AUG: 8,
184
- SEP: 9,
185
- OCT: 10,
186
- NOV: 11,
187
- DEC: 12
188
- };
189
180
  function monthName(token, months2) {
190
- return months2[token] || months2[monthTokens[token]];
181
+ return months2[+token];
191
182
  }
192
183
  function monthRange(bounds, months2) {
193
184
  return "von " + monthName(bounds[0], months2) + " bis " + monthName(bounds[1], months2);
@@ -326,7 +317,7 @@ function duringHours(ir, times, sep) {
326
317
  if (windows.length <= 3 || times.kind !== "fires") {
327
318
  return joinList(windows);
328
319
  }
329
- return "in den Stunden von " + joinList(times.fires.map(String)) + " Uhr";
320
+ return "in den Stunden " + joinList(times.fires.map(String)) + " Uhr";
330
321
  }
331
322
  function renderEverySecond() {
332
323
  return everyUnit(UNITS.second);
@@ -349,13 +340,37 @@ function renderSecondsWithinMinute(ir, plan) {
349
340
  }
350
341
  return secondsLead(ir) + ", in Minute " + ir.pattern.minute + " jeder Stunde";
351
342
  }
343
+ function wholeHour(hour) {
344
+ if (hour === 0) {
345
+ return "der Mitternachtsstunde";
346
+ }
347
+ if (hour === 12) {
348
+ return "der Mittagsstunde";
349
+ }
350
+ return "der " + hour + "-Uhr-Stunde";
351
+ }
352
352
  function renderMinuteSpanInHour(ir, plan, opts) {
353
+ if (ir.pattern.minute === "*") {
354
+ return "jede Minute " + wholeHour(plan.hour);
355
+ }
353
356
  const sep = opts.style.sep;
354
357
  return "jede Minute von " + spanTime(plan.hour, plan.span[0], sep) + " bis " + spanTime(plan.hour, plan.span[1], sep) + " Uhr";
355
358
  }
356
359
  function renderComposeSeconds(ir, plan, opts) {
360
+ if (composeMinuteZero(ir, plan)) {
361
+ return secondsLead(ir) + " " + clockMinuteGenitive(plan.rest.times, opts.style.sep);
362
+ }
357
363
  return secondsLead(ir) + ", " + render(ir, plan.rest, opts);
358
364
  }
365
+ function composeMinuteZero(ir, plan) {
366
+ return plan.rest.kind === "clockTimes" && plan.rest.times.every((time) => +time.minute === 0);
367
+ }
368
+ function clockMinuteGenitive(times, sep) {
369
+ const clocks = times.map(function clock(time) {
370
+ return time.hour + sep + pad(time.minute);
371
+ });
372
+ return clocks.length === 1 ? "der Minute " + clocks[0] : "der Minuten " + joinList(clocks);
373
+ }
359
374
  function renderMinutesAcrossHours(ir, plan, opts) {
360
375
  const sep = opts.style.sep;
361
376
  if (plan.form === "wildcard") {
@@ -464,8 +479,14 @@ function qualifier(ir, months2) {
464
479
  return "";
465
480
  }
466
481
  var LEADING_PLANS = /* @__PURE__ */ new Set(["clockTimes"]);
482
+ function leadsQualifier(ir) {
483
+ return LEADING_PLANS.has(ir.plan.kind) || isComposeMinuteZero(ir);
484
+ }
485
+ function isComposeMinuteZero(ir) {
486
+ return ir.plan.kind === "composeSeconds" && composeMinuteZero(ir, ir.plan);
487
+ }
467
488
  function needsDailyFrame(ir) {
468
- if (ir.plan.kind === "clockTimes") {
489
+ if (ir.plan.kind === "clockTimes" || isComposeMinuteZero(ir)) {
469
490
  return true;
470
491
  }
471
492
  return ir.plan.kind === "hourStep" && !cleanStep(stepSegment(ir.analyses.segments.hour), 24);
@@ -508,7 +529,7 @@ function describe(ir, opts) {
508
529
  const qual = qualifier(ir, opts.style.months);
509
530
  let base = core;
510
531
  if (qual) {
511
- base = LEADING_PLANS.has(ir.plan.kind) ? qual + " " + core : core + " " + qual;
532
+ base = leadsQualifier(ir) ? qual + " " + core : core + " " + qual;
512
533
  } else if (needsDailyFrame(ir)) {
513
534
  base = "t\xE4glich " + core;
514
535
  }
@@ -519,7 +540,9 @@ var de2 = {
519
540
  fallback: "ein unlesbares Cron-Muster",
520
541
  options: normalizeOptions,
521
542
  reboot: "beim Systemstart",
522
- sentence: (description) => "L\xE4uft " + description + "."
543
+ // A description ending in a German ordinal already carries its period
544
+ // ("…am 8."), so closing the sentence must not double it.
545
+ sentence: (description) => "L\xE4uft " + description + (description.endsWith(".") ? "" : ".")
523
546
  };
524
547
  var index_default = de2;
525
548
  export {