cronli5 0.1.0 → 0.1.2
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/CHANGELOG.md +70 -32
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +70 -44
- package/dist/cronli5.js +70 -44
- package/dist/lang/de.cjs +10 -3
- package/dist/lang/de.js +10 -3
- package/dist/lang/en.cjs +6 -12
- package/dist/lang/en.js +6 -12
- package/dist/lang/es.cjs +6 -12
- package/dist/lang/es.js +6 -12
- package/dist/lang/fi.cjs +8 -16
- package/dist/lang/fi.js +8 -16
- package/dist/lang/zh.cjs +5 -15
- package/dist/lang/zh.js +5 -15
- package/package.json +1 -1
- package/src/core/analyze.ts +69 -31
- package/src/core/ir.ts +6 -0
- package/src/core/normalize.ts +54 -6
- package/src/lang/de/index.ts +12 -3
- package/src/lang/en/index.ts +22 -19
- package/src/lang/es/index.ts +16 -17
- package/src/lang/fi/index.ts +19 -20
- package/src/lang/zh/index.ts +19 -28
- package/types/core/ir.d.ts +1 -0
package/dist/lang/zh.cjs
CHANGED
|
@@ -197,7 +197,8 @@ function hourFrame(ir) {
|
|
|
197
197
|
return "\u5728" + hourList(ir) + "\uFF0C";
|
|
198
198
|
}
|
|
199
199
|
function renderMinuteFrequency(ir, plan) {
|
|
200
|
-
const
|
|
200
|
+
const minuteStep = stepSegment(ir, "minute");
|
|
201
|
+
const base = minuteStep.startToken === "*" ? cadence(minuteStep.interval, UNITS.minute) : renderMinutePast(ir);
|
|
201
202
|
const { hours } = plan;
|
|
202
203
|
if (hours.kind === "step") {
|
|
203
204
|
return cadence(stepSegment(ir, "hour").interval, UNITS.hour) + base;
|
|
@@ -265,9 +266,6 @@ function renderHourStep(ir) {
|
|
|
265
266
|
if (segment.fires.length <= 2) {
|
|
266
267
|
return joinAnd(segment.fires.map(hourWord));
|
|
267
268
|
}
|
|
268
|
-
if (24 % segment.interval !== 0) {
|
|
269
|
-
return "\u4ECE" + hourWord(segment.fires[0]) + "\u8D77\uFF0C" + cadence(segment.interval, UNITS.hour);
|
|
270
|
-
}
|
|
271
269
|
return cadence(segment.interval, UNITS.hour);
|
|
272
270
|
}
|
|
273
271
|
function renderRangeOfMinutes(ir) {
|
|
@@ -320,15 +318,12 @@ function composeSecondsOnHour(ir, plan, opts) {
|
|
|
320
318
|
const sec = secondClause(ir);
|
|
321
319
|
const { rest } = plan;
|
|
322
320
|
const restText = render(ir, rest, opts);
|
|
323
|
-
if (rest.kind === "everyHour") {
|
|
324
|
-
return sec + "\uFF0C\u6BCF\u5C0F\u65F6";
|
|
325
|
-
}
|
|
326
|
-
if (rest.kind === "hourStep") {
|
|
327
|
-
return sec + "\uFF0C" + restText;
|
|
328
|
-
}
|
|
329
321
|
if ((rest.kind === "clockTimes" || rest.kind === "compactClockTimes") && isDaily(ir)) {
|
|
330
322
|
return "\u6BCF\u5929" + restText + sec;
|
|
331
323
|
}
|
|
324
|
+
if (rest.kind === "singleMinute") {
|
|
325
|
+
return restText + "\uFF0C" + sec;
|
|
326
|
+
}
|
|
332
327
|
return restText + sec;
|
|
333
328
|
}
|
|
334
329
|
function composeSecondsCadence(ir) {
|
|
@@ -348,17 +343,12 @@ function composeSecondsCadence(ir) {
|
|
|
348
343
|
function composeSecondsListed(ir) {
|
|
349
344
|
const sec = secondClause(ir);
|
|
350
345
|
const minutes = "\u6BCF\u5C0F\u65F6" + valueList(fieldSegments(ir, "minute"), "\u5206");
|
|
351
|
-
const minuteSegs = fieldSegments(ir, "minute");
|
|
352
346
|
if (ir.shapes.hour === "wildcard") {
|
|
353
347
|
return minutes + "\uFF0C" + sec;
|
|
354
348
|
}
|
|
355
349
|
if (isHourCadence(ir)) {
|
|
356
350
|
return cadence(stepSegment(ir, "hour").interval, UNITS.hour) + "\uFF0C" + minutes + "\uFF0C" + sec;
|
|
357
351
|
}
|
|
358
|
-
if (ir.shapes.hour === "range" && minuteSegs.length === 1 && minuteSegs[0].kind === "range") {
|
|
359
|
-
const [from, to] = fieldSegments(ir, "hour")[0].bounds;
|
|
360
|
-
return "\u5728" + hourWord(+from) + "\u81F3" + to + "\u70B9" + minuteSegs[0].bounds[1] + "\u5206\u4E4B\u95F4\uFF0C" + sec;
|
|
361
|
-
}
|
|
362
352
|
return hourFrame(ir) + minutes + "\uFF0C" + sec;
|
|
363
353
|
}
|
|
364
354
|
function renderComposeSeconds(ir, plan, opts) {
|
package/dist/lang/zh.js
CHANGED
|
@@ -171,7 +171,8 @@ function hourFrame(ir) {
|
|
|
171
171
|
return "\u5728" + hourList(ir) + "\uFF0C";
|
|
172
172
|
}
|
|
173
173
|
function renderMinuteFrequency(ir, plan) {
|
|
174
|
-
const
|
|
174
|
+
const minuteStep = stepSegment(ir, "minute");
|
|
175
|
+
const base = minuteStep.startToken === "*" ? cadence(minuteStep.interval, UNITS.minute) : renderMinutePast(ir);
|
|
175
176
|
const { hours } = plan;
|
|
176
177
|
if (hours.kind === "step") {
|
|
177
178
|
return cadence(stepSegment(ir, "hour").interval, UNITS.hour) + base;
|
|
@@ -239,9 +240,6 @@ function renderHourStep(ir) {
|
|
|
239
240
|
if (segment.fires.length <= 2) {
|
|
240
241
|
return joinAnd(segment.fires.map(hourWord));
|
|
241
242
|
}
|
|
242
|
-
if (24 % segment.interval !== 0) {
|
|
243
|
-
return "\u4ECE" + hourWord(segment.fires[0]) + "\u8D77\uFF0C" + cadence(segment.interval, UNITS.hour);
|
|
244
|
-
}
|
|
245
243
|
return cadence(segment.interval, UNITS.hour);
|
|
246
244
|
}
|
|
247
245
|
function renderRangeOfMinutes(ir) {
|
|
@@ -294,15 +292,12 @@ function composeSecondsOnHour(ir, plan, opts) {
|
|
|
294
292
|
const sec = secondClause(ir);
|
|
295
293
|
const { rest } = plan;
|
|
296
294
|
const restText = render(ir, rest, opts);
|
|
297
|
-
if (rest.kind === "everyHour") {
|
|
298
|
-
return sec + "\uFF0C\u6BCF\u5C0F\u65F6";
|
|
299
|
-
}
|
|
300
|
-
if (rest.kind === "hourStep") {
|
|
301
|
-
return sec + "\uFF0C" + restText;
|
|
302
|
-
}
|
|
303
295
|
if ((rest.kind === "clockTimes" || rest.kind === "compactClockTimes") && isDaily(ir)) {
|
|
304
296
|
return "\u6BCF\u5929" + restText + sec;
|
|
305
297
|
}
|
|
298
|
+
if (rest.kind === "singleMinute") {
|
|
299
|
+
return restText + "\uFF0C" + sec;
|
|
300
|
+
}
|
|
306
301
|
return restText + sec;
|
|
307
302
|
}
|
|
308
303
|
function composeSecondsCadence(ir) {
|
|
@@ -322,17 +317,12 @@ function composeSecondsCadence(ir) {
|
|
|
322
317
|
function composeSecondsListed(ir) {
|
|
323
318
|
const sec = secondClause(ir);
|
|
324
319
|
const minutes = "\u6BCF\u5C0F\u65F6" + valueList(fieldSegments(ir, "minute"), "\u5206");
|
|
325
|
-
const minuteSegs = fieldSegments(ir, "minute");
|
|
326
320
|
if (ir.shapes.hour === "wildcard") {
|
|
327
321
|
return minutes + "\uFF0C" + sec;
|
|
328
322
|
}
|
|
329
323
|
if (isHourCadence(ir)) {
|
|
330
324
|
return cadence(stepSegment(ir, "hour").interval, UNITS.hour) + "\uFF0C" + minutes + "\uFF0C" + sec;
|
|
331
325
|
}
|
|
332
|
-
if (ir.shapes.hour === "range" && minuteSegs.length === 1 && minuteSegs[0].kind === "range") {
|
|
333
|
-
const [from, to] = fieldSegments(ir, "hour")[0].bounds;
|
|
334
|
-
return "\u5728" + hourWord(+from) + "\u81F3" + to + "\u70B9" + minuteSegs[0].bounds[1] + "\u5206\u4E4B\u95F4\uFF0C" + sec;
|
|
335
|
-
}
|
|
336
326
|
return hourFrame(ir) + minutes + "\uFF0C" + sec;
|
|
337
327
|
}
|
|
338
328
|
function renderComposeSeconds(ir, plan, opts) {
|
package/package.json
CHANGED
package/src/core/analyze.ts
CHANGED
|
@@ -269,10 +269,13 @@ function planSeconds(
|
|
|
269
269
|
return null;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
// The second makes the cadence sub-minute, so a minute of 0 is a real
|
|
273
|
+
// restriction that must be stated, not absorbed into an hourly idiom (which
|
|
274
|
+
// would silently drop it). Route minute 0 to the minute-explicit forms.
|
|
272
275
|
return {
|
|
273
276
|
kind: 'composeSeconds',
|
|
274
|
-
rest: planMinutes(pattern, shapes, analyses) ||
|
|
275
|
-
planHours(pattern, shapes, analyses)
|
|
277
|
+
rest: planMinutes(pattern, shapes, analyses, true) ||
|
|
278
|
+
planHours(pattern, shapes, analyses, true)
|
|
276
279
|
};
|
|
277
280
|
}
|
|
278
281
|
|
|
@@ -303,7 +306,8 @@ function planStandaloneSeconds(
|
|
|
303
306
|
function planMinutes(
|
|
304
307
|
pattern: Pattern,
|
|
305
308
|
shapes: Shapes,
|
|
306
|
-
analyses: Analyses
|
|
309
|
+
analyses: Analyses,
|
|
310
|
+
subMinuteSecond = false
|
|
307
311
|
): PlanNode | undefined {
|
|
308
312
|
if (shapes.minute === 'step') {
|
|
309
313
|
return {
|
|
@@ -333,7 +337,7 @@ function planMinutes(
|
|
|
333
337
|
}
|
|
334
338
|
|
|
335
339
|
if (pattern.hour === '*') {
|
|
336
|
-
return planMinutesUnderOpenHour(pattern, shapes);
|
|
340
|
+
return planMinutesUnderOpenHour(pattern, shapes, subMinuteSecond);
|
|
337
341
|
}
|
|
338
342
|
}
|
|
339
343
|
|
|
@@ -451,7 +455,8 @@ function planMinutesAcrossHours(
|
|
|
451
455
|
// Minute strategies that only stand on their own under a wildcard hour.
|
|
452
456
|
function planMinutesUnderOpenHour(
|
|
453
457
|
pattern: Pattern,
|
|
454
|
-
shapes: Shapes
|
|
458
|
+
shapes: Shapes,
|
|
459
|
+
subMinuteSecond: boolean
|
|
455
460
|
): PlanNode | undefined {
|
|
456
461
|
if (shapes.minute === 'range') {
|
|
457
462
|
return {kind: 'rangeOfMinutes'};
|
|
@@ -465,56 +470,89 @@ function planMinutesUnderOpenHour(
|
|
|
465
470
|
return {kind: 'everyMinute'};
|
|
466
471
|
}
|
|
467
472
|
|
|
468
|
-
|
|
473
|
+
// Minute 0 normally defers to "every hour" so a standalone `0 * * * *`
|
|
474
|
+
// stays terse; under a sub-minute second it must be stated, so name it.
|
|
475
|
+
if (pattern.minute !== '0' || subMinuteSecond) {
|
|
469
476
|
return {kind: 'singleMinute'};
|
|
470
477
|
}
|
|
471
478
|
}
|
|
472
479
|
|
|
473
|
-
// Hour strategies: the chain's last resort always produces a plan.
|
|
480
|
+
// Hour strategies: the chain's last resort always produces a plan. Under a
|
|
481
|
+
// sub-minute second a minute of 0 is a real restriction, so the absorbing
|
|
482
|
+
// idioms (hour range, hour step, every hour) are skipped for it and the hour
|
|
483
|
+
// is enumerated as clock times instead, stating the :00.
|
|
474
484
|
function planHours(
|
|
475
485
|
pattern: Pattern,
|
|
476
486
|
shapes: Shapes,
|
|
477
|
-
analyses: Analyses
|
|
487
|
+
analyses: Analyses,
|
|
488
|
+
subMinuteSecond = false
|
|
478
489
|
): PlanNode {
|
|
479
|
-
|
|
480
|
-
const bounds = pattern.hour.split('-');
|
|
481
|
-
let minuteForm: 'lead' | 'wildcard' | 'range' = 'lead';
|
|
482
|
-
|
|
483
|
-
if (pattern.minute === '*') {
|
|
484
|
-
minuteForm = 'wildcard';
|
|
485
|
-
}
|
|
486
|
-
else if (shapes.minute === 'range') {
|
|
487
|
-
minuteForm = 'range';
|
|
488
|
-
}
|
|
490
|
+
const absorbsMinuteZero = subMinuteSecond && pattern.minute === '0';
|
|
489
491
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
kind: 'hourRange',
|
|
493
|
-
last: analyses.lastMinuteFire,
|
|
494
|
-
minuteForm,
|
|
495
|
-
to: +bounds[1]
|
|
496
|
-
};
|
|
492
|
+
if (shapes.hour === 'range' && !absorbsMinuteZero) {
|
|
493
|
+
return planHourRange(pattern, shapes, analyses);
|
|
497
494
|
}
|
|
498
495
|
|
|
499
|
-
if (shapes.hour === 'step' && pattern.minute === '0') {
|
|
496
|
+
if (shapes.hour === 'step' && pattern.minute === '0' && !subMinuteSecond) {
|
|
500
497
|
return {kind: 'hourStep'};
|
|
501
498
|
}
|
|
502
499
|
|
|
503
|
-
if (pattern.hour === '*') {
|
|
500
|
+
if (pattern.hour === '*' && !absorbsMinuteZero) {
|
|
504
501
|
return {kind: 'everyHour'};
|
|
505
502
|
}
|
|
506
503
|
|
|
507
|
-
|
|
504
|
+
// When minute 0 must be stated, enumerate the on-the-hour times explicitly:
|
|
505
|
+
// the compact fold of a contiguous hour range would otherwise restate the
|
|
506
|
+
// hour-range idiom ("every hour from X through Y") and re-drop the :00.
|
|
507
|
+
return planClockTimes(pattern, analyses, absorbsMinuteZero);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// The hour-range plan: a window from the first hour through the last. The
|
|
511
|
+
// minute clause leads (a single fire or a list), fires every minute (a range),
|
|
512
|
+
// or fills the window (a wildcard). A multi-valued minute (list or range)
|
|
513
|
+
// closes the window on the bare hour, stating its minutes separately; a single
|
|
514
|
+
// fire or a wildcard names an exact closing minute (its fire, or the wildcard's
|
|
515
|
+
// last :59) — otherwise the glued last fire reads as a continuous span.
|
|
516
|
+
function planHourRange(
|
|
517
|
+
pattern: Pattern,
|
|
518
|
+
shapes: Shapes,
|
|
519
|
+
analyses: Analyses
|
|
520
|
+
): PlanNode {
|
|
521
|
+
const bounds = pattern.hour.split('-');
|
|
522
|
+
let minuteForm: 'lead' | 'wildcard' | 'range' = 'lead';
|
|
523
|
+
|
|
524
|
+
if (pattern.minute === '*') {
|
|
525
|
+
minuteForm = 'wildcard';
|
|
526
|
+
}
|
|
527
|
+
else if (shapes.minute === 'range') {
|
|
528
|
+
minuteForm = 'range';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const multiValued = shapes.minute === 'range' || shapes.minute === 'list';
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
boundMinute: multiValued ? null : analyses.lastMinuteFire,
|
|
535
|
+
from: +bounds[0],
|
|
536
|
+
kind: 'hourRange',
|
|
537
|
+
last: analyses.lastMinuteFire,
|
|
538
|
+
minuteForm,
|
|
539
|
+
to: +bounds[1]
|
|
540
|
+
};
|
|
508
541
|
}
|
|
509
542
|
|
|
510
543
|
// Enumerated clock times up to the cap; past it, a compact form (a single
|
|
511
544
|
// minute folds into hour-segment windows; a minute list leads with its own
|
|
512
|
-
// clause).
|
|
513
|
-
|
|
545
|
+
// clause). `enumerate` forces the explicit list past the cap, used when a
|
|
546
|
+
// minute restriction must be named rather than folded into an hour idiom.
|
|
547
|
+
function planClockTimes(
|
|
548
|
+
pattern: Pattern,
|
|
549
|
+
analyses: Analyses,
|
|
550
|
+
enumerate = false
|
|
551
|
+
): PlanNode {
|
|
514
552
|
const hours = enumerateFires(pattern.hour, 0, 23);
|
|
515
553
|
const minutes = enumerateValues(pattern.minute);
|
|
516
554
|
|
|
517
|
-
if (hours.length * minutes.length > maxClockTimes) {
|
|
555
|
+
if (!enumerate && hours.length * minutes.length > maxClockTimes) {
|
|
518
556
|
return {
|
|
519
557
|
fold: minutes.length === 1,
|
|
520
558
|
kind: 'compactClockTimes',
|
package/src/core/ir.ts
CHANGED
|
@@ -78,6 +78,12 @@ export type PlanNode =
|
|
|
78
78
|
from: number;
|
|
79
79
|
to: number;
|
|
80
80
|
last: number;
|
|
81
|
+
// The minute to show on the closing bound, or `null` to close on the
|
|
82
|
+
// bare hour with the minutes stated separately. A single fire or a
|
|
83
|
+
// wildcard names an exact closing minute (the fire, or `:59`); a minute
|
|
84
|
+
// list or range would otherwise glue its last fire onto the bound and
|
|
85
|
+
// read as a continuous span, so it closes bare instead.
|
|
86
|
+
boundMinute: number | null;
|
|
81
87
|
minuteForm: 'lead' | 'wildcard' | 'range';
|
|
82
88
|
}
|
|
83
89
|
| {kind: 'hourStep'}
|
package/src/core/normalize.ts
CHANGED
|
@@ -4,10 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
import {fieldOrder, fieldSpecs} from './specs.js';
|
|
6
6
|
import type {CronLike, FieldSpec} from './specs.js';
|
|
7
|
-
import type {Pattern} from './ir.js';
|
|
7
|
+
import type {Field, Pattern} from './ir.js';
|
|
8
8
|
import {includes, toFieldNumber, unique} from './util.js';
|
|
9
9
|
import {isQuartzDate, isQuartzWeekday} from './validate.js';
|
|
10
10
|
|
|
11
|
+
// The fixed-cycle time fields: their step intervals are measured against a
|
|
12
|
+
// closed cycle (60 seconds, 60 minutes, 24 hours), so a step is a true
|
|
13
|
+
// "every N" cadence only when it tiles that cycle. The calendar fields
|
|
14
|
+
// (date/month/weekday) have variable cycles and keep their step form.
|
|
15
|
+
const timeFieldCycle: Partial<Record<Field, number>> = {
|
|
16
|
+
hour: 24,
|
|
17
|
+
minute: 60,
|
|
18
|
+
second: 60
|
|
19
|
+
};
|
|
20
|
+
|
|
11
21
|
// Quartz aliases: `?` reads "no specific value" (equivalent to `*`) in the
|
|
12
22
|
// date and weekday fields, and a bare `L` weekday means Saturday.
|
|
13
23
|
function applyQuartzAliases(cronPattern: CronLike): void {
|
|
@@ -40,7 +50,7 @@ function normalizeCronPattern(cronPattern: CronLike): Pattern {
|
|
|
40
50
|
return;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
cronPattern[field] = normalizeField(value, fieldSpecs[field]);
|
|
53
|
+
cronPattern[field] = normalizeField(value, field, fieldSpecs[field]);
|
|
44
54
|
});
|
|
45
55
|
|
|
46
56
|
// Every field is now a canonical string.
|
|
@@ -48,17 +58,20 @@ function normalizeCronPattern(cronPattern: CronLike): Pattern {
|
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
// Canonicalize a single validated field value to a string.
|
|
51
|
-
function normalizeField(value: string, spec: FieldSpec): string {
|
|
61
|
+
function normalizeField(value: string, field: Field, spec: FieldSpec): string {
|
|
52
62
|
const stringValue = '' + value;
|
|
53
63
|
|
|
54
64
|
if (stringValue === '*') {
|
|
55
65
|
return stringValue;
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
const cycle = timeFieldCycle[field];
|
|
58
69
|
const segments = stringValue.split(',').map(function canonical(segment) {
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
return enumerateNonUniformStep(
|
|
71
|
+
collapseDegenerateRange(
|
|
72
|
+
collapseOnceStep(collapseUnitStep(segment, spec), spec), spec),
|
|
73
|
+
spec, cycle);
|
|
74
|
+
}).join(',').split(',');
|
|
62
75
|
|
|
63
76
|
// A full-cycle segment covers the whole field.
|
|
64
77
|
if (segments.indexOf('*') !== -1) {
|
|
@@ -115,6 +128,41 @@ function collapseOnceStep(segment: string, spec: FieldSpec): string {
|
|
|
115
128
|
return start === '*' ? '' + spec.min : start;
|
|
116
129
|
}
|
|
117
130
|
|
|
131
|
+
// An unbounded step in a fixed-cycle time field is a true "every N" cadence
|
|
132
|
+
// only when it tiles the cycle: the interval divides it evenly and the start
|
|
133
|
+
// falls within the first interval (`*/15`, `5/6`). A step that fails either
|
|
134
|
+
// test fires at irregular points within the cycle, so it reads as the literal
|
|
135
|
+
// list of those fires (`*/7` is `0,7,14,…`), the same as if it were written
|
|
136
|
+
// out. Calendar fields (no `cycle`), bounded steps (`9-17/2`, a per-window
|
|
137
|
+
// stride), and non-step segments are left untouched.
|
|
138
|
+
function enumerateNonUniformStep(
|
|
139
|
+
segment: string,
|
|
140
|
+
spec: FieldSpec,
|
|
141
|
+
cycle: number | undefined
|
|
142
|
+
): string {
|
|
143
|
+
const parts = segment.split('/');
|
|
144
|
+
|
|
145
|
+
if (typeof cycle !== 'number' || parts.length !== 2 ||
|
|
146
|
+
includes(parts[0], '-')) {
|
|
147
|
+
return segment;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const interval = +parts[1];
|
|
151
|
+
const start = parts[0] === '*' ? spec.min : toFieldNumber(parts[0]);
|
|
152
|
+
|
|
153
|
+
if (cycle % interval === 0 && start < interval) {
|
|
154
|
+
return segment;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const fires = [];
|
|
158
|
+
|
|
159
|
+
for (let value = start; value <= (spec.top as number); value += interval) {
|
|
160
|
+
fires.push(value);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return fires.join(',');
|
|
164
|
+
}
|
|
165
|
+
|
|
118
166
|
// A degenerate range (`9-9`) fires once, so it reads as its single value.
|
|
119
167
|
// A stepped degenerate range (`9-9/5`) likewise fires only at its start.
|
|
120
168
|
function collapseDegenerateRange(segment: string, spec: FieldSpec): string {
|
package/src/lang/de/index.ts
CHANGED
|
@@ -462,7 +462,9 @@ function duringHours(ir: IR, times: HourTimesPlan, sep: string): string {
|
|
|
462
462
|
return joinList(windows);
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
-
|
|
465
|
+
// A discrete set of hours is a list, not a range, so it takes no "von"
|
|
466
|
+
// (which would read as "von X bis Y"); it mirrors the minute list form.
|
|
467
|
+
return 'in den Stunden ' + joinList(times.fires.map(String)) + ' Uhr';
|
|
466
468
|
}
|
|
467
469
|
|
|
468
470
|
// --- Renderers. ---
|
|
@@ -657,7 +659,11 @@ function renderHourRange(
|
|
|
657
659
|
plan: Extract<PlanNode, {kind: 'hourRange'}>,
|
|
658
660
|
opts: Opts
|
|
659
661
|
): string {
|
|
660
|
-
|
|
662
|
+
// A bare close (`boundMinute` null) lands on the top of the final hour
|
|
663
|
+
// (minute 0), matching the minute-0 baseline, with the minutes stated
|
|
664
|
+
// separately; a single fire or wildcard names an exact closing minute.
|
|
665
|
+
const window = hourWindow(plan.from, plan.to, plan.boundMinute ?? 0,
|
|
666
|
+
opts.style.sep);
|
|
661
667
|
|
|
662
668
|
if (plan.minuteForm === 'wildcard') {
|
|
663
669
|
return 'jede Minute ' + window;
|
|
@@ -814,7 +820,10 @@ const de: Language<GermanStyle> = {
|
|
|
814
820
|
fallback: 'ein unlesbares Cron-Muster',
|
|
815
821
|
options: normalizeOptions,
|
|
816
822
|
reboot: 'beim Systemstart',
|
|
817
|
-
|
|
823
|
+
// A description ending in a German ordinal already carries its period
|
|
824
|
+
// ("…am 8."), so closing the sentence must not double it.
|
|
825
|
+
sentence: (description) =>
|
|
826
|
+
'Läuft ' + description + (description.endsWith('.') ? '' : '.')
|
|
818
827
|
};
|
|
819
828
|
|
|
820
829
|
export default de;
|
package/src/lang/en/index.ts
CHANGED
|
@@ -376,7 +376,7 @@ function renderEveryHour(ir: IR, plan: PlanOf<'everyHour'>,
|
|
|
376
376
|
// minute; a discrete minute anchors as a lead clause.
|
|
377
377
|
function renderHourRange(ir: IR, plan: PlanOf<'hourRange'>,
|
|
378
378
|
opts: NormalizedOptions): string {
|
|
379
|
-
const window = hourWindow(plan, opts);
|
|
379
|
+
const window = hourWindow(boundedWindow(plan), opts);
|
|
380
380
|
|
|
381
381
|
if (plan.minuteForm === 'wildcard') {
|
|
382
382
|
return 'every minute ' + window + trailingQualifier(ir, opts);
|
|
@@ -411,6 +411,14 @@ function renderHourStep(ir: IR, plan: PlanOf<'hourStep'>,
|
|
|
411
411
|
trailingQualifier(ir, opts);
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
// The hour-range plan as a window whose closing minute honors `boundMinute`:
|
|
415
|
+
// a bare close (`null`) lands on the top of the final hour (`:00`), matching
|
|
416
|
+
// the minute-0 baseline, with the minutes stated separately elsewhere.
|
|
417
|
+
function boundedWindow(plan: PlanOf<'hourRange'>):
|
|
418
|
+
{from: number; to: number; last: number} {
|
|
419
|
+
return {from: plan.from, last: plan.boundMinute ?? 0, to: plan.to};
|
|
420
|
+
}
|
|
421
|
+
|
|
414
422
|
// An hour window phrase, e.g. "from 9 a.m. through 5:45 p.m.". Windows
|
|
415
423
|
// open at the top of the first hour and close at the minute field's last
|
|
416
424
|
// fire within the final hour.
|
|
@@ -539,7 +547,10 @@ const renderers = {
|
|
|
539
547
|
// Phrase a `start/interval` step segment for a field that cycles every 60
|
|
540
548
|
// units (seconds and minutes). `unit` is the singular noun and `anchor` is
|
|
541
549
|
// the larger unit the values are counted against. Interval-one steps never
|
|
542
|
-
// arrive here: normalization collapses them to ranges or `*`.
|
|
550
|
+
// arrive here: normalization collapses them to ranges or `*`. Nor do uneven
|
|
551
|
+
// steps that fail to tile the cycle: normalization rewrites those to the
|
|
552
|
+
// literal list of their fires, so only a clean cadence (interval dividing
|
|
553
|
+
// 60, start within the first interval) reaches a step renderer.
|
|
543
554
|
function stepCycle60(segment: StepSegment, unit: string,
|
|
544
555
|
anchor: string, opts: NormalizedOptions): string {
|
|
545
556
|
// A bounded start (`a-b/n`) applies the interval within the range.
|
|
@@ -551,6 +562,8 @@ function stepCycle60(segment: StepSegment, unit: string,
|
|
|
551
562
|
const interval = segment.interval;
|
|
552
563
|
|
|
553
564
|
if (start !== 0) {
|
|
565
|
+
// A short offset cadence lists its fires; a longer one names the
|
|
566
|
+
// interval and its starting offset ("every six minutes from five …").
|
|
554
567
|
if (segment.fires.length <= 3) {
|
|
555
568
|
return listPastThe(numberWords(segment.fires, opts), unit, anchor,
|
|
556
569
|
opts);
|
|
@@ -561,18 +574,8 @@ function stepCycle60(segment: StepSegment, unit: string,
|
|
|
561
574
|
' past the ' + anchor;
|
|
562
575
|
}
|
|
563
576
|
|
|
564
|
-
// A
|
|
565
|
-
|
|
566
|
-
if (60 % interval === 0) {
|
|
567
|
-
return 'every ' + getNumber(interval, opts) + ' ' + unit + 's';
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (segment.fires.length <= 2) {
|
|
571
|
-
return listPastThe(numberWords(segment.fires, opts), unit, anchor, opts);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return 'every ' + getNumber(interval, opts) + ' ' + unit +
|
|
575
|
-
's past the ' + anchor;
|
|
577
|
+
// A clean stride from the top of the cycle is the bare cadence.
|
|
578
|
+
return 'every ' + getNumber(interval, opts) + ' ' + unit + 's';
|
|
576
579
|
}
|
|
577
580
|
|
|
578
581
|
// Phrase a `start/interval` step segment for the hour field (cycles every
|
|
@@ -586,18 +589,18 @@ function stepHours(segment: StepSegment, opts: NormalizedOptions): string {
|
|
|
586
589
|
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
587
590
|
const interval = segment.interval;
|
|
588
591
|
|
|
589
|
-
|
|
592
|
+
// A clean stride from midnight is the bare cadence. (An uneven stride is
|
|
593
|
+
// rewritten to its fires upstream and never reaches here.)
|
|
594
|
+
if (start === 0) {
|
|
590
595
|
return 'every ' + getNumber(interval, opts) + ' hours';
|
|
591
596
|
}
|
|
592
597
|
|
|
598
|
+
// A short offset cadence lists its fires; a longer one names the interval
|
|
599
|
+
// and its start ("every three hours from 2 a.m.").
|
|
593
600
|
if (segment.fires.length <= 3) {
|
|
594
601
|
return 'at ' + hourTimes(segment.fires, opts);
|
|
595
602
|
}
|
|
596
603
|
|
|
597
|
-
if (start === 0) {
|
|
598
|
-
return 'every ' + getNumber(interval, opts) + ' hours from midnight';
|
|
599
|
-
}
|
|
600
|
-
|
|
601
604
|
return 'every ' + getNumber(interval, opts) + ' hours from ' +
|
|
602
605
|
getTime({hour: start, minute: 0}, opts);
|
|
603
606
|
}
|
package/src/lang/es/index.ts
CHANGED
|
@@ -525,7 +525,7 @@ function renderHourRange(
|
|
|
525
525
|
plan: Extract<PlanNode, {kind: 'hourRange'}>,
|
|
526
526
|
opts: Opts
|
|
527
527
|
): string {
|
|
528
|
-
const window = hourWindow(plan, opts);
|
|
528
|
+
const window = hourWindow(boundedWindow(plan), opts);
|
|
529
529
|
|
|
530
530
|
if (plan.minuteForm === 'wildcard') {
|
|
531
531
|
return 'cada minuto ' + window + trailingQualifier(ir, opts);
|
|
@@ -558,6 +558,15 @@ function renderHourStep(
|
|
|
558
558
|
trailingQualifier(ir, opts);
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
+
// The hour-range plan as a window whose closing minute honors `boundMinute`:
|
|
562
|
+
// a bare close (`null`) lands on the top of the final hour (minute 0),
|
|
563
|
+
// matching the minute-0 baseline, with the minutes stated separately.
|
|
564
|
+
function boundedWindow(
|
|
565
|
+
plan: Extract<PlanNode, {kind: 'hourRange'}>
|
|
566
|
+
): {from: number; to: number; last: number} {
|
|
567
|
+
return {from: plan.from, last: plan.boundMinute ?? 0, to: plan.to};
|
|
568
|
+
}
|
|
569
|
+
|
|
561
570
|
// "de las 9:00 a las 17:45": a window from the top of the first hour to
|
|
562
571
|
// the minute field's last fire within the final hour.
|
|
563
572
|
function hourWindow(
|
|
@@ -1006,17 +1015,9 @@ function stepCycle60(
|
|
|
1006
1015
|
unit + ' ' + start + ' de cada ' + anchor;
|
|
1007
1016
|
}
|
|
1008
1017
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
if (segment.fires.length <= 2) {
|
|
1014
|
-
return 'en los ' + unit + 's ' + joinList(wordList(segment.fires)) +
|
|
1015
|
-
' de cada ' + anchor;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
return 'cada ' + numero(interval, opts) + ' ' + unit + 's de cada ' +
|
|
1019
|
-
anchor;
|
|
1018
|
+
// A clean stride from the top of the cycle is the bare cadence. (An uneven
|
|
1019
|
+
// stride is rewritten to its fires upstream and never reaches here.)
|
|
1020
|
+
return 'cada ' + numero(interval, opts) + ' ' + unit + 's';
|
|
1020
1021
|
}
|
|
1021
1022
|
|
|
1022
1023
|
// "cada seis horas", "a las 9:00, a las 11:00 y a la 1:00", or "cada
|
|
@@ -1029,7 +1030,9 @@ function stepHours(segment: StepSegment, opts: Opts): string {
|
|
|
1029
1030
|
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
1030
1031
|
const interval = segment.interval;
|
|
1031
1032
|
|
|
1032
|
-
|
|
1033
|
+
// A clean stride from midnight is the bare cadence. (An uneven stride is
|
|
1034
|
+
// rewritten to its fires upstream and never reaches here.)
|
|
1035
|
+
if (start === 0) {
|
|
1033
1036
|
return 'cada ' + numero(interval, opts) + ' horas';
|
|
1034
1037
|
}
|
|
1035
1038
|
|
|
@@ -1037,10 +1040,6 @@ function stepHours(segment: StepSegment, opts: Opts): string {
|
|
|
1037
1040
|
return groupClockTimesByArticle(atTimes(segment.fires, opts));
|
|
1038
1041
|
}
|
|
1039
1042
|
|
|
1040
|
-
if (start === 0) {
|
|
1041
|
-
return 'cada ' + numero(interval, opts) + ' horas desde medianoche';
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
1043
|
return 'cada ' + numero(interval, opts) + ' horas a partir de ' +
|
|
1045
1044
|
timePhrase(start, 0, null, opts);
|
|
1046
1045
|
}
|