cronli5 0.3.4 → 0.8.0

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 (51) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +63 -13
  3. package/cronli5.min.js +2 -2
  4. package/dist/cronli5.cjs +72 -9
  5. package/dist/cronli5.js +72 -9
  6. package/dist/lang/de.cjs +14 -6
  7. package/dist/lang/de.js +14 -6
  8. package/dist/lang/en.cjs +14 -6
  9. package/dist/lang/en.js +14 -6
  10. package/dist/lang/es.cjs +14 -6
  11. package/dist/lang/es.js +14 -6
  12. package/dist/lang/fi.cjs +14 -6
  13. package/dist/lang/fi.js +14 -6
  14. package/dist/lang/fr.cjs +1211 -0
  15. package/dist/lang/fr.js +1187 -0
  16. package/dist/lang/pt.cjs +1592 -0
  17. package/dist/lang/pt.js +1568 -0
  18. package/dist/lang/zh.cjs +58 -9
  19. package/dist/lang/zh.js +58 -9
  20. package/package.json +13 -2
  21. package/src/core/cadence.ts +25 -12
  22. package/src/core/index.ts +7 -3
  23. package/src/core/quartz.ts +97 -0
  24. package/src/core/schedule.ts +1 -0
  25. package/src/core/specs.ts +2 -2
  26. package/src/cronli5.ts +20 -3
  27. package/src/lang/de/index.ts +3 -2
  28. package/src/lang/en/index.ts +3 -2
  29. package/src/lang/es/index.ts +3 -2
  30. package/src/lang/fi/index.ts +3 -2
  31. package/src/lang/fr/dialects.ts +49 -0
  32. package/src/lang/fr/index.ts +2116 -0
  33. package/src/lang/fr/notes.md +280 -0
  34. package/src/lang/fr/status.json +8 -0
  35. package/src/lang/pt/dialects.ts +56 -0
  36. package/src/lang/pt/index.ts +2804 -0
  37. package/src/lang/pt/notes.md +199 -0
  38. package/src/lang/pt/status.json +8 -0
  39. package/src/lang/zh/index.ts +61 -5
  40. package/src/lang/zh/notes.md +16 -4
  41. package/src/lang/zh/status.json +10 -1
  42. package/src/types.ts +44 -0
  43. package/types/core/cadence.d.ts +1 -0
  44. package/types/core/quartz.d.ts +4 -0
  45. package/types/core/schedule.d.ts +1 -0
  46. package/types/cronli5.d.ts +4 -4
  47. package/types/lang/fr/dialects.d.ts +11 -0
  48. package/types/lang/fr/index.d.ts +4 -0
  49. package/types/lang/pt/dialects.d.ts +13 -0
  50. package/types/lang/pt/index.d.ts +4 -0
  51. package/types/types.d.ts +39 -0
package/dist/lang/zh.cjs CHANGED
@@ -59,13 +59,20 @@ function singleValues(segments) {
59
59
  function offsetCleanStride(stride) {
60
60
  return stride.start < stride.interval && 24 % stride.interval === 0;
61
61
  }
62
+ function lastTileOf(start, interval, cycle) {
63
+ return cycle - 1 - (cycle - 1 - start) % interval;
64
+ }
62
65
  function renderStride(spec, parts) {
63
- const { start, interval, cycle } = spec;
64
- const tiles = cycle % interval === 0;
65
- if (start === 0 && tiles) {
66
+ const { start, interval, last, cycle } = spec;
67
+ const open = cycle % interval === 0 && last === lastTileOf(
68
+ start,
69
+ interval,
70
+ cycle
71
+ );
72
+ if (start === 0 && open) {
66
73
  return parts.bare();
67
74
  }
68
- if (start < interval && tiles) {
75
+ if (start < interval && open) {
69
76
  return parts.offset();
70
77
  }
71
78
  return parts.bounded();
@@ -165,6 +172,35 @@ function resolveDialect(dialect) {
165
172
  // src/lang/zh/index.ts
166
173
  var UNITS = { hour: "\u5C0F\u65F6", minute: "\u5206\u949F", second: "\u79D2" };
167
174
  var WEEKDAYS = ["\u5468\u65E5", "\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D"];
175
+ var HANT = {
176
+ \u4E2A: "\u500B",
177
+ \u4ECE: "\u5F9E",
178
+ \u5185: "\u5167",
179
+ \u522B: "\u5225",
180
+ \u52A8: "\u52D5",
181
+ \u5355: "\u55AE",
182
+ \u53CC: "\u96D9",
183
+ \u540E: "\u5F8C",
184
+ \u542F: "\u555F",
185
+ \u5468: "\u9031",
186
+ \u6570: "\u6578",
187
+ \u65E0: "\u7121",
188
+ \u65F6: "\u6642",
189
+ \u70B9: "\u9EDE",
190
+ \u7EDF: "\u7D71",
191
+ \u8BC6: "\u8B58",
192
+ \u8FBE: "\u9054",
193
+ \u8FD0: "\u904B",
194
+ \u949F: "\u9418",
195
+ \u95F4: "\u9593"
196
+ };
197
+ function toVariant(text, variant) {
198
+ if (variant !== "Hant") {
199
+ return text;
200
+ }
201
+ return Array.from(text, (glyph) => HANT[glyph] ?? glyph).join("");
202
+ }
203
+ var activeVariant = "Hans";
168
204
  function joinAnd(items) {
169
205
  if (items.length < 2) {
170
206
  return items.join("");
@@ -177,7 +213,7 @@ function cadence(interval, unit) {
177
213
  function renderStride2(stride) {
178
214
  const { interval, start, last, cycle, unit, mark, anchor } = stride;
179
215
  const lead = anchor + "\u4ECE" + start + mark + "\u8D77" + cadence(interval, unit);
180
- return renderStride({ start, interval, cycle }, {
216
+ return renderStride({ start, interval, last, cycle }, {
181
217
  bare: () => cadence(interval, unit),
182
218
  offset: () => lead,
183
219
  bounded: () => lead + "\uFF0C\u81F3" + last + mark
@@ -873,6 +909,9 @@ function hourCadenceApplies(schedule) {
873
909
  return hourCadenceText(schedule) !== null;
874
910
  }
875
911
  function describe(schedule, opts) {
912
+ return toVariant(describeHans(schedule, opts), opts.style.variant);
913
+ }
914
+ function describeHans(schedule, opts) {
876
915
  const { kind } = schedule.plan;
877
916
  const core = render(schedule, schedule.plan, opts);
878
917
  let composed = core;
@@ -901,21 +940,31 @@ function describe(schedule, opts) {
901
940
  }
902
941
  function normalizeOptions(options) {
903
942
  options = options || {};
943
+ const style = resolveDialect(options.dialect);
944
+ activeVariant = style.variant;
904
945
  return {
905
946
  ampm: typeof options.ampm === "boolean" ? options.ampm : false,
906
947
  lenient: !!options.lenient,
948
+ quartz: !!options.quartz,
907
949
  seconds: !!options.seconds,
908
950
  short: !!options.short,
909
- style: resolveDialect(options.dialect),
951
+ style,
910
952
  years: !!options.years
911
953
  };
912
954
  }
913
955
  var zh2 = {
914
956
  describe,
915
- fallback: "\u65E0\u6CD5\u8BC6\u522B\u7684 cron \u8868\u8FBE\u5F0F",
957
+ // `reboot`/`fallback` are contract-fixed strings the core reads without
958
+ // `opts`; getters honor the variant `options()` latched, keeping the shared
959
+ // Language contract unchanged while the Traditional dialect still applies.
960
+ get fallback() {
961
+ return toVariant("\u65E0\u6CD5\u8BC6\u522B\u7684 cron \u8868\u8FBE\u5F0F", activeVariant);
962
+ },
916
963
  options: normalizeOptions,
917
- reboot: "\u7CFB\u7EDF\u542F\u52A8\u65F6",
918
- sentence: (description) => "\u8FD0\u884C\u65F6\u95F4\uFF1A" + description + "\u3002"
964
+ get reboot() {
965
+ return toVariant("\u7CFB\u7EDF\u542F\u52A8\u65F6", activeVariant);
966
+ },
967
+ sentence: (description) => toVariant("\u8FD0\u884C\u65F6\u95F4\uFF1A", activeVariant) + description + "\u3002"
919
968
  };
920
969
  var index_default = zh2;
921
970
  module.exports = module.exports.default;
package/dist/lang/zh.js CHANGED
@@ -33,13 +33,20 @@ function singleValues(segments) {
33
33
  function offsetCleanStride(stride) {
34
34
  return stride.start < stride.interval && 24 % stride.interval === 0;
35
35
  }
36
+ function lastTileOf(start, interval, cycle) {
37
+ return cycle - 1 - (cycle - 1 - start) % interval;
38
+ }
36
39
  function renderStride(spec, parts) {
37
- const { start, interval, cycle } = spec;
38
- const tiles = cycle % interval === 0;
39
- if (start === 0 && tiles) {
40
+ const { start, interval, last, cycle } = spec;
41
+ const open = cycle % interval === 0 && last === lastTileOf(
42
+ start,
43
+ interval,
44
+ cycle
45
+ );
46
+ if (start === 0 && open) {
40
47
  return parts.bare();
41
48
  }
42
- if (start < interval && tiles) {
49
+ if (start < interval && open) {
43
50
  return parts.offset();
44
51
  }
45
52
  return parts.bounded();
@@ -139,6 +146,35 @@ function resolveDialect(dialect) {
139
146
  // src/lang/zh/index.ts
140
147
  var UNITS = { hour: "\u5C0F\u65F6", minute: "\u5206\u949F", second: "\u79D2" };
141
148
  var WEEKDAYS = ["\u5468\u65E5", "\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D"];
149
+ var HANT = {
150
+ \u4E2A: "\u500B",
151
+ \u4ECE: "\u5F9E",
152
+ \u5185: "\u5167",
153
+ \u522B: "\u5225",
154
+ \u52A8: "\u52D5",
155
+ \u5355: "\u55AE",
156
+ \u53CC: "\u96D9",
157
+ \u540E: "\u5F8C",
158
+ \u542F: "\u555F",
159
+ \u5468: "\u9031",
160
+ \u6570: "\u6578",
161
+ \u65E0: "\u7121",
162
+ \u65F6: "\u6642",
163
+ \u70B9: "\u9EDE",
164
+ \u7EDF: "\u7D71",
165
+ \u8BC6: "\u8B58",
166
+ \u8FBE: "\u9054",
167
+ \u8FD0: "\u904B",
168
+ \u949F: "\u9418",
169
+ \u95F4: "\u9593"
170
+ };
171
+ function toVariant(text, variant) {
172
+ if (variant !== "Hant") {
173
+ return text;
174
+ }
175
+ return Array.from(text, (glyph) => HANT[glyph] ?? glyph).join("");
176
+ }
177
+ var activeVariant = "Hans";
142
178
  function joinAnd(items) {
143
179
  if (items.length < 2) {
144
180
  return items.join("");
@@ -151,7 +187,7 @@ function cadence(interval, unit) {
151
187
  function renderStride2(stride) {
152
188
  const { interval, start, last, cycle, unit, mark, anchor } = stride;
153
189
  const lead = anchor + "\u4ECE" + start + mark + "\u8D77" + cadence(interval, unit);
154
- return renderStride({ start, interval, cycle }, {
190
+ return renderStride({ start, interval, last, cycle }, {
155
191
  bare: () => cadence(interval, unit),
156
192
  offset: () => lead,
157
193
  bounded: () => lead + "\uFF0C\u81F3" + last + mark
@@ -847,6 +883,9 @@ function hourCadenceApplies(schedule) {
847
883
  return hourCadenceText(schedule) !== null;
848
884
  }
849
885
  function describe(schedule, opts) {
886
+ return toVariant(describeHans(schedule, opts), opts.style.variant);
887
+ }
888
+ function describeHans(schedule, opts) {
850
889
  const { kind } = schedule.plan;
851
890
  const core = render(schedule, schedule.plan, opts);
852
891
  let composed = core;
@@ -875,21 +914,31 @@ function describe(schedule, opts) {
875
914
  }
876
915
  function normalizeOptions(options) {
877
916
  options = options || {};
917
+ const style = resolveDialect(options.dialect);
918
+ activeVariant = style.variant;
878
919
  return {
879
920
  ampm: typeof options.ampm === "boolean" ? options.ampm : false,
880
921
  lenient: !!options.lenient,
922
+ quartz: !!options.quartz,
881
923
  seconds: !!options.seconds,
882
924
  short: !!options.short,
883
- style: resolveDialect(options.dialect),
925
+ style,
884
926
  years: !!options.years
885
927
  };
886
928
  }
887
929
  var zh2 = {
888
930
  describe,
889
- fallback: "\u65E0\u6CD5\u8BC6\u522B\u7684 cron \u8868\u8FBE\u5F0F",
931
+ // `reboot`/`fallback` are contract-fixed strings the core reads without
932
+ // `opts`; getters honor the variant `options()` latched, keeping the shared
933
+ // Language contract unchanged while the Traditional dialect still applies.
934
+ get fallback() {
935
+ return toVariant("\u65E0\u6CD5\u8BC6\u522B\u7684 cron \u8868\u8FBE\u5F0F", activeVariant);
936
+ },
890
937
  options: normalizeOptions,
891
- reboot: "\u7CFB\u7EDF\u542F\u52A8\u65F6",
892
- sentence: (description) => "\u8FD0\u884C\u65F6\u95F4\uFF1A" + description + "\u3002"
938
+ get reboot() {
939
+ return toVariant("\u7CFB\u7EDF\u542F\u52A8\u65F6", activeVariant);
940
+ },
941
+ sentence: (description) => toVariant("\u8FD0\u884C\u65F6\u95F4\uFF1A", activeVariant) + description + "\u3002"
893
942
  };
894
943
  var index_default = zh2;
895
944
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cronli5",
3
- "version": "0.3.4",
3
+ "version": "0.8.0",
4
4
  "description": "Cron Like I'm Five: A Cron to English Utility",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,6 +37,16 @@
37
37
  "import": "./dist/lang/fi.js",
38
38
  "require": "./dist/lang/fi.cjs"
39
39
  },
40
+ "./lang/fr": {
41
+ "types": "./types/lang/fr/index.d.ts",
42
+ "import": "./dist/lang/fr.js",
43
+ "require": "./dist/lang/fr.cjs"
44
+ },
45
+ "./lang/pt": {
46
+ "types": "./types/lang/pt/index.d.ts",
47
+ "import": "./dist/lang/pt.js",
48
+ "require": "./dist/lang/pt.cjs"
49
+ },
40
50
  "./lang/zh": {
41
51
  "types": "./types/lang/zh/index.d.ts",
42
52
  "import": "./dist/lang/zh.js",
@@ -61,6 +71,7 @@
61
71
  "build": "node scripts/build.mjs && npm run types",
62
72
  "docs": "node --import tsx scripts/docs.mjs",
63
73
  "conciseness": "node --import tsx tooling/scripts/conciseness.mjs",
74
+ "divergence": "node --import tsx tooling/scripts/cronstrue-divergence.mjs",
64
75
  "fuzz": "node --import tsx scripts/fuzz-lang.mjs",
65
76
  "lint": "eslint src test cli.js eslint.config.js scripts tooling/scripts",
66
77
  "metamorphic": "node --import tsx tooling/scripts/metamorphic.mjs",
@@ -75,7 +86,7 @@
75
86
  "prepublishOnly": "npm run lint && npm run typecheck && npm run test:types && npm run build && npm test"
76
87
  },
77
88
  "engines": {
78
- "node": ">=18"
89
+ "node": ">=20"
79
90
  },
80
91
  "keywords": [
81
92
  "cron",
@@ -94,28 +94,41 @@ interface StrideParts {
94
94
  bounded(): string;
95
95
  }
96
96
 
97
+ // The last value a stride reaches within `[0, cycle)`: the largest value below
98
+ // the cycle that is congruent to `start` modulo `interval`. A clean, full-field
99
+ // stride runs to this tile; a bounded one (`a-b/n`) stops short of it, and that
100
+ // shortfall is what distinguishes the open cadence from a bounded set.
101
+ function lastTileOf(start: number, interval: number, cycle: number): number {
102
+ return cycle - 1 - (cycle - 1 - start) % interval;
103
+ }
104
+
97
105
  // 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
106
+ // emit the renderer's words for it. A clean stride from the top of the cycle is
107
+ // the bare cadence; a uniform offset (start within the first interval, the
100
108
  // 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
109
+ // with no distinct endpoint; a non-uniform stride (start >= interval, an
110
+ // interval that does not tile the cycle, or one whose last fire stops short of
111
+ // the cycle's final tile a bounded `a-b/n`) pins both endpoints so the
112
+ // bounded, non-wrapping set reads unambiguously. This is the one decision tree
113
+ // every renderer's `renderStride`/`hourStrideCadence` shared (cycle 60 for
105
114
  // minute/second, 24 for the hour); the branch lives here once, the prose in
106
115
  // each language's `parts`.
107
116
  function renderStride(
108
- spec: {start: number; interval: number; cycle: number},
117
+ spec: {start: number; interval: number; last: number; cycle: number},
109
118
  parts: StrideParts
110
119
  ): string {
111
- const {start, interval, cycle} = spec;
112
- const tiles = cycle % interval === 0;
113
-
114
- if (start === 0 && tiles) {
120
+ const {start, interval, last, cycle} = spec;
121
+ // A stride wraps the full field only when it both tiles the cycle and runs to
122
+ // the cycle's last tile; one that stops short (`0-20/2`, last 20 of 22) is a
123
+ // bounded set, not the open `*/n`, so it keeps its endpoint-pinning cadence.
124
+ const open = cycle % interval === 0 && last === lastTileOf(start, interval,
125
+ cycle);
126
+
127
+ if (start === 0 && open) {
115
128
  return parts.bare();
116
129
  }
117
130
 
118
- if (start < interval && tiles) {
131
+ if (start < interval && open) {
119
132
  return parts.offset();
120
133
  }
121
134
 
package/src/core/index.ts CHANGED
@@ -5,16 +5,20 @@
5
5
 
6
6
  import {applyQuartzAliases, normalizeCronPattern} from './normalize.js';
7
7
  import type {NormalizedOptions, Pattern} from './schedule.js';
8
+ import {applyQuartz} from './quartz.js';
8
9
  import {parseCronPattern} from './parse.js';
9
10
  import type {CronPattern} from '../types.js';
10
11
  import {validateCronPattern} from './validate.js';
11
12
 
12
- // Parse, alias, validate, and normalize cron input into a canonical
13
- // cron-like object of string fields, ready for semantic analysis and
14
- // rendering.
13
+ // Parse, apply Quartz semantics, alias, validate, and normalize cron input
14
+ // into a canonical cron-like object of string fields, ready for semantic
15
+ // analysis and rendering. Quartz handling runs first: it gates the `?` token
16
+ // (rejected unless `quartz`) and re-indexes the Quartz day-of-week to the
17
+ // canonical cron numbering the rest of the core expects.
15
18
  function prepare(cronPattern: CronPattern, opts: NormalizedOptions): Pattern {
16
19
  const pattern = parseCronPattern(cronPattern, opts);
17
20
 
21
+ applyQuartz(pattern, opts.quartz);
18
22
  applyQuartzAliases(pattern);
19
23
  validateCronPattern(pattern);
20
24
 
@@ -0,0 +1,97 @@
1
+ // Quartz input semantics. cronli5 accepts Quartz tokens (`?`, `L`, `W`, `#`),
2
+ // but Quartz numbers the day-of-week differently from standard cron: Quartz is
3
+ // 1 = Sunday … 7 = Saturday, while cron is 0/7 = Sunday, 1 = Monday. Reading a
4
+ // Quartz pattern with cron indexing silently shifts every weekday by one, so a
5
+ // Quartz `2` (Monday) would read as Tuesday. This module gates and re-indexes
6
+ // the input so the rest of the core keeps facing canonical cron values.
7
+ //
8
+ // The `?` token (Quartz's "no specific value", mandatory in Quartz, absent in
9
+ // standard cron) is the unambiguous mark of a Quartz pattern. Outside Quartz
10
+ // mode it is rejected outright rather than aliased to `*`, so a real Quartz
11
+ // cron errors loudly instead of being mis-read; inside Quartz mode it is the
12
+ // equivalent of `*`.
13
+
14
+ import type {CronLike} from './specs.js';
15
+ import {isNonNegativeInteger} from './util.js';
16
+
17
+ // The error a `?` raises outside Quartz mode: a clear pointer at the option
18
+ // that makes Quartz semantics (and `?`) available.
19
+ const quartzTokenMessage =
20
+ '`?` is a Quartz token — pass { quartz: true } to enable Quartz semantics.';
21
+
22
+ // In standard (non-Quartz) mode, `?` is not a valid value: reject it with a
23
+ // pointer at the `quartz` option. In Quartz mode, accept `?` as `*` and
24
+ // re-index the day-of-week from Quartz numbering (1 = Sunday) to the canonical
25
+ // cron numbering (0 = Sunday) the rest of the core expects. Operates in place
26
+ // on the raw cron-like object, before aliasing and validation.
27
+ function applyQuartz(cronPattern: CronLike, quartz: boolean): void {
28
+ if (!quartz) {
29
+ rejectQuartzToken(cronPattern.date);
30
+ rejectQuartzToken(cronPattern.weekday);
31
+
32
+ return;
33
+ }
34
+
35
+ if ('' + cronPattern.date === '?') {
36
+ cronPattern.date = '*';
37
+ }
38
+
39
+ if ('' + cronPattern.weekday === '?') {
40
+ cronPattern.weekday = '*';
41
+
42
+ return;
43
+ }
44
+
45
+ cronPattern.weekday = reindexWeekday('' + cronPattern.weekday);
46
+ }
47
+
48
+ // Throw the Quartz-token error if a field is exactly `?`.
49
+ function rejectQuartzToken(value: string | number): void {
50
+ if ('' + value === '?') {
51
+ throw new Error(quartzTokenMessage);
52
+ }
53
+ }
54
+
55
+ // Re-index a Quartz day-of-week field to canonical cron numbering. Every
56
+ // numeric weekday position maps n -> n-1 (Quartz 1 = Sunday becomes cron 0),
57
+ // across singles, ranges, and step bounds, and inside the DOW operators `nL`
58
+ // (last weekday) and `n#k` (kth weekday). Day NAMES (`MON`) are unambiguous and
59
+ // left untouched, as is the bare `L` alias (Saturday in both numberings) and
60
+ // `*`. Quartz has no weekday 0; it is rejected here.
61
+ function reindexWeekday(value: string): string {
62
+ if (value === '*') {
63
+ return value;
64
+ }
65
+
66
+ return value.split(',').map(reindexSegment).join(',');
67
+ }
68
+
69
+ // Re-index one comma-separated weekday segment: a single, a range, or either
70
+ // of those followed by a `#k` or `L` operator (or a `/step`).
71
+ function reindexSegment(segment: string): string {
72
+ const operator = (/(#\d+|L)$/).exec(segment);
73
+ const suffix = operator ? operator[0] : '';
74
+ const core = suffix ? segment.slice(0, -suffix.length) : segment;
75
+ const step = core.split('/');
76
+ const range = step[0].split('-').map(reindexNumber).join('-');
77
+ const head = step.length === 2 ? range + '/' + step[1] : range;
78
+
79
+ return head + suffix;
80
+ }
81
+
82
+ // Re-index a single weekday token: a number maps n -> n-1 (rejecting 0, which
83
+ // Quartz does not use); a name passes through unchanged.
84
+ function reindexNumber(token: string): string {
85
+ if (!isNonNegativeInteger(token)) {
86
+ return token;
87
+ }
88
+
89
+ if (token === '0') {
90
+ throw new Error('`cronli5` was passed an invalid Quartz day-of-week ' +
91
+ 'value "0"; Quartz numbers weekdays 1 (Sunday) through 7 (Saturday).');
92
+ }
93
+
94
+ return '' + (+token - 1);
95
+ }
96
+
97
+ export {applyQuartz, quartzTokenMessage};
@@ -147,6 +147,7 @@ export interface DialectStyle {
147
147
  export interface NormalizedOptions<Style = DialectStyle> {
148
148
  ampm: boolean;
149
149
  lenient: boolean;
150
+ quartz: boolean;
150
151
  seconds: boolean;
151
152
  short: boolean;
152
153
  style: Style;
package/src/core/specs.ts CHANGED
@@ -55,9 +55,9 @@ const fieldSpecs: Record<Field, FieldSpec> = {
55
55
  second: {cyclic: true, max: 59, min: 0, top: 59},
56
56
  minute: {cyclic: true, max: 59, min: 0, top: 59},
57
57
  hour: {cyclic: true, max: 23, min: 0, top: 23},
58
- date: {aliases: {'?': '*'}, cyclic: true, max: 31, min: 1, top: 31},
58
+ date: {cyclic: true, max: 31, min: 1, top: 31},
59
59
  month: {cyclic: true, max: 12, min: 1, numbers: monthNumbers, top: 12},
60
- weekday: {aliases: {'?': '*', L: '6'}, cyclic: true, max: 7, min: 0,
60
+ weekday: {aliases: {L: '6'}, cyclic: true, max: 7, min: 0,
61
61
  numbers: weekdayNumbers, top: 6},
62
62
  year: {max: 9999, min: 1970}
63
63
  };
package/src/cronli5.ts CHANGED
@@ -29,7 +29,7 @@
29
29
 
30
30
  import {analyze, prepare} from './core/index.js';
31
31
  import type {NormalizedOptions} from './core/schedule.js';
32
- import type {CronPattern, Cronli5Language, Cronli5Options}
32
+ import type {Cronli5, CronPattern, Cronli5Language, Cronli5Options}
33
33
  from './types.js';
34
34
  import en from './lang/en/index.js';
35
35
 
@@ -88,8 +88,25 @@ function interpretCronPattern(
88
88
  return lang.describe({...schedule, plan}, opts);
89
89
  }
90
90
 
91
- export default cronli5;
91
+ // Two named convenience methods are attached to the callable export as sugar
92
+ // over the `sentence` option: `.sentence(...)` forces the capitalized
93
+ // standalone, `.fragment(...)` forces the embeddable fragment (the default).
94
+ // The method's own intent wins, so a passed-through `sentence` flag is
95
+ // overridden. There is no `toString` method on purpose — it would shadow
96
+ // `Function.prototype.toString` (called arg-less by `String()`, template
97
+ // literals, and console/debug) and break coercion; named methods sidestep that.
98
+ function sentence(cronPattern: CronPattern, options?: Cronli5Options): string {
99
+ return cronli5(cronPattern, {...options, sentence: true});
100
+ }
101
+
102
+ function fragment(cronPattern: CronPattern, options?: Cronli5Options): string {
103
+ return cronli5(cronPattern, {...options, sentence: false});
104
+ }
105
+
106
+ const callable: Cronli5 = Object.assign(cronli5, {sentence, fragment});
107
+
108
+ export default callable;
92
109
  export type {
93
- Cronli5Dialect, Cronli5Language, Cronli5Options, CronPattern,
110
+ Cronli5, Cronli5Dialect, Cronli5Language, Cronli5Options, CronPattern,
94
111
  CronPatternObject
95
112
  } from './types.js';
@@ -90,7 +90,7 @@ function renderStride(stride: Stride): string {
90
90
  // the cadence keeps its endpoints but drops the "jeder Stunde" tail.
91
91
  const tail = anchor ? ' ' + anchor : '';
92
92
 
93
- return chooseStride({start, interval, cycle}, {
93
+ return chooseStride({start, interval, last, cycle}, {
94
94
  bare: () => cadence,
95
95
  offset: () => cadence + ' ab ' + unit.singular + ' ' + start + tail,
96
96
  bounded: () =>
@@ -1019,7 +1019,7 @@ function hourStrideCadence(
1019
1019
  const {start, interval, last} = stride;
1020
1020
  const cadence = everyN(interval, UNITS.hour);
1021
1021
 
1022
- return chooseStride({start, interval, cycle: 24}, {
1022
+ return chooseStride({start, interval, last, cycle: 24}, {
1023
1023
  bare: () => cadence,
1024
1024
  offset: () => cadence + ' ab ' + start + ' Uhr',
1025
1025
  bounded: () => cadence + ' von ' + start + ' bis ' + last + ' Uhr'
@@ -1464,6 +1464,7 @@ function normalizeOptions(options?: Cronli5Options): Opts {
1464
1464
  return {
1465
1465
  ampm: typeof options.ampm === 'boolean' ? options.ampm : false,
1466
1466
  lenient: !!options.lenient,
1467
+ quartz: !!options.quartz,
1467
1468
  seconds: !!options.seconds,
1468
1469
  short: !!options.short,
1469
1470
  style,
@@ -137,6 +137,7 @@ function normalizeOptions(options?: Cronli5Options): NormalizedOptions {
137
137
  return {
138
138
  ampm: typeof options.ampm === 'boolean' ? options.ampm : true,
139
139
  lenient: !!options.lenient,
140
+ quartz: !!options.quartz,
140
141
  seconds: !!options.seconds,
141
142
  short: !!options.short,
142
143
  style: resolveDialect(options.dialect),
@@ -1313,7 +1314,7 @@ function renderStride(stride: Stride, opts: NormalizedOptions): string {
1313
1314
  const {interval, start, last, cycle, unit, anchor} = stride;
1314
1315
  const cadence = 'every ' + getNumber(interval, opts) + ' ' + unit + 's';
1315
1316
 
1316
- return chooseStride({start, interval, cycle}, {
1317
+ return chooseStride({start, interval, last, cycle}, {
1317
1318
  bare: () => cadence,
1318
1319
 
1319
1320
  // A clean wrap from a non-zero offset: name the start, no endpoint.
@@ -1419,7 +1420,7 @@ function hourStrideCadence(stride: {start: number; interval: number;
1419
1420
  const {start, interval, last} = stride;
1420
1421
  const cadence = 'every ' + getNumber(interval, opts) + ' hours';
1421
1422
 
1422
- return chooseStride({start, interval, cycle: 24}, {
1423
+ return chooseStride({start, interval, last, cycle: 24}, {
1423
1424
  bare: () => cadence,
1424
1425
  offset: () => cadence + ' from ' + getTime({hour: start, minute: 0}, opts),
1425
1426
  bounded: () =>
@@ -125,6 +125,7 @@ function normalizeOptions(options?: Cronli5Options): Opts {
125
125
  // 12-hour for Mexico/US); an explicit `{ampm}` option overrides it.
126
126
  ampm: typeof options.ampm === 'boolean' ? options.ampm : style.ampm,
127
127
  lenient: !!options.lenient,
128
+ quartz: !!options.quartz,
128
129
  seconds: !!options.seconds,
129
130
  short: !!options.short,
130
131
  style,
@@ -1326,7 +1327,7 @@ function renderStride(stride: Stride, opts: Opts): string {
1326
1327
  // the cadence keeps its endpoints but drops the "de cada <anchor>" tail.
1327
1328
  const tail = anchor ? ' de cada ' + anchor : '';
1328
1329
 
1329
- return chooseStride({start, interval, cycle}, {
1330
+ return chooseStride({start, interval, last, cycle}, {
1330
1331
  bare: () => cadence,
1331
1332
  offset: () => cadence + ' a partir del ' + unit + ' ' + start + tail,
1332
1333
  bounded: () =>
@@ -1429,7 +1430,7 @@ function hourStrideCadence(
1429
1430
  const {start, interval, last} = stride;
1430
1431
  const cadence = 'cada ' + numero(interval, opts) + ' horas';
1431
1432
 
1432
- return chooseStride({start, interval, cycle: 24}, {
1433
+ return chooseStride({start, interval, last, cycle: 24}, {
1433
1434
  bare: () => cadence,
1434
1435
  offset: () => cadence + ' a partir de ' + timePhrase(start, 0, null, opts),
1435
1436
  bounded: () => cadence + ' de ' + timePhrase(start, 0, null, opts) + ' a ' +
@@ -200,6 +200,7 @@ function normalizeOptions(options?: Cronli5Options): NormalizedOptions {
200
200
  return {
201
201
  ampm: false,
202
202
  lenient: !!options.lenient,
203
+ quartz: !!options.quartz,
203
204
  seconds: !!options.seconds,
204
205
  short: !!options.short,
205
206
  style: resolveDialect(options.dialect),
@@ -1108,7 +1109,7 @@ function renderStride(stride: Stride, opts: NormalizedOptions): string {
1108
1109
  const {interval, start, last, cycle, unit} = stride;
1109
1110
  const cadence = genitive(interval, opts) + ' ' + unit.gen + ' välein';
1110
1111
 
1111
- return chooseStride({start, interval, cycle}, {
1112
+ return chooseStride({start, interval, last, cycle}, {
1112
1113
  bare: () => cadence,
1113
1114
  offset: () =>
1114
1115
  cadence + ' ' + unit.anchor + ' ' + unit.ela + ' ' + start + ' alkaen',
@@ -1206,7 +1207,7 @@ function hourStrideCadence(
1206
1207
  const {start, interval, last} = stride;
1207
1208
  const cadence = genitive(interval, opts) + ' tunnin välein';
1208
1209
 
1209
- return chooseStride({start, interval, cycle: 24}, {
1210
+ return chooseStride({start, interval, last, cycle: 24}, {
1210
1211
  bare: () => cadence,
1211
1212
  offset: () => cadence + ' klo ' + hourElatives[start] + ' alkaen',
1212
1213
  bounded: () => cadence + ' ' +
@@ -0,0 +1,49 @@
1
+ // French dialect style tables. Dialect names are language-scoped; the default
2
+ // `fr` style is anchored to the fr-FR norm (Imprimerie nationale / Académie
3
+ // française, plus cronstrue `fr`); see notes.md. Custom objects merge over the
4
+ // `fr` defaults. fr-CA is a future dialect axis (notes.md §"Dialect axis"); no
5
+ // regional dialect ships yet.
6
+ import type {Cronli5Options} from '../../types.js';
7
+
8
+ /**
9
+ * French's own resolved style shape: a separator and the spacing of the `h`
10
+ * clock mark. fr is 24-hour only, so there is no `ampm`/`meridiem` axis.
11
+ */
12
+ export interface FrenchStyle {
13
+ // The mark between hour and minute on the default clock. fr-FR writes the
14
+ // typographic "h" ("9 h 30"); a custom style can opt into a colon or other
15
+ // separator ("9:30"), which replaces the spaced "h" form entirely.
16
+ sep: string;
17
+ // When `sep` is the default "h", whether to drop the surrounding spaces. The
18
+ // spaced "9 h 30" is the ratified fr-FR default; the unspaced "9h30" is the
19
+ // opt-in casual register (notes.md §Dialect axis).
20
+ unspaced: boolean;
21
+ }
22
+
23
+ // The fr-FR default: the spaced "h" clock mark.
24
+ const fr: FrenchStyle = {
25
+ sep: 'h',
26
+ unspaced: false
27
+ };
28
+
29
+ // One `fr` table today = fr-FR. fr-CA is a future dialect axis (notes.md);
30
+ // it would clear its own native panel before shipping, so it is not declared
31
+ // here yet.
32
+ const dialects: {[name: string]: FrenchStyle} = {
33
+ fr,
34
+ // France is the default; named explicitly so it is a recognized choice and
35
+ // has a home if it ever diverges.
36
+ 'fr-FR': fr
37
+ };
38
+
39
+ // Resolve the `dialect` option to a style table.
40
+ function resolveDialect(dialect: Cronli5Options['dialect']): FrenchStyle {
41
+ if (typeof dialect === 'object' && dialect !== null) {
42
+ return {...dialects.fr, ...dialect};
43
+ }
44
+
45
+ // A string dialect indexes the table; unknown names fall back to `fr`.
46
+ return dialects[dialect as string] || dialects.fr;
47
+ }
48
+
49
+ export {resolveDialect};