cronli5 0.2.0 → 0.2.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 +25 -0
- package/README.md +4 -4
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +123 -104
- package/dist/cronli5.js +123 -104
- package/dist/lang/de.cjs +65 -65
- package/dist/lang/de.js +65 -65
- package/dist/lang/en.cjs +122 -101
- package/dist/lang/en.js +122 -101
- package/dist/lang/es.cjs +71 -72
- package/dist/lang/es.js +71 -72
- package/dist/lang/fi.cjs +71 -66
- package/dist/lang/fi.js +71 -66
- package/dist/lang/zh.cjs +36 -36
- package/dist/lang/zh.js +36 -36
- package/package.json +1 -1
- package/src/core/analyze.ts +14 -13
- package/src/core/ir.ts +8 -8
- package/src/core/shapes.ts +8 -1
- package/src/core/util.ts +86 -3
- package/src/core/validate.ts +1 -1
- package/src/cronli5.ts +3 -3
- package/src/lang/de/index.ts +30 -99
- package/src/lang/en/index.ts +163 -188
- package/src/lang/es/index.ts +36 -120
- package/src/lang/fi/index.ts +33 -104
- package/src/lang/zh/index.ts +23 -48
- package/src/types.ts +2 -2
- package/types/core/analyze.d.ts +2 -2
- package/types/core/ir.d.ts +7 -7
- package/types/core/shapes.d.ts +2 -1
- package/types/core/util.d.ts +17 -2
- package/types/types.d.ts +1 -1
package/dist/lang/zh.js
CHANGED
|
@@ -41,6 +41,22 @@ function orderWeekdaysForDisplay(segments) {
|
|
|
41
41
|
function toFieldNumber(token, numberMap) {
|
|
42
42
|
return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
|
|
43
43
|
}
|
|
44
|
+
function segmentsOf(ir, field) {
|
|
45
|
+
return ir.analyses.segments[field] ?? [];
|
|
46
|
+
}
|
|
47
|
+
function stepSegment(ir, field) {
|
|
48
|
+
return segmentsOf(ir, field)[0];
|
|
49
|
+
}
|
|
50
|
+
function singleValues(segments) {
|
|
51
|
+
const values = [];
|
|
52
|
+
for (const segment of segments) {
|
|
53
|
+
if (segment.kind !== "single") {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
values.push(+segment.value);
|
|
57
|
+
}
|
|
58
|
+
return values;
|
|
59
|
+
}
|
|
44
60
|
|
|
45
61
|
// src/core/specs.ts
|
|
46
62
|
var weekdayNumbers = {
|
|
@@ -88,12 +104,6 @@ function joinAnd(items) {
|
|
|
88
104
|
}
|
|
89
105
|
return items.slice(0, -1).join("\u3001") + "\u548C" + items[items.length - 1];
|
|
90
106
|
}
|
|
91
|
-
function fieldSegments(ir, field) {
|
|
92
|
-
return ir.analyses.segments[field] || [];
|
|
93
|
-
}
|
|
94
|
-
function stepSegment(ir, field) {
|
|
95
|
-
return fieldSegments(ir, field)[0];
|
|
96
|
-
}
|
|
97
107
|
function cadence(interval, unit) {
|
|
98
108
|
return interval === 1 ? "\u6BCF" + unit : "\u6BCF" + interval + unit;
|
|
99
109
|
}
|
|
@@ -106,16 +116,6 @@ function renderStride(stride) {
|
|
|
106
116
|
const lead = anchor + "\u4ECE" + start + mark + "\u8D77" + cadence(interval, unit);
|
|
107
117
|
return start < interval && tiles ? lead : lead + "\uFF0C\u81F3" + last + mark;
|
|
108
118
|
}
|
|
109
|
-
function singleValues(segments) {
|
|
110
|
-
const values = [];
|
|
111
|
-
for (const segment of segments) {
|
|
112
|
-
if (segment.kind !== "single") {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
values.push(+segment.value);
|
|
116
|
-
}
|
|
117
|
-
return values;
|
|
118
|
-
}
|
|
119
119
|
function strideFromSegments(segments, unit, mark, anchor) {
|
|
120
120
|
const values = singleValues(segments);
|
|
121
121
|
const step = values && arithmeticStep(values);
|
|
@@ -179,7 +179,7 @@ function hourWord(hour) {
|
|
|
179
179
|
}
|
|
180
180
|
function hourFires(ir) {
|
|
181
181
|
const fires = [];
|
|
182
|
-
|
|
182
|
+
segmentsOf(ir, "hour").forEach(function expand(segment) {
|
|
183
183
|
if (segment.kind === "step") {
|
|
184
184
|
fires.push(...segment.fires);
|
|
185
185
|
} else if (segment.kind === "range") {
|
|
@@ -234,7 +234,7 @@ function renderEveryHour() {
|
|
|
234
234
|
return "\u6BCF\u5C0F\u65F6";
|
|
235
235
|
}
|
|
236
236
|
function minuteHourClause(ir) {
|
|
237
|
-
const segments =
|
|
237
|
+
const segments = segmentsOf(ir, "minute");
|
|
238
238
|
if (ir.shapes.minute === "step") {
|
|
239
239
|
return stepClause(stepSegment(ir, "minute"), "\u5206\u949F", "\u5206", "\u6BCF\u5C0F\u65F6");
|
|
240
240
|
}
|
|
@@ -253,12 +253,12 @@ function hourSegmentWords(segment) {
|
|
|
253
253
|
return [hourWord(+segment.value)];
|
|
254
254
|
}
|
|
255
255
|
function hourList(ir) {
|
|
256
|
-
const words =
|
|
256
|
+
const words = segmentsOf(ir, "hour").flatMap(hourSegmentWords);
|
|
257
257
|
return joinAnd(words);
|
|
258
258
|
}
|
|
259
259
|
function hourFrame(ir) {
|
|
260
260
|
if (ir.shapes.hour === "range") {
|
|
261
|
-
const [from, to] =
|
|
261
|
+
const [from, to] = segmentsOf(ir, "hour")[0].bounds;
|
|
262
262
|
return "\u5728" + hourWord(+from) + "\u81F3" + hourWord(+to) + "\u4E4B\u95F4\uFF0C";
|
|
263
263
|
}
|
|
264
264
|
return "\u5728" + hourList(ir) + "\uFF0C";
|
|
@@ -329,7 +329,7 @@ function renderCompactClockTimes(ir, plan) {
|
|
|
329
329
|
return cad;
|
|
330
330
|
}
|
|
331
331
|
const compact = plan;
|
|
332
|
-
const secs =
|
|
332
|
+
const secs = segmentsOf(ir, "second");
|
|
333
333
|
const tail = secs.length && ir.pattern.second !== "0" ? "\uFF0C\u7B2C" + valueText(secs) + "\u79D2" : "";
|
|
334
334
|
if (!compact.fold) {
|
|
335
335
|
const hourCad = hourCadencePhrase(ir);
|
|
@@ -343,12 +343,12 @@ function renderCompactClockTimes(ir, plan) {
|
|
|
343
343
|
function renderHourRange(ir, plan) {
|
|
344
344
|
const range = plan;
|
|
345
345
|
if (range.minuteForm === "lead") {
|
|
346
|
-
const minuteSegs =
|
|
346
|
+
const minuteSegs = segmentsOf(ir, "minute");
|
|
347
347
|
const past = minuteSegs.length && ir.pattern.minute !== "0" ? minuteHourClause(ir) : "\u6BCF\u5C0F\u65F6";
|
|
348
348
|
return "\u5728" + hourWord(range.from) + "\u81F3" + hourWord(range.to) + "\u4E4B\u95F4\uFF0C" + past;
|
|
349
349
|
}
|
|
350
350
|
if (range.minuteForm === "range") {
|
|
351
|
-
return "\u5728" + hourWord(range.from) + "\u81F3" + hourWord(range.to) + "\u4E4B\u95F4\uFF0C\u6BCF\u5C0F\u65F6" + valueList(
|
|
351
|
+
return "\u5728" + hourWord(range.from) + "\u81F3" + hourWord(range.to) + "\u4E4B\u95F4\uFF0C\u6BCF\u5C0F\u65F6" + valueList(segmentsOf(ir, "minute"), "\u5206") + "\uFF0C\u6BCF\u5206\u949F";
|
|
352
352
|
}
|
|
353
353
|
return "\u5728" + hourWord(range.from) + "\u81F3" + range.to + "\u70B9" + range.last + "\u5206\u4E4B\u95F4\uFF0C\u6BCF\u5206\u949F";
|
|
354
354
|
}
|
|
@@ -360,7 +360,7 @@ function renderHourStep(ir) {
|
|
|
360
360
|
return hourCadencePhrase(ir);
|
|
361
361
|
}
|
|
362
362
|
function hourStride(ir) {
|
|
363
|
-
const segments =
|
|
363
|
+
const segments = segmentsOf(ir, "hour");
|
|
364
364
|
if (segments.length === 1 && segments[0].kind === "step") {
|
|
365
365
|
const { fires, interval } = segments[0];
|
|
366
366
|
return { interval, start: fires[0], last: fires[fires.length - 1] };
|
|
@@ -427,7 +427,7 @@ function renderRangeOfMinutes(ir) {
|
|
|
427
427
|
return minuteHourClause(ir) + "\uFF0C\u6BCF\u5206\u949F";
|
|
428
428
|
}
|
|
429
429
|
function renderStandaloneSeconds(ir) {
|
|
430
|
-
const segs =
|
|
430
|
+
const segs = segmentsOf(ir, "second");
|
|
431
431
|
const first = segs[0];
|
|
432
432
|
if (segs.length === 1 && first.kind === "step" && first.startToken === "*") {
|
|
433
433
|
return cadence(first.interval, UNITS.second);
|
|
@@ -435,11 +435,11 @@ function renderStandaloneSeconds(ir) {
|
|
|
435
435
|
return strideFromSegments(segs, "\u79D2", "\u79D2", "\u6BCF\u5206\u949F") ?? "\u6BCF\u5206\u949F\u7B2C" + valueText(segs) + "\u79D2";
|
|
436
436
|
}
|
|
437
437
|
function renderSecondPastMinute(ir) {
|
|
438
|
-
return "\u6BCF\u5206\u949F\u7B2C" + valueText(
|
|
438
|
+
return "\u6BCF\u5206\u949F\u7B2C" + valueText(segmentsOf(ir, "second")) + "\u79D2";
|
|
439
439
|
}
|
|
440
440
|
function renderSecondsWithinMinute(ir) {
|
|
441
441
|
const base = "\u6BCF\u5C0F\u65F6" + ir.pattern.minute + "\u5206";
|
|
442
|
-
const segs =
|
|
442
|
+
const segs = segmentsOf(ir, "second");
|
|
443
443
|
const first = segs[0];
|
|
444
444
|
if (segs.length === 1 && first.kind === "step" && first.startToken === "*") {
|
|
445
445
|
return base + "\uFF0C" + cadence(first.interval, UNITS.second);
|
|
@@ -447,7 +447,7 @@ function renderSecondsWithinMinute(ir) {
|
|
|
447
447
|
return base + "\u7B2C" + valueText(segs) + "\u79D2";
|
|
448
448
|
}
|
|
449
449
|
function secondClause(ir) {
|
|
450
|
-
const segs =
|
|
450
|
+
const segs = segmentsOf(ir, "second");
|
|
451
451
|
if (!segs.length) {
|
|
452
452
|
return "\u6BCF\u79D2";
|
|
453
453
|
}
|
|
@@ -464,7 +464,7 @@ function minuteClause(ir) {
|
|
|
464
464
|
if (ir.shapes.minute === "step") {
|
|
465
465
|
return cadence(stepSegment(ir, "minute").interval, UNITS.minute);
|
|
466
466
|
}
|
|
467
|
-
return valueList(
|
|
467
|
+
return valueList(segmentsOf(ir, "minute"), "\u5206");
|
|
468
468
|
}
|
|
469
469
|
function clockRestCarriesSecond(rest) {
|
|
470
470
|
return rest.kind === "clockTimes" && rest.times.some((time) => Boolean(time.second));
|
|
@@ -499,13 +499,13 @@ function composeMinuteZeroClocks(ir, sec) {
|
|
|
499
499
|
const clocks = hourFires(ir).map(function clock(hour) {
|
|
500
500
|
return hour === 12 ? "\u6B63\u5348" : hourWord(hour) + "0\u5206";
|
|
501
501
|
});
|
|
502
|
-
const nested = strideFromSegments(
|
|
502
|
+
const nested = strideFromSegments(segmentsOf(ir, "second"), "\u79D2", "\u79D2", "");
|
|
503
503
|
const tail = sec === "\u6BCF\u79D2" ? "\u7684\u6BCF\u4E00\u79D2" : "\u7684" + (nested ?? sec);
|
|
504
504
|
const core = joinAnd(clocks) + tail;
|
|
505
505
|
return isDaily(ir) ? "\u6BCF\u5929" + core : core;
|
|
506
506
|
}
|
|
507
507
|
function hasHourWindow(ir) {
|
|
508
|
-
return
|
|
508
|
+
return segmentsOf(ir, "hour").some(function range(segment) {
|
|
509
509
|
return segment.kind === "range";
|
|
510
510
|
});
|
|
511
511
|
}
|
|
@@ -541,7 +541,7 @@ function composeSecondsListed(ir) {
|
|
|
541
541
|
const sec = secondClause(ir);
|
|
542
542
|
const minutes = minuteHourClause(ir);
|
|
543
543
|
if (ir.shapes.hour === "single" && sec === "\u6BCF\u79D2") {
|
|
544
|
-
const minuteSegs =
|
|
544
|
+
const minuteSegs = segmentsOf(ir, "minute");
|
|
545
545
|
const minuteCad = strideFromSegments(minuteSegs, "\u5206\u949F", "\u5206", "") ?? valueList(minuteSegs, "\u5206");
|
|
546
546
|
return hourWord(hourFires(ir)[0]) + minuteCad + "\u7684\u6BCF\u4E00\u79D2";
|
|
547
547
|
}
|
|
@@ -597,7 +597,7 @@ function monthPhrase(ir) {
|
|
|
597
597
|
if (ir.pattern.month === "*") {
|
|
598
598
|
return "";
|
|
599
599
|
}
|
|
600
|
-
const segs =
|
|
600
|
+
const segs = segmentsOf(ir, "month");
|
|
601
601
|
const first = segs[0];
|
|
602
602
|
if (segs.length === 1 && first.kind === "step" && first.interval === 2) {
|
|
603
603
|
return "\u6BCF\u4E2A" + (first.fires[0] % 2 ? "\u5947" : "\u5076") + "\u6570\u6708";
|
|
@@ -616,7 +616,7 @@ function monthPhrase(ir) {
|
|
|
616
616
|
return nums.join("\u3001") + "\u6708";
|
|
617
617
|
}
|
|
618
618
|
function dayList(ir) {
|
|
619
|
-
const segs =
|
|
619
|
+
const segs = segmentsOf(ir, "date");
|
|
620
620
|
if (segs.every((seg) => seg.kind === "single")) {
|
|
621
621
|
return segs.map((seg) => seg.value).join("\u3001") + "\u65E5";
|
|
622
622
|
}
|
|
@@ -691,7 +691,7 @@ function weekdayPhrase(ir, orContext, monthPrefix) {
|
|
|
691
691
|
if (ir.shapes.weekday === "quartz") {
|
|
692
692
|
return quartzWeekday(ir.pattern.weekday, monthPrefix);
|
|
693
693
|
}
|
|
694
|
-
const segs =
|
|
694
|
+
const segs = segmentsOf(ir, "weekday");
|
|
695
695
|
if (segs.length === 1 && segs[0].kind === "range") {
|
|
696
696
|
const [from, to] = segs[0].bounds;
|
|
697
697
|
return "\u6BCF" + weekdayName(from) + "\u81F3" + weekdayName(to);
|
|
@@ -785,7 +785,7 @@ function describe(ir, opts) {
|
|
|
785
785
|
if (ir.pattern.year === "*") {
|
|
786
786
|
return composed;
|
|
787
787
|
}
|
|
788
|
-
const year =
|
|
788
|
+
const year = segmentsOf(ir, "year").map(function part(seg) {
|
|
789
789
|
if (seg.kind === "range") {
|
|
790
790
|
return seg.bounds[0] + "\u5E74\u81F3" + seg.bounds[1] + "\u5E74";
|
|
791
791
|
}
|
package/package.json
CHANGED
package/src/core/analyze.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Semantic analysis of canonical fields: fire enumeration, windows, shape
|
|
2
|
-
// classification, and description-
|
|
2
|
+
// classification, and description-plan selection (the `plan`). The
|
|
3
3
|
// resulting IR is descriptive. Language modules handle rendering into
|
|
4
4
|
// words (docs/i18n-design.md §2.2).
|
|
5
5
|
|
|
@@ -14,7 +14,8 @@ 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,
|
|
@@ -217,15 +218,15 @@ function analyze(pattern: Pattern): IR {
|
|
|
217
218
|
|
|
218
219
|
const content: Content = {analyses, pattern, shapes};
|
|
219
220
|
|
|
220
|
-
return {...content, plan:
|
|
221
|
+
return {...content, plan: selectPlan(content)};
|
|
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 content-plan / overridable-
|
|
224
|
+
// Select the description plan from the neutral content. This is the
|
|
225
|
+
// core's *suggestion*: a language may override it via `Language.plan`
|
|
226
|
+
// without re-deriving it (the content-plan / overridable-plan split).
|
|
226
227
|
// The selection mirrors the interpreter chain ordering exactly; renderers
|
|
227
228
|
// must not re-derive it.
|
|
228
|
-
function
|
|
229
|
+
function selectPlan(content: Content): PlanNode {
|
|
229
230
|
const {analyses, pattern, shapes} = content;
|
|
230
231
|
|
|
231
232
|
if (pattern.second !== '0') {
|
|
@@ -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};
|
package/src/core/ir.ts
CHANGED
|
@@ -51,7 +51,7 @@ export type HourTimesPlan =
|
|
|
51
51
|
| {kind: 'segments'};
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* The rendering
|
|
54
|
+
* The rendering plan the core selects for a pattern. The `kind`
|
|
55
55
|
* discriminant tells a renderer which fields are present.
|
|
56
56
|
*/
|
|
57
57
|
export type PlanNode =
|
|
@@ -100,8 +100,8 @@ export interface Analyses {
|
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
102
|
* The neutral content plan: the language-independent facts about a pattern,
|
|
103
|
-
* carrying no phrasing decision. `analyze` produces this; `
|
|
104
|
-
* reads it to suggest a `plan`. The phrasing
|
|
103
|
+
* carrying no phrasing decision. `analyze` produces this; `selectPlan`
|
|
104
|
+
* reads it to suggest a `plan`. The phrasing plan is deliberately *not*
|
|
105
105
|
* part of the neutral content (docs/i18n-design.md §2.2).
|
|
106
106
|
*/
|
|
107
107
|
export interface Content {
|
|
@@ -113,7 +113,7 @@ export interface Content {
|
|
|
113
113
|
/**
|
|
114
114
|
* The semantic intermediate representation a language renders: the neutral
|
|
115
115
|
* `Content` plus the selected `plan`. A language may widen `plan` with its
|
|
116
|
-
* own `Extra`
|
|
116
|
+
* own `Extra` plan kinds via `Language.plan`; by default there are
|
|
117
117
|
* none, so `IR` is the neutral content with a core `PlanNode`.
|
|
118
118
|
*/
|
|
119
119
|
export interface IR<Extra extends {kind: string} = never> extends Content {
|
|
@@ -156,8 +156,8 @@ export interface NormalizedOptions<Style = DialectStyle> {
|
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
158
|
* The interface every language module's default export implements. `Extra`
|
|
159
|
-
* lets a language add its own
|
|
160
|
-
* `
|
|
159
|
+
* lets a language add its own plan kinds (default: none), which its
|
|
160
|
+
* `plan` override emits and its `describe` renders.
|
|
161
161
|
*/
|
|
162
162
|
export interface Language<
|
|
163
163
|
Style = DialectStyle,
|
|
@@ -170,9 +170,9 @@ export interface Language<
|
|
|
170
170
|
// Wrap a rendered description into a complete standalone sentence (the CLI
|
|
171
171
|
// form); each language owns its lead verb and punctuation.
|
|
172
172
|
sentence(description: string): string;
|
|
173
|
-
// Optionally override the core's suggested
|
|
173
|
+
// Optionally override the core's suggested plan. Receives the neutral
|
|
174
174
|
// `content` and the core's suggestion (`base`), so overriding is a thin
|
|
175
175
|
// remap, not a re-derivation. Omitted by languages that accept the core's
|
|
176
176
|
// choice (all of en/de/es/fi today).
|
|
177
|
-
|
|
177
|
+
plan?(content: Content, base: PlanNode): PlanNode | Extra;
|
|
178
178
|
}
|
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/util.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// Small shared utilities for the core.
|
|
2
2
|
|
|
3
|
-
import type {Segment} from './ir.js';
|
|
3
|
+
import type {Field, IR, Segment} from './ir.js';
|
|
4
|
+
|
|
5
|
+
// A step segment of a classified field, carrying its `fires`/`interval`/
|
|
6
|
+
// `startToken`. The plan only routes step-shaped fields to step phrasing,
|
|
7
|
+
// where the first segment is always a step segment.
|
|
8
|
+
type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
4
9
|
|
|
5
10
|
function includes(str: string | number, sub: string): boolean {
|
|
6
11
|
return ('' + str).indexOf(sub) !== -1;
|
|
@@ -103,7 +108,85 @@ function toFieldNumber(
|
|
|
103
108
|
// weekday) reach here. They always have an associated `numberMap`.
|
|
104
109
|
return isNonNegativeInteger(token) ? +token : numberMap![token.toUpperCase()];
|
|
105
110
|
}
|
|
111
|
+
// A field's classified segments, or an empty list when the field is a
|
|
112
|
+
// wildcard or Quartz shape (no segments). Renderers reach a non-empty list
|
|
113
|
+
// only on the field shapes the analysis segmented; the empty fallback keeps
|
|
114
|
+
// callers that touch a possibly-unsegmented field (a `.map`/`.forEach`) safe.
|
|
115
|
+
function segmentsOf(ir: IR, field: Field): Segment[] {
|
|
116
|
+
return ir.analyses.segments[field] ?? [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The first segment of a step field, narrowed to its step variant. The plan
|
|
120
|
+
// only routes step shapes here, whose (single) segment always classifies as a
|
|
121
|
+
// step; this asserts what the analysis guarantees but the type cannot express.
|
|
122
|
+
function stepSegment(ir: IR, field: Field): StepSegment {
|
|
123
|
+
return segmentsOf(ir, field)[0] as StepSegment;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The sorted numeric values a field's segments cover, or null if any segment
|
|
127
|
+
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
128
|
+
function singleValues(segments: Segment[]): number[] | null {
|
|
129
|
+
const values: number[] = [];
|
|
130
|
+
|
|
131
|
+
for (const segment of segments) {
|
|
132
|
+
if (segment.kind !== 'single') {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
values.push(+segment.value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return values;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Whether an hour stride wraps the day cleanly from within its first interval
|
|
143
|
+
// (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
|
|
144
|
+
// stride has no distinct endpoint and keeps its bare or "from M" cadence. Every
|
|
145
|
+
// other stride — a uneven interval, or one starting at or past its interval (a
|
|
146
|
+
// bounded `a-b/n`) — is a bounded set the cadence pins both endpoints of.
|
|
147
|
+
function offsetCleanStride(
|
|
148
|
+
stride: {start: number; interval: number}
|
|
149
|
+
): boolean {
|
|
150
|
+
return stride.start < stride.interval && 24 % stride.interval === 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// An hour list's arithmetic progression, or null when its values are not a
|
|
154
|
+
// step the renderer should speak as a cadence. The core rewrites a uneven hour
|
|
155
|
+
// step (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its
|
|
156
|
+
// literal fire list, indistinguishable in the IR from a hand-written list; the
|
|
157
|
+
// renderer recovers the cadence from the values. A progression starting at
|
|
158
|
+
// zero is a `*/n` step however short (0,7,14,21 is `*/7`); a non-zero one is
|
|
159
|
+
// only a step when it is too long to be a deliberate clock-time list (e.g.
|
|
160
|
+
// 9,17 is two named times, not a cadence), the same length the minute/second
|
|
161
|
+
// list path uses. Interval one is a plain range, never a step.
|
|
162
|
+
function hourListStride(
|
|
163
|
+
values: number[]
|
|
164
|
+
): {start: number; interval: number; last: number} | null {
|
|
165
|
+
if (values.length < 2) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const interval = values[1] - values[0];
|
|
170
|
+
|
|
171
|
+
if (interval < 2) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (let i = 2; i < values.length; i += 1) {
|
|
176
|
+
if (values[i] - values[i - 1] !== interval) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (values[0] !== 0 && values.length < 5) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {interval, last: values[values.length - 1], start: values[0]};
|
|
186
|
+
}
|
|
187
|
+
|
|
106
188
|
export {
|
|
107
|
-
arithmeticStep, includes, isNonNegativeInteger,
|
|
108
|
-
|
|
189
|
+
arithmeticStep, hourListStride, includes, isNonNegativeInteger,
|
|
190
|
+
offsetCleanStride, orderWeekdaysForDisplay, segmentsOf, singleValues,
|
|
191
|
+
stepSegment, toFieldNumber, unique
|
|
109
192
|
};
|
package/src/core/validate.ts
CHANGED
package/src/cronli5.ts
CHANGED
|
@@ -80,10 +80,10 @@ function interpretCronPattern(
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// Analyze into the neutral content + the core's suggested plan, then let the
|
|
83
|
-
// language optionally override the
|
|
84
|
-
// without a `
|
|
83
|
+
// language optionally override the plan before rendering. A language
|
|
84
|
+
// without a `plan` hook renders the core's suggestion unchanged.
|
|
85
85
|
const ir = analyze(prepare(cronPattern, opts));
|
|
86
|
-
const plan = lang.
|
|
86
|
+
const plan = lang.plan ? lang.plan(ir, ir.plan) : ir.plan;
|
|
87
87
|
|
|
88
88
|
return lang.describe({...ir, plan}, opts);
|
|
89
89
|
}
|