cronli5 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/README.md +4 -4
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +514 -407
- package/dist/cronli5.js +514 -407
- package/dist/lang/de.cjs +296 -225
- package/dist/lang/de.js +296 -225
- package/dist/lang/en.cjs +471 -364
- package/dist/lang/en.js +471 -364
- package/dist/lang/es.cjs +318 -281
- package/dist/lang/es.js +318 -281
- package/dist/lang/fi.cjs +326 -276
- package/dist/lang/fi.js +326 -276
- package/dist/lang/zh.cjs +308 -236
- package/dist/lang/zh.js +308 -236
- package/package.json +1 -1
- package/src/core/analyze.ts +22 -21
- package/src/core/cadence.ts +164 -0
- package/src/core/index.ts +3 -1
- package/src/core/normalize.ts +3 -3
- package/src/core/parse.ts +1 -1
- package/src/core/{ir.ts → schedule.ts} +23 -24
- package/src/core/shapes.ts +8 -1
- package/src/core/specs.ts +1 -1
- package/src/core/util.ts +4 -83
- package/src/core/validate.ts +2 -2
- package/src/core/weekday.ts +54 -0
- package/src/cronli5.ts +7 -7
- package/src/lang/de/index.ts +329 -288
- package/src/lang/en/dialects.ts +1 -1
- package/src/lang/en/index.ts +640 -516
- package/src/lang/es/index.ts +342 -374
- package/src/lang/es/notes.md +1 -1
- package/src/lang/fi/dialects.ts +1 -1
- package/src/lang/fi/index.ts +367 -372
- package/src/lang/fi/notes.md +23 -8
- package/src/lang/fi/status.json +1 -1
- package/src/lang/zh/index.ts +344 -262
- package/src/types.ts +6 -6
- package/types/core/analyze.d.ts +4 -4
- package/types/core/cadence.d.ts +33 -0
- package/types/core/index.d.ts +3 -1
- package/types/core/normalize.d.ts +1 -1
- package/types/core/parse.d.ts +1 -1
- package/types/core/{ir.d.ts → schedule.d.ts} +16 -21
- package/types/core/shapes.d.ts +2 -1
- package/types/core/specs.d.ts +1 -1
- package/types/core/util.d.ts +1 -15
- package/types/core/weekday.d.ts +10 -0
- package/types/lang/de/index.d.ts +1 -1
- package/types/lang/en/dialects.d.ts +1 -1
- package/types/lang/en/index.d.ts +1 -1
- package/types/lang/es/index.d.ts +1 -1
- package/types/lang/fi/dialects.d.ts +1 -1
- package/types/lang/fi/index.d.ts +1 -1
- package/types/lang/zh/index.d.ts +1 -1
- package/types/types.d.ts +5 -5
package/src/lang/de/index.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
// The German language module: renders the analyzed cron pattern (
|
|
1
|
+
// The German language module: renders the analyzed cron pattern (Schedule) as
|
|
2
2
|
// German. Anchored to Duden; see notes.md for the decisions.
|
|
3
3
|
|
|
4
4
|
import {pad} from '../../core/format.js';
|
|
5
5
|
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
6
6
|
import {
|
|
7
|
-
arithmeticStep,
|
|
8
|
-
|
|
7
|
+
arithmeticStep, hourListStride, offsetCleanStride,
|
|
8
|
+
renderStride as chooseStride, segmentsOf, singleValues, stepSegment
|
|
9
|
+
} from '../../core/cadence.js';
|
|
10
|
+
import {orderWeekdaysForDisplay} from '../../core/weekday.js';
|
|
11
|
+
import {isOpenStep} from '../../core/shapes.js';
|
|
12
|
+
import {toFieldNumber} from '../../core/util.js';
|
|
9
13
|
import type {Cronli5Options} from '../../types.js';
|
|
10
14
|
import type {
|
|
11
|
-
Field, HourTimesPlan,
|
|
12
|
-
} from '../../core/
|
|
15
|
+
Field, HourTimesPlan, Schedule, Language, NormalizedOptions, PlanNode, Segment
|
|
16
|
+
} from '../../core/schedule.js';
|
|
13
17
|
import {resolveDialect, type GermanStyle} from './dialects.js';
|
|
14
18
|
|
|
15
19
|
type Opts = NormalizedOptions<GermanStyle>;
|
|
16
|
-
type Renderer = (
|
|
20
|
+
type Renderer = (schedule: Schedule, plan: PlanNode, opts: Opts) => string;
|
|
17
21
|
type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
18
22
|
|
|
19
23
|
// A time unit: its singular and plural noun, and the gender-agreeing form of
|
|
@@ -59,11 +63,6 @@ function withAnchor(clause: string, anchor: string): string {
|
|
|
59
63
|
return anchor ? clause + ' ' + anchor : clause;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
// The first segment of a step field, which the plan guarantees is step-kinded.
|
|
63
|
-
function stepSegment(segments: Segment[] | null): StepSegment {
|
|
64
|
-
return (segments as Segment[])[0] as StepSegment;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
66
|
// A step is "clean" when it starts at 0 and evenly divides its cycle (60 for
|
|
68
67
|
// minutes/seconds, 24 for hours) — only then does "alle N" describe it; an
|
|
69
68
|
// uneven step fires at discrete points that must be listed.
|
|
@@ -86,22 +85,17 @@ function cleanStep(segment: StepSegment, cycle: number): boolean {
|
|
|
86
85
|
function renderStride(stride: Stride): string {
|
|
87
86
|
const {interval, start, last, cycle, unit, anchor} = stride;
|
|
88
87
|
const cadence = everyN(interval, unit);
|
|
89
|
-
const tiles = cycle % interval === 0;
|
|
90
|
-
|
|
91
|
-
if (start === 0 && tiles) {
|
|
92
|
-
return cadence;
|
|
93
|
-
}
|
|
94
88
|
|
|
95
89
|
// A context that supplies its own trailing scope passes an empty anchor, so
|
|
96
90
|
// the cadence keeps its endpoints but drops the "jeder Stunde" tail.
|
|
97
91
|
const tail = anchor ? ' ' + anchor : '';
|
|
98
92
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
return chooseStride({start, interval, cycle}, {
|
|
94
|
+
bare: () => cadence,
|
|
95
|
+
offset: () => cadence + ' ab ' + unit.singular + ' ' + start + tail,
|
|
96
|
+
bounded: () =>
|
|
97
|
+
cadence + ' von ' + unit.singular + ' ' + start + ' bis ' + last + tail
|
|
98
|
+
});
|
|
105
99
|
}
|
|
106
100
|
|
|
107
101
|
// A step *shape* segment as its cadence ("alle 6 Minuten ab Minute 5 jeder
|
|
@@ -131,27 +125,11 @@ function stepClause(segment: StepSegment, unit: Unit, anchor: string): string {
|
|
|
131
125
|
});
|
|
132
126
|
}
|
|
133
127
|
|
|
134
|
-
// The sorted numeric values a field's segments cover, or null if any segment
|
|
135
|
-
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
136
|
-
function singleValues(segments: Segment[]): number[] | null {
|
|
137
|
-
const values: number[] = [];
|
|
138
|
-
|
|
139
|
-
for (const segment of segments) {
|
|
140
|
-
if (segment.kind !== 'single') {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
values.push(+segment.value);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return values;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
128
|
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
151
129
|
// form an arithmetic progression long enough to beat the list (the core
|
|
152
|
-
// enumerates an offset/uneven step to this fire list; the
|
|
153
|
-
// the renderer recognizes the progression). Returns null for a
|
|
154
|
-
// or a too-short list, leaving the caller to enumerate.
|
|
130
|
+
// enumerates an offset/uneven step to this fire list; the Schedule is
|
|
131
|
+
// unchanged, so the renderer recognizes the progression). Returns null for a
|
|
132
|
+
// non-progression or a too-short list, leaving the caller to enumerate.
|
|
155
133
|
function strideFromSegments(
|
|
156
134
|
segments: Segment[],
|
|
157
135
|
unit: Unit,
|
|
@@ -177,10 +155,6 @@ const weekdayNames = [
|
|
|
177
155
|
'freitags', 'samstags'
|
|
178
156
|
];
|
|
179
157
|
|
|
180
|
-
function fieldSegments(ir: IR, field: Field): Segment[] {
|
|
181
|
-
return ir.analyses.segments[field] as Segment[];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
158
|
// Expand step segments into their fires as singles so a name list reads flat.
|
|
185
159
|
function flattenSteps(segments: Segment[]): NameSegment[] {
|
|
186
160
|
return segments.flatMap(function flat(segment): NameSegment[] {
|
|
@@ -216,10 +190,10 @@ function weekdayRange(bounds: [string, string]): string {
|
|
|
216
190
|
}
|
|
217
191
|
|
|
218
192
|
// "montags", "montags bis freitags", "montags, mittwochs und freitags".
|
|
219
|
-
function weekdayQualifier(
|
|
193
|
+
function weekdayQualifier(schedule: Schedule): string {
|
|
220
194
|
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
221
|
-
// form. The
|
|
222
|
-
const segments = orderWeekdaysForDisplay(
|
|
195
|
+
// form. The Schedule stays canonical (Sunday=0). The helper flattens steps.
|
|
196
|
+
const segments = orderWeekdaysForDisplay(segmentsOf(schedule, 'weekday'));
|
|
223
197
|
|
|
224
198
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
225
199
|
return weekdayRange(segments[0].bounds);
|
|
@@ -313,6 +287,29 @@ function quartzDate(field: string): string | null {
|
|
|
313
287
|
return null;
|
|
314
288
|
}
|
|
315
289
|
|
|
290
|
+
// An open interval-2 day-of-month step covers a parity set, so it reads as the
|
|
291
|
+
// parity class ("an jedem ungeraden Tag des Monats") rather than enumerating
|
|
292
|
+
// its 16 fires — the enumeration would bury the union beside the "oder". `*/2`
|
|
293
|
+
// and `1/2` are the odd days, `2/2` the even; any other start enumerates.
|
|
294
|
+
// Mirrors en's odd/even-numbered-day idiom. Null when not such a step.
|
|
295
|
+
function oddEvenDay(dateField: string): string | null {
|
|
296
|
+
if (!isOpenStep(dateField)) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const [start, step] = dateField.split('/');
|
|
301
|
+
|
|
302
|
+
if (+step !== 2) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (start === '*' || start === '1') {
|
|
307
|
+
return 'an jedem ungeraden Tag des Monats';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return start === '2' ? 'an jedem geraden Tag des Monats' : null;
|
|
311
|
+
}
|
|
312
|
+
|
|
316
313
|
type Months = GermanStyle['months'];
|
|
317
314
|
|
|
318
315
|
// The month names are dialect-scoped (resolved from `opts.style.months`);
|
|
@@ -328,8 +325,8 @@ function monthRange(bounds: [string, string], months: Months): string {
|
|
|
328
325
|
}
|
|
329
326
|
|
|
330
327
|
// Bare month names: "Januar", "Januar und Juli", "von Juni bis August".
|
|
331
|
-
function monthNamesList(
|
|
332
|
-
return joinList(flattenSteps(
|
|
328
|
+
function monthNamesList(schedule: Schedule, months: Months): string {
|
|
329
|
+
return joinList(flattenSteps(segmentsOf(schedule, 'month'))
|
|
333
330
|
.map(function name(segment): string {
|
|
334
331
|
return segment.kind === 'range' ?
|
|
335
332
|
monthRange(segment.bounds, months) :
|
|
@@ -339,19 +336,21 @@ function monthNamesList(ir: IR, months: Months): string {
|
|
|
339
336
|
|
|
340
337
|
// The month qualifier: "im Januar", "im Januar und Juli", "von Juni bis
|
|
341
338
|
// August". A lone range carries its own "von … bis"; names take "im".
|
|
342
|
-
function monthClause(
|
|
343
|
-
const segments = flattenSteps(
|
|
339
|
+
function monthClause(schedule: Schedule, months: Months): string {
|
|
340
|
+
const segments = flattenSteps(segmentsOf(schedule, 'month'));
|
|
344
341
|
|
|
345
342
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
346
343
|
return monthRange(segments[0].bounds, months);
|
|
347
344
|
}
|
|
348
345
|
|
|
349
|
-
return 'im ' + monthNamesList(
|
|
346
|
+
return 'im ' + monthNamesList(schedule, months);
|
|
350
347
|
}
|
|
351
348
|
|
|
352
349
|
// The month appended after a weekday: " im Januar" or "".
|
|
353
|
-
function monthScope(
|
|
354
|
-
return
|
|
350
|
+
function monthScope(schedule: Schedule, months: Months): string {
|
|
351
|
+
return schedule.pattern.month === '*' ?
|
|
352
|
+
'' :
|
|
353
|
+
' ' + monthClause(schedule, months);
|
|
355
354
|
}
|
|
356
355
|
|
|
357
356
|
// A day-of-month ordinal: a numeral with a period ("1.").
|
|
@@ -366,8 +365,8 @@ function dateRange(bounds: [string, string]): string {
|
|
|
366
365
|
|
|
367
366
|
// The bare date clause, without a month: "am 1.", "am 1. und 15.", "vom 1.
|
|
368
367
|
// bis zum 5.", "vom 1. bis zum 5. und am 10.".
|
|
369
|
-
function dateClauseBare(
|
|
370
|
-
const segments = flattenSteps(
|
|
368
|
+
function dateClauseBare(schedule: Schedule): string {
|
|
369
|
+
const segments = flattenSteps(segmentsOf(schedule, 'date'));
|
|
371
370
|
|
|
372
371
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
373
372
|
return dateRange(segments[0].bounds);
|
|
@@ -392,19 +391,19 @@ function dateClauseBare(ir: IR): string {
|
|
|
392
391
|
// The date qualifier with its month. Month names fold bare onto the date
|
|
393
392
|
// ("am 1. Januar", "am 1. Januar und Juli"); a month range cannot, so it
|
|
394
393
|
// trails as a scoped clause after a comma ("am 1., von Juni bis August").
|
|
395
|
-
function datePhrase(
|
|
396
|
-
const clause = dateClauseBare(
|
|
394
|
+
function datePhrase(schedule: Schedule, months: Months): string {
|
|
395
|
+
const clause = dateClauseBare(schedule);
|
|
397
396
|
|
|
398
|
-
if (
|
|
397
|
+
if (schedule.pattern.month === '*') {
|
|
399
398
|
return clause;
|
|
400
399
|
}
|
|
401
400
|
|
|
402
|
-
const monthRanged = flattenSteps(
|
|
401
|
+
const monthRanged = flattenSteps(segmentsOf(schedule, 'month'))
|
|
403
402
|
.some((segment) => segment.kind === 'range');
|
|
404
403
|
|
|
405
404
|
return monthRanged ?
|
|
406
|
-
clause + ', ' + monthClause(
|
|
407
|
-
clause + ' ' + monthNamesList(
|
|
405
|
+
clause + ', ' + monthClause(schedule, months) :
|
|
406
|
+
clause + ' ' + monthNamesList(schedule, months);
|
|
408
407
|
}
|
|
409
408
|
|
|
410
409
|
// A bare clock time: "9" on the hour, "14:30", or "0:00:30" with a second.
|
|
@@ -449,8 +448,8 @@ function hourWindow(
|
|
|
449
448
|
}
|
|
450
449
|
|
|
451
450
|
// A field's values as strings, a range rendered "a bis b".
|
|
452
|
-
function fieldValues(
|
|
453
|
-
return flattenSteps(
|
|
451
|
+
function fieldValues(schedule: Schedule, field: Field): string[] {
|
|
452
|
+
return flattenSteps(segmentsOf(schedule, field)).map(function value(segment) {
|
|
454
453
|
return segment.kind === 'range' ?
|
|
455
454
|
segment.bounds[0] + ' bis ' + segment.bounds[1] :
|
|
456
455
|
String(segment.value);
|
|
@@ -459,16 +458,16 @@ function fieldValues(ir: IR, field: Field): string[] {
|
|
|
459
458
|
|
|
460
459
|
// "in Minute 5", "in den Minuten 5, 10 und 30", "in den Minuten 0 bis 30".
|
|
461
460
|
function countedPhrase(
|
|
462
|
-
|
|
461
|
+
schedule: Schedule,
|
|
463
462
|
field: Field,
|
|
464
463
|
singular: string,
|
|
465
464
|
plural: string
|
|
466
465
|
): string {
|
|
467
|
-
if (
|
|
468
|
-
return 'in ' + singular + ' ' +
|
|
466
|
+
if (schedule.shapes[field] === 'single') {
|
|
467
|
+
return 'in ' + singular + ' ' + schedule.pattern[field];
|
|
469
468
|
}
|
|
470
469
|
|
|
471
|
-
return 'in den ' + plural + ' ' + joinList(fieldValues(
|
|
470
|
+
return 'in den ' + plural + ' ' + joinList(fieldValues(schedule, field));
|
|
472
471
|
}
|
|
473
472
|
|
|
474
473
|
// The minute scope for a seconds clause: "jeder Minute" only when the minute
|
|
@@ -477,15 +476,15 @@ function countedPhrase(
|
|
|
477
476
|
// clause drops the scope — "jeder Minute" would otherwise contradict the fixed
|
|
478
477
|
// minute ("in Sekunde 30 jeder Minute, in Minute 30" fires at second 30 of
|
|
479
478
|
// minute 30, not every minute).
|
|
480
|
-
function minuteAnchor(
|
|
481
|
-
return
|
|
479
|
+
function minuteAnchor(schedule: Schedule): string {
|
|
480
|
+
return schedule.pattern.minute === '*' ? 'jeder Minute' : '';
|
|
482
481
|
}
|
|
483
482
|
|
|
484
483
|
// The seconds clause: "alle 30 Sekunden" for a step, "in Sekunde 15 jeder
|
|
485
484
|
// Minute" under a wildcard minute, else the bare "in Sekunde 15" when the
|
|
486
485
|
// minute is fixed (its own clause names it).
|
|
487
|
-
function secondsLead(
|
|
488
|
-
return secondsClause(
|
|
486
|
+
function secondsLead(schedule: Schedule): string {
|
|
487
|
+
return secondsClause(schedule, minuteAnchor(schedule));
|
|
489
488
|
}
|
|
490
489
|
|
|
491
490
|
// The second clause counted against an arbitrary anchor. The anchor is "jeder
|
|
@@ -493,22 +492,24 @@ function secondsLead(ir: IR): string {
|
|
|
493
492
|
// minute 0 into the hour and counts the second "jeder Stunde" instead ("in
|
|
494
493
|
// Sekunde 30 jeder Stunde"), so the minute-0 confinement is stated, not
|
|
495
494
|
// dropped.
|
|
496
|
-
function secondsClause(
|
|
497
|
-
if (
|
|
495
|
+
function secondsClause(schedule: Schedule, anchor: string): string {
|
|
496
|
+
if (schedule.pattern.second === '*') {
|
|
498
497
|
return 'jede Sekunde';
|
|
499
498
|
}
|
|
500
499
|
|
|
501
|
-
const segments =
|
|
500
|
+
const segments = schedule.analyses.segments.second;
|
|
502
501
|
|
|
503
502
|
// A step shape speaks its cadence directly; an offset/uneven step the core
|
|
504
503
|
// enumerated to a list is recognized as a progression. Both fall back to the
|
|
505
504
|
// counted list (a short or irregular set).
|
|
506
|
-
if (
|
|
507
|
-
return stepClause(stepSegment(
|
|
505
|
+
if (schedule.shapes.second === 'step') {
|
|
506
|
+
return stepClause(stepSegment(schedule, 'second'), UNITS.second, anchor);
|
|
508
507
|
}
|
|
509
508
|
|
|
510
509
|
return strideFromSegments(segments as Segment[], UNITS.second, anchor) ??
|
|
511
|
-
withAnchor(
|
|
510
|
+
withAnchor(
|
|
511
|
+
countedPhrase(schedule, 'second', 'Sekunde', 'Sekunden'), anchor
|
|
512
|
+
);
|
|
512
513
|
}
|
|
513
514
|
|
|
514
515
|
// A clock time that always shows its minutes: "9:00", "9:30".
|
|
@@ -522,8 +523,8 @@ function atHours(hours: number[]): string {
|
|
|
522
523
|
}
|
|
523
524
|
|
|
524
525
|
// The discrete hour fires, single and step values flattened: [9, 17, 19, …].
|
|
525
|
-
function hourFires(
|
|
526
|
-
return flattenSteps(
|
|
526
|
+
function hourFires(schedule: Schedule): number[] {
|
|
527
|
+
return flattenSteps(segmentsOf(schedule, 'hour')).map(function fire(segment) {
|
|
527
528
|
return segment.kind === 'range' ? +segment.bounds[0] : +segment.value;
|
|
528
529
|
});
|
|
529
530
|
}
|
|
@@ -545,12 +546,12 @@ function partTime(
|
|
|
545
546
|
// The hour segments as parts: a range is a window, a single an "um H Uhr", a
|
|
546
547
|
// step its fires. `minute`/`second` attach to each.
|
|
547
548
|
function hourSegmentParts(
|
|
548
|
-
|
|
549
|
+
schedule: Schedule,
|
|
549
550
|
minute: number,
|
|
550
551
|
second: number | undefined,
|
|
551
552
|
sep: string
|
|
552
553
|
): string[] {
|
|
553
|
-
return
|
|
554
|
+
return segmentsOf(schedule, 'hour').map(function part(segment): string {
|
|
554
555
|
if (segment.kind === 'range') {
|
|
555
556
|
return 'von ' + partTime(+segment.bounds[0], minute, second, sep) +
|
|
556
557
|
' bis ' + partTime(+segment.bounds[1], minute, second, sep) + ' Uhr';
|
|
@@ -568,14 +569,16 @@ function hourSegmentParts(
|
|
|
568
569
|
|
|
569
570
|
// Each "during" hour as a full window (H:00–H:59); a range spans one window,
|
|
570
571
|
// a step its fires.
|
|
571
|
-
function duringWindows(
|
|
572
|
+
function duringWindows(
|
|
573
|
+
schedule: Schedule, times: HourTimesPlan, sep: string
|
|
574
|
+
): string[] {
|
|
572
575
|
if (times.kind === 'fires') {
|
|
573
576
|
return times.fires.map(function each(hour) {
|
|
574
577
|
return hourWindow(hour, hour, 59, sep);
|
|
575
578
|
});
|
|
576
579
|
}
|
|
577
580
|
|
|
578
|
-
return
|
|
581
|
+
return segmentsOf(schedule, 'hour').flatMap(function part(segment): string[] {
|
|
579
582
|
if (segment.kind === 'range') {
|
|
580
583
|
return [hourWindow(+segment.bounds[0], +segment.bounds[1], 59, sep)];
|
|
581
584
|
}
|
|
@@ -593,8 +596,10 @@ function duringWindows(ir: IR, times: HourTimesPlan, sep: string): string[] {
|
|
|
593
596
|
// The "during" hours of a confined cadence: a few hours read as windows ("von
|
|
594
597
|
// 9 bis 9:59 Uhr und …"); many read better as a compact list ("in den Stunden
|
|
595
598
|
// von 9, 11, 13, 15 und 17 Uhr") instead of sprawling windows.
|
|
596
|
-
function duringHours(
|
|
597
|
-
|
|
599
|
+
function duringHours(
|
|
600
|
+
schedule: Schedule, times: HourTimesPlan, sep: string
|
|
601
|
+
): string {
|
|
602
|
+
const windows = duringWindows(schedule, times, sep);
|
|
598
603
|
|
|
599
604
|
if (windows.length <= 3 || times.kind !== 'fires') {
|
|
600
605
|
return joinList(windows);
|
|
@@ -625,35 +630,35 @@ function renderEveryHour(): string {
|
|
|
625
630
|
|
|
626
631
|
// The open-minute seconds clause: "alle 30 Sekunden", "in Sekunde 15 jeder
|
|
627
632
|
// Minute". Serves standaloneSeconds (step) and secondPastMinute (single).
|
|
628
|
-
function renderSeconds(
|
|
629
|
-
return secondsLead(
|
|
633
|
+
function renderSeconds(schedule: Schedule): string {
|
|
634
|
+
return secondsLead(schedule);
|
|
630
635
|
}
|
|
631
636
|
|
|
632
637
|
// The minute-past-the-hour clause: "in Minute 5 jeder Stunde", "in den
|
|
633
638
|
// Minuten 5, 10 und 30 jeder Stunde". An offset/uneven step the core
|
|
634
639
|
// enumerated to this list reads as a stride cadence when the fires form a
|
|
635
640
|
// long-enough progression ("alle 2 Minuten von Minute 3 bis 59 jeder Stunde").
|
|
636
|
-
function minutePastClause(
|
|
637
|
-
return strideFromSegments(
|
|
641
|
+
function minutePastClause(schedule: Schedule): string {
|
|
642
|
+
return strideFromSegments(segmentsOf(schedule, 'minute'), UNITS.minute,
|
|
638
643
|
'jeder Stunde') ??
|
|
639
|
-
countedPhrase(
|
|
644
|
+
countedPhrase(schedule, 'minute', 'Minute', 'Minuten') + ' jeder Stunde';
|
|
640
645
|
}
|
|
641
646
|
|
|
642
|
-
function renderMinutePast(
|
|
643
|
-
return minutePastClause(
|
|
647
|
+
function renderMinutePast(schedule: Schedule): string {
|
|
648
|
+
return minutePastClause(schedule);
|
|
644
649
|
}
|
|
645
650
|
|
|
646
651
|
// A specific minute and second: "in Minute 0 und Sekunde 30 jeder Stunde".
|
|
647
652
|
function renderSecondsWithinMinute(
|
|
648
|
-
|
|
653
|
+
schedule: Schedule,
|
|
649
654
|
plan: Extract<PlanNode, {kind: 'secondsWithinMinute'}>
|
|
650
655
|
): string {
|
|
651
656
|
if (plan.singleSecond) {
|
|
652
|
-
return 'in Minute ' +
|
|
653
|
-
|
|
657
|
+
return 'in Minute ' + schedule.pattern.minute + ' und Sekunde ' +
|
|
658
|
+
schedule.pattern.second + ' jeder Stunde';
|
|
654
659
|
}
|
|
655
660
|
|
|
656
|
-
return secondsLead(
|
|
661
|
+
return secondsLead(schedule) + ', in Minute ' + schedule.pattern.minute +
|
|
657
662
|
' jeder Stunde';
|
|
658
663
|
}
|
|
659
664
|
|
|
@@ -676,11 +681,11 @@ function wholeHour(hour: number): string {
|
|
|
676
681
|
// Minute der 9-Uhr-Stunde") rather than a synthesized "von 9:00 bis 9:59"
|
|
677
682
|
// range the source never stated; a plain range is a real window and keeps it.
|
|
678
683
|
function renderMinuteSpanInHour(
|
|
679
|
-
|
|
684
|
+
schedule: Schedule,
|
|
680
685
|
plan: Extract<PlanNode, {kind: 'minuteSpanInHour'}>,
|
|
681
686
|
opts: Opts
|
|
682
687
|
): string {
|
|
683
|
-
if (
|
|
688
|
+
if (schedule.pattern.minute === '*') {
|
|
684
689
|
return 'jede Minute ' + wholeHour(plan.hour);
|
|
685
690
|
}
|
|
686
691
|
|
|
@@ -698,21 +703,22 @@ function renderMinuteSpanInHour(
|
|
|
698
703
|
// English. Other strides, a restricted hour, and an hour cadence keep the
|
|
699
704
|
// juxtaposed form.
|
|
700
705
|
function isEveryOtherMinuteSeconds(
|
|
701
|
-
|
|
706
|
+
schedule: Schedule,
|
|
702
707
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
703
708
|
): boolean {
|
|
704
709
|
if (plan.rest.kind !== 'minuteFrequency' ||
|
|
705
|
-
|
|
710
|
+
schedule.shapes.second !== 'wildcard' ||
|
|
711
|
+
schedule.shapes.hour !== 'wildcard') {
|
|
706
712
|
return false;
|
|
707
713
|
}
|
|
708
714
|
|
|
709
|
-
const minuteStep = stepSegment(
|
|
715
|
+
const minuteStep = stepSegment(schedule, 'minute');
|
|
710
716
|
|
|
711
717
|
return minuteStep.startToken === '*' && minuteStep.interval === 2;
|
|
712
718
|
}
|
|
713
719
|
|
|
714
720
|
function renderComposeSeconds(
|
|
715
|
-
|
|
721
|
+
schedule: Schedule,
|
|
716
722
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
717
723
|
opts: Opts
|
|
718
724
|
): string {
|
|
@@ -722,9 +728,10 @@ function renderComposeSeconds(
|
|
|
722
728
|
// clock-time rest would otherwise cross-multiply the hours.
|
|
723
729
|
if ((plan.rest.kind === 'clockTimes' ||
|
|
724
730
|
plan.rest.kind === 'compactClockTimes') &&
|
|
725
|
-
|
|
726
|
-
const minute = +
|
|
727
|
-
const cadence = hourCadence(
|
|
731
|
+
schedule.shapes.minute === 'single') {
|
|
732
|
+
const minute = +schedule.pattern.minute;
|
|
733
|
+
const cadence = hourCadence(schedule, minute) ??
|
|
734
|
+
hourRangeCadence(schedule, minute);
|
|
728
735
|
|
|
729
736
|
if (cadence !== null) {
|
|
730
737
|
return cadence;
|
|
@@ -736,15 +743,15 @@ function renderComposeSeconds(
|
|
|
736
743
|
// the one-minute confinement — 60 fires in :00, not 3,600 across the hour).
|
|
737
744
|
// Bind the seconds into the explicit clock minute in the genitive ("der
|
|
738
745
|
// Minute 9:00"); the recurring "täglich"/day frame is added in `describe`.
|
|
739
|
-
if (composeMinuteZero(
|
|
740
|
-
return secondsLead(
|
|
746
|
+
if (composeMinuteZero(schedule, plan)) {
|
|
747
|
+
return secondsLead(schedule) + ' ' +
|
|
741
748
|
clockMinuteGenitive(plan.rest.times, opts.style.sep);
|
|
742
749
|
}
|
|
743
750
|
|
|
744
751
|
// A wildcard second under a minute */2 with a wildcard hour binds in the
|
|
745
752
|
// genitive ("jede Sekunde jeder zweiten Minute").
|
|
746
|
-
if (isEveryOtherMinuteSeconds(
|
|
747
|
-
return secondsLead(
|
|
753
|
+
if (isEveryOtherMinuteSeconds(schedule, plan)) {
|
|
754
|
+
return secondsLead(schedule) + ' jeder zweiten Minute';
|
|
748
755
|
}
|
|
749
756
|
|
|
750
757
|
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
@@ -752,17 +759,17 @@ function renderComposeSeconds(
|
|
|
752
759
|
// double it. A wildcard or stepped second is not folded there (no
|
|
753
760
|
// clockSecond), so it still leads its own clause here.
|
|
754
761
|
const restOwnsLead = plan.rest.kind === 'compactClockTimes' &&
|
|
755
|
-
|
|
756
|
-
const lead = restOwnsLead ? '' : secondsLead(
|
|
762
|
+
schedule.analyses.clockSecond;
|
|
763
|
+
const lead = restOwnsLead ? '' : secondsLead(schedule) + ', ';
|
|
757
764
|
|
|
758
|
-
return lead + render(
|
|
765
|
+
return lead + render(schedule, plan.rest, opts);
|
|
759
766
|
}
|
|
760
767
|
|
|
761
768
|
// True when a compose-seconds plan is a sub-minute second over a minute-0
|
|
762
769
|
// clock-time rest — the case that reads as the bare hour and so must surface
|
|
763
770
|
// the pinned clock minute.
|
|
764
771
|
function composeMinuteZero(
|
|
765
|
-
|
|
772
|
+
schedule: Schedule,
|
|
766
773
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
767
774
|
): plan is Extract<PlanNode, {kind: 'composeSeconds'}> &
|
|
768
775
|
{rest: Extract<PlanNode, {kind: 'clockTimes'}>} {
|
|
@@ -789,25 +796,25 @@ function clockMinuteGenitive(
|
|
|
789
796
|
// A minute clause across discrete hours: "in den Minuten 0 bis 30, um 9 und
|
|
790
797
|
// 17 Uhr".
|
|
791
798
|
function renderMinutesAcrossHours(
|
|
792
|
-
|
|
799
|
+
schedule: Schedule,
|
|
793
800
|
plan: Extract<PlanNode, {kind: 'minutesAcrossHours'}>,
|
|
794
801
|
opts: Opts
|
|
795
802
|
): string {
|
|
796
803
|
const sep = opts.style.sep;
|
|
797
804
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence,
|
|
798
805
|
// not a wall of hour columns.
|
|
799
|
-
const cadence = unevenHourCadence(
|
|
806
|
+
const cadence = unevenHourCadence(schedule);
|
|
800
807
|
|
|
801
808
|
// The wildcard form means every minute *during* each hour: render windows.
|
|
802
809
|
if (plan.form === 'wildcard') {
|
|
803
810
|
return cadence ?
|
|
804
811
|
'jede Minute, ' + cadence :
|
|
805
|
-
'jede Minute ' + duringHours(
|
|
812
|
+
'jede Minute ' + duringHours(schedule, plan.times, sep);
|
|
806
813
|
}
|
|
807
814
|
|
|
808
815
|
const minuteLead =
|
|
809
|
-
strideFromSegments(
|
|
810
|
-
countedPhrase(
|
|
816
|
+
strideFromSegments(segmentsOf(schedule, 'minute'), UNITS.minute, '') ??
|
|
817
|
+
countedPhrase(schedule, 'minute', 'Minute', 'Minuten');
|
|
811
818
|
|
|
812
819
|
if (cadence !== null) {
|
|
813
820
|
return minuteLead + ', ' + cadence;
|
|
@@ -815,7 +822,7 @@ function renderMinutesAcrossHours(
|
|
|
815
822
|
|
|
816
823
|
const hours = plan.times.kind === 'fires' ?
|
|
817
824
|
atHours(plan.times.fires) :
|
|
818
|
-
joinList(hourSegmentParts(
|
|
825
|
+
joinList(hourSegmentParts(schedule, 0, 0, sep));
|
|
819
826
|
|
|
820
827
|
return minuteLead + ', ' + hours;
|
|
821
828
|
}
|
|
@@ -825,31 +832,33 @@ function renderMinutesAcrossHours(
|
|
|
825
832
|
// Minute in jeder zweiten Stunde"); a range or list leads with its minutes and
|
|
826
833
|
// trails the same cadence ("in den Minuten 0 bis 30, in jeder zweiten Stunde").
|
|
827
834
|
function renderMinuteSpanAcrossHourStep(
|
|
828
|
-
|
|
835
|
+
schedule: Schedule,
|
|
829
836
|
plan: Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>
|
|
830
837
|
): string {
|
|
831
838
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence; an
|
|
832
839
|
// offset-clean stride keeps its "in jeder N-ten Stunde" confinement.
|
|
833
|
-
const cadence = unevenHourCadence(
|
|
840
|
+
const cadence = unevenHourCadence(schedule);
|
|
834
841
|
|
|
835
842
|
// A wildcard minute over a stepped hour is reached only for a clean stride (a
|
|
836
843
|
// bounded or uneven step routes through minutesAcrossHours instead).
|
|
837
844
|
if (plan.form === 'wildcard') {
|
|
838
845
|
return 'jede Minute ' +
|
|
839
|
-
everyNthHour(stepSegment(
|
|
846
|
+
everyNthHour(stepSegment(schedule, 'hour'));
|
|
840
847
|
}
|
|
841
848
|
|
|
842
849
|
// The minute (range or list) leads; the hour trails. A clean stride confines
|
|
843
850
|
// to "in jeder N-ten Stunde" — the same cadence the wildcard form and the
|
|
844
851
|
// minute-step compositions use, never a juxtaposed second frequency. A
|
|
845
852
|
// bounded or uneven stride trails its endpoint-pinning cadence instead.
|
|
846
|
-
const segment = stepSegment(
|
|
853
|
+
const segment = stepSegment(schedule, 'hour');
|
|
847
854
|
const hours = cadence ?? (confinedHourStride(segment) ?
|
|
848
855
|
everyNthHour(segment) :
|
|
849
856
|
atHours(segment.fires));
|
|
850
857
|
|
|
851
|
-
return (
|
|
852
|
-
|
|
858
|
+
return (
|
|
859
|
+
strideFromSegments(segmentsOf(schedule, 'minute'), UNITS.minute, '') ??
|
|
860
|
+
countedPhrase(schedule, 'minute', 'Minute', 'Minuten')
|
|
861
|
+
) + ', ' + hours;
|
|
853
862
|
}
|
|
854
863
|
|
|
855
864
|
// Compact minutes across discrete hours: "in den Minuten 5 und 10, um 9, 17,
|
|
@@ -858,7 +867,7 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
858
867
|
// or list is a daily enumeration of its times ("täglich um 0:05, 2:05, …"),
|
|
859
868
|
// never hourly.
|
|
860
869
|
function renderCompactClockTimes(
|
|
861
|
-
|
|
870
|
+
schedule: Schedule,
|
|
862
871
|
plan: Extract<PlanNode, {kind: 'compactClockTimes'}>,
|
|
863
872
|
opts: Opts
|
|
864
873
|
): string {
|
|
@@ -868,46 +877,48 @@ function renderCompactClockTimes(
|
|
|
868
877
|
// An hour step or range (or arithmetic-progression hour list) under the
|
|
869
878
|
// single pinned minute reads as a cadence or window, not a wall of clock
|
|
870
879
|
// times. (Returns null for an irregular list, which keeps folding below.)
|
|
871
|
-
const cadence = hourCadence(
|
|
872
|
-
hourRangeCadence(
|
|
880
|
+
const cadence = hourCadence(schedule, plan.minute) ??
|
|
881
|
+
hourRangeCadence(schedule, plan.minute);
|
|
873
882
|
|
|
874
883
|
if (cadence !== null) {
|
|
875
884
|
return cadence;
|
|
876
885
|
}
|
|
877
886
|
|
|
878
|
-
const hourly =
|
|
887
|
+
const hourly = segmentsOf(schedule, 'hour')
|
|
879
888
|
.some((segment) => segment.kind === 'range');
|
|
880
889
|
|
|
881
890
|
return (hourly ? 'stündlich ' : 'täglich ') +
|
|
882
|
-
joinList(hourSegmentParts(
|
|
891
|
+
joinList(hourSegmentParts(
|
|
892
|
+
schedule, plan.minute, schedule.analyses.clockSecond, sep
|
|
893
|
+
));
|
|
883
894
|
}
|
|
884
895
|
|
|
885
896
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence; else
|
|
886
897
|
// a range among the hours reads as a window, otherwise a flat hour list.
|
|
887
|
-
const hours = unevenHourCadence(
|
|
888
|
-
(
|
|
889
|
-
joinList(hourSegmentParts(
|
|
890
|
-
atHours(hourFires(
|
|
898
|
+
const hours = unevenHourCadence(schedule) ??
|
|
899
|
+
(segmentsOf(schedule, 'hour').some((segment) => segment.kind === 'range') ?
|
|
900
|
+
joinList(hourSegmentParts(schedule, 0, 0, sep)) :
|
|
901
|
+
atHours(hourFires(schedule)));
|
|
891
902
|
|
|
892
903
|
// A folded second has no single clock time to attach to here, so it leads
|
|
893
904
|
// as its own clause ("in Sekunde 30, ..."). It is the bare second (not
|
|
894
905
|
// secondsLead's "… jeder Minute") because the minutes are constrained.
|
|
895
|
-
const lead =
|
|
896
|
-
countedPhrase(
|
|
906
|
+
const lead = schedule.analyses.clockSecond ?
|
|
907
|
+
countedPhrase(schedule, 'second', 'Sekunde', 'Sekunden') + ', ' : '';
|
|
897
908
|
|
|
898
909
|
return lead +
|
|
899
|
-
(strideFromSegments(
|
|
900
|
-
countedPhrase(
|
|
910
|
+
(strideFromSegments(segmentsOf(schedule, 'minute'), UNITS.minute, '') ??
|
|
911
|
+
countedPhrase(schedule, 'minute', 'Minute', 'Minuten')) + ', ' + hours;
|
|
901
912
|
}
|
|
902
913
|
|
|
903
914
|
// A repeating minute step, optionally within an hour window: "alle 5
|
|
904
915
|
// Minuten", "alle 15 Minuten von 9 bis 17:45 Uhr".
|
|
905
916
|
function renderMinuteFrequency(
|
|
906
|
-
|
|
917
|
+
schedule: Schedule,
|
|
907
918
|
plan: Extract<PlanNode, {kind: 'minuteFrequency'}>,
|
|
908
919
|
opts: Opts
|
|
909
920
|
): string {
|
|
910
|
-
const segment = stepSegment(
|
|
921
|
+
const segment = stepSegment(schedule, 'minute');
|
|
911
922
|
const sep = opts.style.sep;
|
|
912
923
|
const clean = cleanStep(segment, 60);
|
|
913
924
|
|
|
@@ -930,18 +941,18 @@ function renderMinuteFrequency(
|
|
|
930
941
|
// A bounded or uneven hour stride confines the minute cadence to its own
|
|
931
942
|
// endpoint-pinning hour cadence ("alle 15 Minuten, alle 5 Stunden von 0 bis
|
|
932
943
|
// 20 Uhr").
|
|
933
|
-
const cadence = unevenHourCadence(
|
|
944
|
+
const cadence = unevenHourCadence(schedule);
|
|
934
945
|
|
|
935
946
|
return cadence ?
|
|
936
947
|
base + ', ' + cadence :
|
|
937
|
-
base + ' ' + duringHours(
|
|
948
|
+
base + ' ' + duringHours(schedule, plan.hours.times, sep);
|
|
938
949
|
}
|
|
939
950
|
|
|
940
951
|
if (plan.hours.kind === 'step') {
|
|
941
952
|
// The plan carries a step only for a clean step (dividing the day):
|
|
942
953
|
// confine the cadence to every Nth hour ("in jeder zweiten Stunde").
|
|
943
954
|
return base + ' ' +
|
|
944
|
-
everyNthHour(stepSegment(
|
|
955
|
+
everyNthHour(stepSegment(schedule, 'hour'));
|
|
945
956
|
}
|
|
946
957
|
|
|
947
958
|
return base;
|
|
@@ -954,14 +965,14 @@ function renderMinuteFrequency(
|
|
|
954
965
|
// bis 17 Uhr"). Shared by the bare hour step and the minute-step compositions.
|
|
955
966
|
// An explicitly bounded step (`a-b/n`) keeps its enumerated hours, matching
|
|
956
967
|
// en/fi/zh; only an OPEN step (`m/n`) reads as the wrapping cadence.
|
|
957
|
-
function hourStepPhrase(
|
|
958
|
-
const cadence = unevenHourCadence(
|
|
968
|
+
function hourStepPhrase(schedule: Schedule): string {
|
|
969
|
+
const cadence = unevenHourCadence(schedule);
|
|
959
970
|
|
|
960
971
|
if (cadence !== null) {
|
|
961
972
|
return cadence;
|
|
962
973
|
}
|
|
963
974
|
|
|
964
|
-
const segment = stepSegment(
|
|
975
|
+
const segment = stepSegment(schedule, 'hour');
|
|
965
976
|
|
|
966
977
|
if (cleanStep(segment, 24)) {
|
|
967
978
|
return everyN(segment.interval, UNITS.hour);
|
|
@@ -971,7 +982,7 @@ function hourStepPhrase(ir: IR): string {
|
|
|
971
982
|
// endpoint: name only its start, the cadence en/fi/zh and the compose paths
|
|
972
983
|
// already speak — never the enumerated hour list. A bounded `a-b/n` keeps its
|
|
973
984
|
// explicit hours.
|
|
974
|
-
const stride = openOffsetCleanStride(
|
|
985
|
+
const stride = openOffsetCleanStride(schedule, segment);
|
|
975
986
|
|
|
976
987
|
return stride ? hourStrideCadence(stride) : atHours(segment.fires);
|
|
977
988
|
}
|
|
@@ -982,13 +993,13 @@ function hourStepPhrase(ir: IR): string {
|
|
|
982
993
|
// (`a-b/n`, startToken carries a `-`) is excluded so it keeps its enumerated
|
|
983
994
|
// hours, matching en/fi/zh.
|
|
984
995
|
function openOffsetCleanStride(
|
|
985
|
-
|
|
996
|
+
schedule: Schedule, segment: StepSegment
|
|
986
997
|
): {start: number; interval: number; last: number} | null {
|
|
987
998
|
if (segment.startToken.indexOf('-') !== -1) {
|
|
988
999
|
return null;
|
|
989
1000
|
}
|
|
990
1001
|
|
|
991
|
-
const stride = hourStride(
|
|
1002
|
+
const stride = hourStride(schedule);
|
|
992
1003
|
|
|
993
1004
|
return stride && offsetCleanStride(stride) ? stride : null;
|
|
994
1005
|
}
|
|
@@ -1007,73 +1018,24 @@ function hourStrideCadence(
|
|
|
1007
1018
|
): string {
|
|
1008
1019
|
const {start, interval, last} = stride;
|
|
1009
1020
|
const cadence = everyN(interval, UNITS.hour);
|
|
1010
|
-
const tiles = 24 % interval === 0;
|
|
1011
|
-
|
|
1012
|
-
if (start === 0 && tiles) {
|
|
1013
|
-
return cadence;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
if (start < interval && tiles) {
|
|
1017
|
-
return cadence + ' ab ' + start + ' Uhr';
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
return cadence + ' von ' + start + ' bis ' + last + ' Uhr';
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// An hour list's arithmetic progression, or null when its values are not a step
|
|
1024
|
-
// the renderer should speak as a cadence. The core rewrites a uneven hour step
|
|
1025
|
-
// (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its literal
|
|
1026
|
-
// fire list, indistinguishable in the IR from a hand-written list; the renderer
|
|
1027
|
-
// recovers the cadence from the values. A progression starting at zero is a
|
|
1028
|
-
// `*/n` step however short (0,7,14,21 is `*/7`); a non-zero progression is only
|
|
1029
|
-
// a step when it is too long to be a deliberate clock-time list (9,17 is two
|
|
1030
|
-
// named times, not a cadence). Interval one is a plain range, never a step.
|
|
1031
|
-
function hourListStride(
|
|
1032
|
-
values: number[]
|
|
1033
|
-
): {start: number; interval: number; last: number} | null {
|
|
1034
|
-
if (values.length < 2) {
|
|
1035
|
-
return null;
|
|
1036
|
-
}
|
|
1037
1021
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
for (let i = 2; i < values.length; i += 1) {
|
|
1045
|
-
if (values[i] - values[i - 1] !== interval) {
|
|
1046
|
-
return null;
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
if (values[0] !== 0 && values.length < 5) {
|
|
1051
|
-
return null;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
return {interval, last: values[values.length - 1], start: values[0]};
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
// Whether an hour stride wraps the day cleanly from within its first interval
|
|
1058
|
-
// (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
|
|
1059
|
-
// stride has no distinct endpoint and keeps its bare or "ab" cadence. Every
|
|
1060
|
-
// other stride — a uneven interval, or one starting at or past its interval (a
|
|
1061
|
-
// bounded `a-b/n`) — is a bounded set the cadence pins both endpoints of.
|
|
1062
|
-
function offsetCleanStride(
|
|
1063
|
-
stride: {start: number; interval: number}
|
|
1064
|
-
): boolean {
|
|
1065
|
-
return stride.start < stride.interval && 24 % stride.interval === 0;
|
|
1022
|
+
return chooseStride({start, interval, cycle: 24}, {
|
|
1023
|
+
bare: () => cadence,
|
|
1024
|
+
offset: () => cadence + ' ab ' + start + ' Uhr',
|
|
1025
|
+
bounded: () => cadence + ' von ' + start + ' bis ' + last + ' Uhr'
|
|
1026
|
+
});
|
|
1066
1027
|
}
|
|
1067
1028
|
|
|
1068
1029
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1069
1030
|
// segment yields its {start, interval, last} directly; an all-single hour list
|
|
1070
1031
|
// yields one only when its values form a step progression (so an irregular list
|
|
1071
|
-
// like 9,17 keeps enumerating). The
|
|
1072
|
-
// the stride and speaks it as a cadence, not the clock-time
|
|
1032
|
+
// like 9,17 keeps enumerating). The Schedule is unchanged — the renderer
|
|
1033
|
+
// recognizes the stride and speaks it as a cadence, not the clock-time
|
|
1034
|
+
// cross-product.
|
|
1073
1035
|
function hourStride(
|
|
1074
|
-
|
|
1036
|
+
schedule: Schedule
|
|
1075
1037
|
): {start: number; interval: number; last: number} | null {
|
|
1076
|
-
const segments =
|
|
1038
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1077
1039
|
|
|
1078
1040
|
// A wildcard hour carries no segments (no discrete hours to stride over).
|
|
1079
1041
|
if (!segments) {
|
|
@@ -1109,8 +1071,8 @@ function hourStride(
|
|
|
1109
1071
|
// ("…, alle 5 Stunden von 0 bis 20 Uhr") than as a wall of clock times. An
|
|
1110
1072
|
// offset-clean stride keeps its existing confinement form, so only the
|
|
1111
1073
|
// endpoint-bearing case routes here.
|
|
1112
|
-
function unevenHourCadence(
|
|
1113
|
-
const stride = hourStride(
|
|
1074
|
+
function unevenHourCadence(schedule: Schedule): string | null {
|
|
1075
|
+
const stride = hourStride(schedule);
|
|
1114
1076
|
|
|
1115
1077
|
if (!stride || offsetCleanStride(stride)) {
|
|
1116
1078
|
return null;
|
|
@@ -1122,8 +1084,8 @@ function unevenHourCadence(ir: IR): string | null {
|
|
|
1122
1084
|
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
1123
1085
|
// fills the minute (a "für eine Minute" frame at minute 0); a single 0 is just
|
|
1124
1086
|
// the top of the minute (no clause); anything else needs its own clause.
|
|
1125
|
-
function subMinuteSecond(
|
|
1126
|
-
return
|
|
1087
|
+
function subMinuteSecond(schedule: Schedule): boolean {
|
|
1088
|
+
return schedule.pattern.second === '*' || schedule.shapes.second === 'step';
|
|
1127
1089
|
}
|
|
1128
1090
|
|
|
1129
1091
|
// The lead clause for an hour-cadence rendering: the second and the pinned
|
|
@@ -1133,24 +1095,26 @@ function subMinuteSecond(ir: IR): boolean {
|
|
|
1133
1095
|
// Minute" frame (the whole minute-0 window). A non-zero minute is a real clock
|
|
1134
1096
|
// minute: the second leads with its own clause (if any), then the minute reads
|
|
1135
1097
|
// "in Minute M".
|
|
1136
|
-
function hourCadenceLead(
|
|
1098
|
+
function hourCadenceLead(schedule: Schedule, minute: number): string {
|
|
1137
1099
|
if (minute === 0) {
|
|
1138
|
-
if (subMinuteSecond(
|
|
1139
|
-
return withAnchor(
|
|
1100
|
+
if (subMinuteSecond(schedule)) {
|
|
1101
|
+
return withAnchor(
|
|
1102
|
+
secondsClause(schedule, minuteAnchor(schedule)), 'für eine Minute'
|
|
1103
|
+
);
|
|
1140
1104
|
}
|
|
1141
1105
|
|
|
1142
|
-
return secondsClause(
|
|
1106
|
+
return secondsClause(schedule, 'jeder Stunde');
|
|
1143
1107
|
}
|
|
1144
1108
|
|
|
1145
1109
|
const minutePhrase = 'in Minute ' + minute;
|
|
1146
1110
|
|
|
1147
1111
|
// A single 0 second is just the top of the minute, so the minute leads
|
|
1148
1112
|
// alone; any other second prefixes its own clause.
|
|
1149
|
-
if (
|
|
1113
|
+
if (schedule.pattern.second === '0') {
|
|
1150
1114
|
return minutePhrase;
|
|
1151
1115
|
}
|
|
1152
1116
|
|
|
1153
|
-
return secondsClause(
|
|
1117
|
+
return secondsClause(schedule, minuteAnchor(schedule)) + ', ' + minutePhrase;
|
|
1154
1118
|
}
|
|
1155
1119
|
|
|
1156
1120
|
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
@@ -1162,9 +1126,9 @@ function hourCadenceLead(ir: IR, minute: number): string {
|
|
|
1162
1126
|
// clock time three digit-groups, so any stride is worth compacting; otherwise
|
|
1163
1127
|
// the stride must exceed the clock-time cap, the same point at which the core
|
|
1164
1128
|
// itself stops enumerating. The renderer returns the bare clause; the day
|
|
1165
|
-
// frame is composed in `describe`. Renderer-only; the
|
|
1166
|
-
function hourCadence(
|
|
1167
|
-
const stride = hourStride(
|
|
1129
|
+
// frame is composed in `describe`. Renderer-only; the Schedule is unchanged.
|
|
1130
|
+
function hourCadence(schedule: Schedule, minute: number): string | null {
|
|
1131
|
+
const stride = hourStride(schedule);
|
|
1168
1132
|
|
|
1169
1133
|
if (!stride) {
|
|
1170
1134
|
return null;
|
|
@@ -1177,7 +1141,7 @@ function hourCadence(ir: IR, minute: number): string | null {
|
|
|
1177
1141
|
// or "ab" form is no shorter than the list. A bounded or uneven stride has no
|
|
1178
1142
|
// clean wrap, so its endpoint-pinning cadence ("alle 5 Stunden von 0 bis 20
|
|
1179
1143
|
// Uhr") reads better however short.
|
|
1180
|
-
if (
|
|
1144
|
+
if (schedule.pattern.second === '0' && fires <= maxClockTimes &&
|
|
1181
1145
|
offsetCleanStride(stride)) {
|
|
1182
1146
|
return null;
|
|
1183
1147
|
}
|
|
@@ -1186,46 +1150,47 @@ function hourCadence(ir: IR, minute: number): string | null {
|
|
|
1186
1150
|
// stride is a confinement, not a juxtaposed cadence: it reads "für eine
|
|
1187
1151
|
// Minute in jeder zweiten Stunde", reusing the every-Nth-hour idiom so the
|
|
1188
1152
|
// minute-0 window is never heard as the bare hour cadence.
|
|
1189
|
-
const segment =
|
|
1190
|
-
const confined = minute === 0 && subMinuteSecond(
|
|
1191
|
-
|
|
1153
|
+
const segment = segmentsOf(schedule, 'hour')[0];
|
|
1154
|
+
const confined = minute === 0 && subMinuteSecond(schedule) &&
|
|
1155
|
+
segmentsOf(schedule, 'hour').length === 1 && segment.kind === 'step' &&
|
|
1192
1156
|
confinedHourStride(segment);
|
|
1193
1157
|
|
|
1194
1158
|
if (confined) {
|
|
1195
|
-
return withAnchor(
|
|
1196
|
-
|
|
1159
|
+
return withAnchor(
|
|
1160
|
+
secondsClause(schedule, minuteAnchor(schedule)), 'für eine Minute'
|
|
1161
|
+
) + ' ' + everyNthHour(segment);
|
|
1197
1162
|
}
|
|
1198
1163
|
|
|
1199
1164
|
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
1200
1165
|
// lead clause to fold in, so the bounded cadence stands on its own ("alle 5
|
|
1201
1166
|
// Stunden von 0 bis 20 Uhr").
|
|
1202
|
-
if (minute === 0 &&
|
|
1167
|
+
if (minute === 0 && schedule.pattern.second === '0') {
|
|
1203
1168
|
return hourStrideCadence(stride);
|
|
1204
1169
|
}
|
|
1205
1170
|
|
|
1206
|
-
return hourCadenceLead(
|
|
1171
|
+
return hourCadenceLead(schedule, minute) + ', ' + hourStrideCadence(stride);
|
|
1207
1172
|
}
|
|
1208
1173
|
|
|
1209
1174
|
// Whether an hour cadence or hour-range window applies to a plan with a single
|
|
1210
1175
|
// pinned minute — the signal that the clause is a cadence/window, not a daily
|
|
1211
1176
|
// clock-time list, so the "täglich" frame must not be added.
|
|
1212
|
-
function hourCadenceApplies(
|
|
1213
|
-
if (
|
|
1177
|
+
function hourCadenceApplies(schedule: Schedule): boolean {
|
|
1178
|
+
if (schedule.shapes.minute !== 'single') {
|
|
1214
1179
|
return false;
|
|
1215
1180
|
}
|
|
1216
1181
|
|
|
1217
|
-
const minute = +
|
|
1182
|
+
const minute = +schedule.pattern.minute;
|
|
1218
1183
|
|
|
1219
|
-
return hourCadence(
|
|
1220
|
-
hourRangeCadence(
|
|
1184
|
+
return hourCadence(schedule, minute) !== null ||
|
|
1185
|
+
hourRangeCadence(schedule, minute) !== null;
|
|
1221
1186
|
}
|
|
1222
1187
|
|
|
1223
1188
|
// Whether the hour field is a range — or a list whose segments include a
|
|
1224
1189
|
// range — and so forms a window rather than a cross-product of clock times.
|
|
1225
1190
|
// A pure single-value list (9,17) has no range to span and still enumerates;
|
|
1226
1191
|
// a step is handled by hourStride/hourCadence.
|
|
1227
|
-
function hasHourWindow(
|
|
1228
|
-
const segments =
|
|
1192
|
+
function hasHourWindow(schedule: Schedule): boolean {
|
|
1193
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1229
1194
|
|
|
1230
1195
|
return !!segments && segments.some(function range(segment) {
|
|
1231
1196
|
return segment.kind === 'range';
|
|
@@ -1240,13 +1205,15 @@ function hasHourWindow(ir: IR): boolean {
|
|
|
1240
1205
|
// suppressed by hourCadenceApplies). Returns null when the hour has no range,
|
|
1241
1206
|
// when the minute is non-zero (a real clock minute the existing window form
|
|
1242
1207
|
// already speaks), or when a plain :00 set carries no clause. Renderer-only;
|
|
1243
|
-
// the
|
|
1244
|
-
function hourRangeCadence(
|
|
1245
|
-
if (minute !== 0 || !hasHourWindow(
|
|
1208
|
+
// the Schedule is unchanged.
|
|
1209
|
+
function hourRangeCadence(schedule: Schedule, minute: number): string | null {
|
|
1210
|
+
if (minute !== 0 || !hasHourWindow(schedule) ||
|
|
1211
|
+
schedule.pattern.second === '0') {
|
|
1246
1212
|
return null;
|
|
1247
1213
|
}
|
|
1248
1214
|
|
|
1249
|
-
return hourCadenceLead(
|
|
1215
|
+
return hourCadenceLead(schedule, minute) + ', ' +
|
|
1216
|
+
hourRangeWindowTail(schedule);
|
|
1250
1217
|
}
|
|
1251
1218
|
|
|
1252
1219
|
// The hour-range window as a cadence tail at the top of each hour: each range
|
|
@@ -1254,15 +1221,15 @@ function hourRangeCadence(ir: IR, minute: number): string | null {
|
|
|
1254
1221
|
// — the same parts the bare "stündlich von 9 bis 17 Uhr" window forms, minus
|
|
1255
1222
|
// the "stündlich" prefix the lead replaces. The minute has folded into the
|
|
1256
1223
|
// lead, so the parts close on the top of their final hour.
|
|
1257
|
-
function hourRangeWindowTail(
|
|
1224
|
+
function hourRangeWindowTail(schedule: Schedule): string {
|
|
1258
1225
|
// Minute 0 with a falsy second renders each part as a bare hour ("von 9 bis
|
|
1259
1226
|
// 17 Uhr", "um 22 Uhr"); the separator is unused in that path.
|
|
1260
|
-
return joinList(hourSegmentParts(
|
|
1227
|
+
return joinList(hourSegmentParts(schedule, 0, 0, ':'));
|
|
1261
1228
|
}
|
|
1262
1229
|
|
|
1263
1230
|
// An hourly window: "stündlich von 9 bis 17 Uhr", or every minute across it.
|
|
1264
1231
|
function renderHourRange(
|
|
1265
|
-
|
|
1232
|
+
schedule: Schedule,
|
|
1266
1233
|
plan: Extract<PlanNode, {kind: 'hourRange'}>,
|
|
1267
1234
|
opts: Opts
|
|
1268
1235
|
): string {
|
|
@@ -1279,7 +1246,7 @@ function renderHourRange(
|
|
|
1279
1246
|
return 'jede Minute ' + window;
|
|
1280
1247
|
}
|
|
1281
1248
|
|
|
1282
|
-
if (plan.minuteForm === 'lead' &&
|
|
1249
|
+
if (plan.minuteForm === 'lead' && schedule.pattern.minute === '0') {
|
|
1283
1250
|
return 'stündlich ' + window;
|
|
1284
1251
|
}
|
|
1285
1252
|
|
|
@@ -1288,24 +1255,25 @@ function renderHourRange(
|
|
|
1288
1255
|
// bounded cadence ("alle 2 Minuten von Minute 3 bis 59 jeder Stunde") instead
|
|
1289
1256
|
// of the wall of fires; an irregular list or a single minute keeps the
|
|
1290
1257
|
// counted form.
|
|
1291
|
-
return (strideFromSegments(
|
|
1258
|
+
return (strideFromSegments(segmentsOf(schedule, 'minute'), UNITS.minute,
|
|
1292
1259
|
'jeder Stunde') ??
|
|
1293
|
-
countedPhrase(
|
|
1260
|
+
countedPhrase(schedule, 'minute', 'Minute', 'Minuten') + ' jeder Stunde') +
|
|
1294
1261
|
', ' + window;
|
|
1295
1262
|
}
|
|
1296
1263
|
|
|
1297
1264
|
// One or more clock times: "um 9 Uhr", "um 14:30 Uhr", "um 9 und 17 Uhr".
|
|
1298
1265
|
function renderClockTimes(
|
|
1299
|
-
|
|
1266
|
+
schedule: Schedule,
|
|
1300
1267
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
1301
1268
|
opts: Opts
|
|
1302
1269
|
): string {
|
|
1303
1270
|
// An hour step or range (or arithmetic-progression hour list) under a single
|
|
1304
1271
|
// pinned minute reads as a cadence or window rather than a cross-product of
|
|
1305
1272
|
// clock times.
|
|
1306
|
-
if (
|
|
1307
|
-
const minute = +
|
|
1308
|
-
const cadence = hourCadence(
|
|
1273
|
+
if (schedule.shapes.minute === 'single') {
|
|
1274
|
+
const minute = +schedule.pattern.minute;
|
|
1275
|
+
const cadence = hourCadence(schedule, minute) ??
|
|
1276
|
+
hourRangeCadence(schedule, minute);
|
|
1309
1277
|
|
|
1310
1278
|
if (cadence !== null) {
|
|
1311
1279
|
return cadence;
|
|
@@ -1336,33 +1304,100 @@ const renderers = {
|
|
|
1336
1304
|
standaloneSeconds: renderSeconds
|
|
1337
1305
|
};
|
|
1338
1306
|
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1341
|
-
|
|
1342
|
-
|
|
1307
|
+
// True when both the day-of-month and the weekday are restricted: cron fires on
|
|
1308
|
+
// the UNION of the two sets ("am 1. oder sonntags"). The month, if any, scopes
|
|
1309
|
+
// the WHOLE union and so leads the description (see `dayUnionMonthLead`) rather
|
|
1310
|
+
// than trailing one half, where it would read as scoping only that half.
|
|
1311
|
+
function isDayUnion(schedule: Schedule): boolean {
|
|
1312
|
+
return schedule.pattern.date !== '*' && schedule.pattern.weekday !== '*';
|
|
1313
|
+
}
|
|
1343
1314
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1315
|
+
// The leading "im Januar " scope for a day union (empty when the month is a
|
|
1316
|
+
// wildcard). The month brackets both or-branches, so it precedes the whole
|
|
1317
|
+
// description; the union clause itself then carries no trailing month.
|
|
1318
|
+
function dayUnionMonthLead(schedule: Schedule, months: Months): string {
|
|
1319
|
+
return schedule.pattern.month === '*' ?
|
|
1320
|
+
'' :
|
|
1321
|
+
monthClause(schedule, months) + ' ';
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// The day-of-month half of a union as a predicate. A Quartz date is its
|
|
1325
|
+
// definite phrase; an open `*/2`-style step is the parity class ("an jedem
|
|
1326
|
+
// ungeraden Tag des Monats"), never a 16-date enumeration that would bury the
|
|
1327
|
+
// union; otherwise the plain date clause ("am 1.", "vom 1. bis zum 15.").
|
|
1328
|
+
function dayUnionDate(schedule: Schedule): string {
|
|
1329
|
+
return quartzDate(schedule.pattern.date) ||
|
|
1330
|
+
oddEvenDay(schedule.pattern.date) ||
|
|
1331
|
+
dateClauseBare(schedule);
|
|
1332
|
+
}
|
|
1349
1333
|
|
|
1350
|
-
|
|
1334
|
+
// The day-of-week half of a union as a predicate. A Quartz weekday is its
|
|
1335
|
+
// definite phrase; the Monday-through-Friday range reads as the weekday class
|
|
1336
|
+
// ("an einem Wochentag (Mo–Fr)"), parallel to the date predicate beside it;
|
|
1337
|
+
// otherwise the adverbial weekday list ("freitags", "montags und mittwochs").
|
|
1338
|
+
function dayUnionWeekday(schedule: Schedule): string {
|
|
1339
|
+
const weekday = schedule.pattern.weekday;
|
|
1340
|
+
const quartz = quartzWeekday(weekday);
|
|
1341
|
+
|
|
1342
|
+
if (quartz) {
|
|
1343
|
+
return quartz;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const segments = segmentsOf(schedule, 'weekday');
|
|
1347
|
+
|
|
1348
|
+
if (segments.length === 1 && segments[0].kind === 'range' &&
|
|
1349
|
+
segments[0].bounds[0] === '1' && segments[0].bounds[1] === '5') {
|
|
1350
|
+
return 'an einem Wochentag (Mo–Fr)';
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
return weekdayQualifier(schedule);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// An open day-of-month step (`*/n`/`a/n`) as a cadence, not its 16-date
|
|
1357
|
+
// enumeration. Interval 2 reads as the parity-neutral cadence ("jeden zweiten
|
|
1358
|
+
// Tag des Monats") in the standalone case (the OR-union prefers the parity
|
|
1359
|
+
// idiom); other open steps fall back to the enumerated date clause. Null when
|
|
1360
|
+
// the date is not an open step.
|
|
1361
|
+
function dateStepCadence(schedule: Schedule): string | null {
|
|
1362
|
+
const date = schedule.pattern.date;
|
|
1363
|
+
|
|
1364
|
+
if (!isOpenStep(date)) {
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const [start, step] = date.split('/');
|
|
1369
|
+
|
|
1370
|
+
return (start === '*' || start === '1') && +step === 2 ?
|
|
1371
|
+
'jeden zweiten Tag des Monats' :
|
|
1372
|
+
null;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// The weekday/day/month frame. Date and weekday together are cron's OR case.
|
|
1376
|
+
function qualifier(schedule: Schedule, months: Months): string {
|
|
1377
|
+
const {date, month, weekday} = schedule.pattern;
|
|
1378
|
+
|
|
1379
|
+
// Date and weekday together are cron's OR: "am 31. oder freitags". Either
|
|
1380
|
+
// side may itself be a Quartz or parity form. The month leads the whole
|
|
1381
|
+
// union (handled in `describe`), so the union clause carries none here.
|
|
1382
|
+
if (isDayUnion(schedule)) {
|
|
1383
|
+
return dayUnionDate(schedule) + ' oder ' + dayUnionWeekday(schedule);
|
|
1351
1384
|
}
|
|
1352
1385
|
|
|
1353
1386
|
if (weekday !== '*') {
|
|
1354
|
-
return (quartzWeekday(weekday) || weekdayQualifier(
|
|
1355
|
-
monthScope(
|
|
1387
|
+
return (quartzWeekday(weekday) || weekdayQualifier(schedule)) +
|
|
1388
|
+
monthScope(schedule, months);
|
|
1356
1389
|
}
|
|
1357
1390
|
|
|
1358
1391
|
if (date !== '*') {
|
|
1359
|
-
const quartz = quartzDate(date);
|
|
1392
|
+
const quartz = quartzDate(date) || dateStepCadence(schedule);
|
|
1360
1393
|
|
|
1361
|
-
return quartz ?
|
|
1394
|
+
return quartz ?
|
|
1395
|
+
quartz + monthScope(schedule, months) :
|
|
1396
|
+
datePhrase(schedule, months);
|
|
1362
1397
|
}
|
|
1363
1398
|
|
|
1364
1399
|
if (month !== '*') {
|
|
1365
|
-
return monthClause(
|
|
1400
|
+
return monthClause(schedule, months);
|
|
1366
1401
|
}
|
|
1367
1402
|
|
|
1368
1403
|
return '';
|
|
@@ -1376,15 +1411,15 @@ const LEADING_PLANS = new Set(['clockTimes']);
|
|
|
1376
1411
|
|
|
1377
1412
|
// True when the leading qualifier should precede the clause: a clock-time
|
|
1378
1413
|
// plan, or the minute-0 compose-seconds clause that surfaces a clock minute.
|
|
1379
|
-
function leadsQualifier(
|
|
1380
|
-
return LEADING_PLANS.has(
|
|
1414
|
+
function leadsQualifier(schedule: Schedule): boolean {
|
|
1415
|
+
return LEADING_PLANS.has(schedule.plan.kind) || isComposeMinuteZero(schedule);
|
|
1381
1416
|
}
|
|
1382
1417
|
|
|
1383
1418
|
// Whether the planned clause is the minute-0 compose-seconds confinement
|
|
1384
1419
|
// (a sub-minute second over a minute-0 clock-time rest).
|
|
1385
|
-
function isComposeMinuteZero(
|
|
1386
|
-
return
|
|
1387
|
-
composeMinuteZero(
|
|
1420
|
+
function isComposeMinuteZero(schedule: Schedule): boolean {
|
|
1421
|
+
return schedule.plan.kind === 'composeSeconds' &&
|
|
1422
|
+
composeMinuteZero(schedule, schedule.plan);
|
|
1388
1423
|
}
|
|
1389
1424
|
|
|
1390
1425
|
// True when the clause is a bare daily clock-time list and so needs the
|
|
@@ -1392,19 +1427,19 @@ function isComposeMinuteZero(ir: IR): boolean {
|
|
|
1392
1427
|
// minute-0 compose-seconds clause (a recurring clock minute), and an uneven
|
|
1393
1428
|
// hour step (rendered as its fire list "um 0, 5, … Uhr", not the cadence "alle
|
|
1394
1429
|
// N Stunden"). A frequency clause already implies recurrence.
|
|
1395
|
-
function needsDailyFrame(
|
|
1430
|
+
function needsDailyFrame(schedule: Schedule): boolean {
|
|
1396
1431
|
// An hour cadence is a sub-daily frequency, not a daily clock-time list, so
|
|
1397
1432
|
// it must not take the "täglich" frame ("alle 2 Stunden", not "täglich alle
|
|
1398
1433
|
// 2 Stunden").
|
|
1399
|
-
if (hourCadenceApplies(
|
|
1434
|
+
if (hourCadenceApplies(schedule)) {
|
|
1400
1435
|
return false;
|
|
1401
1436
|
}
|
|
1402
1437
|
|
|
1403
|
-
if (
|
|
1438
|
+
if (schedule.plan.kind === 'clockTimes' || isComposeMinuteZero(schedule)) {
|
|
1404
1439
|
return true;
|
|
1405
1440
|
}
|
|
1406
1441
|
|
|
1407
|
-
if (
|
|
1442
|
+
if (schedule.plan.kind !== 'hourStep') {
|
|
1408
1443
|
return false;
|
|
1409
1444
|
}
|
|
1410
1445
|
|
|
@@ -1412,14 +1447,14 @@ function needsDailyFrame(ir: IR): boolean {
|
|
|
1412
1447
|
// frequency, not a daily clock-time list, so it takes no "täglich" frame —
|
|
1413
1448
|
// only a bounded `a-b/n` step that enumerates its hours ("um 1, 3, … Uhr")
|
|
1414
1449
|
// needs the recurring frame.
|
|
1415
|
-
const segment = stepSegment(
|
|
1450
|
+
const segment = stepSegment(schedule, 'hour');
|
|
1416
1451
|
|
|
1417
|
-
return !cleanStep(segment, 24) && !openOffsetCleanStride(
|
|
1452
|
+
return !cleanStep(segment, 24) && !openOffsetCleanStride(schedule, segment);
|
|
1418
1453
|
}
|
|
1419
1454
|
|
|
1420
|
-
function render(
|
|
1455
|
+
function render(schedule: Schedule, plan: PlanNode, opts: Opts): string {
|
|
1421
1456
|
return (renderers[plan.kind as keyof typeof renderers] as Renderer)(
|
|
1422
|
-
|
|
1457
|
+
schedule, plan, opts);
|
|
1423
1458
|
}
|
|
1424
1459
|
|
|
1425
1460
|
function normalizeOptions(options?: Cronli5Options): Opts {
|
|
@@ -1438,8 +1473,8 @@ function normalizeOptions(options?: Cronli5Options): Opts {
|
|
|
1438
1473
|
|
|
1439
1474
|
// Append the year frame: "im Jahr 2026", "in den Jahren 2025 und 2027", "von
|
|
1440
1475
|
// 2025 bis 2027".
|
|
1441
|
-
function applyYear(description: string,
|
|
1442
|
-
const year =
|
|
1476
|
+
function applyYear(description: string, schedule: Schedule): string {
|
|
1477
|
+
const year = schedule.pattern.year;
|
|
1443
1478
|
|
|
1444
1479
|
if (year === '*') {
|
|
1445
1480
|
return description;
|
|
@@ -1458,21 +1493,27 @@ function applyYear(description: string, ir: IR): string {
|
|
|
1458
1493
|
return description + ' im Jahr ' + year;
|
|
1459
1494
|
}
|
|
1460
1495
|
|
|
1461
|
-
function describe(
|
|
1462
|
-
const core = render(
|
|
1463
|
-
const qual = qualifier(
|
|
1496
|
+
function describe(schedule: Schedule, opts: Opts): string {
|
|
1497
|
+
const core = render(schedule, schedule.plan, opts);
|
|
1498
|
+
const qual = qualifier(schedule, opts.style.months);
|
|
1464
1499
|
let base = core;
|
|
1465
1500
|
|
|
1466
1501
|
if (qual) {
|
|
1467
|
-
base = leadsQualifier(
|
|
1502
|
+
base = leadsQualifier(schedule) ?
|
|
1468
1503
|
qual + ' ' + core :
|
|
1469
1504
|
core + ' ' + qual;
|
|
1470
1505
|
}
|
|
1471
|
-
else if (needsDailyFrame(
|
|
1506
|
+
else if (needsDailyFrame(schedule)) {
|
|
1472
1507
|
base = 'täglich ' + core;
|
|
1473
1508
|
}
|
|
1474
1509
|
|
|
1475
|
-
|
|
1510
|
+
// A day union's month brackets both or-branches, so it leads the whole
|
|
1511
|
+
// description rather than trailing one half (the qualifier left it off).
|
|
1512
|
+
if (isDayUnion(schedule)) {
|
|
1513
|
+
base = dayUnionMonthLead(schedule, opts.style.months) + base;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
return applyYear(base, schedule);
|
|
1476
1517
|
}
|
|
1477
1518
|
|
|
1478
1519
|
const de: Language<GermanStyle> = {
|