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.
Files changed (98) hide show
  1. package/CHANGELOG.md +276 -0
  2. package/LICENSE.md +2 -2
  3. package/README.md +328 -91
  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 -559
  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/ampm.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/CHANGELOG.md ADDED
@@ -0,0 +1,276 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ - **Per-language documentation** under `docs/lang/` (`en.md`, `es.md`,
12
+ `fi.md`): usage, style anchors, dialects, and the language's
13
+ distinctive conventions, each with a generated cronli5-vs-cRonstrue
14
+ table against the matching cRonstrue locale. The tables share a
15
+ twelve-row cross-language pattern set plus per-language grammar rows,
16
+ are regenerated by `npm run docs`, and are covered by the same
17
+ `--check` staleness gate as the English head-to-head. The README
18
+ gained a Languages section and no longer describes the library as
19
+ English-only.
20
+ - Language subpaths (`cronli5/lang/en`, `cronli5/lang/es`,
21
+ `cronli5/lang/fi`) now ship dual built artifacts:
22
+ `require('cronli5/lang/es')` works alongside `import`, and each
23
+ subpath carries a `types` condition (a shared `Cronli5Language`
24
+ declaration in `lang.d.ts`). Previously the subpaths exposed ESM
25
+ source only.
26
+ - **Finnish** (`cronli5/lang/fi`), the agglutinative stress test from
27
+ the i18n design (docs/i18n-design.md §5): a full natural-Finnish
28
+ renderer anchored to Kielitoimiston ohjepankki and SFS 4175 —
29
+ "maanantaista perjantaihin klo 9.30", "kuukauden viimeisenä
30
+ perjantaina keskiyöllä", "viiden minuutin välein". Weekdays are
31
+ stored as inflected forms (consonant gradation: keskiviikosta),
32
+ ranges are case pairs on names and en-dash notation on digits
33
+ ("klo 9.00–17.45"), and written Finnish being 24-hour only, the
34
+ `ampm` option is ignored. Ships with a reviewed corpus, minimal
35
+ pairs, language notes, and a review log under `test/lang/fi/`.
36
+ Like Spanish, Finnish required zero core changes.
37
+
38
+ - Descriptions for **lists containing range or step segments** (e.g.
39
+ `0-30,45` or `9,17-19`) in every field. Minute and second lists read their
40
+ spans discretely (`at five through ten and 20 minutes past the hour`), hour
41
+ list segments expand into clock times, and date/month/weekday list segments
42
+ render as ordinals, names, or weekday spans.
43
+ - A minute wildcard or plain range now composes with an **hour list**
44
+ (`every minute during the 9 a.m. and 5 p.m. hours`) and an **hour step**
45
+ (`every minute from zero through 30 past the hour, every two hours`)
46
+ instead of collapsing to the bare hour description.
47
+ - A meaningful second under a restricted minute or hour now **leads with its
48
+ own clause** (`every 15 seconds, every day at 9:30 a.m.`) instead of being
49
+ silently dropped.
50
+ - Property-based tests covering mixed-list fields, asserting interpretation
51
+ never throws or leaks `NaN`/`undefined` for valid patterns.
52
+ - A `lenient` option: invalid input returns the fallback description
53
+ `'an unrecognizable cron pattern'` instead of throwing, making `cronli5`
54
+ safe to embed in UIs that render arbitrary user crontabs.
55
+ - A `dialect` option anchored to named style guides. `'us'` (the default)
56
+ follows the Chicago Manual of Style; `'gb'` follows the Guardian style
57
+ guide: `cronli5('30 9 * * MON-FRI', {dialect: 'gb'})` reads "every Monday
58
+ to Friday at 9.30am", with no serial comma, "to" ranges, closed-up
59
+ full-point times, "midday"/"midnight", and day-first dates ("1 January").
60
+ `'house'` preserves cronli5's legacy voice ("9:30 AM", "Monday - Friday",
61
+ ordinal dates like "January 1st") on a Chicago base, and a custom style
62
+ object may be passed directly, with omitted fields inheriting the US
63
+ defaults (`{dialect: {through: ' until '}}`). The full style-field
64
+ reference lives in `docs/dialects.md`.
65
+ - Input normalization: list segments are described in ascending fire order
66
+ (`17,9` reads "at 9 a.m. and 5 p.m."), duplicate segments collapse, and
67
+ degenerate ranges (`9-9`) read as their single value.
68
+ - **Quartz-style tokens** in the date and weekday fields: `L` ("on the last
69
+ day of the month"), `L-n` ("five days before the last day of the month"),
70
+ `LW`/`WL` ("on the last weekday of the month"), `nW` ("on the weekday
71
+ nearest the 15th"), `nL` ("on the last Friday of the month"), `n#m` ("on
72
+ the second Monday of the month"), and `?` (no specific value).
73
+ - **Wrap-around ranges** in cyclic fields: `0 22-2 * * *` reads "every hour
74
+ from 10 p.m. through 2 a.m.", `FRI-MON` reads "Friday-Monday", and
75
+ `11-2` reads "November through February". Reversed ranges remain invalid
76
+ where the cycle metaphor breaks down: step bounds and the year field.
77
+ - A head-to-head cronli5 vs. cRonstrue comparison:
78
+ `docs/cronli5-vs-cronstrue.md` carries two generated output tables
79
+ (everyday patterns, and compound patterns where the gap is widest),
80
+ regenerated in place by `npm run docs` (with a `--check` mode CI runs);
81
+ `docs/cronstrue-comparison.md` holds the full architectural comparison;
82
+ the README links to both.
83
+ - **Seven-field (Quartz-style) patterns** parse without any option: seven
84
+ fields are unambiguous (`second minute hour date month weekday year`), so
85
+ `'0 0 12 1 1 * 2030'` reads "on January 1, 2030 at noon". The
86
+ `years` option remains as the six-field disambiguator.
87
+ - An explicitly supplied year is now always described: object input with a
88
+ `year` property (e.g. `{hour: 9, year: 2030}`) previously validated the
89
+ year and then silently dropped it from the description.
90
+
91
+ ### Changed
92
+
93
+ - **The English `'uk'` dialect was renamed to `'gb'`.** BCP-47 reserves
94
+ `uk` for the Ukrainian language, so the British-English style now uses the
95
+ ISO-3166 country code `'gb'`. `{ dialect: 'uk' }` still works as a
96
+ deprecated alias for `'gb'` and will be removed in a future release.
97
+
98
+ - **Spanish now defaults to the 24-hour clock** (`a las 09:30`,
99
+ `a las 17:00`), matching RAE convention for written Spanish. Pass
100
+ `{ampm: true}` for the previous 12-hour behavior with day periods
101
+ (`a las 9:30 de la mañana`, `al mediodía`, `a medianoche`). As part of
102
+ the change, one o'clock now keeps its singular article on the 24-hour
103
+ clock as well (`a la 01:00`, not `a las 1:00`). English stays 12-hour
104
+ by default; Finnish was already 24-hour only.
105
+ - **Spanish 24-hour clock times now zero-pad the hour**, like the
106
+ minutes already did: `a las 09:00` (was `a las 9:00`), `a la 01:00`.
107
+ This matches English 24-hour output (`ampm: false`), which already
108
+ padded. Finnish deliberately does **not** pad the hour (`klo 9.30`,
109
+ `klo 9–17`), per SFS 4175, where the hour is written without a leading
110
+ zero and only the minute pads.
111
+ - Finnish anchored minutes/seconds use the **`kohdalla` mark form**:
112
+ `30 * * * *` reads `joka tunti 30 minuutin kohdalla` (was the calque
113
+ `jokaisen tunnin minuutilla 30`), `15 * * * * *` reads `joka minuutti
114
+ 15 sekunnin kohdalla`. The old adessive form was flagged as an English
115
+ calque by an independent review and corroborated by cRonstrue's human
116
+ Finnish locale.
117
+ - Spanish weekday qualifiers drop the redundant `todos`: `0 9 * * MON`
118
+ reads `los lunes a las 09:00` (was `todos los lunes…`). The plural
119
+ definite article `los lunes` already means "every Monday" in Spanish,
120
+ and the other weekday forms (ranges, trailing, date-or-weekday) already
121
+ omitted it. `todos los días` is unchanged, since `los días` alone does
122
+ not mean "every day".
123
+ - Internal restructure toward i18n (see `docs/i18n-design.md`): the
124
+ language-independent core (parsing, validation, normalization, semantic
125
+ analysis) now lives in `src/core/`, and all English — phrases, dialect
126
+ tables, names, time formatting — lives in `src/lang/en/`. The public API
127
+ and every description are unchanged; the only output difference is that
128
+ the too-many-fields error message no longer varies with the `short`
129
+ option.
130
+ - The test tree mirrors the i18n architecture: English's expectation suite
131
+ is a language corpus like any other (`test/lang/en/`), validation and
132
+ error tests live with the core (`test/core/`), and `test/property/`
133
+ holds the shared invariants.
134
+ - **Spanish** (`cronli5/lang/es`), the i18n pilot: a full natural-Spanish
135
+ renderer over the semantic IR, anchored to RAE/FundéuRAE conventions
136
+ ("todos los lunes a las 9:30 de la mañana", "el 25 de diciembre de 2030
137
+ al mediodía"), selected per call via the new `lang` option
138
+ (`cronli5(pattern, {lang: es})`). Ships with a reviewed corpus, minimal
139
+ pairs, language notes, and a review log under `test/lang/es/`, hardened
140
+ against the full English pattern set plus a hazard-hour matrix via the
141
+ new review-packet generator (`scripts/review-lang.mjs`). Spanish month
142
+ ranges read with repeated prepositions where folding would garden-path
143
+ ("el 1 de cada mes, de junio a septiembre"; "en enero y de marzo a
144
+ junio"), and step segments in month/weekday lists flatten into their
145
+ fires ("todos los domingos, lunes, miércoles y viernes").
146
+ - Description-strategy selection now lives in the core as a semantic IR:
147
+ `analyze()` classifies field shapes and segments, precomputes windows
148
+ and enumerations, and selects the plan; the English module is a pure
149
+ plan renderer whose only input is the IR. Covered by a dedicated IR
150
+ spec (`test/core/analyze.js`); every description is unchanged.
151
+ - Default output now adheres to the **Chicago Manual of Style**: serial
152
+ commas in lists of three or more ("9 a.m., noon, and 5 p.m."), lowercase
153
+ dotted meridiems ("9:30 a.m.", previously "9:30 AM"), on-the-hour times
154
+ without minutes ("9 a.m.", previously "9:00 AM"), "noon" and "midnight"
155
+ for exact 12:00, and cardinal month-day dates ("January 1", previously
156
+ "January 1st"). Bare days of the month keep their ordinals ("on the 1st
157
+ and 15th").
158
+ - Hour windows now end at the **last actual fire** instead of the top of the
159
+ final hour: `*/15 9-17 * * *` reads "every 15 minutes from 9 a.m. through
160
+ 5:45 p.m." (previously "through 5 p.m."), and `*/15 9 * * *` reads "through
161
+ 9:45 a.m." (previously "through 9:59 a.m.").
162
+ - Weekday ranges read as prose: `MON-FRI` is "every Monday through Friday"
163
+ (previously "every Monday-Friday").
164
+ - The `short` option now compacts ranges consistently: every "A through B"
165
+ becomes "A-B" (`Mon-Fri` as before, and now also `Jan-Mar`, `1st-5th`,
166
+ `0-30` in minute lists, and clock-time windows like `9 a.m.-5:45 p.m.`).
167
+ Previously short mode abbreviated names but left "through" in place for
168
+ every field except weekdays.
169
+
170
+ ### Fixed
171
+
172
+ - A contiguous hour range with extra discrete hours now reads with the
173
+ hour-range frame instead of an ambiguous clock-time span:
174
+ `0 9-20,22 * * *` reads "every hour from 9 a.m. through 8 p.m. and at
175
+ 10 p.m." (was "every day at 9 a.m. through 8 p.m. and 10 p.m.", where
176
+ the trailing "and 10 p.m." could read as part of the span). This mirrors
177
+ the existing pure-range rendering (`0 9-17 * * *` → "every hour from 9
178
+ a.m. through 5 p.m."); the per-minute (`30 9-20,22` → "at 30 minutes
179
+ past the hour from 9 a.m. through 8:30 p.m. and at 10:30 p.m.") and
180
+ multiple-range (`0 9-12,14-20` → "every hour from 9 a.m. through noon
181
+ and from 2 p.m. through 8 p.m.") forms follow the same frame.
182
+ - Clock-time lists no longer mix the words "noon"/"midnight" with numeral
183
+ times: `0 22-2,12 * * *` reads "every day at 12 p.m., 10 p.m., 11 p.m.,
184
+ 12 a.m., 1 a.m., and 2 a.m." (was "noon, ..., midnight, ..."), the same
185
+ consistency rule the number series follows. A list that is only
186
+ noon/midnight keeps the words (`0 0,12 * * *` → "midnight and noon"),
187
+ as does a single time (`0 12 * * *` → "every day at noon").
188
+ - English minute and second number series are now internally consistent
189
+ in their number style: when any value in a list or range exceeds ten,
190
+ the whole series uses numerals instead of mixing spelled words with
191
+ digits. `0-29 * * * *` reads "every minute from 0 through 29 past the
192
+ hour" (was "from zero through 29"), and `5,10,15 * * * * *` reads "at
193
+ 5, 10, and 15 seconds past the minute" (was "five, ten, and 15"). All-
194
+ small series stay spelled ("at five and ten seconds"), as do single
195
+ values ("30 minutes past the hour"). This follows the Chicago rule the
196
+ default dialect already targets.
197
+ - A month **range** no longer folds into a calendar date: `0 0 1 6-9 *`
198
+ reads "on the 1st in June through September" (previously "on June
199
+ through September 1", which parses as "(June) through (September 1)").
200
+ Single months and flat name lists still fold ("on June 1", "on June and
201
+ December 1"). With both a date and a weekday, the month scopes the whole
202
+ alternation once: "on the 1st or on Friday in June through September".
203
+ - Minute and second lists containing **step segments** enumerate the
204
+ step's fires instead of leaking the raw token: `5,30-40/5 * * * *`
205
+ reads "at 5, 30, 35, and 40 minutes past the hour" (previously
206
+ "at five and 30-40/5 minutes past the hour"), matching how standalone
207
+ bounded steps and date lists already read.
208
+ - Hour lists containing range or step segments (e.g. `0-30 9,17-19 * * *`)
209
+ no longer throw.
210
+ - Minute lists containing a range under a specific hour (e.g.
211
+ `0-30,45 9 * * *`) no longer render `NaN` clock times or garbled bounds.
212
+ - A second step under a specific minute and hour (e.g. `*/15 30 9 * * *`) no
213
+ longer loses its cadence.
214
+ - Second- and minute-anchored descriptions (`30 minutes past the hour, every
215
+ hour`, `every 15 seconds`, etc.) no longer drop their trailing day
216
+ qualifier (e.g. `on Monday`).
217
+ - Month, date, and weekday lists containing ranges or steps no longer render
218
+ `undefined` or garbled bounds.
219
+ - A weekday combined with a month (e.g. `0 0 * 6 MON`) no longer drops the
220
+ weekday: it reads "every Monday in June at midnight".
221
+ - Interval-one steps normalize to their equivalent range, fixing inaccurate
222
+ or garbled output in every field: `1/1 * * * *` read "every minute"
223
+ though minute :00 is excluded (now "every minute from one through 59 past
224
+ the hour"), `0 1/1 * * *` read "every hour" though hour 0 is excluded
225
+ (now "every hour from 1 a.m. through 11 p.m."), and `0 0 2/1 * *` read
226
+ "every 1st day of the month from the 2nd" (now "on the 2nd through 31st").
227
+ - Offset steps starting at one are no longer ungrammatical: `1/3 * * * *`
228
+ reads "every three minutes from one minute past the hour" (was "one
229
+ minutes").
230
+ - A discrete minute under an hour step is no longer dropped: `5 */6 * * *`
231
+ reads "every day at 12:05 a.m., 6:05 a.m., 12:05 p.m., and 6:05 p.m." (previously
232
+ "every six hours", and bounded steps even displayed ":00" times for jobs
233
+ firing at :05).
234
+ - Clock-time enumeration is capped at six times. Beyond the cap, a single
235
+ minute folds into a compact hour phrase and a minute list leads with its
236
+ own clause instead of cross-multiplying into a wall of times
237
+ (`0,30 8-18/2 * * *` reads "at 0 and 30 minutes past the hour, at 8
238
+ a.m., 10 a.m., ..." — six times, not twelve).
239
+
240
+ ## [0.1.0]
241
+
242
+ First non-beta release.
243
+
244
+ ### Added
245
+
246
+ - Idiomatic descriptions for **lists** (`,`), **ranges** (`-`), and **compound**
247
+ patterns that combine multiple non-trivial fields (e.g.
248
+ `at 30 minutes past the hour from 9 a.m. through 5 p.m.`).
249
+ - Trailing day qualifiers for bare frequencies (e.g. `every minute on Monday`,
250
+ `every hour on January 13`).
251
+ - Dual **ESM** and **CommonJS** builds plus a minified **browser** global, an
252
+ `exports` map, and bundled **TypeScript** type definitions (`cronli5.d.ts`).
253
+ - Continuous integration (GitHub Actions) across Node 18/20/22, with a
254
+ coverage gate.
255
+ - Code coverage via **c8** with enforced thresholds (`npm run coverage`).
256
+ - Property-based tests (**fast-check**), smoke tests against the built
257
+ ESM/CJS artifacts, and type tests (**tsd**, `npm run test:types`).
258
+
259
+ ### Changed
260
+
261
+ - Source is now authored as an ES module in `src/` and bundled with esbuild.
262
+ - Date descriptions always use suffixed numeric ordinals (`1st`, `2nd`, ...).
263
+ - Modernized the toolchain: ESLint 9 (flat config), Mocha 11, Chai 4.
264
+ - Enforced explicit ESLint budgets for cyclomatic `complexity`, `max-depth`,
265
+ and `max-params` as regression guards.
266
+
267
+ ### Fixed
268
+
269
+ - Weekday/date/month-only patterns no longer drop their qualifier.
270
+ - A specific minute within an hour range is no longer dropped.
271
+
272
+ ### Security
273
+
274
+ - Resolved all `npm audit` advisories in the (dev-only) toolchain: bumped
275
+ `esbuild`, and pinned patched `diff`/`serialize-javascript` via `overrides`.
276
+ No runtime dependencies are affected.
package/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # The MIT License (MIT)
2
2
 
3
- _Copyright (c) 2016 Andrew Broz_
3
+ _Copyright (c) 2026 Andrew Brož_
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
21
+ THE SOFTWARE.