cronli5 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -0
- package/cli.js +9 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +280 -64
- package/dist/cronli5.js +280 -64
- package/dist/lang/de.cjs +227 -42
- package/dist/lang/de.js +227 -42
- package/dist/lang/en.cjs +216 -50
- package/dist/lang/en.js +216 -50
- package/dist/lang/es.cjs +259 -54
- package/dist/lang/es.js +259 -54
- package/dist/lang/fi.cjs +230 -69
- package/dist/lang/fi.js +230 -69
- package/dist/lang/zh.cjs +190 -19
- package/dist/lang/zh.js +190 -19
- package/package.json +3 -1
- package/src/core/analyze.ts +7 -0
- package/src/core/ir.ts +1 -1
- package/src/core/normalize.ts +94 -4
- package/src/core/util.ts +31 -1
- package/src/lang/de/index.ts +449 -46
- package/src/lang/en/index.ts +433 -63
- package/src/lang/es/index.ts +505 -63
- package/src/lang/fi/index.ts +455 -89
- package/src/lang/zh/index.ts +393 -30
- package/types/core/ir.d.ts +1 -1
- package/types/core/util.d.ts +6 -1
package/dist/lang/zh.js
CHANGED
|
@@ -3,6 +3,21 @@ function isNonNegativeInteger(value) {
|
|
|
3
3
|
const digits = /^\d+$/;
|
|
4
4
|
return digits.test(value);
|
|
5
5
|
}
|
|
6
|
+
function arithmeticStep(values) {
|
|
7
|
+
if (values.length < 5) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const interval = values[1] - values[0];
|
|
11
|
+
if (interval < 2) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
for (let i = 2; i < values.length; i += 1) {
|
|
15
|
+
if (values[i] - values[i - 1] !== interval) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { start: values[0], interval, last: values[values.length - 1] };
|
|
20
|
+
}
|
|
6
21
|
function toFieldNumber(token, numberMap) {
|
|
7
22
|
return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
|
|
8
23
|
}
|
|
@@ -31,6 +46,7 @@ var monthNumbers = {
|
|
|
31
46
|
NOV: 11,
|
|
32
47
|
DEC: 12
|
|
33
48
|
};
|
|
49
|
+
var maxClockTimes = 6;
|
|
34
50
|
|
|
35
51
|
// src/lang/zh/dialects.ts
|
|
36
52
|
var zh = { variant: "Hans" };
|
|
@@ -61,6 +77,46 @@ function stepSegment(ir, field) {
|
|
|
61
77
|
function cadence(interval, unit) {
|
|
62
78
|
return interval === 1 ? "\u6BCF" + unit : "\u6BCF" + interval + unit;
|
|
63
79
|
}
|
|
80
|
+
function renderStride(stride) {
|
|
81
|
+
const { interval, start, last, cycle, unit, mark, anchor } = stride;
|
|
82
|
+
const tiles = cycle % interval === 0;
|
|
83
|
+
if (start === 0 && tiles) {
|
|
84
|
+
return cadence(interval, unit);
|
|
85
|
+
}
|
|
86
|
+
const lead = anchor + "\u4ECE" + start + mark + "\u8D77" + cadence(interval, unit);
|
|
87
|
+
return start < interval && tiles ? lead : lead + "\uFF0C\u81F3" + last + mark;
|
|
88
|
+
}
|
|
89
|
+
function singleValues(segments) {
|
|
90
|
+
const values = [];
|
|
91
|
+
for (const segment of segments) {
|
|
92
|
+
if (segment.kind !== "single") {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
values.push(+segment.value);
|
|
96
|
+
}
|
|
97
|
+
return values;
|
|
98
|
+
}
|
|
99
|
+
function strideFromSegments(segments, unit, mark, anchor) {
|
|
100
|
+
const values = singleValues(segments);
|
|
101
|
+
const step = values && arithmeticStep(values);
|
|
102
|
+
return step ? renderStride({ ...step, cycle: 60, unit, mark, anchor }) : null;
|
|
103
|
+
}
|
|
104
|
+
function stepClause(segment, unit, mark, anchor) {
|
|
105
|
+
const start = segment.startToken === "*" ? 0 : +segment.startToken;
|
|
106
|
+
const short = start !== 0 && segment.fires.length <= 3;
|
|
107
|
+
if (segment.startToken.indexOf("-") !== -1 || short) {
|
|
108
|
+
return anchor + segment.fires.join("\u3001") + mark;
|
|
109
|
+
}
|
|
110
|
+
return renderStride({
|
|
111
|
+
interval: segment.interval,
|
|
112
|
+
start,
|
|
113
|
+
last: segment.fires[segment.fires.length - 1],
|
|
114
|
+
cycle: 60,
|
|
115
|
+
unit,
|
|
116
|
+
mark,
|
|
117
|
+
anchor
|
|
118
|
+
});
|
|
119
|
+
}
|
|
64
120
|
function dayPeriod(hour) {
|
|
65
121
|
if (hour < 6) {
|
|
66
122
|
return "\u51CC\u6668";
|
|
@@ -157,8 +213,15 @@ function renderEveryMinute() {
|
|
|
157
213
|
function renderEveryHour() {
|
|
158
214
|
return "\u6BCF\u5C0F\u65F6";
|
|
159
215
|
}
|
|
216
|
+
function minuteHourClause(ir) {
|
|
217
|
+
const segments = fieldSegments(ir, "minute");
|
|
218
|
+
if (ir.shapes.minute === "step") {
|
|
219
|
+
return stepClause(stepSegment(ir, "minute"), "\u5206\u949F", "\u5206", "\u6BCF\u5C0F\u65F6");
|
|
220
|
+
}
|
|
221
|
+
return strideFromSegments(segments, "\u5206\u949F", "\u5206", "\u6BCF\u5C0F\u65F6") ?? "\u6BCF\u5C0F\u65F6" + valueList(segments, "\u5206");
|
|
222
|
+
}
|
|
160
223
|
function renderMinutePast(ir) {
|
|
161
|
-
return
|
|
224
|
+
return minuteHourClause(ir);
|
|
162
225
|
}
|
|
163
226
|
function hourList(ir) {
|
|
164
227
|
return joinAnd(hourFires(ir).map(hourWord));
|
|
@@ -172,10 +235,11 @@ function hourFrame(ir) {
|
|
|
172
235
|
}
|
|
173
236
|
function renderMinuteFrequency(ir, plan) {
|
|
174
237
|
const minuteStep = stepSegment(ir, "minute");
|
|
175
|
-
const base =
|
|
238
|
+
const base = stepClause(minuteStep, UNITS.minute, "\u5206", "\u6BCF\u5C0F\u65F6");
|
|
176
239
|
const { hours } = plan;
|
|
177
240
|
if (hours.kind === "step") {
|
|
178
|
-
|
|
241
|
+
const hourStep = stepSegment(ir, "hour");
|
|
242
|
+
return hourStep.startToken === "*" ? cadence(hourStep.interval, UNITS.hour) + base : "\u5728" + hourList(ir) + "\uFF0C" + base;
|
|
179
243
|
}
|
|
180
244
|
if (hours.kind === "single" || hours.kind === "window" && hours.from === hours.to) {
|
|
181
245
|
return "\u5728" + hourWord(hours.from) + "\u81F3" + hours.from + "\u70B9" + hours.last + "\u5206\u4E4B\u95F4\uFF0C" + base;
|
|
@@ -190,6 +254,9 @@ function renderMinuteFrequency(ir, plan) {
|
|
|
190
254
|
}
|
|
191
255
|
function renderMinuteSpanInHour(ir, plan) {
|
|
192
256
|
const span = plan;
|
|
257
|
+
if (ir.pattern.minute === "*") {
|
|
258
|
+
return hourWord(span.hour) + "\u7684\u6BCF\u4E00\u5206\u949F";
|
|
259
|
+
}
|
|
193
260
|
return "\u5728" + hourWord(span.hour) + "\u81F3" + span.hour + "\u70B9" + span.span[1] + "\u5206\u4E4B\u95F4\uFF0C\u6BCF\u5206\u949F";
|
|
194
261
|
}
|
|
195
262
|
function renderMinutesAcrossHours(ir, plan) {
|
|
@@ -197,26 +264,46 @@ function renderMinutesAcrossHours(ir, plan) {
|
|
|
197
264
|
if (form === "wildcard") {
|
|
198
265
|
return "\u5728" + hourList(ir) + "\uFF0C\u6BCF\u5206\u949F";
|
|
199
266
|
}
|
|
200
|
-
return hourList(ir) + "\uFF0C
|
|
267
|
+
return hourList(ir) + "\uFF0C" + minuteHourClause(ir) + "\uFF0C\u6BCF\u5206\u949F";
|
|
201
268
|
}
|
|
202
269
|
function renderMinuteSpanAcrossHourStep(ir, plan) {
|
|
203
|
-
const
|
|
270
|
+
const hourStep = stepSegment(ir, "hour");
|
|
204
271
|
const { form } = plan;
|
|
205
|
-
if (form === "
|
|
206
|
-
return
|
|
272
|
+
if (form === "list") {
|
|
273
|
+
return renderMinutePast(ir) + "\uFF0C\u5728" + hourList(ir);
|
|
274
|
+
}
|
|
275
|
+
const minuteTail = form === "wildcard" ? "\u6BCF\u5206\u949F" : minuteHourClause(ir) + "\uFF0C\u6BCF\u5206\u949F";
|
|
276
|
+
if (hourStep.startToken !== "*") {
|
|
277
|
+
return form === "wildcard" ? "\u5728" + hourList(ir) + "\uFF0C" + minuteTail : hourList(ir) + "\uFF0C" + minuteTail;
|
|
278
|
+
}
|
|
279
|
+
if (hourStep.interval === 2 && form === "wildcard") {
|
|
280
|
+
return "\u5728\u5076\u6570\u5C0F\u65F6\uFF0C" + minuteTail;
|
|
207
281
|
}
|
|
208
|
-
|
|
282
|
+
const cad = cadence(hourStep.interval, UNITS.hour);
|
|
283
|
+
return form === "wildcard" ? cad + "\u5185\uFF0C" + minuteTail : cad + "\uFF0C" + minuteTail;
|
|
209
284
|
}
|
|
210
285
|
function renderClockTimes(ir, plan, opts) {
|
|
286
|
+
if (ir.shapes.minute === "single") {
|
|
287
|
+
const cad = hourCadence(ir);
|
|
288
|
+
if (cad !== null) {
|
|
289
|
+
return cad;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
211
292
|
const { times } = plan;
|
|
212
293
|
return joinAnd(times.map((t) => clockTime(t.hour, t.minute, t.second, opts)));
|
|
213
294
|
}
|
|
214
295
|
function renderCompactClockTimes(ir, plan) {
|
|
296
|
+
if (ir.shapes.minute === "single") {
|
|
297
|
+
const cad = hourCadence(ir);
|
|
298
|
+
if (cad !== null) {
|
|
299
|
+
return cad;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
215
302
|
const { minute } = plan;
|
|
216
303
|
const secs = fieldSegments(ir, "second");
|
|
217
304
|
const tail = secs.length && ir.pattern.second !== "0" ? "\uFF0C\u7B2C" + valueText(secs) + "\u79D2" : "";
|
|
218
305
|
if (minute > 0) {
|
|
219
|
-
return
|
|
306
|
+
return minuteHourClause(ir) + "\uFF0C\u5728" + hourList(ir) + tail;
|
|
220
307
|
}
|
|
221
308
|
return hourList(ir) + tail;
|
|
222
309
|
}
|
|
@@ -224,7 +311,7 @@ function renderHourRange(ir, plan) {
|
|
|
224
311
|
const range = plan;
|
|
225
312
|
if (range.minuteForm === "lead") {
|
|
226
313
|
const minuteSegs = fieldSegments(ir, "minute");
|
|
227
|
-
const past = minuteSegs.length && ir.pattern.minute !== "0" ?
|
|
314
|
+
const past = minuteSegs.length && ir.pattern.minute !== "0" ? minuteHourClause(ir) : "\u6BCF\u5C0F\u65F6";
|
|
228
315
|
return "\u5728" + hourWord(range.from) + "\u81F3" + hourWord(range.to) + "\u4E4B\u95F4\uFF0C" + past;
|
|
229
316
|
}
|
|
230
317
|
if (range.minuteForm === "range") {
|
|
@@ -242,8 +329,52 @@ function renderHourStep(ir) {
|
|
|
242
329
|
}
|
|
243
330
|
return cadence(segment.interval, UNITS.hour);
|
|
244
331
|
}
|
|
332
|
+
function hourStride(ir) {
|
|
333
|
+
const segments = fieldSegments(ir, "hour");
|
|
334
|
+
if (segments.length === 1 && segments[0].kind === "step") {
|
|
335
|
+
const segment = segments[0];
|
|
336
|
+
const start = segment.startToken === "*" ? 0 : +segment.startToken;
|
|
337
|
+
if (start !== 0 || 24 % segment.interval !== 0) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
interval: segment.interval,
|
|
342
|
+
last: segment.fires[segment.fires.length - 1]
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const values = singleValues(segments);
|
|
346
|
+
const step = values && arithmeticStep(values);
|
|
347
|
+
if (!step || step.start !== 0 || 24 % step.interval !== 0) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return { interval: step.interval, last: step.last };
|
|
351
|
+
}
|
|
352
|
+
function hourCadence(ir) {
|
|
353
|
+
const stride = hourStride(ir);
|
|
354
|
+
if (!stride) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const fires = stride.last / stride.interval + 1;
|
|
358
|
+
if (ir.pattern.second === "0" && fires <= maxClockTimes) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const prefix = cadence(stride.interval, UNITS.hour);
|
|
362
|
+
const minute = +ir.pattern.minute;
|
|
363
|
+
const subMinute = ir.pattern.second === "*" || ir.shapes.second === "step";
|
|
364
|
+
if (minute === 0 && subMinute) {
|
|
365
|
+
return stride.interval === 2 ? "\u5728\u5076\u6570\u5C0F\u65F60\u5206" + secondTail(ir) : null;
|
|
366
|
+
}
|
|
367
|
+
if (minute === 0) {
|
|
368
|
+
return prefix + "0\u5206" + secondTail(ir);
|
|
369
|
+
}
|
|
370
|
+
return ir.pattern.second === "0" ? prefix + minute + "\u5206" : prefix + minute + "\u5206" + secondTail(ir);
|
|
371
|
+
}
|
|
372
|
+
function secondTail(ir) {
|
|
373
|
+
const sec = secondClause(ir);
|
|
374
|
+
return sec === "\u6BCF\u79D2" ? "\u7684\u6BCF\u4E00\u79D2" : "\u7684" + sec;
|
|
375
|
+
}
|
|
245
376
|
function renderRangeOfMinutes(ir) {
|
|
246
|
-
return
|
|
377
|
+
return minuteHourClause(ir) + "\uFF0C\u6BCF\u5206\u949F";
|
|
247
378
|
}
|
|
248
379
|
function renderStandaloneSeconds(ir) {
|
|
249
380
|
const segs = fieldSegments(ir, "second");
|
|
@@ -251,7 +382,7 @@ function renderStandaloneSeconds(ir) {
|
|
|
251
382
|
if (segs.length === 1 && first.kind === "step" && first.startToken === "*") {
|
|
252
383
|
return cadence(first.interval, UNITS.second);
|
|
253
384
|
}
|
|
254
|
-
return "\u6BCF\u5206\u949F\u7B2C" + valueText(segs) + "\u79D2";
|
|
385
|
+
return strideFromSegments(segs, "\u79D2", "\u79D2", "\u6BCF\u5206\u949F") ?? "\u6BCF\u5206\u949F\u7B2C" + valueText(segs) + "\u79D2";
|
|
255
386
|
}
|
|
256
387
|
function renderSecondPastMinute(ir) {
|
|
257
388
|
return "\u6BCF\u5206\u949F\u7B2C" + valueText(fieldSegments(ir, "second")) + "\u79D2";
|
|
@@ -274,7 +405,7 @@ function secondClause(ir) {
|
|
|
274
405
|
if (segs.length === 1 && first.kind === "step" && first.startToken === "*") {
|
|
275
406
|
return cadence(first.interval, UNITS.second);
|
|
276
407
|
}
|
|
277
|
-
return "\u7B2C" + valueText(segs) + "\u79D2";
|
|
408
|
+
return strideFromSegments(segs, "\u79D2", "\u79D2", "\u6BCF\u5206\u949F") ?? "\u7B2C" + valueText(segs) + "\u79D2";
|
|
278
409
|
}
|
|
279
410
|
function minuteClause(ir) {
|
|
280
411
|
if (ir.pattern.minute === "*") {
|
|
@@ -291,15 +422,36 @@ function isHourCadence(ir) {
|
|
|
291
422
|
function composeSecondsOnHour(ir, plan, opts) {
|
|
292
423
|
const sec = secondClause(ir);
|
|
293
424
|
const { rest } = plan;
|
|
425
|
+
const composedClock = rest.kind === "clockTimes" || rest.kind === "compactClockTimes";
|
|
426
|
+
if (composedClock) {
|
|
427
|
+
const hourCad = hourCadence(ir);
|
|
428
|
+
if (hourCad !== null) {
|
|
429
|
+
return hourCad;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (composedClock && ir.pattern.minute === "0") {
|
|
433
|
+
return composeMinuteZeroClocks(ir, sec);
|
|
434
|
+
}
|
|
294
435
|
const restText = render(ir, rest, opts);
|
|
295
|
-
if (
|
|
296
|
-
|
|
436
|
+
if (rest.kind === "clockTimes" || rest.kind === "compactClockTimes") {
|
|
437
|
+
if (isDaily(ir)) {
|
|
438
|
+
return "\u6BCF\u5929" + restText + sec;
|
|
439
|
+
}
|
|
297
440
|
}
|
|
298
441
|
if (rest.kind === "singleMinute") {
|
|
299
442
|
return restText + "\uFF0C" + sec;
|
|
300
443
|
}
|
|
301
444
|
return restText + sec;
|
|
302
445
|
}
|
|
446
|
+
function composeMinuteZeroClocks(ir, sec) {
|
|
447
|
+
const clocks = hourFires(ir).map(function clock(hour) {
|
|
448
|
+
return hour === 12 ? "\u6B63\u5348" : hourWord(hour) + "0\u5206";
|
|
449
|
+
});
|
|
450
|
+
const nested = strideFromSegments(fieldSegments(ir, "second"), "\u79D2", "\u79D2", "");
|
|
451
|
+
const tail = sec === "\u6BCF\u79D2" ? "\u7684\u6BCF\u4E00\u79D2" : "\u7684" + (nested ?? sec);
|
|
452
|
+
const core = joinAnd(clocks) + tail;
|
|
453
|
+
return isDaily(ir) ? "\u6BCF\u5929" + core : core;
|
|
454
|
+
}
|
|
303
455
|
function composeSecondsCadence(ir) {
|
|
304
456
|
const sec = secondClause(ir);
|
|
305
457
|
const tail = minuteClause(ir) + sec;
|
|
@@ -310,13 +462,24 @@ function composeSecondsCadence(ir) {
|
|
|
310
462
|
return hourWord(hourFires(ir)[0]) + "\u7684" + tail;
|
|
311
463
|
}
|
|
312
464
|
if (ir.shapes.hour === "wildcard") {
|
|
465
|
+
if (ir.shapes.minute === "step" && sec === "\u6BCF\u79D2") {
|
|
466
|
+
const minuteStep = stepSegment(ir, "minute");
|
|
467
|
+
if (minuteStep.startToken === "*" && minuteStep.interval === 2) {
|
|
468
|
+
return "\u6BCF\u5076\u6570\u5206\u949F\u7684\u6BCF\u4E00\u79D2";
|
|
469
|
+
}
|
|
470
|
+
}
|
|
313
471
|
return sec + "\uFF0C" + minuteClause(ir);
|
|
314
472
|
}
|
|
315
473
|
return hourFrame(ir) + tail;
|
|
316
474
|
}
|
|
317
475
|
function composeSecondsListed(ir) {
|
|
318
476
|
const sec = secondClause(ir);
|
|
319
|
-
const minutes =
|
|
477
|
+
const minutes = minuteHourClause(ir);
|
|
478
|
+
if (ir.shapes.hour === "single" && sec === "\u6BCF\u79D2") {
|
|
479
|
+
const minuteSegs = fieldSegments(ir, "minute");
|
|
480
|
+
const minuteCad = strideFromSegments(minuteSegs, "\u5206\u949F", "\u5206", "") ?? valueList(minuteSegs, "\u5206");
|
|
481
|
+
return hourWord(hourFires(ir)[0]) + minuteCad + "\u7684\u6BCF\u4E00\u79D2";
|
|
482
|
+
}
|
|
320
483
|
if (ir.shapes.hour === "wildcard") {
|
|
321
484
|
return minutes + "\uFF0C" + sec;
|
|
322
485
|
}
|
|
@@ -326,10 +489,13 @@ function composeSecondsListed(ir) {
|
|
|
326
489
|
return hourFrame(ir) + minutes + "\uFF0C" + sec;
|
|
327
490
|
}
|
|
328
491
|
function renderComposeSeconds(ir, plan, opts) {
|
|
329
|
-
|
|
492
|
+
const { rest } = plan;
|
|
493
|
+
const composedClock = rest.kind === "clockTimes" || rest.kind === "compactClockTimes";
|
|
494
|
+
if (ir.pattern.minute === "0" || composedClock && ir.shapes.minute === "single") {
|
|
330
495
|
return composeSecondsOnHour(ir, plan, opts);
|
|
331
496
|
}
|
|
332
|
-
|
|
497
|
+
const minuteCadence = ir.pattern.minute === "*" || ir.shapes.minute === "step" && stepSegment(ir, "minute").startToken === "*";
|
|
498
|
+
if (minuteCadence) {
|
|
333
499
|
return composeSecondsCadence(ir);
|
|
334
500
|
}
|
|
335
501
|
return composeSecondsListed(ir);
|
|
@@ -529,11 +695,16 @@ function composeCadence(ir, core) {
|
|
|
529
695
|
function composeWindow(ir, core) {
|
|
530
696
|
return qualifier(ir) + core;
|
|
531
697
|
}
|
|
698
|
+
function hourCadenceApplies(ir) {
|
|
699
|
+
return ir.shapes.minute === "single" && hourCadence(ir) !== null;
|
|
700
|
+
}
|
|
532
701
|
function describe(ir, opts) {
|
|
533
702
|
const { kind } = ir.plan;
|
|
534
703
|
const core = render(ir, ir.plan, opts);
|
|
535
704
|
let composed = core;
|
|
536
|
-
if (
|
|
705
|
+
if (hourCadenceApplies(ir)) {
|
|
706
|
+
composed = composeCadence(ir, core);
|
|
707
|
+
} else if (kind === "clockTimes" || kind === "compactClockTimes" && ir.pattern.minute === "0") {
|
|
537
708
|
composed = composePoint(ir, core);
|
|
538
709
|
} else if (kind === "hourStep" || kind === "minuteFrequency" || kind === "minuteSpanAcrossHourStep" || kind === "compactClockTimes") {
|
|
539
710
|
composed = composeCadence(ir, core);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cronli5",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Cron Like I'm Five: A Cron to English Utility",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,8 +60,10 @@
|
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "node scripts/build.mjs && npm run types",
|
|
62
62
|
"docs": "node --import tsx scripts/docs.mjs",
|
|
63
|
+
"conciseness": "node --import tsx tooling/scripts/conciseness.mjs",
|
|
63
64
|
"fuzz": "node --import tsx scripts/fuzz-lang.mjs",
|
|
64
65
|
"lint": "eslint src test cli.js eslint.config.js scripts tooling/scripts",
|
|
66
|
+
"metamorphic": "node --import tsx tooling/scripts/metamorphic.mjs",
|
|
65
67
|
"lint:fix": "eslint src test cli.js eslint.config.js scripts tooling/scripts --fix",
|
|
66
68
|
"test": "mocha",
|
|
67
69
|
"types": "tsc -p tsconfig.types.json",
|
package/src/core/analyze.ts
CHANGED
|
@@ -376,6 +376,13 @@ function planMinuteUnderHourStep(
|
|
|
376
376
|
return {form: 'range', kind: 'minuteSpanAcrossHourStep'};
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
// A minute list under a clean stride keeps the cadence too, so the hour
|
|
380
|
+
// reads the same whatever the minute's shape. An unclean stride falls
|
|
381
|
+
// through to compactClockTimes and enumerates its hours.
|
|
382
|
+
if (shapes.minute === 'list' && cleanHourStride(pattern.hour)) {
|
|
383
|
+
return {form: 'list', kind: 'minuteSpanAcrossHourStep'};
|
|
384
|
+
}
|
|
385
|
+
|
|
379
386
|
return null;
|
|
380
387
|
}
|
|
381
388
|
|
package/src/core/ir.ts
CHANGED
|
@@ -71,7 +71,7 @@ export type PlanNode =
|
|
|
71
71
|
form: 'wildcard' | 'range' | 'list';
|
|
72
72
|
times: HourTimesPlan;
|
|
73
73
|
}
|
|
74
|
-
| {kind: 'minuteSpanAcrossHourStep'; form: 'wildcard' | 'range'}
|
|
74
|
+
| {kind: 'minuteSpanAcrossHourStep'; form: 'wildcard' | 'range' | 'list'}
|
|
75
75
|
| {kind: 'everyHour'}
|
|
76
76
|
| {
|
|
77
77
|
kind: 'hourRange';
|
package/src/core/normalize.ts
CHANGED
|
@@ -67,10 +67,13 @@ function normalizeField(value: string, field: Field, spec: FieldSpec): string {
|
|
|
67
67
|
|
|
68
68
|
const cycle = timeFieldCycle[field];
|
|
69
69
|
const segments = stringValue.split(',').map(function canonical(segment) {
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
return canonicalizeTokens(collapseFullSpanRange(
|
|
71
|
+
enumerateNonUniformStep(
|
|
72
|
+
collapseFullSpanStep(
|
|
73
|
+
collapseDegenerateRange(
|
|
74
|
+
collapseOnceStep(collapseUnitStep(segment, spec), spec), spec),
|
|
75
|
+
spec),
|
|
76
|
+
spec, cycle), spec), spec);
|
|
74
77
|
}).join(',').split(',');
|
|
75
78
|
|
|
76
79
|
// A full-cycle segment covers the whole field.
|
|
@@ -83,6 +86,40 @@ function normalizeField(value: string, field: Field, spec: FieldSpec): string {
|
|
|
83
86
|
}).join(',');
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
// Rewrite a segment's value tokens to their canonical numbers: a name
|
|
90
|
+
// (`MON`, `JAN`) becomes its index, and a weekday `7` (Sunday again, above
|
|
91
|
+
// `top`) folds to the field minimum (`0`). Applied to every token position —
|
|
92
|
+
// a single, both range bounds, and a step's start — so a normalized field
|
|
93
|
+
// never carries a surface name or the 7-for-Sunday alias. `*` and Quartz
|
|
94
|
+
// tokens (resolved earlier) are left untouched. Output is unchanged: a
|
|
95
|
+
// renderer maps the number back to its own name.
|
|
96
|
+
function canonicalizeTokens(segment: string, spec: FieldSpec): string {
|
|
97
|
+
// Only the named fields (month, weekday) carry name tokens or the
|
|
98
|
+
// weekday `7`-for-Sunday alias; every other field is already numeric.
|
|
99
|
+
if (!spec.numbers) {
|
|
100
|
+
return segment;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parts = segment.split('/');
|
|
104
|
+
const start = parts[0].split('-').map(function fold(token) {
|
|
105
|
+
return canonicalizeToken(token, spec);
|
|
106
|
+
}).join('-');
|
|
107
|
+
|
|
108
|
+
return parts.length === 2 ? start + '/' + parts[1] : start;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// A single token to its canonical number string (`*` passes through).
|
|
112
|
+
function canonicalizeToken(token: string, spec: FieldSpec): string {
|
|
113
|
+
if (token === '*') {
|
|
114
|
+
return token;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const number = toFieldNumber(token, spec.numbers);
|
|
118
|
+
|
|
119
|
+
// A value above `top` (weekday 7) is the field minimum again.
|
|
120
|
+
return '' + (number > (spec.top as number) ? spec.min : number);
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
// An interval-one step enumerates every value from its start, so it reads
|
|
87
124
|
// as the equivalent range: `1/1` is `1-59` and `5-30/1` is `5-30`. A start
|
|
88
125
|
// at the bottom of the cycle covers the whole field (`0/1` is `*`).
|
|
@@ -163,6 +200,59 @@ function enumerateNonUniformStep(
|
|
|
163
200
|
return fires.join(',');
|
|
164
201
|
}
|
|
165
202
|
|
|
203
|
+
// A step whose range part covers the whole field (`0-59/2`, `0-23/2`) strides
|
|
204
|
+
// across the entire cycle, so the bound adds nothing — it reads as the
|
|
205
|
+
// unbounded `*/N` step (which the cycle test then renders as "every N" or its
|
|
206
|
+
// fires). Partial-range steps (`9-17/2`) keep their window.
|
|
207
|
+
function collapseFullSpanStep(segment: string, spec: FieldSpec): string {
|
|
208
|
+
const parts = segment.split('/');
|
|
209
|
+
|
|
210
|
+
if (parts.length !== 2 || !includes(parts[0], '-')) {
|
|
211
|
+
return segment;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return collapseFullSpanRange(parts[0], spec) === '*' ?
|
|
215
|
+
'*/' + parts[1] :
|
|
216
|
+
segment;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A plain range whose enumerated values cover the whole field imposes no
|
|
220
|
+
// restriction, so it reads identically to `*` (`0-59` minute, `0-23` hour,
|
|
221
|
+
// `1-31` date, `1-12` month, and every seven-day weekday range — `0-6`,
|
|
222
|
+
// `1-7`, `0-7`, `SUN-SAT` — since cron's 7 is Sunday again, folding to the
|
|
223
|
+
// field minimum). Only bare ranges qualify: a step (`0-59/2`) keeps its
|
|
224
|
+
// cadence, so segments carrying a `/` are left untouched.
|
|
225
|
+
function collapseFullSpanRange(segment: string, spec: FieldSpec): string {
|
|
226
|
+
if (typeof spec.top !== 'number' || includes(segment, '/') ||
|
|
227
|
+
!includes(segment, '-')) {
|
|
228
|
+
return segment;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const bounds = segment.split('-');
|
|
232
|
+
const low = toFieldNumber(bounds[0], spec.numbers);
|
|
233
|
+
const high = toFieldNumber(bounds[1], spec.numbers);
|
|
234
|
+
|
|
235
|
+
if (low > high) {
|
|
236
|
+
return segment;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// The full field is min..top; a value above top (weekday 7) folds to min.
|
|
240
|
+
const top = spec.top as number;
|
|
241
|
+
const fired: Record<number, boolean> = {};
|
|
242
|
+
|
|
243
|
+
for (let value = low; value <= high; value += 1) {
|
|
244
|
+
fired[value > top ? spec.min : value] = true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (let value = spec.min; value <= top; value += 1) {
|
|
248
|
+
if (!fired[value]) {
|
|
249
|
+
return segment;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return '*';
|
|
254
|
+
}
|
|
255
|
+
|
|
166
256
|
// A degenerate range (`9-9`) fires once, so it reads as its single value.
|
|
167
257
|
// A stepped degenerate range (`9-9/5`) likewise fires only at its start.
|
|
168
258
|
function collapseDegenerateRange(segment: string, spec: FieldSpec): string {
|
package/src/core/util.ts
CHANGED
|
@@ -16,6 +16,34 @@ function isNonNegativeInteger(value: string): boolean {
|
|
|
16
16
|
return digits.test(value);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Recognize an arithmetic progression in a sorted, distinct numeric set: a
|
|
20
|
+
// run of length >= 5 whose consecutive gaps are all equal and >= 2. Returns
|
|
21
|
+
// its {start, interval, last}; null for anything shorter, with a gap of one
|
|
22
|
+
// (a plain run, which reads as a range), or irregular. Output-neutral and
|
|
23
|
+
// language-agnostic: renderers use it to speak a bounded/offset step cadence
|
|
24
|
+
// ("every N from M [through K]") instead of enumerating the fires. The set is
|
|
25
|
+
// the field's full value list, which the core has already sorted and deduped.
|
|
26
|
+
function arithmeticStep(values: number[]):
|
|
27
|
+
{start: number; interval: number; last: number} | null {
|
|
28
|
+
if (values.length < 5) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const interval = values[1] - values[0];
|
|
33
|
+
|
|
34
|
+
if (interval < 2) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (let i = 2; i < values.length; i += 1) {
|
|
39
|
+
if (values[i] - values[i - 1] !== interval) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {start: values[0], interval, last: values[values.length - 1]};
|
|
45
|
+
}
|
|
46
|
+
|
|
19
47
|
// Resolve a numeric or named field token (e.g. '5' or 'FRI') to its number.
|
|
20
48
|
function toFieldNumber(
|
|
21
49
|
token: string,
|
|
@@ -25,4 +53,6 @@ function toFieldNumber(
|
|
|
25
53
|
// weekday) reach here. They always have an associated `numberMap`.
|
|
26
54
|
return isNonNegativeInteger(token) ? +token : numberMap![token.toUpperCase()];
|
|
27
55
|
}
|
|
28
|
-
export {
|
|
56
|
+
export {
|
|
57
|
+
arithmeticStep, includes, isNonNegativeInteger, toFieldNumber, unique
|
|
58
|
+
};
|