cronli5 0.2.1 → 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 (54) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/cronli5.min.js +2 -2
  3. package/dist/cronli5.cjs +471 -383
  4. package/dist/cronli5.js +471 -383
  5. package/dist/lang/de.cjs +286 -215
  6. package/dist/lang/de.js +286 -215
  7. package/dist/lang/en.cjs +413 -327
  8. package/dist/lang/en.js +413 -327
  9. package/dist/lang/es.cjs +303 -265
  10. package/dist/lang/es.js +303 -265
  11. package/dist/lang/fi.cjs +311 -266
  12. package/dist/lang/fi.js +311 -266
  13. package/dist/lang/zh.cjs +308 -236
  14. package/dist/lang/zh.js +308 -236
  15. package/package.json +1 -1
  16. package/src/core/analyze.ts +12 -12
  17. package/src/core/cadence.ts +164 -0
  18. package/src/core/index.ts +3 -1
  19. package/src/core/normalize.ts +3 -3
  20. package/src/core/parse.ts +1 -1
  21. package/src/core/{ir.ts → schedule.ts} +17 -18
  22. package/src/core/specs.ts +1 -1
  23. package/src/core/util.ts +3 -165
  24. package/src/core/validate.ts +1 -1
  25. package/src/core/weekday.ts +54 -0
  26. package/src/cronli5.ts +5 -5
  27. package/src/lang/de/index.ts +329 -219
  28. package/src/lang/en/dialects.ts +1 -1
  29. package/src/lang/en/index.ts +521 -372
  30. package/src/lang/es/index.ts +338 -286
  31. package/src/lang/es/notes.md +1 -1
  32. package/src/lang/fi/dialects.ts +1 -1
  33. package/src/lang/fi/index.ts +365 -299
  34. package/src/lang/fi/notes.md +23 -8
  35. package/src/lang/fi/status.json +1 -1
  36. package/src/lang/zh/index.ts +344 -237
  37. package/src/types.ts +6 -6
  38. package/types/core/analyze.d.ts +3 -3
  39. package/types/core/cadence.d.ts +33 -0
  40. package/types/core/index.d.ts +3 -1
  41. package/types/core/normalize.d.ts +1 -1
  42. package/types/core/parse.d.ts +1 -1
  43. package/types/core/{ir.d.ts → schedule.d.ts} +11 -16
  44. package/types/core/specs.d.ts +1 -1
  45. package/types/core/util.d.ts +1 -30
  46. package/types/core/weekday.d.ts +10 -0
  47. package/types/lang/de/index.d.ts +1 -1
  48. package/types/lang/en/dialects.d.ts +1 -1
  49. package/types/lang/en/index.d.ts +1 -1
  50. package/types/lang/es/index.d.ts +1 -1
  51. package/types/lang/fi/dialects.d.ts +1 -1
  52. package/types/lang/fi/index.d.ts +1 -1
  53. package/types/lang/zh/index.d.ts +1 -1
  54. 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.1",
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,14 +1,14 @@
1
1
  // Semantic analysis of canonical fields: fire enumeration, windows, shape
2
2
  // classification, and description-plan selection (the `plan`). The
3
- // resulting IR is descriptive. Language modules handle rendering into
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';
@@ -198,8 +198,8 @@ function fieldSegments(
198
198
  }
199
199
 
200
200
  // Analyze a prepared (parsed, validated, normalized) cron pattern into the
201
- // IR a language module renders from.
202
- function analyze(pattern: Pattern): IR {
201
+ // Schedule a language module renders from.
202
+ function analyze(pattern: Pattern): Schedule {
203
203
  const shapes = {} as Shapes;
204
204
  const segments = {} as Analyses['segments'];
205
205
 
@@ -216,18 +216,18 @@ function analyze(pattern: Pattern): IR {
216
216
  segments
217
217
  };
218
218
 
219
- const content: Content = {analyses, pattern, shapes};
219
+ const facts: ScheduleFacts = {analyses, pattern, shapes};
220
220
 
221
- return {...content, plan: selectPlan(content)};
221
+ return {...facts, plan: selectPlan(facts)};
222
222
  }
223
223
 
224
- // Select the description plan from the neutral content. This is the
224
+ // Select the description plan from the neutral facts. This is the
225
225
  // core's *suggestion*: a language may override it via `Language.plan`
226
- // without re-deriving it (the content-plan / overridable-plan split).
226
+ // without re-deriving it (the facts / overridable-plan split).
227
227
  // The selection mirrors the interpreter chain ordering exactly; renderers
228
228
  // must not re-derive it.
229
- function selectPlan(content: Content): PlanNode {
230
- const {analyses, pattern, shapes} = content;
229
+ function selectPlan(facts: ScheduleFacts): PlanNode {
230
+ const {analyses, pattern, shapes} = facts;
231
231
 
232
232
  if (pattern.second !== '0') {
233
233
  const seconds = planSeconds(pattern, shapes, analyses);
@@ -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 =
@@ -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,
100
+ * The neutral schedule facts: the language-independent facts about a pattern,
103
101
  * carrying no phrasing decision. `analyze` produces this; `selectPlan`
104
102
  * reads it to suggest a `plan`. The phrasing plan is deliberately *not*
105
- * part of the neutral content (docs/i18n-design.md §2.2).
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` plan kinds via `Language.plan`; 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
 
@@ -163,7 +162,7 @@ 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;
@@ -171,8 +170,8 @@ export interface Language<
171
170
  // form); each language owns its lead verb and punctuation.
172
171
  sentence(description: string): string;
173
172
  // Optionally override the core's suggested plan. Receives the neutral
174
- // `content` and the core's suggestion (`base`), so overriding is a thin
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
- plan?(content: Content, base: PlanNode): PlanNode | Extra;
176
+ plan?(facts: ScheduleFacts, base: PlanNode): PlanNode | Extra;
178
177
  }
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,12 +1,6 @@
1
- // Small shared utilities for the core.
2
-
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'}>;
1
+ // Generic pure helpers shared across the core.
9
2
 
3
+ // Whether a string contains a substring (coercing numbers to strings).
10
4
  function includes(str: string | number, sub: string): boolean {
11
5
  return ('' + str).indexOf(sub) !== -1;
12
6
  }
@@ -23,82 +17,6 @@ function isNonNegativeInteger(value: string): boolean {
23
17
  return digits.test(value);
24
18
  }
25
19
 
26
- // Recognize an arithmetic progression in a sorted, distinct numeric set: a
27
- // run of length >= 5 whose consecutive gaps are all equal and >= 2. Returns
28
- // its {start, interval, last}; null for anything shorter, with a gap of one
29
- // (a plain run, which reads as a range), or irregular. Output-neutral and
30
- // language-agnostic: renderers use it to speak a bounded/offset step cadence
31
- // ("every N from M [through K]") instead of enumerating the fires. The set is
32
- // the field's full value list, which the core has already sorted and deduped.
33
- function arithmeticStep(values: number[]):
34
- {start: number; interval: number; last: number} | null {
35
- if (values.length < 5) {
36
- return null;
37
- }
38
-
39
- const interval = values[1] - values[0];
40
-
41
- if (interval < 2) {
42
- return null;
43
- }
44
-
45
- for (let i = 2; i < values.length; i += 1) {
46
- if (values[i] - values[i - 1] !== interval) {
47
- return null;
48
- }
49
- }
50
-
51
- return {start: values[0], interval, last: values[values.length - 1]};
52
- }
53
-
54
- // The display sort key for a canonical weekday number: Monday (1) first,
55
- // Sunday (0) last. The IR keeps Sunday=0 canonical; this is display-only.
56
- function weekdayDisplayKey(value: number): number {
57
- return value === 0 ? 7 : value;
58
- }
59
-
60
- // A weekday display segment: a single day or a (possibly wrap) range. Steps
61
- // are flattened away into singles before sorting, so the result is only these
62
- // two kinds; each renderer turns them into names exactly as it does today.
63
- type WeekdaySegment =
64
- | {kind: 'single'; value: string}
65
- | {kind: 'range'; bounds: [string, string]};
66
-
67
- // Reorder weekday segments Monday-first (Sunday last) for display, so a weekend
68
- // list reads "Saturday and Sunday" rather than the canonical Sunday-first
69
- // "Sunday and Saturday". Display-only: the IR / canonical order is unchanged (a
70
- // fresh array is returned). A step expands to its fires as singles so the days
71
- // sort into the list; a range stays one unit and keeps its own bounds order (a
72
- // wrap range is not reordered into a list), sorting by its opening bound — so a
73
- // lone range sorts to a one-element list and is unchanged. The sort is stable,
74
- // so equal opening days keep input order.
75
- function orderWeekdaysForDisplay(segments: Segment[]): WeekdaySegment[] {
76
- const flattened: WeekdaySegment[] = segments.flatMap(function flat(segment) {
77
- return segment.kind === 'step' ?
78
- segment.fires.map(function single(value): WeekdaySegment {
79
- return {kind: 'single', value: '' + value};
80
- }) :
81
- [segment];
82
- });
83
-
84
- function key(segment: WeekdaySegment): number {
85
- return segment.kind === 'range' ?
86
- weekdayDisplayKey(+segment.bounds[0]) :
87
- weekdayDisplayKey(+segment.value);
88
- }
89
-
90
- return flattened
91
- .map(function index(segment, position): [WeekdaySegment, number] {
92
- return [segment, position];
93
- })
94
- .sort(function byDisplayKey(a, b): number {
95
- return key(a[0]) - key(b[0]) || a[1] - b[1];
96
- })
97
- .map(function unwrap(pair): WeekdaySegment {
98
- return pair[0];
99
- });
100
- }
101
-
102
20
  // Resolve a numeric or named field token (e.g. '5' or 'FRI') to its number.
103
21
  function toFieldNumber(
104
22
  token: string,
@@ -108,85 +26,5 @@ function toFieldNumber(
108
26
  // weekday) reach here. They always have an associated `numberMap`.
109
27
  return isNonNegativeInteger(token) ? +token : numberMap![token.toUpperCase()];
110
28
  }
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
29
 
188
- export {
189
- arithmeticStep, hourListStride, includes, isNonNegativeInteger,
190
- offsetCleanStride, orderWeekdaysForDisplay, segmentsOf, singleValues,
191
- stepSegment, toFieldNumber, unique
192
- };
30
+ export {includes, isNonNegativeInteger, toFieldNumber, unique};
@@ -3,7 +3,7 @@
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
82
+ // Analyze into the neutral facts + the core's suggested plan, then let the
83
83
  // language optionally override the plan before rendering. A language
84
84
  // without a `plan` hook renders the core's suggestion unchanged.
85
- const ir = analyze(prepare(cronPattern, opts));
86
- const plan = lang.plan ? lang.plan(ir, ir.plan) : ir.plan;
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;