cronli5 0.0.0-beta.6 → 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.
- package/CHANGELOG.md +276 -0
- package/LICENSE.md +2 -2
- package/README.md +328 -91
- package/cli.js +73 -10
- package/cronli5.min.js +3 -0
- package/dist/cronli5.cjs +1556 -0
- package/dist/cronli5.js +1532 -0
- package/dist/lang/de.cjs +546 -0
- package/dist/lang/de.js +522 -0
- package/dist/lang/en.cjs +830 -0
- package/dist/lang/en.js +806 -0
- package/dist/lang/es.cjs +1024 -0
- package/dist/lang/es.js +1000 -0
- package/dist/lang/fi.cjs +942 -0
- package/dist/lang/fi.js +918 -0
- package/dist/lang/zh.cjs +612 -0
- package/dist/lang/zh.js +588 -0
- package/package.json +92 -13
- package/src/browser.ts +9 -0
- package/src/core/analyze.ts +549 -0
- package/src/core/format.ts +51 -0
- package/src/core/index.ts +28 -0
- package/src/core/ir.ts +167 -0
- package/src/core/normalize.ts +143 -0
- package/src/core/parse.ts +132 -0
- package/src/core/shapes.ts +41 -0
- package/src/core/specs.ts +93 -0
- package/src/core/util.ts +28 -0
- package/src/core/validate.ts +170 -0
- package/src/cronli5.ts +95 -0
- package/src/lang/de/dialects.ts +52 -0
- package/src/lang/de/index.ts +820 -0
- package/src/lang/de/notes.md +79 -0
- package/src/lang/de/status.json +18 -0
- package/src/lang/en/dialects.ts +70 -0
- package/src/lang/en/index.ts +1290 -0
- package/src/lang/en/notes.md +44 -0
- package/src/lang/en/status.json +20 -0
- package/src/lang/es/dialects.ts +55 -0
- package/src/lang/es/index.ts +1737 -0
- package/src/lang/es/notes.md +63 -0
- package/src/lang/es/status.json +19 -0
- package/src/lang/fi/dialects.ts +31 -0
- package/src/lang/fi/index.ts +1499 -0
- package/src/lang/fi/notes.md +163 -0
- package/src/lang/fi/status.json +7 -0
- package/src/lang/zh/dialects.ts +27 -0
- package/src/lang/zh/index.ts +863 -0
- package/src/lang/zh/notes.md +118 -0
- package/src/lang/zh/status.json +7 -0
- package/src/types.ts +143 -0
- package/types/browser.d.ts +2 -0
- package/types/core/analyze.d.ts +13 -0
- package/types/core/format.d.ts +16 -0
- package/types/core/index.d.ts +8 -0
- package/types/core/ir.d.ts +185 -0
- package/types/core/normalize.d.ts +5 -0
- package/types/core/parse.d.ts +5 -0
- package/types/core/shapes.d.ts +6 -0
- package/types/core/specs.d.ts +27 -0
- package/types/core/util.d.ts +7 -0
- package/types/core/validate.d.ts +5 -0
- package/types/cronli5.d.ts +7 -0
- package/types/lang/de/dialects.d.ts +7 -0
- package/types/lang/de/index.d.ts +4 -0
- package/types/lang/en/dialects.d.ts +4 -0
- package/types/lang/en/index.d.ts +3 -0
- package/types/lang/es/dialects.d.ts +13 -0
- package/types/lang/es/index.d.ts +4 -0
- package/types/lang/fi/dialects.d.ts +4 -0
- package/types/lang/fi/index.d.ts +3 -0
- package/types/lang/zh/dialects.d.ts +6 -0
- package/types/lang/zh/index.d.ts +4 -0
- package/types/types.d.ts +113 -0
- package/.eslintrc.json +0 -217
- package/.npmignore +0 -2
- package/conli5.min.js +0 -4
- package/cronli5.js +0 -559
- package/test/.eslintrc.json +0 -10
- package/test/bad_input/arrays.js +0 -34
- package/test/bad_input/bad-types.js +0 -33
- package/test/bad_input/error-types.js +0 -7
- package/test/bad_input/objects.js +0 -47
- package/test/bad_input/strings.js +0 -10
- package/test/baseline/baseline.js +0 -14
- package/test/basic/arrays.js +0 -76
- package/test/basic/objects.js +0 -70
- package/test/basic/strings.js +0 -76
- package/test/complex/steps/strings.js +0 -42
- package/test/mocha.opts +0 -5
- package/test/options/ampm.js +0 -17
- package/test/options/seconds.js +0 -0
- package/test/options/short.js +0 -27
- package/test/options/years.js +0 -0
- package/test/runner.js +0 -52
- package/test/simple/arrays.js +0 -33
- package/test/simple/objects.js +0 -23
- package/test/simple/strings.js +0 -33
package/README.md
CHANGED
|
@@ -1,141 +1,378 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
3
|
+
[](https://github.com/andrewbroz/cronli5/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/cronli5)
|
|
5
|
+
[](./src/types.ts)
|
|
6
|
+
[](https://bundlephobia.com/package/cronli5)
|
|
7
|
+
[](./LICENSE.md)
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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,
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
59
|
+
# If you plan to use the cli:
|
|
60
|
+
npm install -g cronli5
|
|
33
61
|
```
|
|
34
|
-
|
|
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.
|
|
39
|
-
_Unsolicited advice: rather than including
|
|
40
|
-
`cronli5` in its own script tag, consider using a bundler like [Browserify]
|
|
41
|
-
[browserify], [Rollup][rollup], or [Webpack][webpack] and `include` or
|
|
42
|
-
`require` instead. See below._
|
|
70
|
+
global in the scripts that follow.
|
|
43
71
|
|
|
44
72
|
## Usage
|
|
45
73
|
|
|
46
|
-
|
|
74
|
+
Import as an ES module:
|
|
75
|
+
```js
|
|
76
|
+
import cronli5 from 'cronli5';
|
|
47
77
|
```
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
|
|
79
|
+
Or with CommonJS `require`:
|
|
80
|
+
```js
|
|
81
|
+
const cronli5 = require('cronli5');
|
|
50
82
|
```
|
|
51
83
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
```
|
|
91
|
+
|
|
92
|
+
TypeScript types are bundled, so usage is fully typed out of the box:
|
|
93
|
+
```ts
|
|
94
|
+
import cronli5, { type Cronli5Options } from 'cronli5';
|
|
54
95
|
|
|
55
|
-
|
|
96
|
+
const options: Cronli5Options = { ampm: false };
|
|
97
|
+
const description: string = cronli5('30 13 * * MON-FRI', options);
|
|
98
|
+
// 'every Monday through Friday at 13:30'
|
|
56
99
|
```
|
|
57
|
-
|
|
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
|
|
58
113
|
```
|
|
59
114
|
|
|
60
|
-
|
|
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 — 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 — via a seven-field pattern, an object's
|
|
153
|
+
`year` property, or a six-field pattern with `years: true` — 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'
|
|
61
160
|
```
|
|
161
|
+
|
|
162
|
+
```js
|
|
62
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'
|
|
63
175
|
```
|
|
64
176
|
|
|
65
|
-
|
|
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 KB gzipped).
|
|
183
|
+
|
|
184
|
+
```js
|
|
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'
|
|
66
192
|
```
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
193
|
+
|
|
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'
|
|
87
268
|
```
|
|
88
269
|
|
|
89
|
-
##
|
|
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 — `5,10 30 9 * * MON` — 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
|
|
90
314
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
`
|
|
95
|
-
* `
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
343
|
+
```
|
|
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`.
|
|
111
348
|
|
|
112
349
|
## About
|
|
113
350
|
|
|
114
|
-
The project name is a reference to the phrase [Explain Like I'm Five (ELI5)]
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
117
354
|
|
|
118
355
|
`cronli5` was partially inspired by [`prettycron`][prettycron], which itself
|
|
119
356
|
is based on code from [a gist by dunse][dunse]. Although `prettycron` was
|
|
120
|
-
close to meeting my needs, I wasn't fully satisfied with the output
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
source does not borrow code, in whole or in part, from [prettycron]
|
|
126
|
-
[prettycron], [Stack Overflow answers][stackoverflow], or any other project.
|
|
127
|
-
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.
|
|
128
362
|
|
|
129
363
|
## License
|
|
130
364
|
|
|
131
365
|
*[MIT License][license]*
|
|
132
|
-
_Copyright ©
|
|
366
|
+
_Copyright © 2026 [Andrew Brož][andrewbroz]_
|
|
133
367
|
|
|
134
|
-
[
|
|
135
|
-
[
|
|
368
|
+
[andrewbroz]: https://github.com/andrewbroz
|
|
369
|
+
[esbuild]: https://esbuild.github.io/
|
|
136
370
|
[dunse]: https://gist.github.com/dunse/3714957
|
|
137
371
|
[eli5]: https://www.reddit.com/r/explainlikeimfive/
|
|
138
|
-
[
|
|
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
|
|
139
376
|
[license]: ./LICENSE.md
|
|
140
377
|
[moment]: http://momentjs.com/
|
|
141
378
|
[moment-timezone]: http://momentjs.com/timezone/
|
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
|
-
//
|
|
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
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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;})();
|