cronli5 0.1.6 → 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 +43 -0
- package/README.md +2 -2
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +39 -7
- package/dist/cronli5.js +39 -7
- package/dist/lang/de.cjs +63 -17
- package/dist/lang/de.js +63 -17
- package/dist/lang/en.cjs +39 -7
- package/dist/lang/en.js +39 -7
- package/dist/lang/es.cjs +68 -5
- package/dist/lang/es.js +68 -5
- package/dist/lang/fi.cjs +21 -1
- package/dist/lang/fi.js +21 -1
- package/dist/lang/zh.cjs +38 -7
- package/dist/lang/zh.js +38 -7
- package/package.json +1 -1
- package/src/core/util.ts +52 -1
- package/src/lang/de/index.ts +95 -25
- package/src/lang/en/index.ts +47 -16
- package/src/lang/es/index.ts +85 -9
- package/src/lang/fi/index.ts +6 -2
- package/src/lang/zh/index.ts +44 -18
- package/types/core/util.d.ts +10 -1
package/dist/lang/zh.cjs
CHANGED
|
@@ -44,6 +44,26 @@ function arithmeticStep(values) {
|
|
|
44
44
|
}
|
|
45
45
|
return { start: values[0], interval, last: values[values.length - 1] };
|
|
46
46
|
}
|
|
47
|
+
function weekdayDisplayKey(value) {
|
|
48
|
+
return value === 0 ? 7 : value;
|
|
49
|
+
}
|
|
50
|
+
function orderWeekdaysForDisplay(segments) {
|
|
51
|
+
const flattened = segments.flatMap(function flat(segment) {
|
|
52
|
+
return segment.kind === "step" ? segment.fires.map(function single(value) {
|
|
53
|
+
return { kind: "single", value: "" + value };
|
|
54
|
+
}) : [segment];
|
|
55
|
+
});
|
|
56
|
+
function key(segment) {
|
|
57
|
+
return segment.kind === "range" ? weekdayDisplayKey(+segment.bounds[0]) : weekdayDisplayKey(+segment.value);
|
|
58
|
+
}
|
|
59
|
+
return flattened.map(function index(segment, position) {
|
|
60
|
+
return [segment, position];
|
|
61
|
+
}).sort(function byDisplayKey(a, b) {
|
|
62
|
+
return key(a[0]) - key(b[0]) || a[1] - b[1];
|
|
63
|
+
}).map(function unwrap(pair) {
|
|
64
|
+
return pair[0];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
47
67
|
function toFieldNumber(token, numberMap) {
|
|
48
68
|
return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
|
|
49
69
|
}
|
|
@@ -384,6 +404,15 @@ function hourCadencePhrase(ir) {
|
|
|
384
404
|
anchor: ""
|
|
385
405
|
});
|
|
386
406
|
}
|
|
407
|
+
function minuteZeroConfinement(ir, stride, prefix) {
|
|
408
|
+
if (stride.interval === 2 && stride.start === 0) {
|
|
409
|
+
return "\u5728\u5076\u6570\u5C0F\u65F60\u5206" + secondTail(ir);
|
|
410
|
+
}
|
|
411
|
+
if (prefix.indexOf("\u4ECE") !== -1) {
|
|
412
|
+
return prefix + "0\u5206" + secondTail(ir);
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
387
416
|
function hourCadence(ir) {
|
|
388
417
|
const stride = hourStride(ir);
|
|
389
418
|
if (!stride) {
|
|
@@ -400,7 +429,7 @@ function hourCadence(ir) {
|
|
|
400
429
|
const minute = +ir.pattern.minute;
|
|
401
430
|
const subMinute = ir.pattern.second === "*" || ir.shapes.second === "step";
|
|
402
431
|
if (minute === 0 && subMinute) {
|
|
403
|
-
return
|
|
432
|
+
return minuteZeroConfinement(ir, stride, prefix);
|
|
404
433
|
}
|
|
405
434
|
if (minute === 0) {
|
|
406
435
|
return prefix + "0\u5206" + secondTail(ir);
|
|
@@ -634,7 +663,7 @@ function quartzDate(token, monthPrefix) {
|
|
|
634
663
|
if (token.startsWith("L-")) {
|
|
635
664
|
return monthPrefix + "\u6700\u540E\u7B2C" + token.slice(2) + "\u5929";
|
|
636
665
|
}
|
|
637
|
-
return "\u6700\u63A5\u8FD1" + token.slice(0, -1) + "\u65E5\u7684\u5DE5\u4F5C\u65E5";
|
|
666
|
+
return monthPrefix + "\u6700\u63A5\u8FD1" + token.slice(0, -1) + "\u65E5\u7684\u5DE5\u4F5C\u65E5";
|
|
638
667
|
}
|
|
639
668
|
function datePhrase(ir) {
|
|
640
669
|
const month = monthPhrase(ir);
|
|
@@ -648,7 +677,11 @@ function datePhrase(ir) {
|
|
|
648
677
|
if (ir.shapes.date === "step") {
|
|
649
678
|
return month + cadence(stepSegment(ir, "date").interval, "\u5929");
|
|
650
679
|
}
|
|
651
|
-
|
|
680
|
+
if (!month) {
|
|
681
|
+
return "\u6BCF\u6708" + dayList(ir);
|
|
682
|
+
}
|
|
683
|
+
const monthMulti = ir.shapes.month === "range" || ir.shapes.month === "list";
|
|
684
|
+
return month + (monthMulti ? "\uFF0C" : "") + dayList(ir);
|
|
652
685
|
}
|
|
653
686
|
function dateCore(ir, quartzPrefix) {
|
|
654
687
|
if (ir.shapes.date === "quartz") {
|
|
@@ -690,10 +723,8 @@ function weekdayPhrase(ir, orContext, monthPrefix) {
|
|
|
690
723
|
return "\u6BCF" + weekdayName(from) + "\u81F3" + weekdayName(to);
|
|
691
724
|
}
|
|
692
725
|
const days = [];
|
|
693
|
-
segs.forEach(function expand(seg) {
|
|
694
|
-
if (seg.kind === "
|
|
695
|
-
days.push(...seg.fires);
|
|
696
|
-
} else if (seg.kind === "single") {
|
|
726
|
+
orderWeekdaysForDisplay(segs).forEach(function expand(seg) {
|
|
727
|
+
if (seg.kind === "single") {
|
|
697
728
|
days.push(toFieldNumber(seg.value, weekdayNumbers));
|
|
698
729
|
}
|
|
699
730
|
});
|
package/dist/lang/zh.js
CHANGED
|
@@ -18,6 +18,26 @@ function arithmeticStep(values) {
|
|
|
18
18
|
}
|
|
19
19
|
return { start: values[0], interval, last: values[values.length - 1] };
|
|
20
20
|
}
|
|
21
|
+
function weekdayDisplayKey(value) {
|
|
22
|
+
return value === 0 ? 7 : value;
|
|
23
|
+
}
|
|
24
|
+
function orderWeekdaysForDisplay(segments) {
|
|
25
|
+
const flattened = segments.flatMap(function flat(segment) {
|
|
26
|
+
return segment.kind === "step" ? segment.fires.map(function single(value) {
|
|
27
|
+
return { kind: "single", value: "" + value };
|
|
28
|
+
}) : [segment];
|
|
29
|
+
});
|
|
30
|
+
function key(segment) {
|
|
31
|
+
return segment.kind === "range" ? weekdayDisplayKey(+segment.bounds[0]) : weekdayDisplayKey(+segment.value);
|
|
32
|
+
}
|
|
33
|
+
return flattened.map(function index(segment, position) {
|
|
34
|
+
return [segment, position];
|
|
35
|
+
}).sort(function byDisplayKey(a, b) {
|
|
36
|
+
return key(a[0]) - key(b[0]) || a[1] - b[1];
|
|
37
|
+
}).map(function unwrap(pair) {
|
|
38
|
+
return pair[0];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
21
41
|
function toFieldNumber(token, numberMap) {
|
|
22
42
|
return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
|
|
23
43
|
}
|
|
@@ -358,6 +378,15 @@ function hourCadencePhrase(ir) {
|
|
|
358
378
|
anchor: ""
|
|
359
379
|
});
|
|
360
380
|
}
|
|
381
|
+
function minuteZeroConfinement(ir, stride, prefix) {
|
|
382
|
+
if (stride.interval === 2 && stride.start === 0) {
|
|
383
|
+
return "\u5728\u5076\u6570\u5C0F\u65F60\u5206" + secondTail(ir);
|
|
384
|
+
}
|
|
385
|
+
if (prefix.indexOf("\u4ECE") !== -1) {
|
|
386
|
+
return prefix + "0\u5206" + secondTail(ir);
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
361
390
|
function hourCadence(ir) {
|
|
362
391
|
const stride = hourStride(ir);
|
|
363
392
|
if (!stride) {
|
|
@@ -374,7 +403,7 @@ function hourCadence(ir) {
|
|
|
374
403
|
const minute = +ir.pattern.minute;
|
|
375
404
|
const subMinute = ir.pattern.second === "*" || ir.shapes.second === "step";
|
|
376
405
|
if (minute === 0 && subMinute) {
|
|
377
|
-
return
|
|
406
|
+
return minuteZeroConfinement(ir, stride, prefix);
|
|
378
407
|
}
|
|
379
408
|
if (minute === 0) {
|
|
380
409
|
return prefix + "0\u5206" + secondTail(ir);
|
|
@@ -608,7 +637,7 @@ function quartzDate(token, monthPrefix) {
|
|
|
608
637
|
if (token.startsWith("L-")) {
|
|
609
638
|
return monthPrefix + "\u6700\u540E\u7B2C" + token.slice(2) + "\u5929";
|
|
610
639
|
}
|
|
611
|
-
return "\u6700\u63A5\u8FD1" + token.slice(0, -1) + "\u65E5\u7684\u5DE5\u4F5C\u65E5";
|
|
640
|
+
return monthPrefix + "\u6700\u63A5\u8FD1" + token.slice(0, -1) + "\u65E5\u7684\u5DE5\u4F5C\u65E5";
|
|
612
641
|
}
|
|
613
642
|
function datePhrase(ir) {
|
|
614
643
|
const month = monthPhrase(ir);
|
|
@@ -622,7 +651,11 @@ function datePhrase(ir) {
|
|
|
622
651
|
if (ir.shapes.date === "step") {
|
|
623
652
|
return month + cadence(stepSegment(ir, "date").interval, "\u5929");
|
|
624
653
|
}
|
|
625
|
-
|
|
654
|
+
if (!month) {
|
|
655
|
+
return "\u6BCF\u6708" + dayList(ir);
|
|
656
|
+
}
|
|
657
|
+
const monthMulti = ir.shapes.month === "range" || ir.shapes.month === "list";
|
|
658
|
+
return month + (monthMulti ? "\uFF0C" : "") + dayList(ir);
|
|
626
659
|
}
|
|
627
660
|
function dateCore(ir, quartzPrefix) {
|
|
628
661
|
if (ir.shapes.date === "quartz") {
|
|
@@ -664,10 +697,8 @@ function weekdayPhrase(ir, orContext, monthPrefix) {
|
|
|
664
697
|
return "\u6BCF" + weekdayName(from) + "\u81F3" + weekdayName(to);
|
|
665
698
|
}
|
|
666
699
|
const days = [];
|
|
667
|
-
segs.forEach(function expand(seg) {
|
|
668
|
-
if (seg.kind === "
|
|
669
|
-
days.push(...seg.fires);
|
|
670
|
-
} else if (seg.kind === "single") {
|
|
700
|
+
orderWeekdaysForDisplay(segs).forEach(function expand(seg) {
|
|
701
|
+
if (seg.kind === "single") {
|
|
671
702
|
days.push(toFieldNumber(seg.value, weekdayNumbers));
|
|
672
703
|
}
|
|
673
704
|
});
|
package/package.json
CHANGED
package/src/core/util.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Small shared utilities for the core.
|
|
2
2
|
|
|
3
|
+
import type {Segment} from './ir.js';
|
|
4
|
+
|
|
3
5
|
function includes(str: string | number, sub: string): boolean {
|
|
4
6
|
return ('' + str).indexOf(sub) !== -1;
|
|
5
7
|
}
|
|
@@ -44,6 +46,54 @@ function arithmeticStep(values: number[]):
|
|
|
44
46
|
return {start: values[0], interval, last: values[values.length - 1]};
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
// The display sort key for a canonical weekday number: Monday (1) first,
|
|
50
|
+
// Sunday (0) last. The IR keeps Sunday=0 canonical; this is display-only.
|
|
51
|
+
function weekdayDisplayKey(value: number): number {
|
|
52
|
+
return value === 0 ? 7 : value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A weekday display segment: a single day or a (possibly wrap) range. Steps
|
|
56
|
+
// are flattened away into singles before sorting, so the result is only these
|
|
57
|
+
// two kinds; each renderer turns them into names exactly as it does today.
|
|
58
|
+
type WeekdaySegment =
|
|
59
|
+
| {kind: 'single'; value: string}
|
|
60
|
+
| {kind: 'range'; bounds: [string, string]};
|
|
61
|
+
|
|
62
|
+
// Reorder weekday segments Monday-first (Sunday last) for display, so a weekend
|
|
63
|
+
// list reads "Saturday and Sunday" rather than the canonical Sunday-first
|
|
64
|
+
// "Sunday and Saturday". Display-only: the IR / canonical order is unchanged (a
|
|
65
|
+
// fresh array is returned). A step expands to its fires as singles so the days
|
|
66
|
+
// sort into the list; a range stays one unit and keeps its own bounds order (a
|
|
67
|
+
// wrap range is not reordered into a list), sorting by its opening bound — so a
|
|
68
|
+
// lone range sorts to a one-element list and is unchanged. The sort is stable,
|
|
69
|
+
// so equal opening days keep input order.
|
|
70
|
+
function orderWeekdaysForDisplay(segments: Segment[]): WeekdaySegment[] {
|
|
71
|
+
const flattened: WeekdaySegment[] = segments.flatMap(function flat(segment) {
|
|
72
|
+
return segment.kind === 'step' ?
|
|
73
|
+
segment.fires.map(function single(value): WeekdaySegment {
|
|
74
|
+
return {kind: 'single', value: '' + value};
|
|
75
|
+
}) :
|
|
76
|
+
[segment];
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
function key(segment: WeekdaySegment): number {
|
|
80
|
+
return segment.kind === 'range' ?
|
|
81
|
+
weekdayDisplayKey(+segment.bounds[0]) :
|
|
82
|
+
weekdayDisplayKey(+segment.value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return flattened
|
|
86
|
+
.map(function index(segment, position): [WeekdaySegment, number] {
|
|
87
|
+
return [segment, position];
|
|
88
|
+
})
|
|
89
|
+
.sort(function byDisplayKey(a, b): number {
|
|
90
|
+
return key(a[0]) - key(b[0]) || a[1] - b[1];
|
|
91
|
+
})
|
|
92
|
+
.map(function unwrap(pair): WeekdaySegment {
|
|
93
|
+
return pair[0];
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
47
97
|
// Resolve a numeric or named field token (e.g. '5' or 'FRI') to its number.
|
|
48
98
|
function toFieldNumber(
|
|
49
99
|
token: string,
|
|
@@ -54,5 +104,6 @@ function toFieldNumber(
|
|
|
54
104
|
return isNonNegativeInteger(token) ? +token : numberMap![token.toUpperCase()];
|
|
55
105
|
}
|
|
56
106
|
export {
|
|
57
|
-
arithmeticStep, includes, isNonNegativeInteger,
|
|
107
|
+
arithmeticStep, includes, isNonNegativeInteger, orderWeekdaysForDisplay,
|
|
108
|
+
toFieldNumber, unique
|
|
58
109
|
};
|
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".
|
|
@@ -888,15 +910,22 @@ function renderMinuteFrequency(
|
|
|
888
910
|
const segment = stepSegment(ir.analyses.segments.minute);
|
|
889
911
|
const sep = opts.style.sep;
|
|
890
912
|
const clean = cleanStep(segment, 60);
|
|
891
|
-
const base = stepClause(segment, UNITS.minute, 'jeder Stunde');
|
|
892
913
|
|
|
893
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');
|
|
894
921
|
const window = hourWindow(plan.hours.from, plan.hours.to, plan.hours.last,
|
|
895
922
|
sep);
|
|
896
923
|
|
|
897
924
|
return clean ? base + ' ' + window : base + ', ' + window;
|
|
898
925
|
}
|
|
899
926
|
|
|
927
|
+
const base = stepClause(segment, UNITS.minute, 'jeder Stunde');
|
|
928
|
+
|
|
900
929
|
if (plan.hours.kind === 'during') {
|
|
901
930
|
// A bounded or uneven hour stride confines the minute cadence to its own
|
|
902
931
|
// endpoint-pinning hour cadence ("alle 15 Minuten, alle 5 Stunden von 0 bis
|
|
@@ -918,9 +947,13 @@ function renderMinuteFrequency(
|
|
|
918
947
|
return base;
|
|
919
948
|
}
|
|
920
949
|
|
|
921
|
-
// A stepped hour field as a phrase:
|
|
922
|
-
// cadence
|
|
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
|
|
923
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.
|
|
924
957
|
function hourStepPhrase(ir: IR): string {
|
|
925
958
|
const cadence = unevenHourCadence(ir);
|
|
926
959
|
|
|
@@ -930,9 +963,34 @@ function hourStepPhrase(ir: IR): string {
|
|
|
930
963
|
|
|
931
964
|
const segment = stepSegment(ir.analyses.segments.hour);
|
|
932
965
|
|
|
933
|
-
|
|
934
|
-
everyN(segment.interval, UNITS.hour)
|
|
935
|
-
|
|
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;
|
|
936
994
|
}
|
|
937
995
|
|
|
938
996
|
// --- Hour-step cadence (the 24-cycle analog of renderStride). ---
|
|
@@ -1078,7 +1136,7 @@ function subMinuteSecond(ir: IR): boolean {
|
|
|
1078
1136
|
function hourCadenceLead(ir: IR, minute: number): string {
|
|
1079
1137
|
if (minute === 0) {
|
|
1080
1138
|
if (subMinuteSecond(ir)) {
|
|
1081
|
-
return secondsClause(ir,
|
|
1139
|
+
return withAnchor(secondsClause(ir, minuteAnchor(ir)), 'für eine Minute');
|
|
1082
1140
|
}
|
|
1083
1141
|
|
|
1084
1142
|
return secondsClause(ir, 'jeder Stunde');
|
|
@@ -1092,7 +1150,7 @@ function hourCadenceLead(ir: IR, minute: number): string {
|
|
|
1092
1150
|
return minutePhrase;
|
|
1093
1151
|
}
|
|
1094
1152
|
|
|
1095
|
-
return secondsClause(ir,
|
|
1153
|
+
return secondsClause(ir, minuteAnchor(ir)) + ', ' + minutePhrase;
|
|
1096
1154
|
}
|
|
1097
1155
|
|
|
1098
1156
|
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
@@ -1134,8 +1192,8 @@ function hourCadence(ir: IR, minute: number): string | null {
|
|
|
1134
1192
|
confinedHourStride(segment);
|
|
1135
1193
|
|
|
1136
1194
|
if (confined) {
|
|
1137
|
-
return secondsClause(ir,
|
|
1138
|
-
everyNthHour(segment);
|
|
1195
|
+
return withAnchor(secondsClause(ir, minuteAnchor(ir)), 'für eine Minute') +
|
|
1196
|
+
' ' + everyNthHour(segment);
|
|
1139
1197
|
}
|
|
1140
1198
|
|
|
1141
1199
|
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
@@ -1208,11 +1266,14 @@ function renderHourRange(
|
|
|
1208
1266
|
plan: Extract<PlanNode, {kind: 'hourRange'}>,
|
|
1209
1267
|
opts: Opts
|
|
1210
1268
|
): string {
|
|
1211
|
-
//
|
|
1212
|
-
//
|
|
1213
|
-
//
|
|
1214
|
-
|
|
1215
|
-
|
|
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);
|
|
1216
1277
|
|
|
1217
1278
|
if (plan.minuteForm === 'wildcard') {
|
|
1218
1279
|
return 'jede Minute ' + window;
|
|
@@ -1343,8 +1404,17 @@ function needsDailyFrame(ir: IR): boolean {
|
|
|
1343
1404
|
return true;
|
|
1344
1405
|
}
|
|
1345
1406
|
|
|
1346
|
-
|
|
1347
|
-
|
|
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);
|
|
1348
1418
|
}
|
|
1349
1419
|
|
|
1350
1420
|
function render(ir: IR, plan: PlanNode, opts: Opts): string {
|
package/src/lang/en/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the core stays semantic, and this module's only input is the IR.
|
|
4
4
|
// See docs/i18n-design.md.
|
|
5
5
|
|
|
6
|
-
import {arithmeticStep} from '../../core/util.js';
|
|
6
|
+
import {arithmeticStep, orderWeekdaysForDisplay} from '../../core/util.js';
|
|
7
7
|
import {maxClockTimes} from '../../core/specs.js';
|
|
8
8
|
import {clockDigits, numeral} from '../../core/format.js';
|
|
9
9
|
import type {Cronli5Options} from '../../types.js';
|
|
@@ -596,12 +596,17 @@ function renderHourStep(ir: IR, plan: PlanOf<'hourStep'>,
|
|
|
596
596
|
trailingQualifier(ir, opts);
|
|
597
597
|
}
|
|
598
598
|
|
|
599
|
-
// The hour-range plan as a window
|
|
600
|
-
//
|
|
601
|
-
//
|
|
599
|
+
// The hour-range plan as a window. The close lands on the top of the final
|
|
600
|
+
// hour (`:00`) unless the minute genuinely runs to the end of that hour — i.e.
|
|
601
|
+
// a wildcard minute, which fills every minute and states no separate clause.
|
|
602
|
+
// A pinned/listed/ranged minute is named in its own lead clause, so folding it
|
|
603
|
+
// into the close too would read as a span ("through 5:05 p.m.") that
|
|
604
|
+
// contradicts the minute clause; the window stays bare ("through 5 p.m.").
|
|
602
605
|
function boundedWindow(plan: PlanOf<'hourRange'>):
|
|
603
606
|
{from: number; to: number; last: number} {
|
|
604
|
-
|
|
607
|
+
const last = plan.minuteForm === 'wildcard' ? plan.boundMinute ?? 0 : 0;
|
|
608
|
+
|
|
609
|
+
return {from: plan.from, last, to: plan.to};
|
|
605
610
|
}
|
|
606
611
|
|
|
607
612
|
// An hour window phrase, e.g. "from 9 a.m. through 5:45 p.m.". Windows
|
|
@@ -1499,29 +1504,51 @@ function monthFoldsIntoDate(ir: IR): boolean {
|
|
|
1499
1504
|
|
|
1500
1505
|
// Compose the "day-of-month or day-of-week" phrase used when both fields
|
|
1501
1506
|
// are restricted: cron fires when either is a match. A restricted month
|
|
1502
|
-
// scopes
|
|
1507
|
+
// scopes BOTH halves, so it attaches to the whole or, never to a single
|
|
1508
|
+
// branch. When the month folds into a calendar date ("on June 13") it also
|
|
1509
|
+
// names itself on the weekday ("or on Friday in June"), keeping both halves
|
|
1510
|
+
// scoped; otherwise (a Quartz date, an open day step, a month range, or the
|
|
1511
|
+
// odd/even frequency) it trails the whole or as ", in <month>".
|
|
1503
1512
|
function dateOrWeekday(ir: IR, opts: NormalizedOptions): string {
|
|
1504
1513
|
const pattern = ir.pattern;
|
|
1505
1514
|
const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) ||
|
|
1506
1515
|
'on ' + weekdayPhrase(ir, opts);
|
|
1516
|
+
|
|
1517
|
+
if (pattern.month !== '*' && monthFoldsIntoDate(ir) &&
|
|
1518
|
+
!quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
|
|
1519
|
+
return 'on ' + monthDatePhrase(ir, opts) + ' or ' + weekdayPart +
|
|
1520
|
+
' in ' + monthName(ir, opts);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
return datePart(ir, opts) + ' or ' + weekdayPart + orMonthScope(ir, opts);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// The day-of-month half of an or-day phrase, without any month scope (the
|
|
1527
|
+
// month scopes the whole or, applied by the caller).
|
|
1528
|
+
function datePart(ir: IR, opts: NormalizedOptions): string {
|
|
1529
|
+
const pattern = ir.pattern;
|
|
1507
1530
|
const quartzDate = quartzDatePhrase(pattern.date, opts);
|
|
1508
1531
|
|
|
1509
1532
|
if (quartzDate) {
|
|
1510
|
-
return quartzDate
|
|
1533
|
+
return quartzDate;
|
|
1511
1534
|
}
|
|
1512
1535
|
|
|
1513
1536
|
if (isOpenStep(pattern.date)) {
|
|
1514
|
-
return stepDates(pattern.date)
|
|
1515
|
-
weekdayPart;
|
|
1537
|
+
return stepDates(pattern.date);
|
|
1516
1538
|
}
|
|
1517
1539
|
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1540
|
+
return 'on the ' + dateOrdinals(ir, opts);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// A trailing month scope for the whole or, set off by a comma so it reads
|
|
1544
|
+
// over both day halves ("…or on Friday, in June"); empty when the month is a
|
|
1545
|
+
// wildcard.
|
|
1546
|
+
function orMonthScope(ir: IR, opts: NormalizedOptions): string {
|
|
1547
|
+
if (ir.pattern.month === '*') {
|
|
1548
|
+
return '';
|
|
1521
1549
|
}
|
|
1522
1550
|
|
|
1523
|
-
return '
|
|
1524
|
-
monthScope(ir, opts);
|
|
1551
|
+
return ', in ' + monthName(ir, opts);
|
|
1525
1552
|
}
|
|
1526
1553
|
|
|
1527
1554
|
// The day-qualifier phrase for a Quartz date field (e.g. "on the last day
|
|
@@ -1665,8 +1692,12 @@ function oddEvenMonth(monthField: string): string | null {
|
|
|
1665
1692
|
// Render the weekday field as names. Ranges read in their connective form
|
|
1666
1693
|
// ("Monday through Friday", or "Mon-Fri" with `short`).
|
|
1667
1694
|
function weekdayPhrase(ir: IR, opts: NormalizedOptions): string {
|
|
1668
|
-
// Reached only with a restricted weekday, which has segments.
|
|
1669
|
-
|
|
1695
|
+
// Reached only with a restricted weekday, which has segments. Weekday lists
|
|
1696
|
+
// display Monday-first (Sunday last) so a weekend reads naturally; the IR
|
|
1697
|
+
// stays canonical (Sunday=0) and ranges keep their form.
|
|
1698
|
+
const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday!);
|
|
1699
|
+
|
|
1700
|
+
return renderSegments(segments, function name(value) {
|
|
1670
1701
|
return getWeekday(value, opts);
|
|
1671
1702
|
}, opts);
|
|
1672
1703
|
}
|