cronli5 0.1.5 → 0.1.7
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 +71 -0
- package/README.md +2 -2
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +156 -27
- package/dist/cronli5.js +156 -27
- package/dist/lang/de.cjs +157 -36
- package/dist/lang/de.js +157 -36
- package/dist/lang/en.cjs +156 -27
- package/dist/lang/en.js +156 -27
- package/dist/lang/es.cjs +156 -18
- package/dist/lang/es.js +156 -18
- package/dist/lang/fi.cjs +128 -20
- package/dist/lang/fi.js +128 -20
- package/dist/lang/zh.cjs +126 -58
- package/dist/lang/zh.js +126 -58
- package/package.json +2 -2
- package/src/core/util.ts +52 -1
- package/src/lang/de/index.ts +331 -74
- package/src/lang/en/index.ts +327 -62
- package/src/lang/es/index.ts +306 -39
- package/src/lang/fi/index.ts +251 -41
- package/src/lang/zh/index.ts +246 -105
- package/types/core/util.d.ts +10 -1
package/src/lang/de/index.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
import {pad} from '../../core/format.js';
|
|
5
5
|
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
arithmeticStep, orderWeekdaysForDisplay, toFieldNumber
|
|
8
|
+
} from '../../core/util.js';
|
|
7
9
|
import type {Cronli5Options} from '../../types.js';
|
|
8
10
|
import type {
|
|
9
11
|
Field, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode, Segment
|
|
@@ -51,6 +53,12 @@ function everyN(interval: number, unit: Unit): string {
|
|
|
51
53
|
return 'alle ' + interval + ' ' + unit.plural;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
// Append a scope anchor to a clause, separated by a space; an empty anchor
|
|
57
|
+
// (a context that names that field in its own clause) leaves the clause bare.
|
|
58
|
+
function withAnchor(clause: string, anchor: string): string {
|
|
59
|
+
return anchor ? clause + ' ' + anchor : clause;
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
// The first segment of a step field, which the plan guarantees is step-kinded.
|
|
55
63
|
function stepSegment(segments: Segment[] | null): StepSegment {
|
|
56
64
|
return (segments as Segment[])[0] as StepSegment;
|
|
@@ -108,8 +116,9 @@ function stepClause(segment: StepSegment, unit: Unit, anchor: string): string {
|
|
|
108
116
|
const short = start !== 0 && segment.fires.length <= 3;
|
|
109
117
|
|
|
110
118
|
if (segment.startToken.indexOf('-') !== -1 || short) {
|
|
111
|
-
return
|
|
112
|
-
' ' +
|
|
119
|
+
return withAnchor(
|
|
120
|
+
'in den ' + unit.plural + ' ' + joinList(segment.fires.map(String)),
|
|
121
|
+
anchor);
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
return renderStride({
|
|
@@ -208,7 +217,9 @@ function weekdayRange(bounds: [string, string]): string {
|
|
|
208
217
|
|
|
209
218
|
// "montags", "montags bis freitags", "montags, mittwochs und freitags".
|
|
210
219
|
function weekdayQualifier(ir: IR): string {
|
|
211
|
-
|
|
220
|
+
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
221
|
+
// form. The IR stays canonical (Sunday=0). The helper flattens steps.
|
|
222
|
+
const segments = orderWeekdaysForDisplay(fieldSegments(ir, 'weekday'));
|
|
212
223
|
|
|
213
224
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
214
225
|
return weekdayRange(segments[0].bounds);
|
|
@@ -460,10 +471,21 @@ function countedPhrase(
|
|
|
460
471
|
return 'in den ' + plural + ' ' + joinList(fieldValues(ir, field));
|
|
461
472
|
}
|
|
462
473
|
|
|
463
|
-
// The seconds clause: "
|
|
464
|
-
//
|
|
474
|
+
// The minute scope for a seconds clause: "jeder Minute" only when the minute
|
|
475
|
+
// is a wildcard (the seconds really do fire in every minute). A restricted
|
|
476
|
+
// minute (single/list/range/step) is named by its own clause, so the seconds
|
|
477
|
+
// clause drops the scope — "jeder Minute" would otherwise contradict the fixed
|
|
478
|
+
// minute ("in Sekunde 30 jeder Minute, in Minute 30" fires at second 30 of
|
|
479
|
+
// minute 30, not every minute).
|
|
480
|
+
function minuteAnchor(ir: IR): string {
|
|
481
|
+
return ir.pattern.minute === '*' ? 'jeder Minute' : '';
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// The seconds clause: "alle 30 Sekunden" for a step, "in Sekunde 15 jeder
|
|
485
|
+
// Minute" under a wildcard minute, else the bare "in Sekunde 15" when the
|
|
486
|
+
// minute is fixed (its own clause names it).
|
|
465
487
|
function secondsLead(ir: IR): string {
|
|
466
|
-
return secondsClause(ir,
|
|
488
|
+
return secondsClause(ir, minuteAnchor(ir));
|
|
467
489
|
}
|
|
468
490
|
|
|
469
491
|
// The second clause counted against an arbitrary anchor. The anchor is "jeder
|
|
@@ -486,7 +508,7 @@ function secondsClause(ir: IR, anchor: string): string {
|
|
|
486
508
|
}
|
|
487
509
|
|
|
488
510
|
return strideFromSegments(segments as Segment[], UNITS.second, anchor) ??
|
|
489
|
-
countedPhrase(ir, 'second', 'Sekunde', 'Sekunden')
|
|
511
|
+
withAnchor(countedPhrase(ir, 'second', 'Sekunde', 'Sekunden'), anchor);
|
|
490
512
|
}
|
|
491
513
|
|
|
492
514
|
// A clock time that always shows its minutes: "9:00", "9:30".
|
|
@@ -670,6 +692,25 @@ function renderMinuteSpanInHour(
|
|
|
670
692
|
|
|
671
693
|
// Seconds composed with the rest: "in den Sekunden 0 und 30 jeder Minute, um
|
|
672
694
|
// 9:05 Uhr".
|
|
695
|
+
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
696
|
+
// cadences that read as contradictory ("jede Sekunde, alle 2 Minuten"). Bind
|
|
697
|
+
// them in the genitive ("jede Sekunde jeder zweiten Minute"), mirroring
|
|
698
|
+
// English. Other strides, a restricted hour, and an hour cadence keep the
|
|
699
|
+
// juxtaposed form.
|
|
700
|
+
function isEveryOtherMinuteSeconds(
|
|
701
|
+
ir: IR,
|
|
702
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
703
|
+
): boolean {
|
|
704
|
+
if (plan.rest.kind !== 'minuteFrequency' ||
|
|
705
|
+
ir.shapes.second !== 'wildcard' || ir.shapes.hour !== 'wildcard') {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const minuteStep = stepSegment(ir.analyses.segments.minute);
|
|
710
|
+
|
|
711
|
+
return minuteStep.startToken === '*' && minuteStep.interval === 2;
|
|
712
|
+
}
|
|
713
|
+
|
|
673
714
|
function renderComposeSeconds(
|
|
674
715
|
ir: IR,
|
|
675
716
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
@@ -682,7 +723,8 @@ function renderComposeSeconds(
|
|
|
682
723
|
if ((plan.rest.kind === 'clockTimes' ||
|
|
683
724
|
plan.rest.kind === 'compactClockTimes') &&
|
|
684
725
|
ir.shapes.minute === 'single') {
|
|
685
|
-
const
|
|
726
|
+
const minute = +ir.pattern.minute;
|
|
727
|
+
const cadence = hourCadence(ir, minute) ?? hourRangeCadence(ir, minute);
|
|
686
728
|
|
|
687
729
|
if (cadence !== null) {
|
|
688
730
|
return cadence;
|
|
@@ -699,21 +741,21 @@ function renderComposeSeconds(
|
|
|
699
741
|
clockMinuteGenitive(plan.rest.times, opts.style.sep);
|
|
700
742
|
}
|
|
701
743
|
|
|
702
|
-
// A wildcard second under a minute */2 with a wildcard hour
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
}
|
|
744
|
+
// A wildcard second under a minute */2 with a wildcard hour binds in the
|
|
745
|
+
// genitive ("jede Sekunde jeder zweiten Minute").
|
|
746
|
+
if (isEveryOtherMinuteSeconds(ir, plan)) {
|
|
747
|
+
return secondsLead(ir) + ' jeder zweiten Minute';
|
|
714
748
|
}
|
|
715
749
|
|
|
716
|
-
|
|
750
|
+
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
751
|
+
// leading clause, so the composer must not prepend a second lead that would
|
|
752
|
+
// double it. A wildcard or stepped second is not folded there (no
|
|
753
|
+
// clockSecond), so it still leads its own clause here.
|
|
754
|
+
const restOwnsLead = plan.rest.kind === 'compactClockTimes' &&
|
|
755
|
+
ir.analyses.clockSecond;
|
|
756
|
+
const lead = restOwnsLead ? '' : secondsLead(ir) + ', ';
|
|
757
|
+
|
|
758
|
+
return lead + render(ir, plan.rest, opts);
|
|
717
759
|
}
|
|
718
760
|
|
|
719
761
|
// True when a compose-seconds plan is a sub-minute second over a minute-0
|
|
@@ -752,18 +794,30 @@ function renderMinutesAcrossHours(
|
|
|
752
794
|
opts: Opts
|
|
753
795
|
): string {
|
|
754
796
|
const sep = opts.style.sep;
|
|
797
|
+
// A bounded or uneven hour stride reads as its endpoint-pinning cadence,
|
|
798
|
+
// not a wall of hour columns.
|
|
799
|
+
const cadence = unevenHourCadence(ir);
|
|
755
800
|
|
|
756
801
|
// The wildcard form means every minute *during* each hour: render windows.
|
|
757
802
|
if (plan.form === 'wildcard') {
|
|
758
|
-
return
|
|
803
|
+
return cadence ?
|
|
804
|
+
'jede Minute, ' + cadence :
|
|
805
|
+
'jede Minute ' + duringHours(ir, plan.times, sep);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const minuteLead =
|
|
809
|
+
strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute, '') ??
|
|
810
|
+
countedPhrase(ir, 'minute', 'Minute', 'Minuten');
|
|
811
|
+
|
|
812
|
+
if (cadence !== null) {
|
|
813
|
+
return minuteLead + ', ' + cadence;
|
|
759
814
|
}
|
|
760
815
|
|
|
761
816
|
const hours = plan.times.kind === 'fires' ?
|
|
762
817
|
atHours(plan.times.fires) :
|
|
763
818
|
joinList(hourSegmentParts(ir, 0, 0, sep));
|
|
764
819
|
|
|
765
|
-
return
|
|
766
|
-
countedPhrase(ir, 'minute', 'Minute', 'Minuten')) + ', ' + hours;
|
|
820
|
+
return minuteLead + ', ' + hours;
|
|
767
821
|
}
|
|
768
822
|
|
|
769
823
|
// A minute clause across a stepped hour range. A wildcard minute (a cadence)
|
|
@@ -774,6 +828,12 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
774
828
|
ir: IR,
|
|
775
829
|
plan: Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>
|
|
776
830
|
): string {
|
|
831
|
+
// A bounded or uneven hour stride reads as its endpoint-pinning cadence; an
|
|
832
|
+
// offset-clean stride keeps its "in jeder N-ten Stunde" confinement.
|
|
833
|
+
const cadence = unevenHourCadence(ir);
|
|
834
|
+
|
|
835
|
+
// A wildcard minute over a stepped hour is reached only for a clean stride (a
|
|
836
|
+
// bounded or uneven step routes through minutesAcrossHours instead).
|
|
777
837
|
if (plan.form === 'wildcard') {
|
|
778
838
|
return 'jede Minute ' +
|
|
779
839
|
everyNthHour(stepSegment(ir.analyses.segments.hour));
|
|
@@ -782,11 +842,11 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
782
842
|
// The minute (range or list) leads; the hour trails. A clean stride confines
|
|
783
843
|
// to "in jeder N-ten Stunde" — the same cadence the wildcard form and the
|
|
784
844
|
// minute-step compositions use, never a juxtaposed second frequency. A
|
|
785
|
-
// bounded
|
|
845
|
+
// bounded or uneven stride trails its endpoint-pinning cadence instead.
|
|
786
846
|
const segment = stepSegment(ir.analyses.segments.hour);
|
|
787
|
-
const hours = confinedHourStride(segment) ?
|
|
847
|
+
const hours = cadence ?? (confinedHourStride(segment) ?
|
|
788
848
|
everyNthHour(segment) :
|
|
789
|
-
atHours(segment.fires);
|
|
849
|
+
atHours(segment.fires));
|
|
790
850
|
|
|
791
851
|
return (strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute, '') ??
|
|
792
852
|
countedPhrase(ir, 'minute', 'Minute', 'Minuten')) + ', ' + hours;
|
|
@@ -805,10 +865,11 @@ function renderCompactClockTimes(
|
|
|
805
865
|
const sep = opts.style.sep;
|
|
806
866
|
|
|
807
867
|
if (plan.fold) {
|
|
808
|
-
// An hour step (or arithmetic-progression hour list) under the
|
|
809
|
-
// pinned minute reads as a cadence, not a wall of clock
|
|
810
|
-
// null for an irregular list
|
|
811
|
-
const cadence = hourCadence(ir, plan.minute)
|
|
868
|
+
// An hour step or range (or arithmetic-progression hour list) under the
|
|
869
|
+
// single pinned minute reads as a cadence or window, not a wall of clock
|
|
870
|
+
// times. (Returns null for an irregular list, which keeps folding below.)
|
|
871
|
+
const cadence = hourCadence(ir, plan.minute) ??
|
|
872
|
+
hourRangeCadence(ir, plan.minute);
|
|
812
873
|
|
|
813
874
|
if (cadence !== null) {
|
|
814
875
|
return cadence;
|
|
@@ -821,11 +882,12 @@ function renderCompactClockTimes(
|
|
|
821
882
|
joinList(hourSegmentParts(ir, plan.minute, ir.analyses.clockSecond, sep));
|
|
822
883
|
}
|
|
823
884
|
|
|
824
|
-
// A
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
885
|
+
// A bounded or uneven hour stride reads as its endpoint-pinning cadence; else
|
|
886
|
+
// a range among the hours reads as a window, otherwise a flat hour list.
|
|
887
|
+
const hours = unevenHourCadence(ir) ??
|
|
888
|
+
(fieldSegments(ir, 'hour').some((segment) => segment.kind === 'range') ?
|
|
889
|
+
joinList(hourSegmentParts(ir, 0, 0, sep)) :
|
|
890
|
+
atHours(hourFires(ir)));
|
|
829
891
|
|
|
830
892
|
// A folded second has no single clock time to attach to here, so it leads
|
|
831
893
|
// as its own clause ("in Sekunde 30, ..."). It is the bare second (not
|
|
@@ -848,17 +910,31 @@ function renderMinuteFrequency(
|
|
|
848
910
|
const segment = stepSegment(ir.analyses.segments.minute);
|
|
849
911
|
const sep = opts.style.sep;
|
|
850
912
|
const clean = cleanStep(segment, 60);
|
|
851
|
-
const base = stepClause(segment, UNITS.minute, 'jeder Stunde');
|
|
852
913
|
|
|
853
914
|
if (plan.hours.kind === 'window') {
|
|
915
|
+
// A single fixed hour (from === to) drops the "jeder Stunde" tail — the
|
|
916
|
+
// window names that one hour, so "jeder Stunde" (every hour) contradicts
|
|
917
|
+
// it. A range keeps it: the cadence truly repeats across each hour.
|
|
918
|
+
const singleHour = plan.hours.from === plan.hours.to;
|
|
919
|
+
const base = stepClause(segment, UNITS.minute,
|
|
920
|
+
singleHour ? '' : 'jeder Stunde');
|
|
854
921
|
const window = hourWindow(plan.hours.from, plan.hours.to, plan.hours.last,
|
|
855
922
|
sep);
|
|
856
923
|
|
|
857
924
|
return clean ? base + ' ' + window : base + ', ' + window;
|
|
858
925
|
}
|
|
859
926
|
|
|
927
|
+
const base = stepClause(segment, UNITS.minute, 'jeder Stunde');
|
|
928
|
+
|
|
860
929
|
if (plan.hours.kind === 'during') {
|
|
861
|
-
|
|
930
|
+
// A bounded or uneven hour stride confines the minute cadence to its own
|
|
931
|
+
// endpoint-pinning hour cadence ("alle 15 Minuten, alle 5 Stunden von 0 bis
|
|
932
|
+
// 20 Uhr").
|
|
933
|
+
const cadence = unevenHourCadence(ir);
|
|
934
|
+
|
|
935
|
+
return cadence ?
|
|
936
|
+
base + ', ' + cadence :
|
|
937
|
+
base + ' ' + duringHours(ir, plan.hours.times, sep);
|
|
862
938
|
}
|
|
863
939
|
|
|
864
940
|
if (plan.hours.kind === 'step') {
|
|
@@ -871,15 +947,50 @@ function renderMinuteFrequency(
|
|
|
871
947
|
return base;
|
|
872
948
|
}
|
|
873
949
|
|
|
874
|
-
// A stepped hour field as a phrase:
|
|
875
|
-
//
|
|
876
|
-
//
|
|
950
|
+
// A stepped hour field as a phrase: a clean stride from midnight is the bare
|
|
951
|
+
// cadence ("alle 2 Stunden"); an open offset-clean stride names only its start
|
|
952
|
+
// ("alle 2 Stunden ab 1 Uhr") since it wraps the day with no distinct
|
|
953
|
+
// endpoint; a bounded or uneven stride pins both ends ("alle 2 Stunden von 9
|
|
954
|
+
// bis 17 Uhr"). Shared by the bare hour step and the minute-step compositions.
|
|
955
|
+
// An explicitly bounded step (`a-b/n`) keeps its enumerated hours, matching
|
|
956
|
+
// en/fi/zh; only an OPEN step (`m/n`) reads as the wrapping cadence.
|
|
877
957
|
function hourStepPhrase(ir: IR): string {
|
|
958
|
+
const cadence = unevenHourCadence(ir);
|
|
959
|
+
|
|
960
|
+
if (cadence !== null) {
|
|
961
|
+
return cadence;
|
|
962
|
+
}
|
|
963
|
+
|
|
878
964
|
const segment = stepSegment(ir.analyses.segments.hour);
|
|
879
965
|
|
|
880
|
-
|
|
881
|
-
everyN(segment.interval, UNITS.hour)
|
|
882
|
-
|
|
966
|
+
if (cleanStep(segment, 24)) {
|
|
967
|
+
return everyN(segment.interval, UNITS.hour);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// An open offset-clean step (`m/n`, m < n dividing 24) wraps the day with no
|
|
971
|
+
// endpoint: name only its start, the cadence en/fi/zh and the compose paths
|
|
972
|
+
// already speak — never the enumerated hour list. A bounded `a-b/n` keeps its
|
|
973
|
+
// explicit hours.
|
|
974
|
+
const stride = openOffsetCleanStride(ir, segment);
|
|
975
|
+
|
|
976
|
+
return stride ? hourStrideCadence(stride) : atHours(segment.fires);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// The stride of an OPEN offset-clean hour step (`m/n`, m < n dividing 24),
|
|
980
|
+
// or null for any other step: such a step wraps the day with no endpoint and
|
|
981
|
+
// reads as the "alle N Stunden ab M Uhr" cadence. An explicitly bounded step
|
|
982
|
+
// (`a-b/n`, startToken carries a `-`) is excluded so it keeps its enumerated
|
|
983
|
+
// hours, matching en/fi/zh.
|
|
984
|
+
function openOffsetCleanStride(
|
|
985
|
+
ir: IR, segment: StepSegment
|
|
986
|
+
): {start: number; interval: number; last: number} | null {
|
|
987
|
+
if (segment.startToken.indexOf('-') !== -1) {
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const stride = hourStride(ir);
|
|
992
|
+
|
|
993
|
+
return stride && offsetCleanStride(stride) ? stride : null;
|
|
883
994
|
}
|
|
884
995
|
|
|
885
996
|
// --- Hour-step cadence (the 24-cycle analog of renderStride). ---
|
|
@@ -909,12 +1020,56 @@ function hourStrideCadence(
|
|
|
909
1020
|
return cadence + ' von ' + start + ' bis ' + last + ' Uhr';
|
|
910
1021
|
}
|
|
911
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
|
+
|
|
1038
|
+
const interval = values[1] - values[0];
|
|
1039
|
+
|
|
1040
|
+
if (interval < 2) {
|
|
1041
|
+
return null;
|
|
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;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
912
1068
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
913
1069
|
// segment yields its {start, interval, last} directly; an all-single hour list
|
|
914
|
-
// yields one only when its values form a
|
|
915
|
-
//
|
|
916
|
-
// the
|
|
917
|
-
// clock-time cross-product.
|
|
1070
|
+
// yields one only when its values form a step progression (so an irregular list
|
|
1071
|
+
// like 9,17 keeps enumerating). The IR is unchanged — the renderer recognizes
|
|
1072
|
+
// the stride and speaks it as a cadence, not the clock-time cross-product.
|
|
918
1073
|
function hourStride(
|
|
919
1074
|
ir: IR
|
|
920
1075
|
): {start: number; interval: number; last: number} | null {
|
|
@@ -927,6 +1082,13 @@ function hourStride(
|
|
|
927
1082
|
|
|
928
1083
|
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
929
1084
|
const segment = segments[0];
|
|
1085
|
+
|
|
1086
|
+
// A bounded step that fires only once (e.g. `9-10/5` -> just 9) is a single
|
|
1087
|
+
// value, not a stride: it has no interval to speak and no endpoint to pin.
|
|
1088
|
+
if (segment.fires.length < 2) {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
930
1092
|
const start = segment.startToken === '*' ?
|
|
931
1093
|
0 :
|
|
932
1094
|
+segment.startToken.split('-')[0];
|
|
@@ -936,9 +1098,25 @@ function hourStride(
|
|
|
936
1098
|
}
|
|
937
1099
|
|
|
938
1100
|
const values = singleValues(segments);
|
|
939
|
-
const step = values && arithmeticStep(values);
|
|
940
1101
|
|
|
941
|
-
return
|
|
1102
|
+
return values && hourListStride(values);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// The bounded cadence for an hour stride that pins both clock-time endpoints,
|
|
1106
|
+
// or null when the hour is not such a stride. The core rewrites a uneven step
|
|
1107
|
+
// to its fire list, so a minute window/list/step crossed with it lands in the
|
|
1108
|
+
// enumerating list paths; there the bounded hour reads better as its cadence
|
|
1109
|
+
// ("…, alle 5 Stunden von 0 bis 20 Uhr") than as a wall of clock times. An
|
|
1110
|
+
// offset-clean stride keeps its existing confinement form, so only the
|
|
1111
|
+
// endpoint-bearing case routes here.
|
|
1112
|
+
function unevenHourCadence(ir: IR): string | null {
|
|
1113
|
+
const stride = hourStride(ir);
|
|
1114
|
+
|
|
1115
|
+
if (!stride || offsetCleanStride(stride)) {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return hourStrideCadence(stride);
|
|
942
1120
|
}
|
|
943
1121
|
|
|
944
1122
|
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
@@ -958,7 +1136,7 @@ function subMinuteSecond(ir: IR): boolean {
|
|
|
958
1136
|
function hourCadenceLead(ir: IR, minute: number): string {
|
|
959
1137
|
if (minute === 0) {
|
|
960
1138
|
if (subMinuteSecond(ir)) {
|
|
961
|
-
return secondsClause(ir,
|
|
1139
|
+
return withAnchor(secondsClause(ir, minuteAnchor(ir)), 'für eine Minute');
|
|
962
1140
|
}
|
|
963
1141
|
|
|
964
1142
|
return secondsClause(ir, 'jeder Stunde');
|
|
@@ -972,7 +1150,7 @@ function hourCadenceLead(ir: IR, minute: number): string {
|
|
|
972
1150
|
return minutePhrase;
|
|
973
1151
|
}
|
|
974
1152
|
|
|
975
|
-
return secondsClause(ir,
|
|
1153
|
+
return secondsClause(ir, minuteAnchor(ir)) + ', ' + minutePhrase;
|
|
976
1154
|
}
|
|
977
1155
|
|
|
978
1156
|
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
@@ -994,7 +1172,13 @@ function hourCadence(ir: IR, minute: number): string | null {
|
|
|
994
1172
|
|
|
995
1173
|
const fires = (stride.last - stride.start) / stride.interval + 1;
|
|
996
1174
|
|
|
997
|
-
|
|
1175
|
+
// A short stride that spells out as few clock times stays an enumeration only
|
|
1176
|
+
// when it wraps cleanly (an offset-clean stride with no endpoint): the bare
|
|
1177
|
+
// or "ab" form is no shorter than the list. A bounded or uneven stride has no
|
|
1178
|
+
// clean wrap, so its endpoint-pinning cadence ("alle 5 Stunden von 0 bis 20
|
|
1179
|
+
// Uhr") reads better however short.
|
|
1180
|
+
if (ir.pattern.second === '0' && fires <= maxClockTimes &&
|
|
1181
|
+
offsetCleanStride(stride)) {
|
|
998
1182
|
return null;
|
|
999
1183
|
}
|
|
1000
1184
|
|
|
@@ -1008,19 +1192,72 @@ function hourCadence(ir: IR, minute: number): string | null {
|
|
|
1008
1192
|
confinedHourStride(segment);
|
|
1009
1193
|
|
|
1010
1194
|
if (confined) {
|
|
1011
|
-
return secondsClause(ir,
|
|
1012
|
-
everyNthHour(segment);
|
|
1195
|
+
return withAnchor(secondsClause(ir, minuteAnchor(ir)), 'für eine Minute') +
|
|
1196
|
+
' ' + everyNthHour(segment);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
1200
|
+
// lead clause to fold in, so the bounded cadence stands on its own ("alle 5
|
|
1201
|
+
// Stunden von 0 bis 20 Uhr").
|
|
1202
|
+
if (minute === 0 && ir.pattern.second === '0') {
|
|
1203
|
+
return hourStrideCadence(stride);
|
|
1013
1204
|
}
|
|
1014
1205
|
|
|
1015
1206
|
return hourCadenceLead(ir, minute) + ', ' + hourStrideCadence(stride);
|
|
1016
1207
|
}
|
|
1017
1208
|
|
|
1018
|
-
// Whether an hour cadence applies to a plan with a single
|
|
1019
|
-
// signal that the clause is a cadence, not a daily
|
|
1020
|
-
// "täglich" frame must not be added.
|
|
1209
|
+
// Whether an hour cadence or hour-range window applies to a plan with a single
|
|
1210
|
+
// pinned minute — the signal that the clause is a cadence/window, not a daily
|
|
1211
|
+
// clock-time list, so the "täglich" frame must not be added.
|
|
1021
1212
|
function hourCadenceApplies(ir: IR): boolean {
|
|
1022
|
-
|
|
1023
|
-
|
|
1213
|
+
if (ir.shapes.minute !== 'single') {
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const minute = +ir.pattern.minute;
|
|
1218
|
+
|
|
1219
|
+
return hourCadence(ir, minute) !== null ||
|
|
1220
|
+
hourRangeCadence(ir, minute) !== null;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Whether the hour field is a range — or a list whose segments include a
|
|
1224
|
+
// range — and so forms a window rather than a cross-product of clock times.
|
|
1225
|
+
// A pure single-value list (9,17) has no range to span and still enumerates;
|
|
1226
|
+
// a step is handled by hourStride/hourCadence.
|
|
1227
|
+
function hasHourWindow(ir: IR): boolean {
|
|
1228
|
+
const segments = fieldSegments(ir, 'hour');
|
|
1229
|
+
|
|
1230
|
+
return !!segments && segments.some(function range(segment) {
|
|
1231
|
+
return segment.kind === 'range';
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Render an hour range (or a list whose segments include a range) under
|
|
1236
|
+
// minute 0 and a meaningful second as the hour-range window — the lead clause,
|
|
1237
|
+
// then "von 9 bis 17 Uhr" (and any non-contiguous hour as "und um 22 Uhr") —
|
|
1238
|
+
// instead of cross-multiplying the hours into a wall of clock times. The
|
|
1239
|
+
// hour-RANGE analog of hourCadence; returns the bare clause (the day frame is
|
|
1240
|
+
// suppressed by hourCadenceApplies). Returns null when the hour has no range,
|
|
1241
|
+
// when the minute is non-zero (a real clock minute the existing window form
|
|
1242
|
+
// already speaks), or when a plain :00 set carries no clause. Renderer-only;
|
|
1243
|
+
// the IR is unchanged.
|
|
1244
|
+
function hourRangeCadence(ir: IR, minute: number): string | null {
|
|
1245
|
+
if (minute !== 0 || !hasHourWindow(ir) || ir.pattern.second === '0') {
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
return hourCadenceLead(ir, minute) + ', ' + hourRangeWindowTail(ir);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// The hour-range window as a cadence tail at the top of each hour: each range
|
|
1253
|
+
// segment is "von X bis Y Uhr", any non-contiguous hour is "um Z Uhr", joined
|
|
1254
|
+
// — the same parts the bare "stündlich von 9 bis 17 Uhr" window forms, minus
|
|
1255
|
+
// the "stündlich" prefix the lead replaces. The minute has folded into the
|
|
1256
|
+
// lead, so the parts close on the top of their final hour.
|
|
1257
|
+
function hourRangeWindowTail(ir: IR): string {
|
|
1258
|
+
// Minute 0 with a falsy second renders each part as a bare hour ("von 9 bis
|
|
1259
|
+
// 17 Uhr", "um 22 Uhr"); the separator is unused in that path.
|
|
1260
|
+
return joinList(hourSegmentParts(ir, 0, 0, ':'));
|
|
1024
1261
|
}
|
|
1025
1262
|
|
|
1026
1263
|
// An hourly window: "stündlich von 9 bis 17 Uhr", or every minute across it.
|
|
@@ -1029,11 +1266,14 @@ function renderHourRange(
|
|
|
1029
1266
|
plan: Extract<PlanNode, {kind: 'hourRange'}>,
|
|
1030
1267
|
opts: Opts
|
|
1031
1268
|
): string {
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
|
|
1036
|
-
|
|
1269
|
+
// The close lands on the top of the final hour (minute 0) unless the minute
|
|
1270
|
+
// genuinely runs to the end of that hour — i.e. a wildcard minute, which
|
|
1271
|
+
// fills every minute and states no separate clause. A pinned/listed/ranged
|
|
1272
|
+
// minute is named in its own lead clause, so folding it into the close too
|
|
1273
|
+
// would read as a span ("bis 17:05 Uhr") that contradicts the minute clause;
|
|
1274
|
+
// the window stays bare ("bis 17 Uhr").
|
|
1275
|
+
const last = plan.minuteForm === 'wildcard' ? plan.boundMinute ?? 0 : 0;
|
|
1276
|
+
const window = hourWindow(plan.from, plan.to, last, opts.style.sep);
|
|
1037
1277
|
|
|
1038
1278
|
if (plan.minuteForm === 'wildcard') {
|
|
1039
1279
|
return 'jede Minute ' + window;
|
|
@@ -1043,9 +1283,15 @@ function renderHourRange(
|
|
|
1043
1283
|
return 'stündlich ' + window;
|
|
1044
1284
|
}
|
|
1045
1285
|
|
|
1046
|
-
// A non-zero single minute ('lead') or a minute range leads the window.
|
|
1047
|
-
|
|
1048
|
-
|
|
1286
|
+
// A non-zero single minute ('lead') or a minute range leads the window. A
|
|
1287
|
+
// non-uniform minute step the core enumerated to a fire list reads as its
|
|
1288
|
+
// bounded cadence ("alle 2 Minuten von Minute 3 bis 59 jeder Stunde") instead
|
|
1289
|
+
// of the wall of fires; an irregular list or a single minute keeps the
|
|
1290
|
+
// counted form.
|
|
1291
|
+
return (strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute,
|
|
1292
|
+
'jeder Stunde') ??
|
|
1293
|
+
countedPhrase(ir, 'minute', 'Minute', 'Minuten') + ' jeder Stunde') +
|
|
1294
|
+
', ' + window;
|
|
1049
1295
|
}
|
|
1050
1296
|
|
|
1051
1297
|
// One or more clock times: "um 9 Uhr", "um 14:30 Uhr", "um 9 und 17 Uhr".
|
|
@@ -1054,10 +1300,12 @@ function renderClockTimes(
|
|
|
1054
1300
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
1055
1301
|
opts: Opts
|
|
1056
1302
|
): string {
|
|
1057
|
-
// An hour step (or arithmetic-progression hour list) under a single
|
|
1058
|
-
// minute reads as a cadence rather than a cross-product of
|
|
1303
|
+
// An hour step or range (or arithmetic-progression hour list) under a single
|
|
1304
|
+
// pinned minute reads as a cadence or window rather than a cross-product of
|
|
1305
|
+
// clock times.
|
|
1059
1306
|
if (ir.shapes.minute === 'single') {
|
|
1060
|
-
const
|
|
1307
|
+
const minute = +ir.pattern.minute;
|
|
1308
|
+
const cadence = hourCadence(ir, minute) ?? hourRangeCadence(ir, minute);
|
|
1061
1309
|
|
|
1062
1310
|
if (cadence !== null) {
|
|
1063
1311
|
return cadence;
|
|
@@ -1156,8 +1404,17 @@ function needsDailyFrame(ir: IR): boolean {
|
|
|
1156
1404
|
return true;
|
|
1157
1405
|
}
|
|
1158
1406
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1407
|
+
if (ir.plan.kind !== 'hourStep') {
|
|
1408
|
+
return false;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// An hour step rendered as a cadence ("alle N Stunden [ab M Uhr]") is a
|
|
1412
|
+
// frequency, not a daily clock-time list, so it takes no "täglich" frame —
|
|
1413
|
+
// only a bounded `a-b/n` step that enumerates its hours ("um 1, 3, … Uhr")
|
|
1414
|
+
// needs the recurring frame.
|
|
1415
|
+
const segment = stepSegment(ir.analyses.segments.hour);
|
|
1416
|
+
|
|
1417
|
+
return !cleanStep(segment, 24) && !openOffsetCleanStride(ir, segment);
|
|
1161
1418
|
}
|
|
1162
1419
|
|
|
1163
1420
|
function render(ir: IR, plan: PlanNode, opts: Opts): string {
|