cronli5 0.1.4 → 0.1.5
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 +25 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +180 -36
- package/dist/cronli5.js +180 -36
- package/dist/lang/de.cjs +172 -8
- package/dist/lang/de.js +172 -8
- package/dist/lang/en.cjs +175 -29
- package/dist/lang/en.js +175 -29
- package/dist/lang/es.cjs +180 -25
- package/dist/lang/es.js +180 -25
- package/dist/lang/fi.cjs +188 -40
- package/dist/lang/fi.js +188 -40
- package/dist/lang/zh.cjs +165 -19
- package/dist/lang/zh.js +165 -19
- package/package.json +2 -1
- package/src/core/analyze.ts +7 -0
- package/src/core/ir.ts +1 -1
- package/src/core/util.ts +31 -1
- package/src/lang/de/index.ts +360 -16
- package/src/lang/en/index.ts +333 -33
- package/src/lang/es/index.ts +373 -40
- package/src/lang/fi/index.ts +404 -72
- package/src/lang/zh/index.ts +327 -35
- package/types/core/ir.d.ts +1 -1
- package/types/core/util.d.ts +6 -1
package/src/lang/de/index.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// German. Anchored to Duden; see notes.md for the decisions.
|
|
3
3
|
|
|
4
4
|
import {pad} from '../../core/format.js';
|
|
5
|
-
import {weekdayNumbers} from '../../core/specs.js';
|
|
6
|
-
import {toFieldNumber} from '../../core/util.js';
|
|
5
|
+
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
6
|
+
import {arithmeticStep, toFieldNumber} from '../../core/util.js';
|
|
7
7
|
import type {Cronli5Options} from '../../types.js';
|
|
8
8
|
import type {
|
|
9
9
|
Field, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode, Segment
|
|
@@ -22,6 +22,19 @@ interface Unit {
|
|
|
22
22
|
singular: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// A step cadence to phrase: the `interval` repeats over a `cycle`-long field
|
|
26
|
+
// (60 for minute/second), running from `start` to `last`. `anchor` is the
|
|
27
|
+
// scope clause ("jeder Stunde"); an empty anchor lets the caller supply its
|
|
28
|
+
// own trailing scope, dropping the tail.
|
|
29
|
+
interface Stride {
|
|
30
|
+
interval: number;
|
|
31
|
+
start: number;
|
|
32
|
+
last: number;
|
|
33
|
+
cycle: number;
|
|
34
|
+
unit: Unit;
|
|
35
|
+
anchor: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
const UNITS: Record<'second' | 'minute' | 'hour', Unit> = {
|
|
26
39
|
hour: {every: 'jede', plural: 'Stunden', singular: 'Stunde'},
|
|
27
40
|
minute: {every: 'jede', plural: 'Minuten', singular: 'Minute'},
|
|
@@ -51,6 +64,98 @@ function cleanStep(segment: StepSegment, cycle: number): boolean {
|
|
|
51
64
|
cycle % segment.interval === 0;
|
|
52
65
|
}
|
|
53
66
|
|
|
67
|
+
// Speak a step cadence over a `cycle`-long field (60 for minute/second). A
|
|
68
|
+
// clean stride from the top of the cycle is the bare cadence ("alle 15
|
|
69
|
+
// Minuten"); a uniform offset (start within the first interval, the interval
|
|
70
|
+
// still dividing the cycle) names only its start, since it wraps cleanly with
|
|
71
|
+
// no distinct endpoint ("alle 6 Minuten ab Minute 5 jeder Stunde"); a
|
|
72
|
+
// non-uniform stride (start >= interval, or an interval that does not divide
|
|
73
|
+
// the cycle) pins both endpoints so the bounded, non-wrapping set reads
|
|
74
|
+
// unambiguously ("alle 2 Minuten von Minute 3 bis 59 jeder Stunde"). This is
|
|
75
|
+
// the one phrasing for every step the renderer speaks, whether the core kept
|
|
76
|
+
// it a step shape (a clean cadence) or enumerated it to a fire list (an
|
|
77
|
+
// offset/uneven set the list path recognizes as a progression).
|
|
78
|
+
function renderStride(stride: Stride): string {
|
|
79
|
+
const {interval, start, last, cycle, unit, anchor} = stride;
|
|
80
|
+
const cadence = everyN(interval, unit);
|
|
81
|
+
const tiles = cycle % interval === 0;
|
|
82
|
+
|
|
83
|
+
if (start === 0 && tiles) {
|
|
84
|
+
return cadence;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A context that supplies its own trailing scope passes an empty anchor, so
|
|
88
|
+
// the cadence keeps its endpoints but drops the "jeder Stunde" tail.
|
|
89
|
+
const tail = anchor ? ' ' + anchor : '';
|
|
90
|
+
|
|
91
|
+
if (start < interval && tiles) {
|
|
92
|
+
return cadence + ' ab ' + unit.singular + ' ' + start + tail;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return cadence + ' von ' + unit.singular + ' ' + start + ' bis ' + last +
|
|
96
|
+
tail;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A step *shape* segment as its cadence ("alle 6 Minuten ab Minute 5 jeder
|
|
100
|
+
// Stunde"). A bounded sub-range step (`a-b/n`) is not a whole-cycle stride, so
|
|
101
|
+
// it lists its fires; a short offset cadence (three fires or fewer) also lists,
|
|
102
|
+
// since the list is no longer than the cadence. Everything else routes through
|
|
103
|
+
// `renderStride`. The uneven whole-cycle step (interval not dividing the cycle)
|
|
104
|
+
// never reaches here as a step shape — the core enumerates it to a fire list,
|
|
105
|
+
// which the list path recognizes instead.
|
|
106
|
+
function stepClause(segment: StepSegment, unit: Unit, anchor: string): string {
|
|
107
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
108
|
+
const short = start !== 0 && segment.fires.length <= 3;
|
|
109
|
+
|
|
110
|
+
if (segment.startToken.indexOf('-') !== -1 || short) {
|
|
111
|
+
return 'in den ' + unit.plural + ' ' + joinList(segment.fires.map(String)) +
|
|
112
|
+
' ' + anchor;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return renderStride({
|
|
116
|
+
interval: segment.interval,
|
|
117
|
+
start,
|
|
118
|
+
last: segment.fires[segment.fires.length - 1],
|
|
119
|
+
cycle: 60,
|
|
120
|
+
unit,
|
|
121
|
+
anchor
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The sorted numeric values a field's segments cover, or null if any segment
|
|
126
|
+
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
127
|
+
function singleValues(segments: Segment[]): number[] | null {
|
|
128
|
+
const values: number[] = [];
|
|
129
|
+
|
|
130
|
+
for (const segment of segments) {
|
|
131
|
+
if (segment.kind !== 'single') {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
values.push(+segment.value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return values;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
142
|
+
// form an arithmetic progression long enough to beat the list (the core
|
|
143
|
+
// enumerates an offset/uneven step to this fire list; the IR is unchanged, so
|
|
144
|
+
// the renderer recognizes the progression). Returns null for a non-progression
|
|
145
|
+
// or a too-short list, leaving the caller to enumerate.
|
|
146
|
+
function strideFromSegments(
|
|
147
|
+
segments: Segment[],
|
|
148
|
+
unit: Unit,
|
|
149
|
+
anchor: string
|
|
150
|
+
): string | null {
|
|
151
|
+
const values = singleValues(segments);
|
|
152
|
+
const step = values && arithmeticStep(values);
|
|
153
|
+
|
|
154
|
+
return step ?
|
|
155
|
+
renderStride({...step, cycle: 60, unit, anchor}) :
|
|
156
|
+
null;
|
|
157
|
+
}
|
|
158
|
+
|
|
54
159
|
type NameToken = string | number;
|
|
55
160
|
type NameSegment =
|
|
56
161
|
| {kind: 'single'; value: NameToken}
|
|
@@ -142,6 +247,20 @@ function everyNthHour(segment: StepSegment): string {
|
|
|
142
247
|
return start === 0 ? base : base + ' ab ' + start + ' Uhr';
|
|
143
248
|
}
|
|
144
249
|
|
|
250
|
+
// Whether an hour step is a clean stride over the whole day — unbounded,
|
|
251
|
+
// dividing 24, and starting within the first interval — so it confines to "in
|
|
252
|
+
// jeder N-ten Stunde" rather than enumerating its fires. Mirrors the core's
|
|
253
|
+
// cleanHourStride: an offset like 1/2 qualifies; bounded (9-17/2) does not.
|
|
254
|
+
function confinedHourStride(segment: StepSegment): boolean {
|
|
255
|
+
if (segment.startToken.indexOf('-') !== -1) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
260
|
+
|
|
261
|
+
return 24 % segment.interval === 0 && start < segment.interval;
|
|
262
|
+
}
|
|
263
|
+
|
|
145
264
|
// The Quartz weekday stem (`5L`, `MON#2`) is not number-canonicalized in the
|
|
146
265
|
// core, so it may still be a name token; resolve it via the core's index.
|
|
147
266
|
function weekdayNoun(token: string): string {
|
|
@@ -344,17 +463,30 @@ function countedPhrase(
|
|
|
344
463
|
// The seconds clause: "alle 30 Sekunden" for a step, else "in Sekunde 15
|
|
345
464
|
// jeder Minute".
|
|
346
465
|
function secondsLead(ir: IR): string {
|
|
466
|
+
return secondsClause(ir, 'jeder Minute');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// The second clause counted against an arbitrary anchor. The anchor is "jeder
|
|
470
|
+
// Minute" in the standalone seconds path; the hour-cadence path folds a pinned
|
|
471
|
+
// minute 0 into the hour and counts the second "jeder Stunde" instead ("in
|
|
472
|
+
// Sekunde 30 jeder Stunde"), so the minute-0 confinement is stated, not
|
|
473
|
+
// dropped.
|
|
474
|
+
function secondsClause(ir: IR, anchor: string): string {
|
|
347
475
|
if (ir.pattern.second === '*') {
|
|
348
476
|
return 'jede Sekunde';
|
|
349
477
|
}
|
|
350
478
|
|
|
351
479
|
const segments = ir.analyses.segments.second;
|
|
352
480
|
|
|
353
|
-
|
|
354
|
-
|
|
481
|
+
// A step shape speaks its cadence directly; an offset/uneven step the core
|
|
482
|
+
// enumerated to a list is recognized as a progression. Both fall back to the
|
|
483
|
+
// counted list (a short or irregular set).
|
|
484
|
+
if (ir.shapes.second === 'step') {
|
|
485
|
+
return stepClause(stepSegment(segments), UNITS.second, anchor);
|
|
355
486
|
}
|
|
356
487
|
|
|
357
|
-
return
|
|
488
|
+
return strideFromSegments(segments as Segment[], UNITS.second, anchor) ??
|
|
489
|
+
countedPhrase(ir, 'second', 'Sekunde', 'Sekunden') + ' ' + anchor;
|
|
358
490
|
}
|
|
359
491
|
|
|
360
492
|
// A clock time that always shows its minutes: "9:00", "9:30".
|
|
@@ -476,9 +608,17 @@ function renderSeconds(ir: IR): string {
|
|
|
476
608
|
}
|
|
477
609
|
|
|
478
610
|
// The minute-past-the-hour clause: "in Minute 5 jeder Stunde", "in den
|
|
479
|
-
// Minuten 5, 10 und 30 jeder Stunde".
|
|
611
|
+
// Minuten 5, 10 und 30 jeder Stunde". An offset/uneven step the core
|
|
612
|
+
// enumerated to this list reads as a stride cadence when the fires form a
|
|
613
|
+
// long-enough progression ("alle 2 Minuten von Minute 3 bis 59 jeder Stunde").
|
|
614
|
+
function minutePastClause(ir: IR): string {
|
|
615
|
+
return strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute,
|
|
616
|
+
'jeder Stunde') ??
|
|
617
|
+
countedPhrase(ir, 'minute', 'Minute', 'Minuten') + ' jeder Stunde';
|
|
618
|
+
}
|
|
619
|
+
|
|
480
620
|
function renderMinutePast(ir: IR): string {
|
|
481
|
-
return
|
|
621
|
+
return minutePastClause(ir);
|
|
482
622
|
}
|
|
483
623
|
|
|
484
624
|
// A specific minute and second: "in Minute 0 und Sekunde 30 jeder Stunde".
|
|
@@ -535,6 +675,20 @@ function renderComposeSeconds(
|
|
|
535
675
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
536
676
|
opts: Opts
|
|
537
677
|
): string {
|
|
678
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
679
|
+
// minute is a cadence, not a wall of clock times: the second/minute lead,
|
|
680
|
+
// then the hour cadence ("in Sekunde 30 jeder Stunde, alle 2 Stunden"). The
|
|
681
|
+
// clock-time rest would otherwise cross-multiply the hours.
|
|
682
|
+
if ((plan.rest.kind === 'clockTimes' ||
|
|
683
|
+
plan.rest.kind === 'compactClockTimes') &&
|
|
684
|
+
ir.shapes.minute === 'single') {
|
|
685
|
+
const cadence = hourCadence(ir, +ir.pattern.minute);
|
|
686
|
+
|
|
687
|
+
if (cadence !== null) {
|
|
688
|
+
return cadence;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
538
692
|
// A sub-minute second with the minute pinned to 0 and a specific hour: the
|
|
539
693
|
// clock-time rest would read "um 9 Uhr", which hides the pinned :00 (and so
|
|
540
694
|
// the one-minute confinement — 60 fires in :00, not 3,600 across the hour).
|
|
@@ -545,6 +699,20 @@ function renderComposeSeconds(
|
|
|
545
699
|
clockMinuteGenitive(plan.rest.times, opts.style.sep);
|
|
546
700
|
}
|
|
547
701
|
|
|
702
|
+
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
703
|
+
// cadences that read as contradictory ("jede Sekunde, alle 2 Minuten"). Bind
|
|
704
|
+
// them in the genitive ("jede Sekunde jeder zweiten Minute"), mirroring
|
|
705
|
+
// English. Other strides, a restricted hour, and an hour cadence keep the
|
|
706
|
+
// juxtaposed form.
|
|
707
|
+
if (plan.rest.kind === 'minuteFrequency' &&
|
|
708
|
+
ir.shapes.second === 'wildcard' && ir.shapes.hour === 'wildcard') {
|
|
709
|
+
const minuteStep = stepSegment(ir.analyses.segments.minute);
|
|
710
|
+
|
|
711
|
+
if (minuteStep.startToken === '*' && minuteStep.interval === 2) {
|
|
712
|
+
return secondsLead(ir) + ' jeder zweiten Minute';
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
548
716
|
return secondsLead(ir) + ', ' + render(ir, plan.rest, opts);
|
|
549
717
|
}
|
|
550
718
|
|
|
@@ -594,13 +762,14 @@ function renderMinutesAcrossHours(
|
|
|
594
762
|
atHours(plan.times.fires) :
|
|
595
763
|
joinList(hourSegmentParts(ir, 0, 0, sep));
|
|
596
764
|
|
|
597
|
-
return
|
|
765
|
+
return (strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute, '') ??
|
|
766
|
+
countedPhrase(ir, 'minute', 'Minute', 'Minuten')) + ', ' + hours;
|
|
598
767
|
}
|
|
599
768
|
|
|
600
769
|
// A minute clause across a stepped hour range. A wildcard minute (a cadence)
|
|
601
770
|
// is reached only for a clean step and is confined to every Nth hour ("jede
|
|
602
|
-
// Minute in jeder zweiten Stunde"); a
|
|
603
|
-
//
|
|
771
|
+
// Minute in jeder zweiten Stunde"); a range or list leads with its minutes and
|
|
772
|
+
// trails the same cadence ("in den Minuten 0 bis 30, in jeder zweiten Stunde").
|
|
604
773
|
function renderMinuteSpanAcrossHourStep(
|
|
605
774
|
ir: IR,
|
|
606
775
|
plan: Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>
|
|
@@ -610,8 +779,17 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
610
779
|
everyNthHour(stepSegment(ir.analyses.segments.hour));
|
|
611
780
|
}
|
|
612
781
|
|
|
613
|
-
|
|
614
|
-
|
|
782
|
+
// The minute (range or list) leads; the hour trails. A clean stride confines
|
|
783
|
+
// to "in jeder N-ten Stunde" — the same cadence the wildcard form and the
|
|
784
|
+
// minute-step compositions use, never a juxtaposed second frequency. A
|
|
785
|
+
// bounded stride (reachable only via a range) enumerates its fires instead.
|
|
786
|
+
const segment = stepSegment(ir.analyses.segments.hour);
|
|
787
|
+
const hours = confinedHourStride(segment) ?
|
|
788
|
+
everyNthHour(segment) :
|
|
789
|
+
atHours(segment.fires);
|
|
790
|
+
|
|
791
|
+
return (strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute, '') ??
|
|
792
|
+
countedPhrase(ir, 'minute', 'Minute', 'Minuten')) + ', ' + hours;
|
|
615
793
|
}
|
|
616
794
|
|
|
617
795
|
// Compact minutes across discrete hours: "in den Minuten 5 und 10, um 9, 17,
|
|
@@ -627,6 +805,15 @@ function renderCompactClockTimes(
|
|
|
627
805
|
const sep = opts.style.sep;
|
|
628
806
|
|
|
629
807
|
if (plan.fold) {
|
|
808
|
+
// An hour step (or arithmetic-progression hour list) under the single
|
|
809
|
+
// pinned minute reads as a cadence, not a wall of clock times. (Returns
|
|
810
|
+
// null for an irregular list or a range, which keep folding below.)
|
|
811
|
+
const cadence = hourCadence(ir, plan.minute);
|
|
812
|
+
|
|
813
|
+
if (cadence !== null) {
|
|
814
|
+
return cadence;
|
|
815
|
+
}
|
|
816
|
+
|
|
630
817
|
const hourly = fieldSegments(ir, 'hour')
|
|
631
818
|
.some((segment) => segment.kind === 'range');
|
|
632
819
|
|
|
@@ -647,7 +834,8 @@ function renderCompactClockTimes(
|
|
|
647
834
|
countedPhrase(ir, 'second', 'Sekunde', 'Sekunden') + ', ' : '';
|
|
648
835
|
|
|
649
836
|
return lead +
|
|
650
|
-
|
|
837
|
+
(strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute, '') ??
|
|
838
|
+
countedPhrase(ir, 'minute', 'Minute', 'Minuten')) + ', ' + hours;
|
|
651
839
|
}
|
|
652
840
|
|
|
653
841
|
// A repeating minute step, optionally within an hour window: "alle 5
|
|
@@ -660,9 +848,7 @@ function renderMinuteFrequency(
|
|
|
660
848
|
const segment = stepSegment(ir.analyses.segments.minute);
|
|
661
849
|
const sep = opts.style.sep;
|
|
662
850
|
const clean = cleanStep(segment, 60);
|
|
663
|
-
const base =
|
|
664
|
-
everyN(segment.interval, UNITS.minute) :
|
|
665
|
-
countedPhrase(ir, 'minute', 'Minute', 'Minuten') + ' jeder Stunde';
|
|
851
|
+
const base = stepClause(segment, UNITS.minute, 'jeder Stunde');
|
|
666
852
|
|
|
667
853
|
if (plan.hours.kind === 'window') {
|
|
668
854
|
const window = hourWindow(plan.hours.from, plan.hours.to, plan.hours.last,
|
|
@@ -696,6 +882,147 @@ function hourStepPhrase(ir: IR): string {
|
|
|
696
882
|
atHours(segment.fires);
|
|
697
883
|
}
|
|
698
884
|
|
|
885
|
+
// --- Hour-step cadence (the 24-cycle analog of renderStride). ---
|
|
886
|
+
|
|
887
|
+
// Speak an hour stride as a cadence with clock-time bounds: a clean stride
|
|
888
|
+
// from midnight is the bare cadence ("alle 2 Stunden"); a clean offset names
|
|
889
|
+
// only its start ("alle 6 Stunden ab 2 Uhr"); a bounded or non-tiling stride
|
|
890
|
+
// pins both clock-time endpoints ("alle 2 Stunden von 9 bis 17 Uhr") so the
|
|
891
|
+
// bounded set reads unambiguously. Used wherever an hour step (or
|
|
892
|
+
// arithmetic-progression hour list) would otherwise be cross-multiplied into a
|
|
893
|
+
// wall of clock times.
|
|
894
|
+
function hourStrideCadence(
|
|
895
|
+
stride: {start: number; interval: number; last: number}
|
|
896
|
+
): string {
|
|
897
|
+
const {start, interval, last} = stride;
|
|
898
|
+
const cadence = everyN(interval, UNITS.hour);
|
|
899
|
+
const tiles = 24 % interval === 0;
|
|
900
|
+
|
|
901
|
+
if (start === 0 && tiles) {
|
|
902
|
+
return cadence;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (start < interval && tiles) {
|
|
906
|
+
return cadence + ' ab ' + start + ' Uhr';
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return cadence + ' von ' + start + ' bis ' + last + ' Uhr';
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
913
|
+
// segment yields its {start, interval, last} directly; an all-single hour list
|
|
914
|
+
// yields one only when its values form a long-enough arithmetic progression
|
|
915
|
+
// (so an irregular list like 9,17 keeps enumerating). The IR is unchanged —
|
|
916
|
+
// the renderer recognizes the stride and speaks it as a cadence instead of the
|
|
917
|
+
// clock-time cross-product.
|
|
918
|
+
function hourStride(
|
|
919
|
+
ir: IR
|
|
920
|
+
): {start: number; interval: number; last: number} | null {
|
|
921
|
+
const segments = fieldSegments(ir, 'hour');
|
|
922
|
+
|
|
923
|
+
// A wildcard hour carries no segments (no discrete hours to stride over).
|
|
924
|
+
if (!segments) {
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
929
|
+
const segment = segments[0];
|
|
930
|
+
const start = segment.startToken === '*' ?
|
|
931
|
+
0 :
|
|
932
|
+
+segment.startToken.split('-')[0];
|
|
933
|
+
|
|
934
|
+
return {interval: segment.interval, last: segment.fires[
|
|
935
|
+
segment.fires.length - 1], start};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const values = singleValues(segments);
|
|
939
|
+
const step = values && arithmeticStep(values);
|
|
940
|
+
|
|
941
|
+
return step || null;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
945
|
+
// fills the minute (a "für eine Minute" frame at minute 0); a single 0 is just
|
|
946
|
+
// the top of the minute (no clause); anything else needs its own clause.
|
|
947
|
+
function subMinuteSecond(ir: IR): boolean {
|
|
948
|
+
return ir.pattern.second === '*' || ir.shapes.second === 'step';
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// The lead clause for an hour-cadence rendering: the second and the pinned
|
|
952
|
+
// minute, before the hour cadence. A pinned minute 0 folds in — a single,
|
|
953
|
+
// list, or range second is counted "jeder Stunde" (the minute-0 is the top of
|
|
954
|
+
// the hour), and a wildcard or sub-minute step second takes a "für eine
|
|
955
|
+
// Minute" frame (the whole minute-0 window). A non-zero minute is a real clock
|
|
956
|
+
// minute: the second leads with its own clause (if any), then the minute reads
|
|
957
|
+
// "in Minute M".
|
|
958
|
+
function hourCadenceLead(ir: IR, minute: number): string {
|
|
959
|
+
if (minute === 0) {
|
|
960
|
+
if (subMinuteSecond(ir)) {
|
|
961
|
+
return secondsClause(ir, 'jeder Minute') + ' für eine Minute';
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return secondsClause(ir, 'jeder Stunde');
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const minutePhrase = 'in Minute ' + minute;
|
|
968
|
+
|
|
969
|
+
// A single 0 second is just the top of the minute, so the minute leads
|
|
970
|
+
// alone; any other second prefixes its own clause.
|
|
971
|
+
if (ir.pattern.second === '0') {
|
|
972
|
+
return minutePhrase;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return secondsClause(ir, 'jeder Minute') + ', ' + minutePhrase;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
979
|
+
// pinned minute and a second as a cadence — the lead clause, then the hour
|
|
980
|
+
// cadence — instead of cross-multiplying the hours into a wall of clock times.
|
|
981
|
+
// Returns null when the hour is not a stride (an irregular list, a single
|
|
982
|
+
// hour, or a range), or when the cross-product is short enough that
|
|
983
|
+
// enumeration is no longer than the cadence: a meaningful second makes every
|
|
984
|
+
// clock time three digit-groups, so any stride is worth compacting; otherwise
|
|
985
|
+
// the stride must exceed the clock-time cap, the same point at which the core
|
|
986
|
+
// itself stops enumerating. The renderer returns the bare clause; the day
|
|
987
|
+
// frame is composed in `describe`. Renderer-only; the IR is unchanged.
|
|
988
|
+
function hourCadence(ir: IR, minute: number): string | null {
|
|
989
|
+
const stride = hourStride(ir);
|
|
990
|
+
|
|
991
|
+
if (!stride) {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const fires = (stride.last - stride.start) / stride.interval + 1;
|
|
996
|
+
|
|
997
|
+
if (ir.pattern.second === '0' && fires <= maxClockTimes) {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// A wildcard or sub-minute step second confined to minute 0 of a clean hour
|
|
1002
|
+
// stride is a confinement, not a juxtaposed cadence: it reads "für eine
|
|
1003
|
+
// Minute in jeder zweiten Stunde", reusing the every-Nth-hour idiom so the
|
|
1004
|
+
// minute-0 window is never heard as the bare hour cadence.
|
|
1005
|
+
const segment = fieldSegments(ir, 'hour')[0];
|
|
1006
|
+
const confined = minute === 0 && subMinuteSecond(ir) &&
|
|
1007
|
+
fieldSegments(ir, 'hour').length === 1 && segment.kind === 'step' &&
|
|
1008
|
+
confinedHourStride(segment);
|
|
1009
|
+
|
|
1010
|
+
if (confined) {
|
|
1011
|
+
return secondsClause(ir, 'jeder Minute') + ' für eine Minute ' +
|
|
1012
|
+
everyNthHour(segment);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return hourCadenceLead(ir, minute) + ', ' + hourStrideCadence(stride);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Whether an hour cadence applies to a plan with a single pinned minute — the
|
|
1019
|
+
// signal that the clause is a cadence, not a daily clock-time list, so the
|
|
1020
|
+
// "täglich" frame must not be added.
|
|
1021
|
+
function hourCadenceApplies(ir: IR): boolean {
|
|
1022
|
+
return ir.shapes.minute === 'single' &&
|
|
1023
|
+
hourCadence(ir, +ir.pattern.minute) !== null;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
699
1026
|
// An hourly window: "stündlich von 9 bis 17 Uhr", or every minute across it.
|
|
700
1027
|
function renderHourRange(
|
|
701
1028
|
ir: IR,
|
|
@@ -727,6 +1054,16 @@ function renderClockTimes(
|
|
|
727
1054
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
728
1055
|
opts: Opts
|
|
729
1056
|
): string {
|
|
1057
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
1058
|
+
// minute reads as a cadence rather than a cross-product of clock times.
|
|
1059
|
+
if (ir.shapes.minute === 'single') {
|
|
1060
|
+
const cadence = hourCadence(ir, +ir.pattern.minute);
|
|
1061
|
+
|
|
1062
|
+
if (cadence !== null) {
|
|
1063
|
+
return cadence;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
730
1067
|
return 'um ' + timesPhrase(plan.times, opts.style.sep);
|
|
731
1068
|
}
|
|
732
1069
|
|
|
@@ -808,6 +1145,13 @@ function isComposeMinuteZero(ir: IR): boolean {
|
|
|
808
1145
|
// hour step (rendered as its fire list "um 0, 5, … Uhr", not the cadence "alle
|
|
809
1146
|
// N Stunden"). A frequency clause already implies recurrence.
|
|
810
1147
|
function needsDailyFrame(ir: IR): boolean {
|
|
1148
|
+
// An hour cadence is a sub-daily frequency, not a daily clock-time list, so
|
|
1149
|
+
// it must not take the "täglich" frame ("alle 2 Stunden", not "täglich alle
|
|
1150
|
+
// 2 Stunden").
|
|
1151
|
+
if (hourCadenceApplies(ir)) {
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
811
1155
|
if (ir.plan.kind === 'clockTimes' || isComposeMinuteZero(ir)) {
|
|
812
1156
|
return true;
|
|
813
1157
|
}
|