cronli5 0.0.0-beta.5 → 0.1.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 (98) hide show
  1. package/CHANGELOG.md +276 -0
  2. package/LICENSE.md +2 -2
  3. package/README.md +332 -67
  4. package/cli.js +73 -10
  5. package/cronli5.min.js +3 -0
  6. package/dist/cronli5.cjs +1556 -0
  7. package/dist/cronli5.js +1532 -0
  8. package/dist/lang/de.cjs +546 -0
  9. package/dist/lang/de.js +522 -0
  10. package/dist/lang/en.cjs +830 -0
  11. package/dist/lang/en.js +806 -0
  12. package/dist/lang/es.cjs +1024 -0
  13. package/dist/lang/es.js +1000 -0
  14. package/dist/lang/fi.cjs +942 -0
  15. package/dist/lang/fi.js +918 -0
  16. package/dist/lang/zh.cjs +612 -0
  17. package/dist/lang/zh.js +588 -0
  18. package/package.json +92 -13
  19. package/src/browser.ts +9 -0
  20. package/src/core/analyze.ts +549 -0
  21. package/src/core/format.ts +51 -0
  22. package/src/core/index.ts +28 -0
  23. package/src/core/ir.ts +167 -0
  24. package/src/core/normalize.ts +143 -0
  25. package/src/core/parse.ts +132 -0
  26. package/src/core/shapes.ts +41 -0
  27. package/src/core/specs.ts +93 -0
  28. package/src/core/util.ts +28 -0
  29. package/src/core/validate.ts +170 -0
  30. package/src/cronli5.ts +95 -0
  31. package/src/lang/de/dialects.ts +52 -0
  32. package/src/lang/de/index.ts +820 -0
  33. package/src/lang/de/notes.md +79 -0
  34. package/src/lang/de/status.json +18 -0
  35. package/src/lang/en/dialects.ts +70 -0
  36. package/src/lang/en/index.ts +1290 -0
  37. package/src/lang/en/notes.md +44 -0
  38. package/src/lang/en/status.json +20 -0
  39. package/src/lang/es/dialects.ts +55 -0
  40. package/src/lang/es/index.ts +1737 -0
  41. package/src/lang/es/notes.md +63 -0
  42. package/src/lang/es/status.json +19 -0
  43. package/src/lang/fi/dialects.ts +31 -0
  44. package/src/lang/fi/index.ts +1499 -0
  45. package/src/lang/fi/notes.md +163 -0
  46. package/src/lang/fi/status.json +7 -0
  47. package/src/lang/zh/dialects.ts +27 -0
  48. package/src/lang/zh/index.ts +863 -0
  49. package/src/lang/zh/notes.md +118 -0
  50. package/src/lang/zh/status.json +7 -0
  51. package/src/types.ts +143 -0
  52. package/types/browser.d.ts +2 -0
  53. package/types/core/analyze.d.ts +13 -0
  54. package/types/core/format.d.ts +16 -0
  55. package/types/core/index.d.ts +8 -0
  56. package/types/core/ir.d.ts +185 -0
  57. package/types/core/normalize.d.ts +5 -0
  58. package/types/core/parse.d.ts +5 -0
  59. package/types/core/shapes.d.ts +6 -0
  60. package/types/core/specs.d.ts +27 -0
  61. package/types/core/util.d.ts +7 -0
  62. package/types/core/validate.d.ts +5 -0
  63. package/types/cronli5.d.ts +7 -0
  64. package/types/lang/de/dialects.d.ts +7 -0
  65. package/types/lang/de/index.d.ts +4 -0
  66. package/types/lang/en/dialects.d.ts +4 -0
  67. package/types/lang/en/index.d.ts +3 -0
  68. package/types/lang/es/dialects.d.ts +13 -0
  69. package/types/lang/es/index.d.ts +4 -0
  70. package/types/lang/fi/dialects.d.ts +4 -0
  71. package/types/lang/fi/index.d.ts +3 -0
  72. package/types/lang/zh/dialects.d.ts +6 -0
  73. package/types/lang/zh/index.d.ts +4 -0
  74. package/types/types.d.ts +113 -0
  75. package/.eslintrc.json +0 -217
  76. package/.npmignore +0 -2
  77. package/conli5.min.js +0 -4
  78. package/cronli5.js +0 -553
  79. package/test/.eslintrc.json +0 -10
  80. package/test/bad_input/arrays.js +0 -34
  81. package/test/bad_input/bad-types.js +0 -33
  82. package/test/bad_input/error-types.js +0 -7
  83. package/test/bad_input/objects.js +0 -47
  84. package/test/bad_input/strings.js +0 -10
  85. package/test/baseline/baseline.js +0 -14
  86. package/test/basic/arrays.js +0 -76
  87. package/test/basic/objects.js +0 -70
  88. package/test/basic/strings.js +0 -76
  89. package/test/complex/steps/strings.js +0 -42
  90. package/test/mocha.opts +0 -5
  91. package/test/options/hh.js +0 -17
  92. package/test/options/seconds.js +0 -0
  93. package/test/options/short.js +0 -27
  94. package/test/options/years.js +0 -0
  95. package/test/runner.js +0 -52
  96. package/test/simple/arrays.js +0 -33
  97. package/test/simple/objects.js +0 -23
  98. package/test/simple/strings.js +0 -33
package/README.md CHANGED
@@ -1,118 +1,383 @@
1
- > ## DISCLAIMER: IN PROGRESS
2
- >
3
- > This is a work in progress and does not yet work in all intended cases. DO
4
- > NOT USE until this discalimer has been removed. If you need something like
5
- > this now, use [prettycron][prettycron].
1
+ # Cron Like I'm Five: Cron Patterns in Plain Language
6
2
 
7
- # Cron Like I'm Five: A Cron to English Utility
3
+ [![CI](https://github.com/andrewbroz/cronli5/actions/workflows/ci.yml/badge.svg)](https://github.com/andrewbroz/cronli5/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/cronli5.svg)](https://www.npmjs.com/package/cronli5)
5
+ [![types included](https://img.shields.io/npm/types/cronli5.svg)](./src/types.ts)
6
+ [![minzipped size](https://img.shields.io/bundlephobia/minzip/cronli5)](https://bundlephobia.com/package/cronli5)
7
+ [![license](https://img.shields.io/github/license/andrewbroz/cronli5.svg)](./LICENSE.md)
8
8
 
9
- Generate English language descriptions of schedules from cron patterns.
10
- Accepts classic (five-part) cron patterns, or extended (six-part) cron
11
- patterns, where the first field is assumed to refer to seconds. Accepts the
12
- standard allowed values and the following operators: asterisks (`*`), commas
13
- (`,`), hyphens (`-`), and slashes (`/`).
9
+ ## Overview
14
10
 
15
- `cronli5` is a good library to use if you need to display an English language
11
+ Cron Like I'm Five (`cronli5`) generates plain-language, idiomatically
12
+ rendered descriptions of schedules from cron patterns in
13
+ [several languages](#languages).
14
+
15
+ - **Zero runtime dependencies.** Tiny and safe to drop into any project.
16
+ - **Runs anywhere.** Ships ESM, CommonJS, and a browser global.
17
+ - **Typed.** Bundled TypeScript definitions, no `@types` needed.
18
+ - **Flexible input.** Accepts strings, arrays, or objects.
19
+ - **Idiomatic output.** Composes lists, ranges, and steps into natural
20
+ sentences, not comma-joined fragments.
21
+ - **Multilingual.** Each language is a full renderer shipped as its own
22
+ module (`cronli5/lang/es`). You bundle only the languages you import.
23
+ - **Input Formats.** Accepts classic (five-part) cron patterns, extended
24
+ (six-part) cron patterns, where the first field is assumed to refer to seconds,
25
+ and full seven-part (Quartz-style) patterns with a trailing year. See [Input Formats](#input-formats) for details.
26
+
27
+ `cronli5` is a good library to use if you need to display a natural-language
16
28
  interpretation of a cron pattern in a Node or in a browser environment. If you
17
- need to do other things with cron patterns, consider a library like [Later.js]
18
- [later]. If you want an alternative to `cronli5`, [prettycron][prettycron] may
19
- also meet your needs as an interpreter.
29
+ need to do other things with cron patterns, such as scheduling or computing
30
+ future run times, consider a library like [`@breejs/later`][later].
31
+
32
+ *Alternatives:* The main alternative for descriptions is [`cRonstrue`][cronstrue].
33
+ See the [head-to-head comparison](#cronli5-vs-cronstrue) below for how the two
34
+ differ and which to pick.
35
+
36
+ ## Contents
37
+
38
+ - [Overview](#overview)
39
+ - [Installation](#installation)
40
+ - [Usage](#usage)
41
+ - [Options](#options)
42
+ - [Languages](#languages)
43
+ - [Output Examples](#output-examples)
44
+ - [cronli5 vs. cRonstrue](#cronli5-vs-cronstrue)
45
+ - [Description Accuracy](#description-accuracy)
46
+ - [Note on Timezones](#note-on-timezones)
47
+ - [Module Formats and Types](#module-formats-and-types)
48
+ - [Development](#development)
49
+ - [About](#about)
50
+ - [License](#license)
20
51
 
21
52
  ## Installation
22
53
 
23
54
  Install using npm:
24
55
  ```
25
- # If you plan to use the cli:
26
- npm install -g cronli5
27
-
28
56
  # For a Node project:
29
57
  npm install --save cronli5
30
- ```
31
58
 
32
- Browser (script tag):
59
+ # If you plan to use the cli:
60
+ npm install -g cronli5
33
61
  ```
34
- <script src="cronli5.min.js" type="text/javascript"></script>
62
+
63
+ Browser (script tag) via a CDN:
64
+ ```html
65
+ <script src="https://unpkg.com/cronli5"></script>
66
+ <!-- or: https://cdn.jsdelivr.net/npm/cronli5 -->
35
67
  ```
36
68
 
37
69
  When included in a script tag, the `cronli5` function will be available as a
38
- global in the scripts that follow. _Unsolicited advice: rather than doing this,consider using a bundler like [Browserify][browserify], [Rollup][rollup], or
39
- [Webpack][webpack] and `include` or `require` instead. See below._
70
+ global in the scripts that follow.
40
71
 
41
72
  ## Usage
42
73
 
43
- As a command line tool:
74
+ Import as an ES module:
75
+ ```js
76
+ import cronli5 from 'cronli5';
44
77
  ```
45
- $ cronli5 "* * * * *"
46
78
 
47
- Runs every minute.
79
+ Or with CommonJS `require`:
80
+ ```js
81
+ const cronli5 = require('cronli5');
48
82
  ```
49
83
 
50
- Including `cronli5.min.js` in a script tage will expose `cronli5` as a global
51
- object.
84
+ A cron pattern can be a string, an array of fields, or an object. All three
85
+ forms below describe the same schedule:
86
+ ```js
87
+ cronli5('*/5 * * * *'); // 'every five minutes'
88
+ cronli5(['*/5', '*', '*', '*', '*']); // 'every five minutes'
89
+ cronli5({ minute: '*/5' }); // 'every five minutes'
90
+ ```
52
91
 
53
- Import with require:
92
+ TypeScript types are bundled, so usage is fully typed out of the box:
93
+ ```ts
94
+ import cronli5, { type Cronli5Options } from 'cronli5';
95
+
96
+ const options: Cronli5Options = { ampm: false };
97
+ const description: string = cronli5('30 13 * * MON-FRI', options);
98
+ // 'every Monday through Friday at 13:30'
54
99
  ```
55
- var cronli5 = require('cronli5');
100
+
101
+ As a command line tool:
102
+ ```bash
103
+ $ cronli5 "*/5 * * * *"
104
+ Runs every five minutes.
105
+
106
+ # Other languages with --lang (de, es, fi, zh):
107
+ $ cronli5 --lang de "0 0 * * *"
108
+ Läuft täglich um Mitternacht.
109
+
110
+ # --fragment prints the bare, embeddable fragment instead of a sentence:
111
+ $ cronli5 --fragment "*/5 * * * *"
112
+ every five minutes
113
+ ```
114
+
115
+ ## Input Formats
116
+
117
+ `cronli5` accepts classic (five-part) cron patterns, extended (six-part) cron
118
+ patterns, where the first field is assumed to refer to seconds, and full seven-part
119
+ (Quartz-style) patterns with a trailing year.
120
+
121
+ It accepts the standard allowed values and the following operators:
122
+ - asterisks (`*`)
123
+ - commas (`,`)
124
+ - hyphens (`-`)
125
+ - slashes (`/`).
126
+
127
+ ### Cron Aliases
128
+
129
+ `@daily` and other cron aliases are supported.
130
+
131
+ ### Extended Format Support
132
+
133
+ Ranges in cyclic fields may wrap around (`22-2` is an overnight window, and
134
+ `FRI-MON` is a long weekend). Quartz-style tokens are also supported in the date and weekday fields: `L` (last day, or `5L` for the last Friday), `W` (nearest weekday, e.g. `15W`), `#` (nth weekday, e.g. `1#2` for the second Monday), and `?` (no specific
135
+ value).
136
+
137
+ ## Options
138
+
139
+ The `cronli5` function takes an `options` object as its 2nd parameter:
140
+
141
+ | Option | Default | Description |
142
+ | --- | --- | --- |
143
+ | `ampm` | `true` (English) | Use a 12-hour clock. Set `false` for 24-hour time. The default is language-specific: English is 12-hour; Spanish and Finnish default to 24-hour (Finnish is 24-hour only). |
144
+ | `dialect` | `'us'` | The English style. `'us'` follows the [Chicago Manual of Style][chicago]: serial commas, `through` ranges, `9 a.m.`/`5:30 p.m.` times, `noon`/`midnight`, and `January 1` dates. `'gb'` follows the [Guardian style guide][guardian]: no serial comma, `to` ranges, `9am`/`5.30pm` times, `midday`/`midnight`, and `1 January` dates. `'house'` is cronli5's legacy voice (`9:30 AM`, `Monday - Friday`). A custom object defines your own style. (`'uk'` is a deprecated alias for `'gb'`.) See [docs/dialects.md](./docs/dialects.md). |
145
+ | `lang` | English | A language module, e.g. `import es from 'cronli5/lang/es'`. Each language owns its words, conventions, and dialects &mdash; see [Languages](#languages). |
146
+ | `lenient` | `false` | Never throw: invalid input returns the language's fallback description (`'an unrecognizable cron pattern'`) instead. Useful when rendering arbitrary user crontabs. |
147
+ | `sentence` | `false` | Return a complete standalone sentence (`'Runs every day at midnight.'`, `'Läuft täglich um Mitternacht.'`) instead of the embeddable fragment. Each language supplies its own wrapping. Wraps a schedule and `@reboot`, but not the lenient `fallback`. |
148
+ | `short` | `false` | Compact output: abbreviated month and weekday names, and hyphenated ranges everywhere `through`/`to` would appear (`Mon-Fri`, `Jan-Mar`, `1st-5th`, `9 a.m.-5:45 p.m.`). |
149
+ | `seconds` | `false` | Always treat the first field of strings and arrays as the `second` field. |
150
+ | `years` | `false` | Treat the last field of a six-field string/array as the `year` field. Otherwise the first field of a six-field pattern is treated as the `second` field. Seven-field patterns are unambiguous (seconds first, year last) and need no option. |
151
+
152
+ When a specific year is given &mdash; via a seven-field pattern, an object's
153
+ `year` property, or a six-field pattern with `years: true` &mdash; it is
154
+ folded into a specific calendar date (`'on January 1, 2030 at noon'`)
155
+ or otherwise trails the description (`'every Friday at 1 p.m. in 2030'`).
156
+
157
+ ```js
158
+ cronli5('0 0 12 1 1 * 2030'); // 'on January 1, 2030 at noon'
159
+ cronli5({ hour: 9, year: 2030 }); // 'every day at 9 a.m. in 2030'
56
160
  ```
57
161
 
58
- Import as an ESNext module:
162
+ ```js
163
+ import cronli5 from 'cronli5';
164
+
165
+ const weekdaysAt1330 = '30 13 * * MON-FRI';
166
+
167
+ cronli5(weekdaysAt1330, { ampm: true, short: false });
168
+ // 'every Monday through Friday at 1:30 p.m.'
169
+
170
+ cronli5(weekdaysAt1330, { ampm: false, short: true });
171
+ // 'every Mon-Fri at 13:30'
172
+
173
+ cronli5(weekdaysAt1330, { dialect: 'gb' });
174
+ // 'every Monday to Friday at 1.30pm'
59
175
  ```
176
+
177
+ ## Languages
178
+
179
+ English is the default. Other languages are full renderers over the same
180
+ language-independent core. A language ships as its own module and is selected
181
+ per call with the `lang` option. If you never import it, it never reaches your
182
+ bundle (each language adds about 3.3&nbsp;KB gzipped).
183
+
184
+ ```js
60
185
  import cronli5 from 'cronli5';
186
+ import es from 'cronli5/lang/es';
187
+ import fi from 'cronli5/lang/fi';
188
+
189
+ cronli5('30 9 * * MON-FRI'); // 'every Monday through Friday at 9:30 a.m.'
190
+ cronli5('30 9 * * MON-FRI', {lang: es}); // 'de lunes a viernes a las 09:30'
191
+ cronli5('30 9 * * MON-FRI', {lang: fi}); // 'maanantaista perjantaihin klo 9.30'
61
192
  ```
62
193
 
63
- Programmatic usage (ES5):
194
+ Each language carries its own conventions and defaults — Spanish and
195
+ Finnish default to the 24-hour clock, for instance (Spanish takes
196
+ `{ampm: true}` for 12-hour times with day periods). See the per-language
197
+ docs below.
198
+
199
+ | Language | Module | Anchors | Doc |
200
+ | --- | --- | --- | --- |
201
+ | English | built in (also `cronli5/lang/en`) | Chicago Manual of Style; Guardian (`'gb'` dialect) | [docs/lang/en.md](./docs/lang/en.md) |
202
+ | German | `cronli5/lang/de` | Duden (`de-AT`/`de-CH` dialects) | [docs/lang/de.md](./docs/lang/de.md) |
203
+ | Spanish | `cronli5/lang/es` | RAE / FundéuRAE | [docs/lang/es.md](./docs/lang/es.md) |
204
+ | Finnish | `cronli5/lang/fi` | Kielitoimiston ohjepankki; SFS 4175 | [docs/lang/fi.md](./docs/lang/fi.md) |
205
+ | Chinese (Mandarin) | `cronli5/lang/zh` | Simplified (`zh-Hans`) default; Traditional (`zh-Hant`) | [docs/lang/zh.md](./docs/lang/zh.md) |
206
+
207
+ ### Language maturity
208
+
209
+ Languages ship as **experimental** → **beta** → **stable** (model-drafted →
210
+ model-validated by a blind persona panel → verified by a fluent human):
211
+
212
+ <!-- BEGIN GENERATED: language-status -->
213
+ | Language | Status |
214
+ | --- | --- |
215
+ | German | beta |
216
+ | English | stable |
217
+ | Spanish | beta |
218
+ | Finnish | beta |
219
+ | Chinese (Mandarin, Simplified) | beta |
220
+ <!-- END GENERATED: language-status -->
221
+
222
+ For the review evidence behind each status, see
223
+ [docs/language-status.md](./docs/language-status.md).
224
+
225
+
226
+ Each language doc includes a generated side-by-side table against the
227
+ matching cRonstrue locale. The architecture is described in
228
+ [docs/i18n-design.md](./docs/i18n-design.md).
229
+
230
+ ## Output Examples
231
+
232
+ Output follows the Chicago Manual of Style by default (serial commas,
233
+ `9 a.m.` times, `noon`/`midnight`, cardinal month-day dates). See the
234
+ [`dialect`](#options) option for Guardian-style British English. Bare days
235
+ of the month use suffixed ordinals (`on the 1st and 15th`).
236
+
237
+ ```js
238
+ // Single values and steps
239
+ cronli5('*/5 * * * *'); // 'every five minutes'
240
+ cronli5('0 9 * * MON'); // 'every Monday at 9 a.m.'
241
+
242
+ // Lists
243
+ cronli5('5,10,15 * * * * *'); // 'at 5, 10, and 15 seconds past the minute'
244
+ cronli5('0 9,17 * * *'); // 'every day at 9 a.m. and 5 p.m.'
245
+ cronli5('0 0 1,15 * *'); // 'on the 1st and 15th at midnight'
246
+ cronli5('0 12 * 6,12 *'); // 'every day in June and December at noon'
247
+
248
+ // Ranges (wrap-around ranges describe overnight and weekend windows)
249
+ cronli5('0-29 * * * *'); // 'every minute from 0 through 29 past the hour'
250
+ cronli5('0 9-17 * * *'); // 'every hour from 9 a.m. through 5 p.m.'
251
+ cronli5('0 0 1-15 * *'); // 'on the 1st through 15th at midnight'
252
+ cronli5('0 22-2 * * *'); // 'every hour from 10 p.m. through 2 a.m.'
253
+ cronli5('0 0 * * FRI-MON'); // 'every Friday through Monday at midnight'
254
+
255
+ // Compound patterns
256
+ cronli5('0,30 9 * * *'); // 'every day at 9 a.m. and 9:30 a.m.'
257
+ cronli5('*/15 9-17 * * *'); // 'every 15 minutes from 9 a.m. through 5:45 p.m.'
258
+ cronli5('30 9-17 * * *');
259
+ // 'at 30 minutes past the hour from 9 a.m. through 5:30 p.m.'
260
+ cronli5('0 12 1 1 *'); // 'on January 1 at noon'
261
+ cronli5('0 * 13 * *'); // 'every hour on the 13th'
262
+
263
+ // Quartz tokens
264
+ cronli5('0 0 L * *'); // 'on the last day of the month at midnight'
265
+ cronli5('0 0 * * 5L'); // 'on the last Friday of the month at midnight'
266
+ cronli5('0 0 * * 1#2'); // 'on the second Monday of the month at midnight'
267
+ cronli5('0 0 15W * *'); // 'on the weekday nearest the 15th at midnight'
64
268
  ```
65
- // Cron patterns can be represented as strings
66
- var cronString = '*/5 * * * *';
67
-
68
- // Cron patterns can be represented as arrays of cron fields
69
- var cronArray = ['*/5', '*', '*', '*', '*'];
70
-
71
- // Cron patterns can be represented as objects
72
- var cronObject = {
73
- minute: '*/5',
74
- hour: '*',
75
- date: '*',
76
- month: '*',
77
- weekday: '*',
78
- };
79
-
80
- var expectedOutput = 'every five minutes';
81
-
82
- expect(cronli5(cronString)).to.equal(expectedOutput);
83
- expect(cronli5(cronArray)).to.equal(expectedOutput);
84
- expect(cronli5(cronObject)).to.equal(expectedOutput);
269
+
270
+ ## cronli5 vs. cRonstrue
271
+
272
+ [`cRonstrue`][cronstrue] is the most widely used cron-description library, but
273
+ it differs from `cronli5` in philosophy. `cronli5` writes one flowing sentence
274
+ and does additional validation; its languages are full renderers
275
+ ([four so far](#languages)). cRonstrue assembles per-field fragments from
276
+ translated templates, which is how it covers 39 locales. The same compound
277
+ pattern &mdash; `5,10 30 9 * * MON` &mdash; in every language:
278
+
279
+ <!-- BEGIN GENERATED: cronstrue-head-to-head -->
280
+ | Language | cronli5 | cRonstrue 3.14.0 |
281
+ | --- | --- | --- |
282
+ | English | at five and ten seconds past the minute, every Monday at 9:30 a.m. | At 5 and 10 seconds past the minute, at 30 minutes past the hour, at 09:00 AM, only on Monday |
283
+ | German | in den Sekunden 5 und 10 jeder Minute, um 9:30 Uhr montags | Bei Sekunde 5 und 10, bei Minute 30, um 09:00, nur jeden Montag |
284
+ | Spanish | los lunes, en los segundos 5 y 10 de las 09:30 | A los 5 y 10 segundos del minuto, a los 30 minutos de la hora, a las 09:00, sólo el lunes |
285
+ | Finnish | 5 ja 10 sekunnin kohdalla, maanantaisin klo 9.30 | 5 ja 10 sekunnnin jälkeen, 30 minuuttia yli, klo 09:00, vain maanantai |
286
+ <!-- END GENERATED: cronstrue-head-to-head -->
287
+
288
+ See [docs/cronli5-vs-cronstrue.md](./docs/cronli5-vs-cronstrue.md) for
289
+ more generated side-by-side output tables.
290
+
291
+ ## Description Accuracy
292
+
293
+ Sometimes minimizing verbosity results in ambiguities. For example, "every two
294
+ minutes" could reasonably refer to two _behaviorally distinct_ cron patterns
295
+ with minute accuracy: `*/2 * * * *` and `1/2 * * * *` (and 120 behaviorally
296
+ distinct cron patterns with second accuracy). As a tradeoff, this library does
297
+ not qualify cases that begin on the first second, minute, or hour of the
298
+ corresponding minute, hour, or day. So `*/3 * * * *` will be "every three
299
+ minutes", while `2/3 * * * *` will be "every three minutes from two minutes
300
+ past the hour".
301
+
302
+ ## Note on Timezones
303
+
304
+ `cronli5` always describes cron patterns with respect to whatever system
305
+ timezone the cron pattern is being run in. This utility does not, nor does it
306
+ ever intend to, deal with timezone conversions. That functionality would
307
+ require some non-trivial dependencies like [moment-timezone] and [moment]
308
+ to even approximate correctness and the output _could still be wrong anyways_
309
+ because [timezones are problematic][timezones]. Associate the displayed
310
+ description with a timezone (e.g. America/Phoenix) when there is the
311
+ possibility for confusion.
312
+
313
+ ## Module Formats and Types
314
+
315
+ `cronli5` is authored as an ES module in `src/`. It is published with dual builds
316
+ so it works everywhere:
317
+
318
+ * **ESM** (`import cronli5 from 'cronli5'`) resolves to `dist/cronli5.js`.
319
+ * **CommonJS** (`const cronli5 = require('cronli5')`) resolves to
320
+ `dist/cronli5.cjs`.
321
+ * **Browser** (`<script src="cronli5.min.js">`) exposes a global `cronli5`
322
+ (English only).
323
+
324
+ Language subpaths (`cronli5/lang/en`, `cronli5/lang/es`, `cronli5/lang/fi`)
325
+ ship the same dual ESM + CJS builds under `dist/lang/`.
326
+
327
+ TypeScript type definitions ship in `cronli5.d.ts` (and `lang.d.ts` for the
328
+ language subpaths) and are picked up automatically: no `@types` package
329
+ required.
330
+
331
+ ## Development
332
+
333
+ The library has no runtime dependencies. The toolchain (ESLint, Mocha, Chai,
334
+ c8, esbuild) lives in `devDependencies`.
335
+
336
+ ```bash
337
+ npm install # install dev dependencies (also wires the git hooks)
338
+ npm test # run the Mocha test suite (runs against src/, no build needed)
339
+ npm run coverage # run tests with c8 coverage and enforce thresholds
340
+ npm run lint # lint source and tests with ESLint
341
+ npm run build # emit dist/ (ESM + CJS) and the minified browser global
342
+ npm run verify # the full CI gate: lint, types, tests, coverage, docs, build
85
343
  ```
86
344
 
345
+ A `pre-push` git hook runs `npm run verify` so a push only lands when the full
346
+ gate is green. It is wired automatically on `npm install` (via `core.hooksPath
347
+ → .githooks/`); bypass it in an emergency with `git push --no-verify`.
348
+
87
349
  ## About
88
350
 
89
- The project name is a reference to the phrase [Explain Like I'm Five (ELI5)]
90
- [eli5], which is used to ask for a friendly, simplified, and layman-accessible
91
- summary of material that may be hard to understand without some background.
351
+ The project name is a reference to the phrase [Explain Like I'm Five (ELI5)][eli5],
352
+ which is used to ask for a friendly, simplified, and layman-accessible summary of
353
+ material that may be hard to understand without some background.
92
354
 
93
355
  `cronli5` was partially inspired by [`prettycron`][prettycron], which itself
94
356
  is based on code from [a gist by dunse][dunse]. Although `prettycron` was
95
- close to meeting my needs, I wasn't fully satisfied with the output and was
96
- limited by the lack of support for extended cron patterns. `cronli5` tries to
97
- render as many cron patterns in as direct and idiomatic English as possible.
98
-
99
- `cronli5` was written from scratch and has no production dependencies. Its
100
- source does not borrow code, in whole or in part, from [prettycron]
101
- [prettycron], [Stack Overflow answers][stackoverflow], or any other project.
102
- Any resemblance to other code, living or dead, is purely coincidental.
357
+ close to meeting my needs, I wasn't fully satisfied with the output. `cronli5`
358
+ tries to render as many cron patterns in as direct and as idiomatic language
359
+ as possible in every target language. Test cases that describe where it
360
+ fails to do so and which prescribe an obviously better description would be
361
+ greatly appreciated. Native speakers of target languages are the best.
103
362
 
104
363
  ## License
105
364
 
106
365
  *[MIT License][license]*
107
- _Copyright &copy; 2016 [Andrew Broz][abroz]_
366
+ _Copyright &copy; 2026 [Andrew Brož][andrewbroz]_
108
367
 
109
- [abroz]: https://github.com/abroz
110
- [browserify]: http://browserify.org/
368
+ [andrewbroz]: https://github.com/andrewbroz
369
+ [esbuild]: https://esbuild.github.io/
111
370
  [dunse]: https://gist.github.com/dunse/3714957
112
371
  [eli5]: https://www.reddit.com/r/explainlikeimfive/
113
- [later]: https://bunkat.github.io/later/
372
+ [chicago]: https://www.chicagomanualofstyle.org/
373
+ [cronstrue]: https://github.com/bradymholt/cRonstrue
374
+ [guardian]: https://www.theguardian.com/guardian-observer-style-guide-a
375
+ [later]: https://github.com/breejs/later
114
376
  [license]: ./LICENSE.md
377
+ [moment]: http://momentjs.com/
378
+ [moment-timezone]: http://momentjs.com/timezone/
115
379
  [prettycron]: https://github.com/azza-bazoo/prettycron
116
380
  [rollup]: http://rollupjs.org/
117
381
  [stackoverflow]: https://stackoverflow.com/
382
+ [timezones]: https://www.w3.org/TR/timezone/
118
383
  [webpack]: https://webpack.github.io/
package/cli.js CHANGED
@@ -1,20 +1,83 @@
1
1
  #! /usr/bin/env node
2
2
 
3
3
  // Need to sanity check a pattern before you schedule something? The cronli5
4
- // cli tool will take a pattern and print a description in English.
4
+ // CLI takes a cron pattern and prints a plain-language description: a complete
5
+ // sentence by default, or the bare fragment with --fragment. English by
6
+ // default; pass --lang <code> (de, es, fi) for another language.
5
7
  //
6
- // Example: `cronli5 * * * * *` (prints "Runs every minute.")
7
- var explain = require('./cronli5');
8
- var pattern = process.argv.slice(2);
8
+ // cronli5 '* * * * *' → Runs every minute.
9
+ // cronli5 --lang de '0 0 * * *' → Läuft täglich um Mitternacht.
10
+ // cronli5 --fragment '0 0 * * *' → every day at midnight
11
+ //
12
+ // It runs against the built dist/, so a plain `node` invocation works as
13
+ // published; in a source checkout, run `npm run build` first.
14
+ import {readdirSync} from 'node:fs';
15
+ import humanize from './dist/cronli5.js';
9
16
 
10
- if (pattern.length === 1) {
11
- pattern = pattern[0];
17
+ // The shippable languages are exactly the built language entry points, derived
18
+ // from dist/ so the list never drifts when a language is added or removed.
19
+ function availableLanguages() {
20
+ return readdirSync(new URL('./dist/lang/', import.meta.url))
21
+ .filter((file) => file.endsWith('.js'))
22
+ .map((file) => file.slice(0, -'.js'.length))
23
+ .sort();
12
24
  }
13
25
 
14
- try {
15
- console.log('Runs ' + explain(pattern) + '.');
26
+ // Pull the optional flags out of the args; whatever remains is the cron
27
+ // pattern. --fragment prints the bare fragment instead of a full sentence.
28
+ const args = process.argv.slice(2);
29
+ const fragmentAt = args.indexOf('--fragment');
30
+ const fragment = fragmentAt !== -1;
31
+
32
+ if (fragment) {
33
+ args.splice(fragmentAt, 1);
16
34
  }
17
- catch (e) {
18
- console.error('Problem parsing the cron pattern provided: ', e.message);
35
+
36
+ const flag = args.findIndex((a) => a === '--lang' || a.startsWith('--lang='));
37
+ let code = '';
38
+ let missingLang = false;
39
+
40
+ if (flag !== -1) {
41
+ const arg = args[flag];
42
+ const inline = arg.indexOf('=') !== -1;
43
+
44
+ // A bare `--lang` as the final argument has no value to consume; treat that
45
+ // as an error rather than silently falling back to English.
46
+ if (!inline && flag === args.length - 1) {
47
+ missingLang = true;
48
+ }
49
+ else {
50
+ code = inline ? arg.slice('--lang='.length) : args[flag + 1];
51
+ args.splice(flag, inline ? 1 : 2);
52
+ }
19
53
  }
20
54
 
55
+ const pattern = args.length === 1 ? args[0] : args;
56
+ const requested = code || 'en';
57
+
58
+ if (missingLang) {
59
+ console.error('Missing language value for --lang');
60
+ process.exitCode = 1;
61
+ }
62
+ else if (availableLanguages().includes(requested)) {
63
+ emit(pattern, (await import('./dist/lang/' + requested + '.js')).default);
64
+ }
65
+ else {
66
+ console.error('Unknown language: ' + code + ' (available: ' +
67
+ availableLanguages().join(', ') + ')');
68
+ process.exitCode = 1;
69
+ }
70
+
71
+ // Print the schedule: a complete sentence by default, the bare fragment under
72
+ // --fragment. The sentence wrapping is the library's own (the `sentence`
73
+ // option), so the CLI stays a thin shell over the API.
74
+ function emit(cronPattern, language) {
75
+ try {
76
+ console.log(humanize(cronPattern, {lang: language, sentence: !fragment}));
77
+ }
78
+ catch (error) {
79
+ console.error('Problem parsing the cron pattern provided: ',
80
+ error.message);
81
+ process.exitCode = 1;
82
+ }
83
+ }
package/cronli5.min.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";(()=>{var qn={SUN:0,MON:1,TUE:2,WED:3,THU:4,FRI:5,SAT:6},Jn={JAN:1,FEB:2,MAR:3,APR:4,MAY:5,JUN:6,JUL:7,AUG:8,SEP:9,OCT:10,NOV:11,DEC:12},h={second:{cyclic:!0,max:59,min:0,top:59},minute:{cyclic:!0,max:59,min:0,top:59},hour:{cyclic:!0,max:23,min:0,top:23},date:{aliases:{"?":"*"},cyclic:!0,max:31,min:1,top:31},month:{cyclic:!0,max:12,min:1,numbers:Jn,top:12},weekday:{aliases:{"?":"*",L:"6"},cyclic:!0,max:7,min:0,numbers:qn,top:6},year:{max:9999,min:1970}},k=["second","minute","hour","date","month","weekday","year"],j={"@annually":"0 0 1 1 *","@yearly":"0 0 1 1 *","@monthly":"0 0 1 * *","@weekly":"0 0 * * 0","@daily":"0 0 * * *","@midnight":"0 0 * * *","@hourly":"0 * * * *"},U=6;function s(n,e){return(""+n).indexOf(e)!==-1}function F(n){return Array.from(new Set(n))}function I(n){return/^\d+$/.test(n)}function d(n,e){return I(n)?+n:e[n.toUpperCase()]}function sn(n){return k.forEach(function(r){Vn(n[r],h[r],r)}),n}function Vn(n,e,r){typeof n!="string"&&typeof n!="number"&&un(n,r);let t=""+n;t!=="*"&&(r==="date"&&C(t)||r==="weekday"&&w(t,e)||t.split(",").forEach(function(o){$n(o,e)||un(o,r)}))}function C(n){if(n==="L"||n==="LW"||n==="WL")return!0;let e=/^L-(\d{1,2})$/.exec(n);if(e)return+e[1]>=1&&+e[1]<=30;let r=/^(\d{1,2})W$|^W(\d{1,2})$/.exec(n);if(r){let t=+(r[1]||r[2]);return t>=1&&t<=31}return!1}function w(n,e){if(/L$/.test(n))return v(n.slice(0,-1),e);let r=n.split("#");return r.length===2?v(r[0],e)&&/^[1-5]$/.test(r[1]):!1}function $n(n,e){return s(n,"/")?Yn(n,e):s(n,"-")?an(n,e):v(n,e)}function Yn(n,e){let r=n.split("/");return r.length!==2||!I(r[1])||+r[1]<1?!1:r[0]==="*"||v(r[0],e)||an(r[0],e,!0)}function an(n,e,r){let t=n.split("-");return t.length!==2||!v(t[0],e)||!v(t[1],e)?!1:e.cyclic&&!r?!0:d(t[0],e.numbers)<=d(t[1],e.numbers)}function v(n,e){return n==="*"?!1:I(n)?+n>=e.min&&+n<=e.max:e.numbers?n.toUpperCase()in e.numbers:!1}function un(n,e){throw new Error('`cronli5` was passed an invalid field value "'+n+'" for the '+e+" field.")}function cn(n){k.forEach(function(r){let t=h[r].aliases,i=t&&t[""+n[r]];i&&(n[r]=i)})}function mn(n){return k.forEach(function(r){let t=""+n[r];if(r==="date"&&C(t)||r==="weekday"&&w(t,h[r])){n[r]=t;return}n[r]=Bn(t,h[r])}),n}function Bn(n,e){let r=""+n;if(r==="*")return r;let t=r.split(",").map(function(o){return Xn(Kn(Gn(o,e),e),e)});return t.indexOf("*")!==-1?"*":F(t).sort(function(o,u){return ln(o,e)-ln(u,e)}).join(",")}function Gn(n,e){let r=n.split("/");if(!e.cyclic||r.length!==2||+r[1]!=1)return n;let t=r[0];return s(t,"-")?t:t==="*"||d(t,e.numbers)===e.min?"*":t+"-"+e.top}function Kn(n,e){let r=n.split("/");if(!e.cyclic||typeof e.top!="number"||r.length!==2||s(r[0],"-"))return n;let t=r[0];return(t==="*"?e.min:d(t,e.numbers))+ +r[1]<=e.top?n:t==="*"?""+e.min:t}function Xn(n,e){let r=n.split("/")[0];if(!s(r,"-"))return n;let t=r.split("-");return d(t[0],e.numbers)!==d(t[1],e.numbers)?n:t[0]}function ln(n,e){let r=n.split("/")[0].split("-")[0];return r==="*"?e.min:d(r,e.numbers)}function fn(n,e){let r=n instanceof Array;if(n===null||typeof n>"u"||n===""||r&&n.length===0)throw new Error("`cronli5` expects a non-empty cron pattern as the first argument.");if(r)return dn(n,e);if(typeof n=="object")return Zn(n);if(typeof n=="string")return _n(n,e);throw new Error("`cronli5` was passed an unexpected type.")}function dn(n,e){if(n.length>7)throw new Error("`cronli5` was passed a cron pattern with more than seven fields.");return!e.seconds&&n.length<(e.years?7:6)&&n.unshift("0"),{second:n[0]||"0",minute:n[1]||"*",hour:n[2]||"*",date:n[3]||"*",month:n[4]||"*",weekday:n[5]||"*",year:n[6]||"*"}}function Zn(n){if(!n.second&&!n.minute&&!n.hour)throw new Error("`cronli5` expects that any object being interpreted as a cron pattern have at least one of the following properties: `second`, `minute`, or `hour`");let e=typeof n.second<"u",r=typeof n.minute<"u",t=e?"*":"0",i=e||r?"*":"0";return{second:N(n.second,"0"),minute:N(n.minute,t),hour:N(n.hour,i),date:N(n.date,"*"),month:N(n.month,"*"),weekday:N(n.weekday,"*"),year:N(n.year,"*")}}function N(n,e){return typeof n>"u"?e:n}function _n(n,e){let r=ne(n).split(/\s+/);return dn(r,e)}function ne(n){let e=n.trim();if(e.charAt(0)!=="@")return n;let r=e.toLowerCase();if(Object.hasOwn(j,r))return j[r];throw new Error("`cronli5` does not recognize the macro `"+e+"`.")}function gn(n){return n!=="*"&&!s(n,",")&&!s(n,"-")&&!s(n,"/")}function Q(n){return s(n,"-")&&!s(n,",")&&!s(n,"/")}function ee(n){return s(n,"/")&&!s(n,",")}function q(n){return n!=="*"&&!s(n,"-")&&!s(n,"/")}function J(n){return n!=="*"&&!Q(n)&&!ee(n)}function R(n,e,r){let t=[],i=n;for(;i<=r;)t.push(i),i+=e;return t}function hn(n,e,r,t){let i=n.split("/"),o=+i[1];if(s(i[0],"-")){let f=i[0].split("-");return R(d(f[0],t),o,d(f[1],t))}let u=i[0]==="*"?e:d(i[0],t);return R(u,o,r)}function V(n,e,r){let t=[];return n.split(",").forEach(function(o){if(s(o,"/"))t.push(...hn(o,e,r));else if(s(o,"-")){let u=o.split("-");+u[0]<=+u[1]?t.push(...R(+u[0],1,+u[1])):(t.push(...R(+u[0],1,r)),t.push(...R(e,1,+u[1])))}else t.push(+o)}),F(t)}function re(n){return q(n)?n.split(",").map(Number):[0]}function te(n){if(n==="*")return[0,59];if(Q(n)){let e=n.split("-");if(+e[0]<=+e[1])return[+e[0],+e[1]]}return null}function ie(n){return n==="*"?59:Math.max(...V(n,0,59))}function oe(n){if(gn(n)&&n!=="0")return+n}function ue(n,e){return n==="*"?"wildcard":e==="date"&&C(n)||e==="weekday"&&w(n,h.weekday)?"quartz":s(n,",")?"list":s(n,"/")?"step":s(n,"-")?"range":"single"}function se(n,e,r){return e==="wildcard"||e==="quartz"?null:n.split(",").map(function(i){if(s(i,"/")){let o=i.split("/");return{fires:hn(i,r.min,r.top,r.numbers),interval:+o[1],kind:"step",startToken:o[0]}}return s(i,"-")?{bounds:i.split("-"),kind:"range"}:{kind:"single",value:i}})}function pn(n){let e={},r={};k.forEach(function(u){e[u]=ue(n[u],u),r[u]=se(n[u],e[u],h[u])});let i={analyses:{clockSecond:oe(n.second),lastMinuteFire:ie(n.minute),minuteSpan:te(n.minute),segments:r},pattern:n,shapes:e};return{...i,plan:ae(i)}}function ae(n){let{analyses:e,pattern:r,shapes:t}=n;if(r.second!=="0"){let i=le(r,t,e);if(i)return i}return yn(r,t,e)||Sn(r,t,e)}function le(n,e,r){let t=ce(n,e);return t||(n.hour==="*"&&e.minute==="single"&&n.second!=="*"?{kind:"secondsWithinMinute",singleSecond:e.second==="single"}:e.second==="single"&&q(n.minute)&&J(n.hour)?null:{kind:"composeSeconds",rest:yn(n,e,r)||Sn(n,e,r)})}function ce(n,e){return n.minute!=="*"||n.hour!=="*"?null:n.second==="*"?{kind:"everySecond"}:e.second==="single"?{kind:"secondPastMinute"}:{kind:"standaloneSeconds"}}function yn(n,e,r){if(e.minute==="step")return{hours:fe(n,e,r),kind:"minuteFrequency"};if(e.hour==="single"&&r.minuteSpan)return{hour:+n.hour,kind:"minuteSpanInHour",span:r.minuteSpan};let t=de(n,e);if(t)return t;let i=me(n,e);if(i)return i;if(n.hour==="*")return ge(n,e)}function bn(n){let[e,r]=n.split("/"),t=e==="*"?0:+e;return e.indexOf("-")===-1&&24%+r===0&&t<+r}function me(n,e){return e.hour!=="step"?null:n.minute==="*"?bn(n.hour)?{form:"wildcard",kind:"minuteSpanAcrossHourStep"}:{form:"wildcard",kind:"minutesAcrossHours",times:x(n.hour)}:e.minute==="range"?{form:"range",kind:"minuteSpanAcrossHourStep"}:null}function fe(n,e,r){if(e.hour==="list")return{kind:"during",times:x(n.hour)};if(e.hour==="range"){let t=n.hour.split("-");return{from:+t[0],kind:"window",last:r.lastMinuteFire,to:+t[1]}}return e.hour==="single"?{from:+n.hour,kind:"window",last:r.lastMinuteFire,to:+n.hour}:e.hour==="step"?bn(n.hour)?{kind:"step"}:{kind:"during",times:x(n.hour)}:{kind:"none"}}function de(n,e){return J(n.hour)?n.minute==="*"?{form:"wildcard",kind:"minutesAcrossHours",times:x(n.hour)}:e.minute==="range"||e.minute==="list"&&s(n.minute,"-")&&!s(n.minute,"/")?{form:e.minute==="range"?"range":"list",kind:"minutesAcrossHours",times:x(n.hour)}:null:null}function ge(n,e){if(e.minute==="range")return{kind:"rangeOfMinutes"};if(e.minute==="list")return{kind:"multipleMinutes"};if(n.minute==="*")return{kind:"everyMinute"};if(n.minute!=="0")return{kind:"singleMinute"}}function Sn(n,e,r){if(e.hour==="range"){let t=n.hour.split("-"),i="lead";return n.minute==="*"?i="wildcard":e.minute==="range"&&(i="range"),{from:+t[0],kind:"hourRange",last:r.lastMinuteFire,minuteForm:i,to:+t[1]}}return e.hour==="step"&&n.minute==="0"?{kind:"hourStep"}:n.hour==="*"?{kind:"everyHour"}:he(n,r)}function he(n,e){let r=V(n.hour,0,23),t=re(n.minute);if(r.length*t.length>U)return{fold:t.length===1,kind:"compactClockTimes",minute:t[0]};let i=[];return r.forEach(function(u){t.forEach(function(l){i.push({hour:u,minute:l,second:e.clockSecond})})}),{kind:"clockTimes",times:i}}function x(n){let e=V(n,0,23);return e.length<=U?{fires:e,kind:"fires"}:{kind:"segments"}}function On(n,e){let r=fn(n,e);return cn(r),sn(r),mn(r)}function $(n){return n=""+n,n.length<2?"0"+n:n}function kn(n,e,r){return r.short?n:e[n]||n}function Y(n,{sep:e,pad:r,lean:t}){let i=r?$(n.hour):""+n.hour;return t&&!n.minute&&!n.second?i:i+e+$(n.minute)+(n.second?e+$(n.second):"")}var B={gb:{am:"am",closeUp:!0,dayFirst:!0,midday:"midday",midnight:"midnight",ordinals:!1,pm:"pm",sep:".",serialComma:!1,through:" to "},us:{am:"a.m.",closeUp:!1,dayFirst:!1,midday:"noon",midnight:"midnight",ordinals:!1,pm:"p.m.",sep:":",serialComma:!0,through:" through "},house:{am:"AM",closeUp:!1,dayFirst:!1,midday:"noon",midnight:"midnight",ordinals:!0,pm:"PM",sep:":",serialComma:!0,through:" - "}};function Nn(n){return typeof n=="object"&&n!==null?{...B.us,...n}:B[n==="uk"?"gb":n]||B.us}var pe=["zero","one","two","three","four","five","six","seven","eight","nine","ten"],G=["th","st","nd","rd"],g=[null,["January","Jan"],["February","Feb"],["March","Mar"],["April","Apr"],["May","May"],["June","Jun"],["July","Jul"],["August","Aug"],["September","Sep"],["October","Oct"],["November","Nov"],["December","Dec"]],y=[["Sunday","Sun"],["Monday","Mon"],["Tuesday","Tue"],["Wednesday","Wed"],["Thursday","Thu"],["Friday","Fri"],["Saturday","Sat"]],ye={JAN:g[1],FEB:g[2],MAR:g[3],APR:g[4],MAY:g[5],JUN:g[6],JUL:g[7],AUG:g[8],SEP:g[9],OCT:g[10],NOV:g[11],DEC:g[12]},be={SUN:y[0],MON:y[1],TUE:y[2],WED:y[3],THU:y[4],FRI:y[5],SAT:y[6]},Se=[null,"first","second","third","fourth","fifth"];function Oe(n){return n=n||{},{ampm:typeof n.ampm=="boolean"?n.ampm:!0,lenient:!!n.lenient,seconds:!!n.seconds,short:!!n.short,style:Nn(n.dialect),years:!!n.years}}function ke(n,e){return Xe(zn(n,n.plan,e),n,e)}function zn(n,e,r){let t=Qe[e.kind];return t(n,e,r)}function Ne(n,e,r){return"every second"+a(n,r)}function ze(n,e,r){return H(n,r)+a(n,r)}function Pe(n,e,r){let t=n.pattern.second;return m(t,r)+" "+P(t,"second")+" past the minute, every minute"+a(n,r)}function ve(n,e,r){let t=n.pattern.minute,i=m(t,r),o=P(t,"minute");if(e.singleSecond){let u=n.pattern.second;return i+" "+o+" and "+m(u,r)+" "+P(u,"second")+" past the hour, every hour"+a(n,r)}return H(n,r)+", "+i+" "+o+" past the hour, every hour"+a(n,r)}function Ce(n,e,r){return H(n,r)+", "+zn(n,e.rest,r)}function H(n,e){let r=n.pattern.second,t=n.shapes.second;if(r==="*")return"every second";if(t==="step")return wn(n.analyses.segments.second[0],"second","minute",e);if(t==="range"){let i=r.split("-"),o=E(i,e);return"every second from "+o(i[0])+O(e)+o(i[1])+" past the minute"}return t==="single"?"at "+m(r,e)+" "+P(r,"second")+" past the minute":b(T(n.analyses.segments.second,e),"second","minute",e)}function we(n,e,r){return"every minute"+a(n,r)}function Re(n,e,r){let t=n.pattern.minute;return m(t,r)+" "+P(t,"minute")+" past the hour, every hour"+a(n,r)}function xe(n,e,r){return L(n.pattern.minute,r)+a(n,r)}function Me(n,e,r){return b(T(n.analyses.segments.minute,r),"minute","hour",r)+a(n,r)}function Te(n,e,r){let t=wn(n.analyses.segments.minute[0],"minute","hour",r);return e.hours.kind==="during"?t+=" during the "+Z(n,e.hours.times,!1,r)+" hours":e.hours.kind==="window"?t+=" "+Cn(e.hours,r):e.hours.kind==="step"&&(t+=" "+Pn(n.analyses.segments.hour[0],r)),t+a(n,r)}function Fe(n,e,r){return"every minute from "+c({hour:e.hour,minute:e.span[0]},r)+O(r)+c({hour:e.hour,minute:e.span[1]},r)+a(n,r)}function Ie(n,e,r){if(e.form==="wildcard")return"every minute during the "+Z(n,e.times,!1,r)+" hours"+a(n,r);let t=Z(n,e.times,!0,r);return(e.form==="range"?L(n.pattern.minute,r):b(T(n.analyses.segments.minute,r),"minute","hour",r))+", at "+t+a(n,r)}var He={2:"other",3:"third",4:"fourth",6:"sixth",8:"eighth",12:"twelfth"};function Pn(n,e){let r="during every "+He[n.interval]+" hour",t=n.startToken==="*"?0:+n.startToken;return t===0?r:r+" starting at "+c({hour:t,minute:0},e)}function Le(n,e,r){let t=n.analyses.segments.hour[0];return e.form==="wildcard"?"every minute "+Pn(t,r)+a(n,r):L(n.pattern.minute,r)+", "+Rn(t,r)+a(n,r)}function L(n,e){let r=n.split("-"),t=E(r,e);return"every minute from "+t(r[0])+O(e)+t(r[1])+" past the hour"}function Ee(n,e,r){return"every hour"+a(n,r)}function Ae(n,e,r){let t=Cn(e,r);return e.minuteForm==="wildcard"?"every minute "+t+a(n,r):e.minuteForm==="range"?L(n.pattern.minute,r)+", "+t+a(n,r):vn(n,r)+" "+t+a(n,r)}function vn(n,e){return n.pattern.minute==="0"?"every hour":b(T(n.analyses.segments.minute,e),"minute","hour",e)}function We(n,e,r){return Rn(n.analyses.segments.hour[0],r)+a(n,r)}function Cn(n,e){return"from "+c({hour:n.from,minute:0},e)+O(e)+c({hour:n.to,minute:n.last},e)}function De(n,e,r){let t=rn(e.times),i=e.times.map(function(u){return c({hour:u.hour,minute:u.minute,second:u.second,plain:t},r)});return xn(n,r)+"at "+S(i,r)}function je(n,e,r){if(e.fold){if(n.analyses.segments.hour.some(function(f){return f.kind==="range"})&&!n.analyses.clockSecond)return Ue(n,e,r)+a(n,r);let o={minute:e.minute,second:n.analyses.clockSecond};return xn(n,r)+"at "+_(n,o,!0,r)}let t=b(T(n.analyses.segments.minute,r),"minute","hour",r)+", at "+_(n,{minute:0,second:null},!0,r)+a(n,r);return n.analyses.clockSecond?H(n,r)+", "+t:t}function Ue(n,e,r){let t=e.minute,i=[],o=[];n.analyses.segments.hour.forEach(function(l){l.kind==="range"?i.push("from "+c({hour:l.bounds[0],minute:0},r)+O(r)+c({hour:l.bounds[1],minute:t},r)):l.kind==="step"?o.push(...l.fires):o.push(+l.value)});let u=vn(n,r)+" "+S(i,r);return o.length&&(u+=" and at "+S(o.map(function(l){return c({hour:l,minute:t},r)}),r)),u}var Qe={clockTimes:De,compactClockTimes:je,composeSeconds:Ce,everyHour:Ee,everyMinute:we,everySecond:Ne,hourRange:Ae,hourStep:We,minuteFrequency:Te,minuteSpanAcrossHourStep:Le,minuteSpanInHour:Fe,minutesAcrossHours:Ie,multipleMinutes:Me,rangeOfMinutes:xe,secondPastMinute:Pe,secondsWithinMinute:ve,singleMinute:Re,standaloneSeconds:ze};function wn(n,e,r,t){if(n.startToken.indexOf("-")!==-1)return b(K(n.fires,t),e,r,t);let i=n.startToken==="*"?0:+n.startToken,o=n.interval;return i!==0?n.fires.length<=3?b(K(n.fires,t),e,r,t):"every "+m(o,t)+" "+e+"s from "+m(i,t)+" "+P(i,e)+" past the "+r:60%o===0?"every "+m(o,t)+" "+e+"s":n.fires.length<=2?b(K(n.fires,t),e,r,t):"every "+m(o,t)+" "+e+"s past the "+r}function Rn(n,e){if(n.startToken.indexOf("-")!==-1)return"at "+X(n.fires,e);let r=n.startToken==="*"?0:+n.startToken,t=n.interval;return r===0&&24%t===0?"every "+m(t,e)+" hours":n.fires.length<=3?"at "+X(n.fires,e):r===0?"every "+m(t,e)+" hours from midnight":"every "+m(t,e)+" hours from "+c({hour:r,minute:0},e)}function E(n,e){let r=n.some(function(i){return+i>10});return function(i){return r?""+i:m(i,e)}}function K(n,e){return n.map(E(n,e))}function T(n,e){let r=n.flatMap(function(o){return o.kind==="range"?o.bounds:o.kind==="step"?o.fires:[o.value]}),t=E(r,e);return n.flatMap(function(o){return o.kind==="range"?[t(o.bounds[0])+O(e)+t(o.bounds[1])]:o.kind==="step"?o.fires.map(t):[t(o.value)]})}function b(n,e,r,t){return"at "+S(n,t)+" "+e+"s past the "+r}function qe(n,e,r){return(+n==0||+n==12)&&+e==0&&!(typeof r=="number"&&r>0)}function rn(n){let e=n.filter(function(t){return qe(t.hour,t.minute,t.second)});return e.length>0&&e.length<n.length}function X(n,e){let r=rn(n.map(function(o){return{hour:o,minute:0}})),t=n.map(function(o){return c({hour:o,minute:0,plain:r},e)});return S(t,e)}function Z(n,e,r,t){return e.kind==="fires"?X(e.fires,t):_(n,{minute:0,second:null},r,t)}function Je(n){return n.kind==="range"?n.bounds:n.kind==="step"?n.fires:[n.value]}function _(n,e,r,t){let{minute:i,second:o}=e,u=n.analyses.segments.hour,f=rn(u.flatMap(function(p){return Je(p).map(function(D){return{hour:+D,minute:i,second:o}})})),l=[];return u.forEach(function(p){p.kind==="step"?l.push(...p.fires.map(function(D){return c({hour:D,minute:i,second:o,plain:f},t)})):p.kind==="range"?l.push(c({hour:p.bounds[0],minute:i,second:o,plain:f},t)+O(t)+c({hour:p.bounds[1],minute:i,second:o,plain:f},t)):l.push(c({hour:p.value,minute:i,second:o,plain:f},t))}),S(Ve(l,u,r),t)}function Ve(n,e,r){let t=e.some(function(o){return o.kind==="range"});return!r||!t?n:n.map(function(o,u){return u===0?o:"at "+o})}function S(n,e){if(n.length<=1)return n.join("");if(n.length===2)return n[0]+" and "+n[1];let r=e.style.serialComma?", and ":" and ";return n.slice(0,-1).join(", ")+r+n[n.length-1]}var $e={all:"",month:"in ",stepDate:"on ",weekday:"on "},Ye={all:"every day",month:"every day in ",stepDate:"",weekday:"every "};function a(n,e){let r=Mn(n,$e,e);return r&&" "+r}function xn(n,e){return Mn(n,Ye,e)+" "}function Mn(n,e,r){let t=n.pattern;return t.date!=="*"&&t.weekday!=="*"?Ge(n,r):t.date!=="*"?Be(n,e,r):t.weekday!=="*"?(In(t.weekday,r)||e.weekday+An(n,r))+z(n,r):t.month!=="*"?e.month+A(n,r):e.all}function Be(n,e,r){let t=n.pattern,i=Fn(t.date,r);return i?i+z(n,r):tn(t.date)?e.stepDate+Ln(t.date)+z(n,r):t.month!=="*"&&!Tn(n)?"on the "+nn(n,r)+z(n,r):t.month!=="*"?"on "+Hn(n,r):"on the "+nn(n,r)}function Tn(n){return!En(n.pattern.month)&&n.analyses.segments.month.every(function(r){return r.kind!=="range"})}function Ge(n,e){let r=n.pattern,t=In(r.weekday,e)||"on "+An(n,e),i=Fn(r.date,e);return i?i+z(n,e)+" or "+t:tn(r.date)?Ln(r.date)+z(n,e)+" or "+t:r.month!=="*"&&Tn(n)?"on "+Hn(n,e)+" or "+t+" in "+A(n,e):"on the "+nn(n,e)+" or "+t+z(n,e)}function Fn(n,e){if(n==="L")return"on the last day of the month";if(n==="LW"||n==="WL")return"on the last weekday of the month";let r=/^L-(\d{1,2})$/.exec(n);if(r)return m(+r[1],e)+" "+P(r[1],"day")+" before the last day of the month";let t=/^(\d{1,2})W$|^W(\d{1,2})$/.exec(n);if(t)return"on the weekday nearest the "+M(t[1]||t[2])}function In(n,e){let r=n.split("#");if(r.length===2)return"on the "+Se[+r[1]]+" "+en(r[0],e)+" of the month";if(/L$/.test(n))return"on the last "+en(n.slice(0,-1),e)+" of the month"}function Hn(n,e){let r=A(n,e),t=W(n.analyses.segments.date,e.style.ordinals?M:Ke,e);return e.style.dayFirst?t+" "+r:r+" "+t}function Ke(n){return""+n}function z(n,e){return n.pattern.month==="*"?"":" in "+A(n,e)}function Ln(n){let e=n.split("/"),r=+e[1],t=e[0],o=(r===2?"every other":"every "+M(r))+" day of the month";return t!=="*"&&t!=="1"&&(o+=" from the "+M(t)),o}function nn(n,e){return W(n.analyses.segments.date,M,e)}function A(n,e){let r=En(n.pattern.month);return r||W(n.analyses.segments.month,function(i){return er(i,e)},e)}function En(n){if(!tn(n))return null;let[e,r]=n.split("/");return+r!=2?null:e==="*"||e==="1"?"every odd-numbered month":e==="2"?"every even-numbered month":null}function An(n,e){return W(n.analyses.segments.weekday,function(t){return en(t,e)},e)}function W(n,e,r){let t=[];return n.forEach(function(o){o.kind==="step"?t.push(...o.fires.map(e)):o.kind==="range"?t.push(o.bounds.map(e).join(O(r))):t.push(e(o.value))}),S(t,r)}function tn(n){return n.indexOf("/")!==-1&&n.indexOf("-")===-1&&n.indexOf(",")===-1}function Xe(n,e,r){let t=e.pattern.year;if(t==="*")return n;if(t.indexOf("/")!==-1)return n+" "+_e(t,r);let i=Ze(t,r);if(t.indexOf("-")===-1&&t.indexOf(",")===-1&&e.pattern.date!=="*"&&n.indexOf(" at ")!==-1){let o=r.style.dayFirst?" ":", ";return n.replace(" at ",o+i+" at ")}return n+" in "+i}function Ze(n,e){return n.indexOf(",")!==-1?S(n.split(","),e):n}function _e(n,e){let r=n.split("/"),t=+r[1],i=r[0];if(t<=1)return"every year";let o="every "+m(t,e)+" years";return i!=="*"&&i!=="0"&&(o+=" from "+i),o}function c(n,e){let{hour:r,minute:t,plain:i}=n,o=typeof n.second=="number"&&n.second>0?n.second:0;return e.ampm?nr({hour:r,minute:t,second:o,plain:i},e):Y({hour:r,minute:t,second:o},{pad:!0,sep:e.style.sep})}function nr(n,e){let{hour:r,minute:t,second:i,plain:o}=n,u=e.style;if(!o&&+t==0&&!i){if(+r==0)return u.midnight;if(+r==12)return u.midday}return Y({hour:r%12||12,minute:t,second:i},{lean:!0,sep:u.sep})+(u.closeUp?"":" ")+(r<12?u.am:u.pm)}function m(n,e){return kn(n,pe,e)}function P(n,e){return+n==1?e:e+"s"}function O(n){return n.short?"-":n.style.through}function M(n){let e=Math.abs(n),r=G[e];return r||(e=(e%100-20)%10,r=G[e]||G[0]),n+r}function er(n,e){let r=g[n]||ye[n];return r&&r[e.short?1:0]}function en(n,e){let r=n===7||n==="7"?0:n,t=y[r]||be[r];return t&&t[e.short?1:0]}var rr={describe:ke,fallback:"an unrecognizable cron pattern",options:Oe,reboot:"at system startup",sentence:n=>"Runs "+n+"."},Wn=rr;/**
2
+ * @license MIT, Copyright (c) 2026 Andrew Brož
3
+ */function tr(n,e){let r=e&&e.lang||Wn,t=r.options(e);if(!t.lenient)return Dn(jn(n,r,t),r,e);try{return Dn(jn(n,r,t),r,e)}catch{return r.fallback}}function Dn(n,e,r){return r&&r.sentence?e.sentence(n):n}function jn(n,e,r){if(typeof n=="string"&&n.trim().toLowerCase()==="@reboot")return e.reboot;let t=pn(On(n,r)),i=e.strategy?e.strategy(t,t.plan):t.plan;return e.describe({...t,plan:i},r)}var on=tr;typeof globalThis<"u"&&Object.assign(globalThis,{cronli5:on});var Dr=on;})();