cronli5 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/README.md +4 -4
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +514 -407
- package/dist/cronli5.js +514 -407
- package/dist/lang/de.cjs +296 -225
- package/dist/lang/de.js +296 -225
- package/dist/lang/en.cjs +471 -364
- package/dist/lang/en.js +471 -364
- package/dist/lang/es.cjs +318 -281
- package/dist/lang/es.js +318 -281
- package/dist/lang/fi.cjs +326 -276
- package/dist/lang/fi.js +326 -276
- package/dist/lang/zh.cjs +308 -236
- package/dist/lang/zh.js +308 -236
- package/package.json +1 -1
- package/src/core/analyze.ts +22 -21
- package/src/core/cadence.ts +164 -0
- package/src/core/index.ts +3 -1
- package/src/core/normalize.ts +3 -3
- package/src/core/parse.ts +1 -1
- package/src/core/{ir.ts → schedule.ts} +23 -24
- package/src/core/shapes.ts +8 -1
- package/src/core/specs.ts +1 -1
- package/src/core/util.ts +4 -83
- package/src/core/validate.ts +2 -2
- package/src/core/weekday.ts +54 -0
- package/src/cronli5.ts +7 -7
- package/src/lang/de/index.ts +329 -288
- package/src/lang/en/dialects.ts +1 -1
- package/src/lang/en/index.ts +640 -516
- package/src/lang/es/index.ts +342 -374
- package/src/lang/es/notes.md +1 -1
- package/src/lang/fi/dialects.ts +1 -1
- package/src/lang/fi/index.ts +367 -372
- package/src/lang/fi/notes.md +23 -8
- package/src/lang/fi/status.json +1 -1
- package/src/lang/zh/index.ts +344 -262
- package/src/types.ts +6 -6
- package/types/core/analyze.d.ts +4 -4
- package/types/core/cadence.d.ts +33 -0
- package/types/core/index.d.ts +3 -1
- package/types/core/normalize.d.ts +1 -1
- package/types/core/parse.d.ts +1 -1
- package/types/core/{ir.d.ts → schedule.d.ts} +16 -21
- package/types/core/shapes.d.ts +2 -1
- package/types/core/specs.d.ts +1 -1
- package/types/core/util.d.ts +1 -15
- package/types/core/weekday.d.ts +10 -0
- package/types/lang/de/index.d.ts +1 -1
- package/types/lang/en/dialects.d.ts +1 -1
- package/types/lang/en/index.d.ts +1 -1
- package/types/lang/es/index.d.ts +1 -1
- package/types/lang/fi/dialects.d.ts +1 -1
- package/types/lang/fi/index.d.ts +1 -1
- package/types/lang/zh/index.d.ts +1 -1
- package/types/types.d.ts +5 -5
package/package.json
CHANGED
package/src/core/analyze.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
// Semantic analysis of canonical fields: fire enumeration, windows, shape
|
|
2
|
-
// classification, and description-
|
|
3
|
-
// resulting
|
|
2
|
+
// classification, and description-plan selection (the `plan`). The
|
|
3
|
+
// resulting Schedule is descriptive. Language modules handle rendering into
|
|
4
4
|
// words (docs/i18n-design.md §2.2).
|
|
5
5
|
|
|
6
6
|
import {fieldOrder, fieldSpecs, maxClockTimes} from './specs.js';
|
|
7
7
|
import type {FieldSpec} from './specs.js';
|
|
8
8
|
import type {
|
|
9
|
-
Analyses, ClockTime,
|
|
10
|
-
PlanNode, Segment, Shape, Shapes
|
|
11
|
-
} from './
|
|
9
|
+
Analyses, ClockTime, Field, HourTimesPlan, HoursPlan, Pattern,
|
|
10
|
+
PlanNode, Schedule, ScheduleFacts, Segment, Shape, Shapes
|
|
11
|
+
} from './schedule.js';
|
|
12
12
|
import {includes, toFieldNumber, unique} from './util.js';
|
|
13
13
|
import {isDiscreteHours, isDiscreteList, isPlainRange, isSingleValue}
|
|
14
14
|
from './shapes.js';
|
|
15
15
|
import {isQuartzDate, isQuartzWeekday} from './validate.js';
|
|
16
16
|
|
|
17
|
-
// List the values a `start/interval` step fires on
|
|
17
|
+
// List the values a `start/interval` step fires on from `start` up to `max`,
|
|
18
|
+
// stepping by `interval`.
|
|
18
19
|
function getOccurrences(
|
|
19
20
|
start: number,
|
|
20
21
|
interval: number,
|
|
@@ -197,8 +198,8 @@ function fieldSegments(
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
// Analyze a prepared (parsed, validated, normalized) cron pattern into the
|
|
200
|
-
//
|
|
201
|
-
function analyze(pattern: Pattern):
|
|
201
|
+
// Schedule a language module renders from.
|
|
202
|
+
function analyze(pattern: Pattern): Schedule {
|
|
202
203
|
const shapes = {} as Shapes;
|
|
203
204
|
const segments = {} as Analyses['segments'];
|
|
204
205
|
|
|
@@ -215,18 +216,18 @@ function analyze(pattern: Pattern): IR {
|
|
|
215
216
|
segments
|
|
216
217
|
};
|
|
217
218
|
|
|
218
|
-
const
|
|
219
|
+
const facts: ScheduleFacts = {analyses, pattern, shapes};
|
|
219
220
|
|
|
220
|
-
return {...
|
|
221
|
+
return {...facts, plan: selectPlan(facts)};
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
// Select the description
|
|
224
|
-
// core's *suggestion*: a language may override it via `Language.
|
|
225
|
-
// without re-deriving it (the
|
|
224
|
+
// Select the description plan from the neutral facts. This is the
|
|
225
|
+
// core's *suggestion*: a language may override it via `Language.plan`
|
|
226
|
+
// without re-deriving it (the facts / overridable-plan split).
|
|
226
227
|
// The selection mirrors the interpreter chain ordering exactly; renderers
|
|
227
228
|
// must not re-derive it.
|
|
228
|
-
function
|
|
229
|
-
const {analyses, pattern, shapes} =
|
|
229
|
+
function selectPlan(facts: ScheduleFacts): PlanNode {
|
|
230
|
+
const {analyses, pattern, shapes} = facts;
|
|
230
231
|
|
|
231
232
|
if (pattern.second !== '0') {
|
|
232
233
|
const seconds = planSeconds(pattern, shapes, analyses);
|
|
@@ -240,7 +241,7 @@ function selectStrategy(content: Content): PlanNode {
|
|
|
240
241
|
planHours(pattern, shapes, analyses);
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
// Seconds
|
|
244
|
+
// Seconds plans, or null when the second folds into the clock time
|
|
244
245
|
// downstream (a single second under discrete minutes and hours).
|
|
245
246
|
function planSeconds(
|
|
246
247
|
pattern: Pattern,
|
|
@@ -301,8 +302,8 @@ function planStandaloneSeconds(
|
|
|
301
302
|
return {kind: 'standaloneSeconds'};
|
|
302
303
|
}
|
|
303
304
|
|
|
304
|
-
// Minute
|
|
305
|
-
// the hour
|
|
305
|
+
// Minute plans, in the interpreter-chain order, or null to defer to
|
|
306
|
+
// the hour plans.
|
|
306
307
|
function planMinutes(
|
|
307
308
|
pattern: Pattern,
|
|
308
309
|
shapes: Shapes,
|
|
@@ -459,7 +460,7 @@ function planMinutesAcrossHours(
|
|
|
459
460
|
return null;
|
|
460
461
|
}
|
|
461
462
|
|
|
462
|
-
// Minute
|
|
463
|
+
// Minute plans that only stand on their own under a wildcard hour.
|
|
463
464
|
function planMinutesUnderOpenHour(
|
|
464
465
|
pattern: Pattern,
|
|
465
466
|
shapes: Shapes,
|
|
@@ -484,7 +485,7 @@ function planMinutesUnderOpenHour(
|
|
|
484
485
|
}
|
|
485
486
|
}
|
|
486
487
|
|
|
487
|
-
// Hour
|
|
488
|
+
// Hour plans: the chain's last resort always produces a plan. Under a
|
|
488
489
|
// sub-minute second a minute of 0 is a real restriction, so the absorbing
|
|
489
490
|
// idioms (hour range, hour step, every hour) are skipped for it and the hour
|
|
490
491
|
// is enumerated as clock times instead, stating the :00.
|
|
@@ -591,4 +592,4 @@ function hourTimesPlan(hourField: string): HourTimesPlan {
|
|
|
591
592
|
}
|
|
592
593
|
|
|
593
594
|
export {analyze, clockSecond, enumerateFires, enumerateStep,
|
|
594
|
-
enumerateValues, getOccurrences, lastMinuteFire, minuteSpan,
|
|
595
|
+
enumerateValues, getOccurrences, lastMinuteFire, minuteSpan, selectPlan};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Cadence analysis: recognizing arithmetic progressions and step strides in a
|
|
2
|
+
// field's values, and the segment accessors renderers use to reach them.
|
|
3
|
+
// Output-neutral and language-agnostic; renderers speak the cadence they find.
|
|
4
|
+
|
|
5
|
+
import type {Field, Schedule, Segment} from './schedule.js';
|
|
6
|
+
|
|
7
|
+
// A step segment of a classified field, carrying its `fires`/`interval`/
|
|
8
|
+
// `startToken`. The plan only routes step-shaped fields to step phrasing,
|
|
9
|
+
// where the first segment is always a step segment.
|
|
10
|
+
type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
11
|
+
|
|
12
|
+
// Recognize an arithmetic progression in a sorted, distinct numeric set: a
|
|
13
|
+
// run of length >= 5 whose consecutive gaps are all equal and >= 2. Returns
|
|
14
|
+
// its {start, interval, last}; null for anything shorter, with a gap of one
|
|
15
|
+
// (a plain run, which reads as a range), or irregular. Output-neutral and
|
|
16
|
+
// language-agnostic: renderers use it to speak a bounded/offset step cadence
|
|
17
|
+
// ("every N from M [through K]") instead of enumerating the fires. The set is
|
|
18
|
+
// the field's full value list, which the core has already sorted and deduped.
|
|
19
|
+
function arithmeticStep(values: number[]):
|
|
20
|
+
{start: number; interval: number; last: number} | null {
|
|
21
|
+
if (values.length < 5) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const interval = values[1] - values[0];
|
|
26
|
+
|
|
27
|
+
if (interval < 2) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (let i = 2; i < values.length; i += 1) {
|
|
32
|
+
if (values[i] - values[i - 1] !== interval) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {start: values[0], interval, last: values[values.length - 1]};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A field's classified segments, or an empty list when the field is a
|
|
41
|
+
// wildcard or Quartz shape (no segments). Renderers reach a non-empty list
|
|
42
|
+
// only on the field shapes the analysis segmented; the empty fallback keeps
|
|
43
|
+
// callers that touch a possibly-unsegmented field (a `.map`/`.forEach`) safe.
|
|
44
|
+
function segmentsOf(schedule: Schedule, field: Field): Segment[] {
|
|
45
|
+
return schedule.analyses.segments[field] ?? [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The first segment of a step field, narrowed to its step variant. The plan
|
|
49
|
+
// only routes step shapes here, whose (single) segment always classifies as a
|
|
50
|
+
// step; this asserts what the analysis guarantees but the type cannot express.
|
|
51
|
+
function stepSegment(schedule: Schedule, field: Field): StepSegment {
|
|
52
|
+
return segmentsOf(schedule, field)[0] as StepSegment;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The sorted numeric values a field's segments cover, or null if any segment
|
|
56
|
+
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
57
|
+
function singleValues(segments: Segment[]): number[] | null {
|
|
58
|
+
const values: number[] = [];
|
|
59
|
+
|
|
60
|
+
for (const segment of segments) {
|
|
61
|
+
if (segment.kind !== 'single') {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
values.push(+segment.value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return values;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Whether an hour stride wraps the day cleanly from within its first interval
|
|
72
|
+
// (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
|
|
73
|
+
// stride has no distinct endpoint and keeps its bare or "from M" cadence. Every
|
|
74
|
+
// other stride — a uneven interval, or one starting at or past its interval (a
|
|
75
|
+
// bounded `a-b/n`) — is a bounded set the cadence pins both endpoints of.
|
|
76
|
+
function offsetCleanStride(
|
|
77
|
+
stride: {start: number; interval: number}
|
|
78
|
+
): boolean {
|
|
79
|
+
return stride.start < stride.interval && 24 % stride.interval === 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The three branches of the stride/cadence decision tree, supplied by a
|
|
83
|
+
// renderer as its own words. The core picks the branch; the language owns the
|
|
84
|
+
// prose. `bare` is the clean cadence ("every N <unit>"); `offset` names only
|
|
85
|
+
// the start (a clean wrap with no distinct endpoint); `bounded` pins both
|
|
86
|
+
// endpoints (a non-wrapping set). Each is a thunk so the renderer evaluates
|
|
87
|
+
// only the branch the core selects (e.g. a stateful bound formatter is never
|
|
88
|
+
// constructed for the bare case). The words differ per language — case
|
|
89
|
+
// inflection, a measure word, a trailing idiom — so the leaves stay here, not
|
|
90
|
+
// in core.
|
|
91
|
+
interface StrideParts {
|
|
92
|
+
bare(): string;
|
|
93
|
+
offset(): string;
|
|
94
|
+
bounded(): string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Choose the stride/cadence branch for a step over a `cycle`-long field and
|
|
98
|
+
// emit the renderer's words for it. A clean stride from the top of the cycle
|
|
99
|
+
// is the bare cadence; a uniform offset (start within the first interval, the
|
|
100
|
+
// interval still tiling the cycle) names only its start, since it wraps cleanly
|
|
101
|
+
// with no distinct endpoint; a non-uniform stride (start >= interval, or an
|
|
102
|
+
// interval that does not tile the cycle) pins both endpoints so the bounded,
|
|
103
|
+
// non-wrapping set reads unambiguously. This is the one decision tree every
|
|
104
|
+
// renderer's `renderStride`/`hourStrideCadence` shared (cycle 60 for
|
|
105
|
+
// minute/second, 24 for the hour); the branch lives here once, the prose in
|
|
106
|
+
// each language's `parts`.
|
|
107
|
+
function renderStride(
|
|
108
|
+
spec: {start: number; interval: number; cycle: number},
|
|
109
|
+
parts: StrideParts
|
|
110
|
+
): string {
|
|
111
|
+
const {start, interval, cycle} = spec;
|
|
112
|
+
const tiles = cycle % interval === 0;
|
|
113
|
+
|
|
114
|
+
if (start === 0 && tiles) {
|
|
115
|
+
return parts.bare();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (start < interval && tiles) {
|
|
119
|
+
return parts.offset();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return parts.bounded();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// An hour list's arithmetic progression, or null when its values are not a
|
|
126
|
+
// step the renderer should speak as a cadence. The core rewrites a uneven hour
|
|
127
|
+
// step (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its
|
|
128
|
+
// literal fire list, indistinguishable in the Schedule from a hand-written
|
|
129
|
+
// list; the renderer recovers the cadence from the values. A progression
|
|
130
|
+
// starting at zero is a `*/n` step however short (0,7,14,21 is `*/7`); a
|
|
131
|
+
// non-zero one is only a step when it is too long to be a deliberate clock-time
|
|
132
|
+
// list (e.g. 9,17 is two named times, not a cadence), the same length the
|
|
133
|
+
// minute/second list path uses. Interval one is a plain range, never a step.
|
|
134
|
+
function hourListStride(
|
|
135
|
+
values: number[]
|
|
136
|
+
): {start: number; interval: number; last: number} | null {
|
|
137
|
+
if (values.length < 2) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const interval = values[1] - values[0];
|
|
142
|
+
|
|
143
|
+
if (interval < 2) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (let i = 2; i < values.length; i += 1) {
|
|
148
|
+
if (values[i] - values[i - 1] !== interval) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (values[0] !== 0 && values.length < 5) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {interval, last: values[values.length - 1], start: values[0]};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export {
|
|
161
|
+
arithmeticStep, hourListStride, offsetCleanStride, renderStride, segmentsOf,
|
|
162
|
+
singleValues, stepSegment
|
|
163
|
+
};
|
|
164
|
+
export type {StrideParts};
|
package/src/core/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// See docs/i18n-design.md.
|
|
5
5
|
|
|
6
6
|
import {applyQuartzAliases, normalizeCronPattern} from './normalize.js';
|
|
7
|
-
import type {NormalizedOptions, Pattern} from './
|
|
7
|
+
import type {NormalizedOptions, Pattern} from './schedule.js';
|
|
8
8
|
import {parseCronPattern} from './parse.js';
|
|
9
9
|
import type {CronPattern} from '../types.js';
|
|
10
10
|
import {validateCronPattern} from './validate.js';
|
|
@@ -24,5 +24,7 @@ function prepare(cronPattern: CronPattern, opts: NormalizedOptions): Pattern {
|
|
|
24
24
|
export {prepare};
|
|
25
25
|
export * from './specs.js';
|
|
26
26
|
export * from './util.js';
|
|
27
|
+
export * from './weekday.js';
|
|
28
|
+
export * from './cadence.js';
|
|
27
29
|
export * from './shapes.js';
|
|
28
30
|
export * from './analyze.js';
|
package/src/core/normalize.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import {fieldOrder, fieldSpecs} from './specs.js';
|
|
6
6
|
import type {CronLike, FieldSpec} from './specs.js';
|
|
7
|
-
import type {Field, Pattern} from './
|
|
7
|
+
import type {Field, Pattern} from './schedule.js';
|
|
8
8
|
import {includes, toFieldNumber, unique} from './util.js';
|
|
9
9
|
import {isQuartzDate, isQuartzWeekday} from './validate.js';
|
|
10
10
|
|
|
@@ -68,7 +68,7 @@ function normalizeField(value: string, field: Field, spec: FieldSpec): string {
|
|
|
68
68
|
const cycle = timeFieldCycle[field];
|
|
69
69
|
const segments = stringValue.split(',').map(function canonical(segment) {
|
|
70
70
|
return canonicalizeTokens(collapseFullSpanRange(
|
|
71
|
-
|
|
71
|
+
enumerateIfNonUniform(
|
|
72
72
|
collapseFullSpanStep(
|
|
73
73
|
collapseDegenerateRange(
|
|
74
74
|
collapseOnceStep(collapseUnitStep(segment, spec), spec), spec),
|
|
@@ -172,7 +172,7 @@ function collapseOnceStep(segment: string, spec: FieldSpec): string {
|
|
|
172
172
|
// list of those fires (`*/7` is `0,7,14,…`), the same as if it were written
|
|
173
173
|
// out. Calendar fields (no `cycle`), bounded steps (`9-17/2`, a per-window
|
|
174
174
|
// stride), and non-step segments are left untouched.
|
|
175
|
-
function
|
|
175
|
+
function enumerateIfNonUniform(
|
|
176
176
|
segment: string,
|
|
177
177
|
spec: FieldSpec,
|
|
178
178
|
cycle: number | undefined
|
package/src/core/parse.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {macros} from './specs.js';
|
|
4
4
|
import type {CronLike} from './specs.js';
|
|
5
|
-
import type {NormalizedOptions} from './
|
|
5
|
+
import type {NormalizedOptions} from './schedule.js';
|
|
6
6
|
import type {CronPattern, CronPatternObject} from '../types.js';
|
|
7
7
|
|
|
8
8
|
// Take a cron pattern as a cron pattern string, an array of cron fields, a
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// docs/i18n-design.md.
|
|
1
|
+
// A `Schedule` is the semantic contract between the core and a language
|
|
2
|
+
// renderer. The core (parse → validate → normalize → analyze) produces a
|
|
3
|
+
// `Schedule`, then a language module renders it to prose. These shapes are
|
|
4
|
+
// what any new language module interacts with. See docs/i18n-design.md.
|
|
6
5
|
|
|
7
6
|
import type {Cronli5Options} from '../types.js';
|
|
8
7
|
|
|
@@ -42,8 +41,7 @@ export type HoursPlan =
|
|
|
42
41
|
| {kind: 'none'}
|
|
43
42
|
| {kind: 'step'}
|
|
44
43
|
| {kind: 'window'; from: number; to: number; last: number}
|
|
45
|
-
| {kind: 'during'; times: HourTimesPlan}
|
|
46
|
-
| {kind: 'single'; from: number; to: number; last: number};
|
|
44
|
+
| {kind: 'during'; times: HourTimesPlan};
|
|
47
45
|
|
|
48
46
|
/** Hour times: enumerated fires, or deferred to per-segment rendering. */
|
|
49
47
|
export type HourTimesPlan =
|
|
@@ -51,7 +49,7 @@ export type HourTimesPlan =
|
|
|
51
49
|
| {kind: 'segments'};
|
|
52
50
|
|
|
53
51
|
/**
|
|
54
|
-
* The rendering
|
|
52
|
+
* The rendering plan the core selects for a pattern. The `kind`
|
|
55
53
|
* discriminant tells a renderer which fields are present.
|
|
56
54
|
*/
|
|
57
55
|
export type PlanNode =
|
|
@@ -99,24 +97,25 @@ export interface Analyses {
|
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
/**
|
|
102
|
-
* The neutral
|
|
103
|
-
* carrying no phrasing decision. `analyze` produces this; `
|
|
104
|
-
* reads it to suggest a `plan`. The phrasing
|
|
105
|
-
* part of the neutral
|
|
100
|
+
* The neutral schedule facts: the language-independent facts about a pattern,
|
|
101
|
+
* carrying no phrasing decision. `analyze` produces this; `selectPlan`
|
|
102
|
+
* reads it to suggest a `plan`. The phrasing plan is deliberately *not*
|
|
103
|
+
* part of the neutral facts (docs/i18n-design.md §2.2).
|
|
106
104
|
*/
|
|
107
|
-
export interface
|
|
105
|
+
export interface ScheduleFacts {
|
|
108
106
|
pattern: Pattern;
|
|
109
107
|
shapes: Shapes;
|
|
110
108
|
analyses: Analyses;
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
/**
|
|
114
|
-
* The semantic
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
112
|
+
* The semantic schedule a language renders: the neutral `ScheduleFacts`
|
|
113
|
+
* plus the selected `plan`. A language may widen `plan` with its own
|
|
114
|
+
* `Extra` plan kinds via `Language.plan`; by default there are none, so
|
|
115
|
+
* `Schedule` is the neutral facts with a core `PlanNode`.
|
|
118
116
|
*/
|
|
119
|
-
export interface
|
|
117
|
+
export interface Schedule<Extra extends {kind: string} = never>
|
|
118
|
+
extends ScheduleFacts {
|
|
120
119
|
plan: PlanNode | Extra;
|
|
121
120
|
}
|
|
122
121
|
|
|
@@ -156,23 +155,23 @@ export interface NormalizedOptions<Style = DialectStyle> {
|
|
|
156
155
|
|
|
157
156
|
/**
|
|
158
157
|
* The interface every language module's default export implements. `Extra`
|
|
159
|
-
* lets a language add its own
|
|
160
|
-
* `
|
|
158
|
+
* lets a language add its own plan kinds (default: none), which its
|
|
159
|
+
* `plan` override emits and its `describe` renders.
|
|
161
160
|
*/
|
|
162
161
|
export interface Language<
|
|
163
162
|
Style = DialectStyle,
|
|
164
163
|
Extra extends {kind: string} = never
|
|
165
164
|
> {
|
|
166
|
-
describe(
|
|
165
|
+
describe(schedule: Schedule<Extra>, opts: NormalizedOptions<Style>): string;
|
|
167
166
|
fallback: string;
|
|
168
167
|
options(options?: Cronli5Options): NormalizedOptions<Style>;
|
|
169
168
|
reboot: string;
|
|
170
169
|
// Wrap a rendered description into a complete standalone sentence (the CLI
|
|
171
170
|
// form); each language owns its lead verb and punctuation.
|
|
172
171
|
sentence(description: string): string;
|
|
173
|
-
// Optionally override the core's suggested
|
|
174
|
-
// `
|
|
172
|
+
// Optionally override the core's suggested plan. Receives the neutral
|
|
173
|
+
// `facts` and the core's suggestion (`base`), so overriding is a thin
|
|
175
174
|
// remap, not a re-derivation. Omitted by languages that accept the core's
|
|
176
175
|
// choice (all of en/de/es/fi today).
|
|
177
|
-
|
|
176
|
+
plan?(facts: ScheduleFacts, base: PlanNode): PlanNode | Extra;
|
|
178
177
|
}
|
package/src/core/shapes.ts
CHANGED
|
@@ -37,5 +37,12 @@ function isDiscreteHours(hourField: string): boolean {
|
|
|
37
37
|
return hourField !== '*' && !isPlainRange(hourField) &&
|
|
38
38
|
!isPlainStep(hourField);
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
// Whether a field is an "open" step (`*/n` or `a/n`, not a bounded range or a
|
|
42
|
+
// list). Open steps read as a frequency rather than an enumeration.
|
|
43
|
+
function isOpenStep(field: string): boolean {
|
|
44
|
+
return field.indexOf('/') !== -1 && field.indexOf('-') === -1 &&
|
|
45
|
+
field.indexOf(',') === -1;
|
|
46
|
+
}
|
|
47
|
+
export {isDiscreteHours, isDiscreteList, isOpenStep, isPlainRange, isPlainStep,
|
|
41
48
|
isSingleValue};
|
package/src/core/specs.ts
CHANGED
package/src/core/util.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
import type {Segment} from './ir.js';
|
|
1
|
+
// Generic pure helpers shared across the core.
|
|
4
2
|
|
|
3
|
+
// Whether a string contains a substring (coercing numbers to strings).
|
|
5
4
|
function includes(str: string | number, sub: string): boolean {
|
|
6
5
|
return ('' + str).indexOf(sub) !== -1;
|
|
7
6
|
}
|
|
@@ -18,82 +17,6 @@ function isNonNegativeInteger(value: string): boolean {
|
|
|
18
17
|
return digits.test(value);
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
// Recognize an arithmetic progression in a sorted, distinct numeric set: a
|
|
22
|
-
// run of length >= 5 whose consecutive gaps are all equal and >= 2. Returns
|
|
23
|
-
// its {start, interval, last}; null for anything shorter, with a gap of one
|
|
24
|
-
// (a plain run, which reads as a range), or irregular. Output-neutral and
|
|
25
|
-
// language-agnostic: renderers use it to speak a bounded/offset step cadence
|
|
26
|
-
// ("every N from M [through K]") instead of enumerating the fires. The set is
|
|
27
|
-
// the field's full value list, which the core has already sorted and deduped.
|
|
28
|
-
function arithmeticStep(values: number[]):
|
|
29
|
-
{start: number; interval: number; last: number} | null {
|
|
30
|
-
if (values.length < 5) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const interval = values[1] - values[0];
|
|
35
|
-
|
|
36
|
-
if (interval < 2) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
for (let i = 2; i < values.length; i += 1) {
|
|
41
|
-
if (values[i] - values[i - 1] !== interval) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return {start: values[0], interval, last: values[values.length - 1]};
|
|
47
|
-
}
|
|
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
|
-
|
|
97
20
|
// Resolve a numeric or named field token (e.g. '5' or 'FRI') to its number.
|
|
98
21
|
function toFieldNumber(
|
|
99
22
|
token: string,
|
|
@@ -103,7 +26,5 @@ function toFieldNumber(
|
|
|
103
26
|
// weekday) reach here. They always have an associated `numberMap`.
|
|
104
27
|
return isNonNegativeInteger(token) ? +token : numberMap![token.toUpperCase()];
|
|
105
28
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
toFieldNumber, unique
|
|
109
|
-
};
|
|
29
|
+
|
|
30
|
+
export {includes, isNonNegativeInteger, toFieldNumber, unique};
|
package/src/core/validate.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Loosely
|
|
1
|
+
// Loosely validate a cron-like object against the field specifications,
|
|
2
2
|
// including Quartz tokens and wrap-around range rules.
|
|
3
3
|
|
|
4
4
|
import {fieldOrder, fieldSpecs} from './specs.js';
|
|
5
5
|
import type {CronLike, FieldSpec} from './specs.js';
|
|
6
|
-
import type {Field} from './
|
|
6
|
+
import type {Field} from './schedule.js';
|
|
7
7
|
import {includes, isNonNegativeInteger, toFieldNumber} from './util.js';
|
|
8
8
|
|
|
9
9
|
// Validate every field of a cron-like object, throwing on the first
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Weekday display ordering: present weekdays Monday-first (Sunday last),
|
|
2
|
+
// independent of the Schedule's canonical Sunday=0 order. Display-only.
|
|
3
|
+
|
|
4
|
+
import type {Segment} from './schedule.js';
|
|
5
|
+
|
|
6
|
+
// The display sort key for a canonical weekday number: Monday (1) first,
|
|
7
|
+
// Sunday (0) last. The Schedule keeps Sunday=0 canonical; this is display-only.
|
|
8
|
+
function weekdayDisplayKey(value: number): number {
|
|
9
|
+
return value === 0 ? 7 : value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// A weekday display segment: a single day or a (possibly wrap) range. Steps
|
|
13
|
+
// are flattened away into singles before sorting, so the result is only these
|
|
14
|
+
// two kinds; each renderer turns them into names exactly as it does today.
|
|
15
|
+
type WeekdaySegment =
|
|
16
|
+
| {kind: 'single'; value: string}
|
|
17
|
+
| {kind: 'range'; bounds: [string, string]};
|
|
18
|
+
|
|
19
|
+
// Reorder weekday segments Monday-first (Sunday last) for display, so a weekend
|
|
20
|
+
// list reads "Saturday and Sunday" rather than the canonical Sunday-first
|
|
21
|
+
// "Sunday and Saturday". Display-only: the Schedule / canonical order is
|
|
22
|
+
// unchanged (a fresh array is returned). A step expands to its fires as singles
|
|
23
|
+
// so the days sort into the list; a range stays one unit and keeps its own
|
|
24
|
+
// bounds order (a wrap range is not reordered into a list), sorting by its
|
|
25
|
+
// opening bound — so a lone range sorts to a one-element list and is unchanged.
|
|
26
|
+
// The sort is stable, so equal opening days keep input order.
|
|
27
|
+
function orderWeekdaysForDisplay(segments: Segment[]): WeekdaySegment[] {
|
|
28
|
+
const flattened: WeekdaySegment[] = segments.flatMap(function flat(segment) {
|
|
29
|
+
return segment.kind === 'step' ?
|
|
30
|
+
segment.fires.map(function single(value): WeekdaySegment {
|
|
31
|
+
return {kind: 'single', value: '' + value};
|
|
32
|
+
}) :
|
|
33
|
+
[segment];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function key(segment: WeekdaySegment): number {
|
|
37
|
+
return segment.kind === 'range' ?
|
|
38
|
+
weekdayDisplayKey(+segment.bounds[0]) :
|
|
39
|
+
weekdayDisplayKey(+segment.value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return flattened
|
|
43
|
+
.map(function index(segment, position): [WeekdaySegment, number] {
|
|
44
|
+
return [segment, position];
|
|
45
|
+
})
|
|
46
|
+
.sort(function byDisplayKey(a, b): number {
|
|
47
|
+
return key(a[0]) - key(b[0]) || a[1] - b[1];
|
|
48
|
+
})
|
|
49
|
+
.map(function unwrap(pair): WeekdaySegment {
|
|
50
|
+
return pair[0];
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {orderWeekdaysForDisplay};
|
package/src/cronli5.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// patterns always parse seconds first and year last)
|
|
29
29
|
|
|
30
30
|
import {analyze, prepare} from './core/index.js';
|
|
31
|
-
import type {NormalizedOptions} from './core/
|
|
31
|
+
import type {NormalizedOptions} from './core/schedule.js';
|
|
32
32
|
import type {CronPattern, Cronli5Language, Cronli5Options}
|
|
33
33
|
from './types.js';
|
|
34
34
|
import en from './lang/en/index.js';
|
|
@@ -79,13 +79,13 @@ function interpretCronPattern(
|
|
|
79
79
|
return lang.reboot;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// Analyze into the neutral
|
|
83
|
-
// language optionally override the
|
|
84
|
-
// without a `
|
|
85
|
-
const
|
|
86
|
-
const plan = lang.
|
|
82
|
+
// Analyze into the neutral facts + the core's suggested plan, then let the
|
|
83
|
+
// language optionally override the plan before rendering. A language
|
|
84
|
+
// without a `plan` hook renders the core's suggestion unchanged.
|
|
85
|
+
const schedule = analyze(prepare(cronPattern, opts));
|
|
86
|
+
const plan = lang.plan ? lang.plan(schedule, schedule.plan) : schedule.plan;
|
|
87
87
|
|
|
88
|
-
return lang.describe({...
|
|
88
|
+
return lang.describe({...schedule, plan}, opts);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
export default cronli5;
|