cronli5 0.2.1 → 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 +65 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +471 -383
- package/dist/cronli5.js +471 -383
- package/dist/lang/de.cjs +286 -215
- package/dist/lang/de.js +286 -215
- package/dist/lang/en.cjs +413 -327
- package/dist/lang/en.js +413 -327
- package/dist/lang/es.cjs +303 -265
- package/dist/lang/es.js +303 -265
- package/dist/lang/fi.cjs +311 -266
- package/dist/lang/fi.js +311 -266
- package/dist/lang/zh.cjs +308 -236
- package/dist/lang/zh.js +308 -236
- package/package.json +1 -1
- package/src/core/analyze.ts +12 -12
- 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} +17 -18
- package/src/core/specs.ts +1 -1
- package/src/core/util.ts +3 -165
- package/src/core/validate.ts +1 -1
- package/src/core/weekday.ts +54 -0
- package/src/cronli5.ts +5 -5
- package/src/lang/de/index.ts +329 -219
- package/src/lang/en/dialects.ts +1 -1
- package/src/lang/en/index.ts +521 -372
- package/src/lang/es/index.ts +338 -286
- package/src/lang/es/notes.md +1 -1
- package/src/lang/fi/dialects.ts +1 -1
- package/src/lang/fi/index.ts +365 -299
- package/src/lang/fi/notes.md +23 -8
- package/src/lang/fi/status.json +1 -1
- package/src/lang/zh/index.ts +344 -237
- package/src/types.ts +6 -6
- package/types/core/analyze.d.ts +3 -3
- 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} +11 -16
- package/types/core/specs.d.ts +1 -1
- package/types/core/util.d.ts +1 -30
- 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,20 +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, hourListStride, offsetCleanStride,
|
|
8
|
-
segmentsOf, singleValues, stepSegment
|
|
9
|
-
} from '../../core/
|
|
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';
|
|
10
13
|
import type {Cronli5Options} from '../../types.js';
|
|
11
14
|
import type {
|
|
12
|
-
Field, HourTimesPlan,
|
|
13
|
-
} from '../../core/
|
|
15
|
+
Field, HourTimesPlan, Schedule, Language, NormalizedOptions, PlanNode, Segment
|
|
16
|
+
} from '../../core/schedule.js';
|
|
14
17
|
import {resolveDialect, type GermanStyle} from './dialects.js';
|
|
15
18
|
|
|
16
19
|
type Opts = NormalizedOptions<GermanStyle>;
|
|
17
|
-
type Renderer = (
|
|
20
|
+
type Renderer = (schedule: Schedule, plan: PlanNode, opts: Opts) => string;
|
|
18
21
|
type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
19
22
|
|
|
20
23
|
// A time unit: its singular and plural noun, and the gender-agreeing form of
|
|
@@ -82,22 +85,17 @@ function cleanStep(segment: StepSegment, cycle: number): boolean {
|
|
|
82
85
|
function renderStride(stride: Stride): string {
|
|
83
86
|
const {interval, start, last, cycle, unit, anchor} = stride;
|
|
84
87
|
const cadence = everyN(interval, unit);
|
|
85
|
-
const tiles = cycle % interval === 0;
|
|
86
|
-
|
|
87
|
-
if (start === 0 && tiles) {
|
|
88
|
-
return cadence;
|
|
89
|
-
}
|
|
90
88
|
|
|
91
89
|
// A context that supplies its own trailing scope passes an empty anchor, so
|
|
92
90
|
// the cadence keeps its endpoints but drops the "jeder Stunde" tail.
|
|
93
91
|
const tail = anchor ? ' ' + anchor : '';
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
});
|
|
101
99
|
}
|
|
102
100
|
|
|
103
101
|
// A step *shape* segment as its cadence ("alle 6 Minuten ab Minute 5 jeder
|
|
@@ -129,9 +127,9 @@ function stepClause(segment: StepSegment, unit: Unit, anchor: string): string {
|
|
|
129
127
|
|
|
130
128
|
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
131
129
|
// form an arithmetic progression long enough to beat the list (the core
|
|
132
|
-
// enumerates an offset/uneven step to this fire list; the
|
|
133
|
-
// the renderer recognizes the progression). Returns null for a
|
|
134
|
-
// 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.
|
|
135
133
|
function strideFromSegments(
|
|
136
134
|
segments: Segment[],
|
|
137
135
|
unit: Unit,
|
|
@@ -192,10 +190,10 @@ function weekdayRange(bounds: [string, string]): string {
|
|
|
192
190
|
}
|
|
193
191
|
|
|
194
192
|
// "montags", "montags bis freitags", "montags, mittwochs und freitags".
|
|
195
|
-
function weekdayQualifier(
|
|
193
|
+
function weekdayQualifier(schedule: Schedule): string {
|
|
196
194
|
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
197
|
-
// form. The
|
|
198
|
-
const segments = orderWeekdaysForDisplay(segmentsOf(
|
|
195
|
+
// form. The Schedule stays canonical (Sunday=0). The helper flattens steps.
|
|
196
|
+
const segments = orderWeekdaysForDisplay(segmentsOf(schedule, 'weekday'));
|
|
199
197
|
|
|
200
198
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
201
199
|
return weekdayRange(segments[0].bounds);
|
|
@@ -289,6 +287,29 @@ function quartzDate(field: string): string | null {
|
|
|
289
287
|
return null;
|
|
290
288
|
}
|
|
291
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
|
+
|
|
292
313
|
type Months = GermanStyle['months'];
|
|
293
314
|
|
|
294
315
|
// The month names are dialect-scoped (resolved from `opts.style.months`);
|
|
@@ -304,8 +325,8 @@ function monthRange(bounds: [string, string], months: Months): string {
|
|
|
304
325
|
}
|
|
305
326
|
|
|
306
327
|
// Bare month names: "Januar", "Januar und Juli", "von Juni bis August".
|
|
307
|
-
function monthNamesList(
|
|
308
|
-
return joinList(flattenSteps(segmentsOf(
|
|
328
|
+
function monthNamesList(schedule: Schedule, months: Months): string {
|
|
329
|
+
return joinList(flattenSteps(segmentsOf(schedule, 'month'))
|
|
309
330
|
.map(function name(segment): string {
|
|
310
331
|
return segment.kind === 'range' ?
|
|
311
332
|
monthRange(segment.bounds, months) :
|
|
@@ -315,19 +336,21 @@ function monthNamesList(ir: IR, months: Months): string {
|
|
|
315
336
|
|
|
316
337
|
// The month qualifier: "im Januar", "im Januar und Juli", "von Juni bis
|
|
317
338
|
// August". A lone range carries its own "von … bis"; names take "im".
|
|
318
|
-
function monthClause(
|
|
319
|
-
const segments = flattenSteps(segmentsOf(
|
|
339
|
+
function monthClause(schedule: Schedule, months: Months): string {
|
|
340
|
+
const segments = flattenSteps(segmentsOf(schedule, 'month'));
|
|
320
341
|
|
|
321
342
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
322
343
|
return monthRange(segments[0].bounds, months);
|
|
323
344
|
}
|
|
324
345
|
|
|
325
|
-
return 'im ' + monthNamesList(
|
|
346
|
+
return 'im ' + monthNamesList(schedule, months);
|
|
326
347
|
}
|
|
327
348
|
|
|
328
349
|
// The month appended after a weekday: " im Januar" or "".
|
|
329
|
-
function monthScope(
|
|
330
|
-
return
|
|
350
|
+
function monthScope(schedule: Schedule, months: Months): string {
|
|
351
|
+
return schedule.pattern.month === '*' ?
|
|
352
|
+
'' :
|
|
353
|
+
' ' + monthClause(schedule, months);
|
|
331
354
|
}
|
|
332
355
|
|
|
333
356
|
// A day-of-month ordinal: a numeral with a period ("1.").
|
|
@@ -342,8 +365,8 @@ function dateRange(bounds: [string, string]): string {
|
|
|
342
365
|
|
|
343
366
|
// The bare date clause, without a month: "am 1.", "am 1. und 15.", "vom 1.
|
|
344
367
|
// bis zum 5.", "vom 1. bis zum 5. und am 10.".
|
|
345
|
-
function dateClauseBare(
|
|
346
|
-
const segments = flattenSteps(segmentsOf(
|
|
368
|
+
function dateClauseBare(schedule: Schedule): string {
|
|
369
|
+
const segments = flattenSteps(segmentsOf(schedule, 'date'));
|
|
347
370
|
|
|
348
371
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
349
372
|
return dateRange(segments[0].bounds);
|
|
@@ -368,19 +391,19 @@ function dateClauseBare(ir: IR): string {
|
|
|
368
391
|
// The date qualifier with its month. Month names fold bare onto the date
|
|
369
392
|
// ("am 1. Januar", "am 1. Januar und Juli"); a month range cannot, so it
|
|
370
393
|
// trails as a scoped clause after a comma ("am 1., von Juni bis August").
|
|
371
|
-
function datePhrase(
|
|
372
|
-
const clause = dateClauseBare(
|
|
394
|
+
function datePhrase(schedule: Schedule, months: Months): string {
|
|
395
|
+
const clause = dateClauseBare(schedule);
|
|
373
396
|
|
|
374
|
-
if (
|
|
397
|
+
if (schedule.pattern.month === '*') {
|
|
375
398
|
return clause;
|
|
376
399
|
}
|
|
377
400
|
|
|
378
|
-
const monthRanged = flattenSteps(segmentsOf(
|
|
401
|
+
const monthRanged = flattenSteps(segmentsOf(schedule, 'month'))
|
|
379
402
|
.some((segment) => segment.kind === 'range');
|
|
380
403
|
|
|
381
404
|
return monthRanged ?
|
|
382
|
-
clause + ', ' + monthClause(
|
|
383
|
-
clause + ' ' + monthNamesList(
|
|
405
|
+
clause + ', ' + monthClause(schedule, months) :
|
|
406
|
+
clause + ' ' + monthNamesList(schedule, months);
|
|
384
407
|
}
|
|
385
408
|
|
|
386
409
|
// A bare clock time: "9" on the hour, "14:30", or "0:00:30" with a second.
|
|
@@ -425,8 +448,8 @@ function hourWindow(
|
|
|
425
448
|
}
|
|
426
449
|
|
|
427
450
|
// A field's values as strings, a range rendered "a bis b".
|
|
428
|
-
function fieldValues(
|
|
429
|
-
return flattenSteps(segmentsOf(
|
|
451
|
+
function fieldValues(schedule: Schedule, field: Field): string[] {
|
|
452
|
+
return flattenSteps(segmentsOf(schedule, field)).map(function value(segment) {
|
|
430
453
|
return segment.kind === 'range' ?
|
|
431
454
|
segment.bounds[0] + ' bis ' + segment.bounds[1] :
|
|
432
455
|
String(segment.value);
|
|
@@ -435,16 +458,16 @@ function fieldValues(ir: IR, field: Field): string[] {
|
|
|
435
458
|
|
|
436
459
|
// "in Minute 5", "in den Minuten 5, 10 und 30", "in den Minuten 0 bis 30".
|
|
437
460
|
function countedPhrase(
|
|
438
|
-
|
|
461
|
+
schedule: Schedule,
|
|
439
462
|
field: Field,
|
|
440
463
|
singular: string,
|
|
441
464
|
plural: string
|
|
442
465
|
): string {
|
|
443
|
-
if (
|
|
444
|
-
return 'in ' + singular + ' ' +
|
|
466
|
+
if (schedule.shapes[field] === 'single') {
|
|
467
|
+
return 'in ' + singular + ' ' + schedule.pattern[field];
|
|
445
468
|
}
|
|
446
469
|
|
|
447
|
-
return 'in den ' + plural + ' ' + joinList(fieldValues(
|
|
470
|
+
return 'in den ' + plural + ' ' + joinList(fieldValues(schedule, field));
|
|
448
471
|
}
|
|
449
472
|
|
|
450
473
|
// The minute scope for a seconds clause: "jeder Minute" only when the minute
|
|
@@ -453,15 +476,15 @@ function countedPhrase(
|
|
|
453
476
|
// clause drops the scope — "jeder Minute" would otherwise contradict the fixed
|
|
454
477
|
// minute ("in Sekunde 30 jeder Minute, in Minute 30" fires at second 30 of
|
|
455
478
|
// minute 30, not every minute).
|
|
456
|
-
function minuteAnchor(
|
|
457
|
-
return
|
|
479
|
+
function minuteAnchor(schedule: Schedule): string {
|
|
480
|
+
return schedule.pattern.minute === '*' ? 'jeder Minute' : '';
|
|
458
481
|
}
|
|
459
482
|
|
|
460
483
|
// The seconds clause: "alle 30 Sekunden" for a step, "in Sekunde 15 jeder
|
|
461
484
|
// Minute" under a wildcard minute, else the bare "in Sekunde 15" when the
|
|
462
485
|
// minute is fixed (its own clause names it).
|
|
463
|
-
function secondsLead(
|
|
464
|
-
return secondsClause(
|
|
486
|
+
function secondsLead(schedule: Schedule): string {
|
|
487
|
+
return secondsClause(schedule, minuteAnchor(schedule));
|
|
465
488
|
}
|
|
466
489
|
|
|
467
490
|
// The second clause counted against an arbitrary anchor. The anchor is "jeder
|
|
@@ -469,22 +492,24 @@ function secondsLead(ir: IR): string {
|
|
|
469
492
|
// minute 0 into the hour and counts the second "jeder Stunde" instead ("in
|
|
470
493
|
// Sekunde 30 jeder Stunde"), so the minute-0 confinement is stated, not
|
|
471
494
|
// dropped.
|
|
472
|
-
function secondsClause(
|
|
473
|
-
if (
|
|
495
|
+
function secondsClause(schedule: Schedule, anchor: string): string {
|
|
496
|
+
if (schedule.pattern.second === '*') {
|
|
474
497
|
return 'jede Sekunde';
|
|
475
498
|
}
|
|
476
499
|
|
|
477
|
-
const segments =
|
|
500
|
+
const segments = schedule.analyses.segments.second;
|
|
478
501
|
|
|
479
502
|
// A step shape speaks its cadence directly; an offset/uneven step the core
|
|
480
503
|
// enumerated to a list is recognized as a progression. Both fall back to the
|
|
481
504
|
// counted list (a short or irregular set).
|
|
482
|
-
if (
|
|
483
|
-
return stepClause(stepSegment(
|
|
505
|
+
if (schedule.shapes.second === 'step') {
|
|
506
|
+
return stepClause(stepSegment(schedule, 'second'), UNITS.second, anchor);
|
|
484
507
|
}
|
|
485
508
|
|
|
486
509
|
return strideFromSegments(segments as Segment[], UNITS.second, anchor) ??
|
|
487
|
-
withAnchor(
|
|
510
|
+
withAnchor(
|
|
511
|
+
countedPhrase(schedule, 'second', 'Sekunde', 'Sekunden'), anchor
|
|
512
|
+
);
|
|
488
513
|
}
|
|
489
514
|
|
|
490
515
|
// A clock time that always shows its minutes: "9:00", "9:30".
|
|
@@ -498,8 +523,8 @@ function atHours(hours: number[]): string {
|
|
|
498
523
|
}
|
|
499
524
|
|
|
500
525
|
// The discrete hour fires, single and step values flattened: [9, 17, 19, …].
|
|
501
|
-
function hourFires(
|
|
502
|
-
return flattenSteps(segmentsOf(
|
|
526
|
+
function hourFires(schedule: Schedule): number[] {
|
|
527
|
+
return flattenSteps(segmentsOf(schedule, 'hour')).map(function fire(segment) {
|
|
503
528
|
return segment.kind === 'range' ? +segment.bounds[0] : +segment.value;
|
|
504
529
|
});
|
|
505
530
|
}
|
|
@@ -521,12 +546,12 @@ function partTime(
|
|
|
521
546
|
// The hour segments as parts: a range is a window, a single an "um H Uhr", a
|
|
522
547
|
// step its fires. `minute`/`second` attach to each.
|
|
523
548
|
function hourSegmentParts(
|
|
524
|
-
|
|
549
|
+
schedule: Schedule,
|
|
525
550
|
minute: number,
|
|
526
551
|
second: number | undefined,
|
|
527
552
|
sep: string
|
|
528
553
|
): string[] {
|
|
529
|
-
return segmentsOf(
|
|
554
|
+
return segmentsOf(schedule, 'hour').map(function part(segment): string {
|
|
530
555
|
if (segment.kind === 'range') {
|
|
531
556
|
return 'von ' + partTime(+segment.bounds[0], minute, second, sep) +
|
|
532
557
|
' bis ' + partTime(+segment.bounds[1], minute, second, sep) + ' Uhr';
|
|
@@ -544,14 +569,16 @@ function hourSegmentParts(
|
|
|
544
569
|
|
|
545
570
|
// Each "during" hour as a full window (H:00–H:59); a range spans one window,
|
|
546
571
|
// a step its fires.
|
|
547
|
-
function duringWindows(
|
|
572
|
+
function duringWindows(
|
|
573
|
+
schedule: Schedule, times: HourTimesPlan, sep: string
|
|
574
|
+
): string[] {
|
|
548
575
|
if (times.kind === 'fires') {
|
|
549
576
|
return times.fires.map(function each(hour) {
|
|
550
577
|
return hourWindow(hour, hour, 59, sep);
|
|
551
578
|
});
|
|
552
579
|
}
|
|
553
580
|
|
|
554
|
-
return segmentsOf(
|
|
581
|
+
return segmentsOf(schedule, 'hour').flatMap(function part(segment): string[] {
|
|
555
582
|
if (segment.kind === 'range') {
|
|
556
583
|
return [hourWindow(+segment.bounds[0], +segment.bounds[1], 59, sep)];
|
|
557
584
|
}
|
|
@@ -569,8 +596,10 @@ function duringWindows(ir: IR, times: HourTimesPlan, sep: string): string[] {
|
|
|
569
596
|
// The "during" hours of a confined cadence: a few hours read as windows ("von
|
|
570
597
|
// 9 bis 9:59 Uhr und …"); many read better as a compact list ("in den Stunden
|
|
571
598
|
// von 9, 11, 13, 15 und 17 Uhr") instead of sprawling windows.
|
|
572
|
-
function duringHours(
|
|
573
|
-
|
|
599
|
+
function duringHours(
|
|
600
|
+
schedule: Schedule, times: HourTimesPlan, sep: string
|
|
601
|
+
): string {
|
|
602
|
+
const windows = duringWindows(schedule, times, sep);
|
|
574
603
|
|
|
575
604
|
if (windows.length <= 3 || times.kind !== 'fires') {
|
|
576
605
|
return joinList(windows);
|
|
@@ -601,35 +630,35 @@ function renderEveryHour(): string {
|
|
|
601
630
|
|
|
602
631
|
// The open-minute seconds clause: "alle 30 Sekunden", "in Sekunde 15 jeder
|
|
603
632
|
// Minute". Serves standaloneSeconds (step) and secondPastMinute (single).
|
|
604
|
-
function renderSeconds(
|
|
605
|
-
return secondsLead(
|
|
633
|
+
function renderSeconds(schedule: Schedule): string {
|
|
634
|
+
return secondsLead(schedule);
|
|
606
635
|
}
|
|
607
636
|
|
|
608
637
|
// The minute-past-the-hour clause: "in Minute 5 jeder Stunde", "in den
|
|
609
638
|
// Minuten 5, 10 und 30 jeder Stunde". An offset/uneven step the core
|
|
610
639
|
// enumerated to this list reads as a stride cadence when the fires form a
|
|
611
640
|
// long-enough progression ("alle 2 Minuten von Minute 3 bis 59 jeder Stunde").
|
|
612
|
-
function minutePastClause(
|
|
613
|
-
return strideFromSegments(segmentsOf(
|
|
641
|
+
function minutePastClause(schedule: Schedule): string {
|
|
642
|
+
return strideFromSegments(segmentsOf(schedule, 'minute'), UNITS.minute,
|
|
614
643
|
'jeder Stunde') ??
|
|
615
|
-
countedPhrase(
|
|
644
|
+
countedPhrase(schedule, 'minute', 'Minute', 'Minuten') + ' jeder Stunde';
|
|
616
645
|
}
|
|
617
646
|
|
|
618
|
-
function renderMinutePast(
|
|
619
|
-
return minutePastClause(
|
|
647
|
+
function renderMinutePast(schedule: Schedule): string {
|
|
648
|
+
return minutePastClause(schedule);
|
|
620
649
|
}
|
|
621
650
|
|
|
622
651
|
// A specific minute and second: "in Minute 0 und Sekunde 30 jeder Stunde".
|
|
623
652
|
function renderSecondsWithinMinute(
|
|
624
|
-
|
|
653
|
+
schedule: Schedule,
|
|
625
654
|
plan: Extract<PlanNode, {kind: 'secondsWithinMinute'}>
|
|
626
655
|
): string {
|
|
627
656
|
if (plan.singleSecond) {
|
|
628
|
-
return 'in Minute ' +
|
|
629
|
-
|
|
657
|
+
return 'in Minute ' + schedule.pattern.minute + ' und Sekunde ' +
|
|
658
|
+
schedule.pattern.second + ' jeder Stunde';
|
|
630
659
|
}
|
|
631
660
|
|
|
632
|
-
return secondsLead(
|
|
661
|
+
return secondsLead(schedule) + ', in Minute ' + schedule.pattern.minute +
|
|
633
662
|
' jeder Stunde';
|
|
634
663
|
}
|
|
635
664
|
|
|
@@ -652,11 +681,11 @@ function wholeHour(hour: number): string {
|
|
|
652
681
|
// Minute der 9-Uhr-Stunde") rather than a synthesized "von 9:00 bis 9:59"
|
|
653
682
|
// range the source never stated; a plain range is a real window and keeps it.
|
|
654
683
|
function renderMinuteSpanInHour(
|
|
655
|
-
|
|
684
|
+
schedule: Schedule,
|
|
656
685
|
plan: Extract<PlanNode, {kind: 'minuteSpanInHour'}>,
|
|
657
686
|
opts: Opts
|
|
658
687
|
): string {
|
|
659
|
-
if (
|
|
688
|
+
if (schedule.pattern.minute === '*') {
|
|
660
689
|
return 'jede Minute ' + wholeHour(plan.hour);
|
|
661
690
|
}
|
|
662
691
|
|
|
@@ -674,21 +703,22 @@ function renderMinuteSpanInHour(
|
|
|
674
703
|
// English. Other strides, a restricted hour, and an hour cadence keep the
|
|
675
704
|
// juxtaposed form.
|
|
676
705
|
function isEveryOtherMinuteSeconds(
|
|
677
|
-
|
|
706
|
+
schedule: Schedule,
|
|
678
707
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
679
708
|
): boolean {
|
|
680
709
|
if (plan.rest.kind !== 'minuteFrequency' ||
|
|
681
|
-
|
|
710
|
+
schedule.shapes.second !== 'wildcard' ||
|
|
711
|
+
schedule.shapes.hour !== 'wildcard') {
|
|
682
712
|
return false;
|
|
683
713
|
}
|
|
684
714
|
|
|
685
|
-
const minuteStep = stepSegment(
|
|
715
|
+
const minuteStep = stepSegment(schedule, 'minute');
|
|
686
716
|
|
|
687
717
|
return minuteStep.startToken === '*' && minuteStep.interval === 2;
|
|
688
718
|
}
|
|
689
719
|
|
|
690
720
|
function renderComposeSeconds(
|
|
691
|
-
|
|
721
|
+
schedule: Schedule,
|
|
692
722
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
693
723
|
opts: Opts
|
|
694
724
|
): string {
|
|
@@ -698,9 +728,10 @@ function renderComposeSeconds(
|
|
|
698
728
|
// clock-time rest would otherwise cross-multiply the hours.
|
|
699
729
|
if ((plan.rest.kind === 'clockTimes' ||
|
|
700
730
|
plan.rest.kind === 'compactClockTimes') &&
|
|
701
|
-
|
|
702
|
-
const minute = +
|
|
703
|
-
const cadence = hourCadence(
|
|
731
|
+
schedule.shapes.minute === 'single') {
|
|
732
|
+
const minute = +schedule.pattern.minute;
|
|
733
|
+
const cadence = hourCadence(schedule, minute) ??
|
|
734
|
+
hourRangeCadence(schedule, minute);
|
|
704
735
|
|
|
705
736
|
if (cadence !== null) {
|
|
706
737
|
return cadence;
|
|
@@ -712,15 +743,15 @@ function renderComposeSeconds(
|
|
|
712
743
|
// the one-minute confinement — 60 fires in :00, not 3,600 across the hour).
|
|
713
744
|
// Bind the seconds into the explicit clock minute in the genitive ("der
|
|
714
745
|
// Minute 9:00"); the recurring "täglich"/day frame is added in `describe`.
|
|
715
|
-
if (composeMinuteZero(
|
|
716
|
-
return secondsLead(
|
|
746
|
+
if (composeMinuteZero(schedule, plan)) {
|
|
747
|
+
return secondsLead(schedule) + ' ' +
|
|
717
748
|
clockMinuteGenitive(plan.rest.times, opts.style.sep);
|
|
718
749
|
}
|
|
719
750
|
|
|
720
751
|
// A wildcard second under a minute */2 with a wildcard hour binds in the
|
|
721
752
|
// genitive ("jede Sekunde jeder zweiten Minute").
|
|
722
|
-
if (isEveryOtherMinuteSeconds(
|
|
723
|
-
return secondsLead(
|
|
753
|
+
if (isEveryOtherMinuteSeconds(schedule, plan)) {
|
|
754
|
+
return secondsLead(schedule) + ' jeder zweiten Minute';
|
|
724
755
|
}
|
|
725
756
|
|
|
726
757
|
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
@@ -728,17 +759,17 @@ function renderComposeSeconds(
|
|
|
728
759
|
// double it. A wildcard or stepped second is not folded there (no
|
|
729
760
|
// clockSecond), so it still leads its own clause here.
|
|
730
761
|
const restOwnsLead = plan.rest.kind === 'compactClockTimes' &&
|
|
731
|
-
|
|
732
|
-
const lead = restOwnsLead ? '' : secondsLead(
|
|
762
|
+
schedule.analyses.clockSecond;
|
|
763
|
+
const lead = restOwnsLead ? '' : secondsLead(schedule) + ', ';
|
|
733
764
|
|
|
734
|
-
return lead + render(
|
|
765
|
+
return lead + render(schedule, plan.rest, opts);
|
|
735
766
|
}
|
|
736
767
|
|
|
737
768
|
// True when a compose-seconds plan is a sub-minute second over a minute-0
|
|
738
769
|
// clock-time rest — the case that reads as the bare hour and so must surface
|
|
739
770
|
// the pinned clock minute.
|
|
740
771
|
function composeMinuteZero(
|
|
741
|
-
|
|
772
|
+
schedule: Schedule,
|
|
742
773
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
743
774
|
): plan is Extract<PlanNode, {kind: 'composeSeconds'}> &
|
|
744
775
|
{rest: Extract<PlanNode, {kind: 'clockTimes'}>} {
|
|
@@ -765,25 +796,25 @@ function clockMinuteGenitive(
|
|
|
765
796
|
// A minute clause across discrete hours: "in den Minuten 0 bis 30, um 9 und
|
|
766
797
|
// 17 Uhr".
|
|
767
798
|
function renderMinutesAcrossHours(
|
|
768
|
-
|
|
799
|
+
schedule: Schedule,
|
|
769
800
|
plan: Extract<PlanNode, {kind: 'minutesAcrossHours'}>,
|
|
770
801
|
opts: Opts
|
|
771
802
|
): string {
|
|
772
803
|
const sep = opts.style.sep;
|
|
773
804
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence,
|
|
774
805
|
// not a wall of hour columns.
|
|
775
|
-
const cadence = unevenHourCadence(
|
|
806
|
+
const cadence = unevenHourCadence(schedule);
|
|
776
807
|
|
|
777
808
|
// The wildcard form means every minute *during* each hour: render windows.
|
|
778
809
|
if (plan.form === 'wildcard') {
|
|
779
810
|
return cadence ?
|
|
780
811
|
'jede Minute, ' + cadence :
|
|
781
|
-
'jede Minute ' + duringHours(
|
|
812
|
+
'jede Minute ' + duringHours(schedule, plan.times, sep);
|
|
782
813
|
}
|
|
783
814
|
|
|
784
815
|
const minuteLead =
|
|
785
|
-
strideFromSegments(segmentsOf(
|
|
786
|
-
countedPhrase(
|
|
816
|
+
strideFromSegments(segmentsOf(schedule, 'minute'), UNITS.minute, '') ??
|
|
817
|
+
countedPhrase(schedule, 'minute', 'Minute', 'Minuten');
|
|
787
818
|
|
|
788
819
|
if (cadence !== null) {
|
|
789
820
|
return minuteLead + ', ' + cadence;
|
|
@@ -791,7 +822,7 @@ function renderMinutesAcrossHours(
|
|
|
791
822
|
|
|
792
823
|
const hours = plan.times.kind === 'fires' ?
|
|
793
824
|
atHours(plan.times.fires) :
|
|
794
|
-
joinList(hourSegmentParts(
|
|
825
|
+
joinList(hourSegmentParts(schedule, 0, 0, sep));
|
|
795
826
|
|
|
796
827
|
return minuteLead + ', ' + hours;
|
|
797
828
|
}
|
|
@@ -801,31 +832,33 @@ function renderMinutesAcrossHours(
|
|
|
801
832
|
// Minute in jeder zweiten Stunde"); a range or list leads with its minutes and
|
|
802
833
|
// trails the same cadence ("in den Minuten 0 bis 30, in jeder zweiten Stunde").
|
|
803
834
|
function renderMinuteSpanAcrossHourStep(
|
|
804
|
-
|
|
835
|
+
schedule: Schedule,
|
|
805
836
|
plan: Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>
|
|
806
837
|
): string {
|
|
807
838
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence; an
|
|
808
839
|
// offset-clean stride keeps its "in jeder N-ten Stunde" confinement.
|
|
809
|
-
const cadence = unevenHourCadence(
|
|
840
|
+
const cadence = unevenHourCadence(schedule);
|
|
810
841
|
|
|
811
842
|
// A wildcard minute over a stepped hour is reached only for a clean stride (a
|
|
812
843
|
// bounded or uneven step routes through minutesAcrossHours instead).
|
|
813
844
|
if (plan.form === 'wildcard') {
|
|
814
845
|
return 'jede Minute ' +
|
|
815
|
-
everyNthHour(stepSegment(
|
|
846
|
+
everyNthHour(stepSegment(schedule, 'hour'));
|
|
816
847
|
}
|
|
817
848
|
|
|
818
849
|
// The minute (range or list) leads; the hour trails. A clean stride confines
|
|
819
850
|
// to "in jeder N-ten Stunde" — the same cadence the wildcard form and the
|
|
820
851
|
// minute-step compositions use, never a juxtaposed second frequency. A
|
|
821
852
|
// bounded or uneven stride trails its endpoint-pinning cadence instead.
|
|
822
|
-
const segment = stepSegment(
|
|
853
|
+
const segment = stepSegment(schedule, 'hour');
|
|
823
854
|
const hours = cadence ?? (confinedHourStride(segment) ?
|
|
824
855
|
everyNthHour(segment) :
|
|
825
856
|
atHours(segment.fires));
|
|
826
857
|
|
|
827
|
-
return (
|
|
828
|
-
|
|
858
|
+
return (
|
|
859
|
+
strideFromSegments(segmentsOf(schedule, 'minute'), UNITS.minute, '') ??
|
|
860
|
+
countedPhrase(schedule, 'minute', 'Minute', 'Minuten')
|
|
861
|
+
) + ', ' + hours;
|
|
829
862
|
}
|
|
830
863
|
|
|
831
864
|
// Compact minutes across discrete hours: "in den Minuten 5 und 10, um 9, 17,
|
|
@@ -834,7 +867,7 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
834
867
|
// or list is a daily enumeration of its times ("täglich um 0:05, 2:05, …"),
|
|
835
868
|
// never hourly.
|
|
836
869
|
function renderCompactClockTimes(
|
|
837
|
-
|
|
870
|
+
schedule: Schedule,
|
|
838
871
|
plan: Extract<PlanNode, {kind: 'compactClockTimes'}>,
|
|
839
872
|
opts: Opts
|
|
840
873
|
): string {
|
|
@@ -844,46 +877,48 @@ function renderCompactClockTimes(
|
|
|
844
877
|
// An hour step or range (or arithmetic-progression hour list) under the
|
|
845
878
|
// single pinned minute reads as a cadence or window, not a wall of clock
|
|
846
879
|
// times. (Returns null for an irregular list, which keeps folding below.)
|
|
847
|
-
const cadence = hourCadence(
|
|
848
|
-
hourRangeCadence(
|
|
880
|
+
const cadence = hourCadence(schedule, plan.minute) ??
|
|
881
|
+
hourRangeCadence(schedule, plan.minute);
|
|
849
882
|
|
|
850
883
|
if (cadence !== null) {
|
|
851
884
|
return cadence;
|
|
852
885
|
}
|
|
853
886
|
|
|
854
|
-
const hourly = segmentsOf(
|
|
887
|
+
const hourly = segmentsOf(schedule, 'hour')
|
|
855
888
|
.some((segment) => segment.kind === 'range');
|
|
856
889
|
|
|
857
890
|
return (hourly ? 'stündlich ' : 'täglich ') +
|
|
858
|
-
joinList(hourSegmentParts(
|
|
891
|
+
joinList(hourSegmentParts(
|
|
892
|
+
schedule, plan.minute, schedule.analyses.clockSecond, sep
|
|
893
|
+
));
|
|
859
894
|
}
|
|
860
895
|
|
|
861
896
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence; else
|
|
862
897
|
// a range among the hours reads as a window, otherwise a flat hour list.
|
|
863
|
-
const hours = unevenHourCadence(
|
|
864
|
-
(segmentsOf(
|
|
865
|
-
joinList(hourSegmentParts(
|
|
866
|
-
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)));
|
|
867
902
|
|
|
868
903
|
// A folded second has no single clock time to attach to here, so it leads
|
|
869
904
|
// as its own clause ("in Sekunde 30, ..."). It is the bare second (not
|
|
870
905
|
// secondsLead's "… jeder Minute") because the minutes are constrained.
|
|
871
|
-
const lead =
|
|
872
|
-
countedPhrase(
|
|
906
|
+
const lead = schedule.analyses.clockSecond ?
|
|
907
|
+
countedPhrase(schedule, 'second', 'Sekunde', 'Sekunden') + ', ' : '';
|
|
873
908
|
|
|
874
909
|
return lead +
|
|
875
|
-
(strideFromSegments(segmentsOf(
|
|
876
|
-
countedPhrase(
|
|
910
|
+
(strideFromSegments(segmentsOf(schedule, 'minute'), UNITS.minute, '') ??
|
|
911
|
+
countedPhrase(schedule, 'minute', 'Minute', 'Minuten')) + ', ' + hours;
|
|
877
912
|
}
|
|
878
913
|
|
|
879
914
|
// A repeating minute step, optionally within an hour window: "alle 5
|
|
880
915
|
// Minuten", "alle 15 Minuten von 9 bis 17:45 Uhr".
|
|
881
916
|
function renderMinuteFrequency(
|
|
882
|
-
|
|
917
|
+
schedule: Schedule,
|
|
883
918
|
plan: Extract<PlanNode, {kind: 'minuteFrequency'}>,
|
|
884
919
|
opts: Opts
|
|
885
920
|
): string {
|
|
886
|
-
const segment = stepSegment(
|
|
921
|
+
const segment = stepSegment(schedule, 'minute');
|
|
887
922
|
const sep = opts.style.sep;
|
|
888
923
|
const clean = cleanStep(segment, 60);
|
|
889
924
|
|
|
@@ -906,18 +941,18 @@ function renderMinuteFrequency(
|
|
|
906
941
|
// A bounded or uneven hour stride confines the minute cadence to its own
|
|
907
942
|
// endpoint-pinning hour cadence ("alle 15 Minuten, alle 5 Stunden von 0 bis
|
|
908
943
|
// 20 Uhr").
|
|
909
|
-
const cadence = unevenHourCadence(
|
|
944
|
+
const cadence = unevenHourCadence(schedule);
|
|
910
945
|
|
|
911
946
|
return cadence ?
|
|
912
947
|
base + ', ' + cadence :
|
|
913
|
-
base + ' ' + duringHours(
|
|
948
|
+
base + ' ' + duringHours(schedule, plan.hours.times, sep);
|
|
914
949
|
}
|
|
915
950
|
|
|
916
951
|
if (plan.hours.kind === 'step') {
|
|
917
952
|
// The plan carries a step only for a clean step (dividing the day):
|
|
918
953
|
// confine the cadence to every Nth hour ("in jeder zweiten Stunde").
|
|
919
954
|
return base + ' ' +
|
|
920
|
-
everyNthHour(stepSegment(
|
|
955
|
+
everyNthHour(stepSegment(schedule, 'hour'));
|
|
921
956
|
}
|
|
922
957
|
|
|
923
958
|
return base;
|
|
@@ -930,14 +965,14 @@ function renderMinuteFrequency(
|
|
|
930
965
|
// bis 17 Uhr"). Shared by the bare hour step and the minute-step compositions.
|
|
931
966
|
// An explicitly bounded step (`a-b/n`) keeps its enumerated hours, matching
|
|
932
967
|
// en/fi/zh; only an OPEN step (`m/n`) reads as the wrapping cadence.
|
|
933
|
-
function hourStepPhrase(
|
|
934
|
-
const cadence = unevenHourCadence(
|
|
968
|
+
function hourStepPhrase(schedule: Schedule): string {
|
|
969
|
+
const cadence = unevenHourCadence(schedule);
|
|
935
970
|
|
|
936
971
|
if (cadence !== null) {
|
|
937
972
|
return cadence;
|
|
938
973
|
}
|
|
939
974
|
|
|
940
|
-
const segment = stepSegment(
|
|
975
|
+
const segment = stepSegment(schedule, 'hour');
|
|
941
976
|
|
|
942
977
|
if (cleanStep(segment, 24)) {
|
|
943
978
|
return everyN(segment.interval, UNITS.hour);
|
|
@@ -947,7 +982,7 @@ function hourStepPhrase(ir: IR): string {
|
|
|
947
982
|
// endpoint: name only its start, the cadence en/fi/zh and the compose paths
|
|
948
983
|
// already speak — never the enumerated hour list. A bounded `a-b/n` keeps its
|
|
949
984
|
// explicit hours.
|
|
950
|
-
const stride = openOffsetCleanStride(
|
|
985
|
+
const stride = openOffsetCleanStride(schedule, segment);
|
|
951
986
|
|
|
952
987
|
return stride ? hourStrideCadence(stride) : atHours(segment.fires);
|
|
953
988
|
}
|
|
@@ -958,13 +993,13 @@ function hourStepPhrase(ir: IR): string {
|
|
|
958
993
|
// (`a-b/n`, startToken carries a `-`) is excluded so it keeps its enumerated
|
|
959
994
|
// hours, matching en/fi/zh.
|
|
960
995
|
function openOffsetCleanStride(
|
|
961
|
-
|
|
996
|
+
schedule: Schedule, segment: StepSegment
|
|
962
997
|
): {start: number; interval: number; last: number} | null {
|
|
963
998
|
if (segment.startToken.indexOf('-') !== -1) {
|
|
964
999
|
return null;
|
|
965
1000
|
}
|
|
966
1001
|
|
|
967
|
-
const stride = hourStride(
|
|
1002
|
+
const stride = hourStride(schedule);
|
|
968
1003
|
|
|
969
1004
|
return stride && offsetCleanStride(stride) ? stride : null;
|
|
970
1005
|
}
|
|
@@ -983,28 +1018,24 @@ function hourStrideCadence(
|
|
|
983
1018
|
): string {
|
|
984
1019
|
const {start, interval, last} = stride;
|
|
985
1020
|
const cadence = everyN(interval, UNITS.hour);
|
|
986
|
-
const tiles = 24 % interval === 0;
|
|
987
1021
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
return cadence + ' ab ' + start + ' Uhr';
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
return cadence + ' von ' + start + ' bis ' + last + ' Uhr';
|
|
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
|
+
});
|
|
997
1027
|
}
|
|
998
1028
|
|
|
999
1029
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1000
1030
|
// segment yields its {start, interval, last} directly; an all-single hour list
|
|
1001
1031
|
// yields one only when its values form a step progression (so an irregular list
|
|
1002
|
-
// like 9,17 keeps enumerating). The
|
|
1003
|
-
// 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.
|
|
1004
1035
|
function hourStride(
|
|
1005
|
-
|
|
1036
|
+
schedule: Schedule
|
|
1006
1037
|
): {start: number; interval: number; last: number} | null {
|
|
1007
|
-
const segments = segmentsOf(
|
|
1038
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1008
1039
|
|
|
1009
1040
|
// A wildcard hour carries no segments (no discrete hours to stride over).
|
|
1010
1041
|
if (!segments) {
|
|
@@ -1040,8 +1071,8 @@ function hourStride(
|
|
|
1040
1071
|
// ("…, alle 5 Stunden von 0 bis 20 Uhr") than as a wall of clock times. An
|
|
1041
1072
|
// offset-clean stride keeps its existing confinement form, so only the
|
|
1042
1073
|
// endpoint-bearing case routes here.
|
|
1043
|
-
function unevenHourCadence(
|
|
1044
|
-
const stride = hourStride(
|
|
1074
|
+
function unevenHourCadence(schedule: Schedule): string | null {
|
|
1075
|
+
const stride = hourStride(schedule);
|
|
1045
1076
|
|
|
1046
1077
|
if (!stride || offsetCleanStride(stride)) {
|
|
1047
1078
|
return null;
|
|
@@ -1053,8 +1084,8 @@ function unevenHourCadence(ir: IR): string | null {
|
|
|
1053
1084
|
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
1054
1085
|
// fills the minute (a "für eine Minute" frame at minute 0); a single 0 is just
|
|
1055
1086
|
// the top of the minute (no clause); anything else needs its own clause.
|
|
1056
|
-
function subMinuteSecond(
|
|
1057
|
-
return
|
|
1087
|
+
function subMinuteSecond(schedule: Schedule): boolean {
|
|
1088
|
+
return schedule.pattern.second === '*' || schedule.shapes.second === 'step';
|
|
1058
1089
|
}
|
|
1059
1090
|
|
|
1060
1091
|
// The lead clause for an hour-cadence rendering: the second and the pinned
|
|
@@ -1064,24 +1095,26 @@ function subMinuteSecond(ir: IR): boolean {
|
|
|
1064
1095
|
// Minute" frame (the whole minute-0 window). A non-zero minute is a real clock
|
|
1065
1096
|
// minute: the second leads with its own clause (if any), then the minute reads
|
|
1066
1097
|
// "in Minute M".
|
|
1067
|
-
function hourCadenceLead(
|
|
1098
|
+
function hourCadenceLead(schedule: Schedule, minute: number): string {
|
|
1068
1099
|
if (minute === 0) {
|
|
1069
|
-
if (subMinuteSecond(
|
|
1070
|
-
return withAnchor(
|
|
1100
|
+
if (subMinuteSecond(schedule)) {
|
|
1101
|
+
return withAnchor(
|
|
1102
|
+
secondsClause(schedule, minuteAnchor(schedule)), 'für eine Minute'
|
|
1103
|
+
);
|
|
1071
1104
|
}
|
|
1072
1105
|
|
|
1073
|
-
return secondsClause(
|
|
1106
|
+
return secondsClause(schedule, 'jeder Stunde');
|
|
1074
1107
|
}
|
|
1075
1108
|
|
|
1076
1109
|
const minutePhrase = 'in Minute ' + minute;
|
|
1077
1110
|
|
|
1078
1111
|
// A single 0 second is just the top of the minute, so the minute leads
|
|
1079
1112
|
// alone; any other second prefixes its own clause.
|
|
1080
|
-
if (
|
|
1113
|
+
if (schedule.pattern.second === '0') {
|
|
1081
1114
|
return minutePhrase;
|
|
1082
1115
|
}
|
|
1083
1116
|
|
|
1084
|
-
return secondsClause(
|
|
1117
|
+
return secondsClause(schedule, minuteAnchor(schedule)) + ', ' + minutePhrase;
|
|
1085
1118
|
}
|
|
1086
1119
|
|
|
1087
1120
|
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
@@ -1093,9 +1126,9 @@ function hourCadenceLead(ir: IR, minute: number): string {
|
|
|
1093
1126
|
// clock time three digit-groups, so any stride is worth compacting; otherwise
|
|
1094
1127
|
// the stride must exceed the clock-time cap, the same point at which the core
|
|
1095
1128
|
// itself stops enumerating. The renderer returns the bare clause; the day
|
|
1096
|
-
// frame is composed in `describe`. Renderer-only; the
|
|
1097
|
-
function hourCadence(
|
|
1098
|
-
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);
|
|
1099
1132
|
|
|
1100
1133
|
if (!stride) {
|
|
1101
1134
|
return null;
|
|
@@ -1108,7 +1141,7 @@ function hourCadence(ir: IR, minute: number): string | null {
|
|
|
1108
1141
|
// or "ab" form is no shorter than the list. A bounded or uneven stride has no
|
|
1109
1142
|
// clean wrap, so its endpoint-pinning cadence ("alle 5 Stunden von 0 bis 20
|
|
1110
1143
|
// Uhr") reads better however short.
|
|
1111
|
-
if (
|
|
1144
|
+
if (schedule.pattern.second === '0' && fires <= maxClockTimes &&
|
|
1112
1145
|
offsetCleanStride(stride)) {
|
|
1113
1146
|
return null;
|
|
1114
1147
|
}
|
|
@@ -1117,46 +1150,47 @@ function hourCadence(ir: IR, minute: number): string | null {
|
|
|
1117
1150
|
// stride is a confinement, not a juxtaposed cadence: it reads "für eine
|
|
1118
1151
|
// Minute in jeder zweiten Stunde", reusing the every-Nth-hour idiom so the
|
|
1119
1152
|
// minute-0 window is never heard as the bare hour cadence.
|
|
1120
|
-
const segment = segmentsOf(
|
|
1121
|
-
const confined = minute === 0 && subMinuteSecond(
|
|
1122
|
-
segmentsOf(
|
|
1153
|
+
const segment = segmentsOf(schedule, 'hour')[0];
|
|
1154
|
+
const confined = minute === 0 && subMinuteSecond(schedule) &&
|
|
1155
|
+
segmentsOf(schedule, 'hour').length === 1 && segment.kind === 'step' &&
|
|
1123
1156
|
confinedHourStride(segment);
|
|
1124
1157
|
|
|
1125
1158
|
if (confined) {
|
|
1126
|
-
return withAnchor(
|
|
1127
|
-
|
|
1159
|
+
return withAnchor(
|
|
1160
|
+
secondsClause(schedule, minuteAnchor(schedule)), 'für eine Minute'
|
|
1161
|
+
) + ' ' + everyNthHour(segment);
|
|
1128
1162
|
}
|
|
1129
1163
|
|
|
1130
1164
|
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
1131
1165
|
// lead clause to fold in, so the bounded cadence stands on its own ("alle 5
|
|
1132
1166
|
// Stunden von 0 bis 20 Uhr").
|
|
1133
|
-
if (minute === 0 &&
|
|
1167
|
+
if (minute === 0 && schedule.pattern.second === '0') {
|
|
1134
1168
|
return hourStrideCadence(stride);
|
|
1135
1169
|
}
|
|
1136
1170
|
|
|
1137
|
-
return hourCadenceLead(
|
|
1171
|
+
return hourCadenceLead(schedule, minute) + ', ' + hourStrideCadence(stride);
|
|
1138
1172
|
}
|
|
1139
1173
|
|
|
1140
1174
|
// Whether an hour cadence or hour-range window applies to a plan with a single
|
|
1141
1175
|
// pinned minute — the signal that the clause is a cadence/window, not a daily
|
|
1142
1176
|
// clock-time list, so the "täglich" frame must not be added.
|
|
1143
|
-
function hourCadenceApplies(
|
|
1144
|
-
if (
|
|
1177
|
+
function hourCadenceApplies(schedule: Schedule): boolean {
|
|
1178
|
+
if (schedule.shapes.minute !== 'single') {
|
|
1145
1179
|
return false;
|
|
1146
1180
|
}
|
|
1147
1181
|
|
|
1148
|
-
const minute = +
|
|
1182
|
+
const minute = +schedule.pattern.minute;
|
|
1149
1183
|
|
|
1150
|
-
return hourCadence(
|
|
1151
|
-
hourRangeCadence(
|
|
1184
|
+
return hourCadence(schedule, minute) !== null ||
|
|
1185
|
+
hourRangeCadence(schedule, minute) !== null;
|
|
1152
1186
|
}
|
|
1153
1187
|
|
|
1154
1188
|
// Whether the hour field is a range — or a list whose segments include a
|
|
1155
1189
|
// range — and so forms a window rather than a cross-product of clock times.
|
|
1156
1190
|
// A pure single-value list (9,17) has no range to span and still enumerates;
|
|
1157
1191
|
// a step is handled by hourStride/hourCadence.
|
|
1158
|
-
function hasHourWindow(
|
|
1159
|
-
const segments = segmentsOf(
|
|
1192
|
+
function hasHourWindow(schedule: Schedule): boolean {
|
|
1193
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1160
1194
|
|
|
1161
1195
|
return !!segments && segments.some(function range(segment) {
|
|
1162
1196
|
return segment.kind === 'range';
|
|
@@ -1171,13 +1205,15 @@ function hasHourWindow(ir: IR): boolean {
|
|
|
1171
1205
|
// suppressed by hourCadenceApplies). Returns null when the hour has no range,
|
|
1172
1206
|
// when the minute is non-zero (a real clock minute the existing window form
|
|
1173
1207
|
// already speaks), or when a plain :00 set carries no clause. Renderer-only;
|
|
1174
|
-
// the
|
|
1175
|
-
function hourRangeCadence(
|
|
1176
|
-
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') {
|
|
1177
1212
|
return null;
|
|
1178
1213
|
}
|
|
1179
1214
|
|
|
1180
|
-
return hourCadenceLead(
|
|
1215
|
+
return hourCadenceLead(schedule, minute) + ', ' +
|
|
1216
|
+
hourRangeWindowTail(schedule);
|
|
1181
1217
|
}
|
|
1182
1218
|
|
|
1183
1219
|
// The hour-range window as a cadence tail at the top of each hour: each range
|
|
@@ -1185,15 +1221,15 @@ function hourRangeCadence(ir: IR, minute: number): string | null {
|
|
|
1185
1221
|
// — the same parts the bare "stündlich von 9 bis 17 Uhr" window forms, minus
|
|
1186
1222
|
// the "stündlich" prefix the lead replaces. The minute has folded into the
|
|
1187
1223
|
// lead, so the parts close on the top of their final hour.
|
|
1188
|
-
function hourRangeWindowTail(
|
|
1224
|
+
function hourRangeWindowTail(schedule: Schedule): string {
|
|
1189
1225
|
// Minute 0 with a falsy second renders each part as a bare hour ("von 9 bis
|
|
1190
1226
|
// 17 Uhr", "um 22 Uhr"); the separator is unused in that path.
|
|
1191
|
-
return joinList(hourSegmentParts(
|
|
1227
|
+
return joinList(hourSegmentParts(schedule, 0, 0, ':'));
|
|
1192
1228
|
}
|
|
1193
1229
|
|
|
1194
1230
|
// An hourly window: "stündlich von 9 bis 17 Uhr", or every minute across it.
|
|
1195
1231
|
function renderHourRange(
|
|
1196
|
-
|
|
1232
|
+
schedule: Schedule,
|
|
1197
1233
|
plan: Extract<PlanNode, {kind: 'hourRange'}>,
|
|
1198
1234
|
opts: Opts
|
|
1199
1235
|
): string {
|
|
@@ -1210,7 +1246,7 @@ function renderHourRange(
|
|
|
1210
1246
|
return 'jede Minute ' + window;
|
|
1211
1247
|
}
|
|
1212
1248
|
|
|
1213
|
-
if (plan.minuteForm === 'lead' &&
|
|
1249
|
+
if (plan.minuteForm === 'lead' && schedule.pattern.minute === '0') {
|
|
1214
1250
|
return 'stündlich ' + window;
|
|
1215
1251
|
}
|
|
1216
1252
|
|
|
@@ -1219,24 +1255,25 @@ function renderHourRange(
|
|
|
1219
1255
|
// bounded cadence ("alle 2 Minuten von Minute 3 bis 59 jeder Stunde") instead
|
|
1220
1256
|
// of the wall of fires; an irregular list or a single minute keeps the
|
|
1221
1257
|
// counted form.
|
|
1222
|
-
return (strideFromSegments(segmentsOf(
|
|
1258
|
+
return (strideFromSegments(segmentsOf(schedule, 'minute'), UNITS.minute,
|
|
1223
1259
|
'jeder Stunde') ??
|
|
1224
|
-
countedPhrase(
|
|
1260
|
+
countedPhrase(schedule, 'minute', 'Minute', 'Minuten') + ' jeder Stunde') +
|
|
1225
1261
|
', ' + window;
|
|
1226
1262
|
}
|
|
1227
1263
|
|
|
1228
1264
|
// One or more clock times: "um 9 Uhr", "um 14:30 Uhr", "um 9 und 17 Uhr".
|
|
1229
1265
|
function renderClockTimes(
|
|
1230
|
-
|
|
1266
|
+
schedule: Schedule,
|
|
1231
1267
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
1232
1268
|
opts: Opts
|
|
1233
1269
|
): string {
|
|
1234
1270
|
// An hour step or range (or arithmetic-progression hour list) under a single
|
|
1235
1271
|
// pinned minute reads as a cadence or window rather than a cross-product of
|
|
1236
1272
|
// clock times.
|
|
1237
|
-
if (
|
|
1238
|
-
const minute = +
|
|
1239
|
-
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);
|
|
1240
1277
|
|
|
1241
1278
|
if (cadence !== null) {
|
|
1242
1279
|
return cadence;
|
|
@@ -1267,33 +1304,100 @@ const renderers = {
|
|
|
1267
1304
|
standaloneSeconds: renderSeconds
|
|
1268
1305
|
};
|
|
1269
1306
|
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
+
}
|
|
1274
1314
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
+
}
|
|
1280
1323
|
|
|
1281
|
-
|
|
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
|
+
}
|
|
1333
|
+
|
|
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);
|
|
1282
1384
|
}
|
|
1283
1385
|
|
|
1284
1386
|
if (weekday !== '*') {
|
|
1285
|
-
return (quartzWeekday(weekday) || weekdayQualifier(
|
|
1286
|
-
monthScope(
|
|
1387
|
+
return (quartzWeekday(weekday) || weekdayQualifier(schedule)) +
|
|
1388
|
+
monthScope(schedule, months);
|
|
1287
1389
|
}
|
|
1288
1390
|
|
|
1289
1391
|
if (date !== '*') {
|
|
1290
|
-
const quartz = quartzDate(date);
|
|
1392
|
+
const quartz = quartzDate(date) || dateStepCadence(schedule);
|
|
1291
1393
|
|
|
1292
|
-
return quartz ?
|
|
1394
|
+
return quartz ?
|
|
1395
|
+
quartz + monthScope(schedule, months) :
|
|
1396
|
+
datePhrase(schedule, months);
|
|
1293
1397
|
}
|
|
1294
1398
|
|
|
1295
1399
|
if (month !== '*') {
|
|
1296
|
-
return monthClause(
|
|
1400
|
+
return monthClause(schedule, months);
|
|
1297
1401
|
}
|
|
1298
1402
|
|
|
1299
1403
|
return '';
|
|
@@ -1307,15 +1411,15 @@ const LEADING_PLANS = new Set(['clockTimes']);
|
|
|
1307
1411
|
|
|
1308
1412
|
// True when the leading qualifier should precede the clause: a clock-time
|
|
1309
1413
|
// plan, or the minute-0 compose-seconds clause that surfaces a clock minute.
|
|
1310
|
-
function leadsQualifier(
|
|
1311
|
-
return LEADING_PLANS.has(
|
|
1414
|
+
function leadsQualifier(schedule: Schedule): boolean {
|
|
1415
|
+
return LEADING_PLANS.has(schedule.plan.kind) || isComposeMinuteZero(schedule);
|
|
1312
1416
|
}
|
|
1313
1417
|
|
|
1314
1418
|
// Whether the planned clause is the minute-0 compose-seconds confinement
|
|
1315
1419
|
// (a sub-minute second over a minute-0 clock-time rest).
|
|
1316
|
-
function isComposeMinuteZero(
|
|
1317
|
-
return
|
|
1318
|
-
composeMinuteZero(
|
|
1420
|
+
function isComposeMinuteZero(schedule: Schedule): boolean {
|
|
1421
|
+
return schedule.plan.kind === 'composeSeconds' &&
|
|
1422
|
+
composeMinuteZero(schedule, schedule.plan);
|
|
1319
1423
|
}
|
|
1320
1424
|
|
|
1321
1425
|
// True when the clause is a bare daily clock-time list and so needs the
|
|
@@ -1323,19 +1427,19 @@ function isComposeMinuteZero(ir: IR): boolean {
|
|
|
1323
1427
|
// minute-0 compose-seconds clause (a recurring clock minute), and an uneven
|
|
1324
1428
|
// hour step (rendered as its fire list "um 0, 5, … Uhr", not the cadence "alle
|
|
1325
1429
|
// N Stunden"). A frequency clause already implies recurrence.
|
|
1326
|
-
function needsDailyFrame(
|
|
1430
|
+
function needsDailyFrame(schedule: Schedule): boolean {
|
|
1327
1431
|
// An hour cadence is a sub-daily frequency, not a daily clock-time list, so
|
|
1328
1432
|
// it must not take the "täglich" frame ("alle 2 Stunden", not "täglich alle
|
|
1329
1433
|
// 2 Stunden").
|
|
1330
|
-
if (hourCadenceApplies(
|
|
1434
|
+
if (hourCadenceApplies(schedule)) {
|
|
1331
1435
|
return false;
|
|
1332
1436
|
}
|
|
1333
1437
|
|
|
1334
|
-
if (
|
|
1438
|
+
if (schedule.plan.kind === 'clockTimes' || isComposeMinuteZero(schedule)) {
|
|
1335
1439
|
return true;
|
|
1336
1440
|
}
|
|
1337
1441
|
|
|
1338
|
-
if (
|
|
1442
|
+
if (schedule.plan.kind !== 'hourStep') {
|
|
1339
1443
|
return false;
|
|
1340
1444
|
}
|
|
1341
1445
|
|
|
@@ -1343,14 +1447,14 @@ function needsDailyFrame(ir: IR): boolean {
|
|
|
1343
1447
|
// frequency, not a daily clock-time list, so it takes no "täglich" frame —
|
|
1344
1448
|
// only a bounded `a-b/n` step that enumerates its hours ("um 1, 3, … Uhr")
|
|
1345
1449
|
// needs the recurring frame.
|
|
1346
|
-
const segment = stepSegment(
|
|
1450
|
+
const segment = stepSegment(schedule, 'hour');
|
|
1347
1451
|
|
|
1348
|
-
return !cleanStep(segment, 24) && !openOffsetCleanStride(
|
|
1452
|
+
return !cleanStep(segment, 24) && !openOffsetCleanStride(schedule, segment);
|
|
1349
1453
|
}
|
|
1350
1454
|
|
|
1351
|
-
function render(
|
|
1455
|
+
function render(schedule: Schedule, plan: PlanNode, opts: Opts): string {
|
|
1352
1456
|
return (renderers[plan.kind as keyof typeof renderers] as Renderer)(
|
|
1353
|
-
|
|
1457
|
+
schedule, plan, opts);
|
|
1354
1458
|
}
|
|
1355
1459
|
|
|
1356
1460
|
function normalizeOptions(options?: Cronli5Options): Opts {
|
|
@@ -1369,8 +1473,8 @@ function normalizeOptions(options?: Cronli5Options): Opts {
|
|
|
1369
1473
|
|
|
1370
1474
|
// Append the year frame: "im Jahr 2026", "in den Jahren 2025 und 2027", "von
|
|
1371
1475
|
// 2025 bis 2027".
|
|
1372
|
-
function applyYear(description: string,
|
|
1373
|
-
const year =
|
|
1476
|
+
function applyYear(description: string, schedule: Schedule): string {
|
|
1477
|
+
const year = schedule.pattern.year;
|
|
1374
1478
|
|
|
1375
1479
|
if (year === '*') {
|
|
1376
1480
|
return description;
|
|
@@ -1389,21 +1493,27 @@ function applyYear(description: string, ir: IR): string {
|
|
|
1389
1493
|
return description + ' im Jahr ' + year;
|
|
1390
1494
|
}
|
|
1391
1495
|
|
|
1392
|
-
function describe(
|
|
1393
|
-
const core = render(
|
|
1394
|
-
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);
|
|
1395
1499
|
let base = core;
|
|
1396
1500
|
|
|
1397
1501
|
if (qual) {
|
|
1398
|
-
base = leadsQualifier(
|
|
1502
|
+
base = leadsQualifier(schedule) ?
|
|
1399
1503
|
qual + ' ' + core :
|
|
1400
1504
|
core + ' ' + qual;
|
|
1401
1505
|
}
|
|
1402
|
-
else if (needsDailyFrame(
|
|
1506
|
+
else if (needsDailyFrame(schedule)) {
|
|
1403
1507
|
base = 'täglich ' + core;
|
|
1404
1508
|
}
|
|
1405
1509
|
|
|
1406
|
-
|
|
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);
|
|
1407
1517
|
}
|
|
1408
1518
|
|
|
1409
1519
|
const de: Language<GermanStyle> = {
|