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/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
- fieldSegments(ir, "hour").forEach(function expand(segment) {
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 = fieldSegments(ir, "minute");
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 = fieldSegments(ir, "hour").flatMap(hourSegmentWords);
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] = fieldSegments(ir, "hour")[0].bounds;
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 = fieldSegments(ir, "second");
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 = fieldSegments(ir, "minute");
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(fieldSegments(ir, "minute"), "\u5206") + "\uFF0C\u6BCF\u5206\u949F";
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 = fieldSegments(ir, "hour");
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 = fieldSegments(ir, "second");
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(fieldSegments(ir, "second")) + "\u79D2";
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 = fieldSegments(ir, "second");
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 = fieldSegments(ir, "second");
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(fieldSegments(ir, "minute"), "\u5206");
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(fieldSegments(ir, "second"), "\u79D2", "\u79D2", "");
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 fieldSegments(ir, "hour").some(function range(segment) {
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 = fieldSegments(ir, "minute");
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 = fieldSegments(ir, "month");
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 = fieldSegments(ir, "date");
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 = fieldSegments(ir, "weekday");
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 = fieldSegments(ir, "year").map(function part(seg) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cronli5",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Cron Like I'm Five: A Cron to English Utility",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  // Semantic analysis of canonical fields: fire enumeration, windows, shape
2
- // classification, and description-strategy selection (the `plan`). The
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 within [0, max].
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: selectStrategy(content)};
221
+ return {...content, plan: selectPlan(content)};
221
222
  }
222
223
 
223
- // Select the description strategy from the neutral content. This is the
224
- // core's *suggestion*: a language may override it via `Language.strategy`
225
- // without re-deriving it (the content-plan / overridable-strategy split).
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 selectStrategy(content: Content): PlanNode {
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 strategies, or null when the second folds into the clock time
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 strategies, in the interpreter-chain order, or null to defer to
305
- // the hour strategies.
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 strategies that only stand on their own under a wildcard hour.
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 strategies: the chain's last resort always produces a plan. Under a
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, selectStrategy};
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 strategy the core selects for a pattern. The `kind`
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; `selectStrategy`
104
- * reads it to suggest a `plan`. The phrasing strategy is deliberately *not*
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` strategy kinds via `Language.strategy`; by default there are
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 strategy kinds (default: none), which its
160
- * `strategy` override emits and its `describe` renders.
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 strategy. Receives the neutral
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
- strategy?(content: Content, base: PlanNode): PlanNode | Extra;
177
+ plan?(content: Content, base: PlanNode): PlanNode | Extra;
178
178
  }
@@ -37,5 +37,12 @@ function isDiscreteHours(hourField: string): boolean {
37
37
  return hourField !== '*' && !isPlainRange(hourField) &&
38
38
  !isPlainStep(hourField);
39
39
  }
40
- export {isDiscreteHours, isDiscreteList, isPlainRange, isPlainStep,
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, orderWeekdaysForDisplay,
108
- toFieldNumber, unique
189
+ arithmeticStep, hourListStride, includes, isNonNegativeInteger,
190
+ offsetCleanStride, orderWeekdaysForDisplay, segmentsOf, singleValues,
191
+ stepSegment, toFieldNumber, unique
109
192
  };
@@ -1,4 +1,4 @@
1
- // Loosely alidate a cron-like object against the field specifications,
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';
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 strategy before rendering. A language
84
- // without a `strategy` hook renders the core's suggestion unchanged.
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.strategy ? lang.strategy(ir, ir.plan) : ir.plan;
86
+ const plan = lang.plan ? lang.plan(ir, ir.plan) : ir.plan;
87
87
 
88
88
  return lang.describe({...ir, plan}, opts);
89
89
  }