cronli5 0.2.0 → 0.3.1

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