cronli5 0.1.4 → 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/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 "\u6BCF\u5C0F\u65F6" + valueList(fieldSegments(ir, "minute"), "\u5206");
224
+ return minuteHourClause(ir);
162
225
  }
163
226
  function hourList(ir) {
164
227
  return joinAnd(hourFires(ir).map(hourWord));
@@ -172,7 +235,7 @@ function hourFrame(ir) {
172
235
  }
173
236
  function renderMinuteFrequency(ir, plan) {
174
237
  const minuteStep = stepSegment(ir, "minute");
175
- const base = minuteStep.startToken === "*" ? cadence(minuteStep.interval, UNITS.minute) : renderMinutePast(ir);
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");
@@ -201,12 +264,15 @@ function renderMinutesAcrossHours(ir, plan) {
201
264
  if (form === "wildcard") {
202
265
  return "\u5728" + hourList(ir) + "\uFF0C\u6BCF\u5206\u949F";
203
266
  }
204
- return hourList(ir) + "\uFF0C\u6BCF\u5C0F\u65F6" + valueList(fieldSegments(ir, "minute"), "\u5206") + "\uFF0C\u6BCF\u5206\u949F";
267
+ return hourList(ir) + "\uFF0C" + minuteHourClause(ir) + "\uFF0C\u6BCF\u5206\u949F";
205
268
  }
206
269
  function renderMinuteSpanAcrossHourStep(ir, plan) {
207
270
  const hourStep = stepSegment(ir, "hour");
208
271
  const { form } = plan;
209
- const minuteTail = form === "wildcard" ? "\u6BCF\u5206\u949F" : "\u6BCF\u5C0F\u65F6" + valueList(fieldSegments(ir, "minute"), "\u5206") + "\uFF0C\u6BCF\u5206\u949F";
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";
210
276
  if (hourStep.startToken !== "*") {
211
277
  return form === "wildcard" ? "\u5728" + hourList(ir) + "\uFF0C" + minuteTail : hourList(ir) + "\uFF0C" + minuteTail;
212
278
  }
@@ -217,15 +283,27 @@ function renderMinuteSpanAcrossHourStep(ir, plan) {
217
283
  return form === "wildcard" ? cad + "\u5185\uFF0C" + minuteTail : cad + "\uFF0C" + minuteTail;
218
284
  }
219
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
+ }
220
292
  const { times } = plan;
221
293
  return joinAnd(times.map((t) => clockTime(t.hour, t.minute, t.second, opts)));
222
294
  }
223
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
+ }
224
302
  const { minute } = plan;
225
303
  const secs = fieldSegments(ir, "second");
226
304
  const tail = secs.length && ir.pattern.second !== "0" ? "\uFF0C\u7B2C" + valueText(secs) + "\u79D2" : "";
227
305
  if (minute > 0) {
228
- return "\u6BCF\u5C0F\u65F6" + valueList(fieldSegments(ir, "minute"), "\u5206") + "\uFF0C\u5728" + hourList(ir) + tail;
306
+ return minuteHourClause(ir) + "\uFF0C\u5728" + hourList(ir) + tail;
229
307
  }
230
308
  return hourList(ir) + tail;
231
309
  }
@@ -233,7 +311,7 @@ function renderHourRange(ir, plan) {
233
311
  const range = plan;
234
312
  if (range.minuteForm === "lead") {
235
313
  const minuteSegs = fieldSegments(ir, "minute");
236
- const past = minuteSegs.length && ir.pattern.minute !== "0" ? "\u6BCF\u5C0F\u65F6" + valueList(minuteSegs, "\u5206") : "\u6BCF\u5C0F\u65F6";
314
+ const past = minuteSegs.length && ir.pattern.minute !== "0" ? minuteHourClause(ir) : "\u6BCF\u5C0F\u65F6";
237
315
  return "\u5728" + hourWord(range.from) + "\u81F3" + hourWord(range.to) + "\u4E4B\u95F4\uFF0C" + past;
238
316
  }
239
317
  if (range.minuteForm === "range") {
@@ -251,8 +329,52 @@ function renderHourStep(ir) {
251
329
  }
252
330
  return cadence(segment.interval, UNITS.hour);
253
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
+ }
254
376
  function renderRangeOfMinutes(ir) {
255
- return "\u6BCF\u5C0F\u65F6" + valueList(fieldSegments(ir, "minute"), "\u5206") + "\uFF0C\u6BCF\u5206\u949F";
377
+ return minuteHourClause(ir) + "\uFF0C\u6BCF\u5206\u949F";
256
378
  }
257
379
  function renderStandaloneSeconds(ir) {
258
380
  const segs = fieldSegments(ir, "second");
@@ -260,7 +382,7 @@ function renderStandaloneSeconds(ir) {
260
382
  if (segs.length === 1 && first.kind === "step" && first.startToken === "*") {
261
383
  return cadence(first.interval, UNITS.second);
262
384
  }
263
- return "\u6BCF\u5206\u949F\u7B2C" + valueText(segs) + "\u79D2";
385
+ return strideFromSegments(segs, "\u79D2", "\u79D2", "\u6BCF\u5206\u949F") ?? "\u6BCF\u5206\u949F\u7B2C" + valueText(segs) + "\u79D2";
264
386
  }
265
387
  function renderSecondPastMinute(ir) {
266
388
  return "\u6BCF\u5206\u949F\u7B2C" + valueText(fieldSegments(ir, "second")) + "\u79D2";
@@ -283,7 +405,7 @@ function secondClause(ir) {
283
405
  if (segs.length === 1 && first.kind === "step" && first.startToken === "*") {
284
406
  return cadence(first.interval, UNITS.second);
285
407
  }
286
- return "\u7B2C" + valueText(segs) + "\u79D2";
408
+ return strideFromSegments(segs, "\u79D2", "\u79D2", "\u6BCF\u5206\u949F") ?? "\u7B2C" + valueText(segs) + "\u79D2";
287
409
  }
288
410
  function minuteClause(ir) {
289
411
  if (ir.pattern.minute === "*") {
@@ -300,13 +422,15 @@ function isHourCadence(ir) {
300
422
  function composeSecondsOnHour(ir, plan, opts) {
301
423
  const sec = secondClause(ir);
302
424
  const { rest } = plan;
303
- if ((rest.kind === "clockTimes" || rest.kind === "compactClockTimes") && ir.pattern.minute === "0") {
304
- const clocks = hourFires(ir).map(function clock(hour) {
305
- return hourWord(hour) + "0\u5206";
306
- });
307
- const tail = sec === "\u6BCF\u79D2" ? "\u7684\u6BCF\u4E00\u79D2" : "\u7684" + sec;
308
- const core = joinAnd(clocks) + tail;
309
- return isDaily(ir) ? "\u6BCF\u5929" + core : core;
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);
310
434
  }
311
435
  const restText = render(ir, rest, opts);
312
436
  if (rest.kind === "clockTimes" || rest.kind === "compactClockTimes") {
@@ -319,6 +443,15 @@ function composeSecondsOnHour(ir, plan, opts) {
319
443
  }
320
444
  return restText + sec;
321
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
+ }
322
455
  function composeSecondsCadence(ir) {
323
456
  const sec = secondClause(ir);
324
457
  const tail = minuteClause(ir) + sec;
@@ -329,15 +462,23 @@ function composeSecondsCadence(ir) {
329
462
  return hourWord(hourFires(ir)[0]) + "\u7684" + tail;
330
463
  }
331
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
+ }
332
471
  return sec + "\uFF0C" + minuteClause(ir);
333
472
  }
334
473
  return hourFrame(ir) + tail;
335
474
  }
336
475
  function composeSecondsListed(ir) {
337
476
  const sec = secondClause(ir);
338
- const minutes = "\u6BCF\u5C0F\u65F6" + valueList(fieldSegments(ir, "minute"), "\u5206");
477
+ const minutes = minuteHourClause(ir);
339
478
  if (ir.shapes.hour === "single" && sec === "\u6BCF\u79D2") {
340
- return hourWord(hourFires(ir)[0]) + valueList(fieldSegments(ir, "minute"), "\u5206") + "\u7684\u6BCF\u4E00\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";
341
482
  }
342
483
  if (ir.shapes.hour === "wildcard") {
343
484
  return minutes + "\uFF0C" + sec;
@@ -554,11 +695,16 @@ function composeCadence(ir, core) {
554
695
  function composeWindow(ir, core) {
555
696
  return qualifier(ir) + core;
556
697
  }
698
+ function hourCadenceApplies(ir) {
699
+ return ir.shapes.minute === "single" && hourCadence(ir) !== null;
700
+ }
557
701
  function describe(ir, opts) {
558
702
  const { kind } = ir.plan;
559
703
  const core = render(ir, ir.plan, opts);
560
704
  let composed = core;
561
- if (kind === "clockTimes" || kind === "compactClockTimes" && ir.pattern.minute === "0") {
705
+ if (hourCadenceApplies(ir)) {
706
+ composed = composeCadence(ir, core);
707
+ } else if (kind === "clockTimes" || kind === "compactClockTimes" && ir.pattern.minute === "0") {
562
708
  composed = composePoint(ir, core);
563
709
  } else if (kind === "hourStep" || kind === "minuteFrequency" || kind === "minuteSpanAcrossHourStep" || kind === "compactClockTimes") {
564
710
  composed = composeCadence(ir, core);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cronli5",
3
- "version": "0.1.4",
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,6 +60,7 @@
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",
65
66
  "metamorphic": "node --import tsx tooling/scripts/metamorphic.mjs",
@@ -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/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 {includes, isNonNegativeInteger, toFieldNumber, unique};
56
+ export {
57
+ arithmeticStep, includes, isNonNegativeInteger, toFieldNumber, unique
58
+ };