cronli5 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +471 -383
- package/dist/cronli5.js +471 -383
- package/dist/lang/de.cjs +286 -215
- package/dist/lang/de.js +286 -215
- package/dist/lang/en.cjs +413 -327
- package/dist/lang/en.js +413 -327
- package/dist/lang/es.cjs +303 -265
- package/dist/lang/es.js +303 -265
- package/dist/lang/fi.cjs +311 -266
- package/dist/lang/fi.js +311 -266
- package/dist/lang/zh.cjs +308 -236
- package/dist/lang/zh.js +308 -236
- package/package.json +1 -1
- package/src/core/analyze.ts +12 -12
- package/src/core/cadence.ts +164 -0
- package/src/core/index.ts +3 -1
- package/src/core/normalize.ts +3 -3
- package/src/core/parse.ts +1 -1
- package/src/core/{ir.ts → schedule.ts} +17 -18
- package/src/core/specs.ts +1 -1
- package/src/core/util.ts +3 -165
- package/src/core/validate.ts +1 -1
- package/src/core/weekday.ts +54 -0
- package/src/cronli5.ts +5 -5
- package/src/lang/de/index.ts +329 -219
- package/src/lang/en/dialects.ts +1 -1
- package/src/lang/en/index.ts +521 -372
- package/src/lang/es/index.ts +338 -286
- package/src/lang/es/notes.md +1 -1
- package/src/lang/fi/dialects.ts +1 -1
- package/src/lang/fi/index.ts +365 -299
- package/src/lang/fi/notes.md +23 -8
- package/src/lang/fi/status.json +1 -1
- package/src/lang/zh/index.ts +344 -237
- package/src/types.ts +6 -6
- package/types/core/analyze.d.ts +3 -3
- package/types/core/cadence.d.ts +33 -0
- package/types/core/index.d.ts +3 -1
- package/types/core/normalize.d.ts +1 -1
- package/types/core/parse.d.ts +1 -1
- package/types/core/{ir.d.ts → schedule.d.ts} +11 -16
- package/types/core/specs.d.ts +1 -1
- package/types/core/util.d.ts +1 -30
- package/types/core/weekday.d.ts +10 -0
- package/types/lang/de/index.d.ts +1 -1
- package/types/lang/en/dialects.d.ts +1 -1
- package/types/lang/en/index.d.ts +1 -1
- package/types/lang/es/index.d.ts +1 -1
- package/types/lang/fi/dialects.d.ts +1 -1
- package/types/lang/fi/index.d.ts +1 -1
- package/types/lang/zh/index.d.ts +1 -1
- package/types/types.d.ts +5 -5
package/src/lang/en/index.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
// The English language module: renders an analyzed cron pattern (the
|
|
1
|
+
// The English language module: renders an analyzed cron pattern (the Schedule
|
|
2
2
|
// produced by core `analyze`) as idiomatic English. All words live here;
|
|
3
|
-
// the core stays semantic, and this module's only input is the
|
|
3
|
+
// the core stays semantic, and this module's only input is the Schedule.
|
|
4
4
|
// See docs/i18n-design.md.
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
arithmeticStep, hourListStride, offsetCleanStride,
|
|
8
|
-
segmentsOf, singleValues, stepSegment
|
|
9
|
-
} from '../../core/
|
|
7
|
+
arithmeticStep, hourListStride, offsetCleanStride,
|
|
8
|
+
renderStride as chooseStride, segmentsOf, singleValues, stepSegment
|
|
9
|
+
} from '../../core/cadence.js';
|
|
10
|
+
import {orderWeekdaysForDisplay} from '../../core/weekday.js';
|
|
10
11
|
import {isOpenStep} from '../../core/shapes.js';
|
|
11
12
|
import {maxClockTimes} from '../../core/specs.js';
|
|
12
13
|
import {clockDigits, numeral, pad} from '../../core/format.js';
|
|
13
14
|
import type {Cronli5Options} from '../../types.js';
|
|
14
15
|
import type {
|
|
15
|
-
HourTimesPlan,
|
|
16
|
-
} from '../../core/
|
|
16
|
+
HourTimesPlan, Schedule, Language, NormalizedOptions, PlanNode, Segment
|
|
17
|
+
} from '../../core/schedule.js';
|
|
17
18
|
import {resolveDialect} from './dialects.js';
|
|
18
19
|
|
|
19
20
|
// The plan node of a given kind: the discriminated-union member a renderer
|
|
@@ -143,91 +144,222 @@ function normalizeOptions(options?: Cronli5Options): NormalizedOptions {
|
|
|
143
144
|
};
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
// Render an analyzed cron pattern (the
|
|
147
|
-
function describe(
|
|
147
|
+
// Render an analyzed cron pattern (the Schedule) as English.
|
|
148
|
+
function describe(schedule: Schedule, opts: NormalizedOptions): string {
|
|
149
|
+
// A dense pattern — a seconds cadence stacked on a minutes cadence under an
|
|
150
|
+
// hours cadence — reads coarse-to-fine with the second nested under the
|
|
151
|
+
// minute, leading with the calendar anchor; it preempts the fine-to-coarse
|
|
152
|
+
// run-on the per-plan composer would otherwise produce.
|
|
153
|
+
const dense = denseCadence(schedule, opts);
|
|
154
|
+
|
|
155
|
+
if (dense !== null) {
|
|
156
|
+
return applyYear(dense, schedule, opts);
|
|
157
|
+
}
|
|
158
|
+
|
|
148
159
|
// A finer leading cadence puts each coarser field in the confinement frame,
|
|
149
160
|
// overriding the per-plan juxtaposed-cadence and duration-frame forms.
|
|
150
|
-
const body = confinement(
|
|
161
|
+
const body = confinement(schedule, opts) ??
|
|
162
|
+
render(schedule, schedule.plan, opts);
|
|
151
163
|
|
|
152
164
|
// A day union scopes the whole clause by its month, which leads the
|
|
153
165
|
// description ("in June <time> whenever the day is …"); the time/cadence and
|
|
154
166
|
// the trailing condition are already in `body`.
|
|
155
|
-
const lead = isDayUnion(
|
|
167
|
+
const lead = isDayUnion(schedule, opts) ?
|
|
168
|
+
dayUnionMonthLead(schedule, opts) : '';
|
|
156
169
|
|
|
157
|
-
return applyYear(lead + body,
|
|
170
|
+
return applyYear(lead + body, schedule, opts);
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
// Render one plan node. `composeSeconds` recurses with its `rest` plan.
|
|
161
|
-
function render(
|
|
174
|
+
function render(schedule: Schedule, plan: PlanNode,
|
|
175
|
+
opts: NormalizedOptions): string {
|
|
162
176
|
// The dispatch table keys each renderer to its own plan kind; the lookup
|
|
163
177
|
// by `plan.kind` cannot prove the node matches the renderer's narrowed
|
|
164
178
|
// parameter, so the call is made through a kind-agnostic signature.
|
|
165
179
|
const renderer = renderers[plan.kind] as
|
|
166
|
-
(
|
|
180
|
+
(schedule: Schedule, plan: PlanNode, opts: NormalizedOptions) => string;
|
|
181
|
+
|
|
182
|
+
return renderer(schedule, plan, opts);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Dense multi-cadence restructure. ---
|
|
186
|
+
|
|
187
|
+
// Whether a field's shape is a true cadence — a repeating pattern (step, range,
|
|
188
|
+
// or enumerated list), not a wildcard or a single pinned value. A dense pattern
|
|
189
|
+
// stacks one of these in the second, the minute, and the hour.
|
|
190
|
+
function isCadenceShape(shape: Schedule['shapes'][keyof Schedule['shapes']]):
|
|
191
|
+
boolean {
|
|
192
|
+
return shape === 'step' || shape === 'range' || shape === 'list';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// A dense pattern is a seconds cadence stacked on a minutes cadence under an
|
|
196
|
+
// hours cadence: three independent cadences whose flat fine-to-coarse run-on
|
|
197
|
+
// reads as a robotic list. It is recognized only on the `composeSeconds` plan
|
|
198
|
+
// (a meaningful second over a coarser rest), with all three of second, minute,
|
|
199
|
+
// and hour a cadence, and no day union (which owns its own leading-month
|
|
200
|
+
// structure). The hour may take any cadence shape — a stride, a range window,
|
|
201
|
+
// or a list/range-with-outlier — each rendered in its own existing leaf form
|
|
202
|
+
// inside the restructured frame. A `clockTimes` rest is excluded: there the
|
|
203
|
+
// minute and hour fold into a named clock-time enumeration ("every 15 seconds
|
|
204
|
+
// of 9:00 a.m., 9:25 a.m., …"), a compact form already better than a run-on, so
|
|
205
|
+
// it is left as is. Restricted to the default dialect's voice — the same scope
|
|
206
|
+
// as the confinement frame — so other dialects and the compact `short` form
|
|
207
|
+
// keep their established phrasing.
|
|
208
|
+
function isDenseCadence(schedule: Schedule, opts: NormalizedOptions): boolean {
|
|
209
|
+
if (!opts.style.untilWindow || opts.short ||
|
|
210
|
+
schedule.plan.kind !== 'composeSeconds' ||
|
|
211
|
+
schedule.plan.rest.kind === 'clockTimes' ||
|
|
212
|
+
isDayUnion(schedule, opts)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
167
215
|
|
|
168
|
-
|
|
169
|
-
}
|
|
216
|
+
const {shapes} = schedule;
|
|
170
217
|
|
|
171
|
-
|
|
218
|
+
return isCadenceShape(shapes.second) && isCadenceShape(shapes.minute) &&
|
|
219
|
+
isCadenceShape(shapes.hour);
|
|
220
|
+
}
|
|
172
221
|
|
|
173
|
-
|
|
222
|
+
// The coarse hour cadence as a standalone fragment: a stride reads as its
|
|
223
|
+
// bounded/bare cadence ("every five hours from midnight through 8 p.m.", "every
|
|
224
|
+
// six hours"); a plain range reads as its window ("from 8 a.m. through 6
|
|
225
|
+
// p.m."), the non-continuous form a stepped minute uses inside the range; a
|
|
226
|
+
// list or range-with-outlier reads as its "during the … hours" frame (the same
|
|
227
|
+
// phrasing the confinement form produces, just hoisted into the dense lead).
|
|
228
|
+
function denseHourFragment(schedule: Schedule,
|
|
174
229
|
opts: NormalizedOptions): string {
|
|
175
|
-
|
|
230
|
+
const stride = hourStride(schedule);
|
|
231
|
+
|
|
232
|
+
if (stride) {
|
|
233
|
+
return hourStrideCadence(stride, opts);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (schedule.shapes.hour === 'range') {
|
|
237
|
+
// A plain range hour, whose single range segment carries the window bounds.
|
|
238
|
+
const segment = segmentsOf(schedule, 'hour').find(function range(part) {
|
|
239
|
+
return part.kind === 'range';
|
|
240
|
+
}) as Extract<Segment, {kind: 'range'}>;
|
|
241
|
+
|
|
242
|
+
return rangeWindow({
|
|
243
|
+
continuous: false,
|
|
244
|
+
from: +segment.bounds[0],
|
|
245
|
+
throughMinute: 0,
|
|
246
|
+
to: +segment.bounds[1]
|
|
247
|
+
}, opts);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// A list or range-with-outlier hour ("9-20,22") reads as the discrete
|
|
251
|
+
// "during the <times> hours" frame, the same construction the hour
|
|
252
|
+
// confinement uses for these shapes.
|
|
253
|
+
return 'during the ' +
|
|
254
|
+
hourSegmentTimes(schedule, {minute: 0, second: null}, false, opts) +
|
|
255
|
+
' hours';
|
|
176
256
|
}
|
|
177
257
|
|
|
178
|
-
|
|
258
|
+
// The minute cadence as a standalone fragment, counted past the hour: a step is
|
|
259
|
+
// its stride phrase, a range its "every minute from M through K" lead, and a
|
|
260
|
+
// list its stride-or-enumeration.
|
|
261
|
+
function denseMinuteFragment(schedule: Schedule,
|
|
179
262
|
opts: NormalizedOptions): string {
|
|
180
|
-
|
|
263
|
+
if (schedule.shapes.minute === 'step') {
|
|
264
|
+
return stepCycle60(stepSegment(schedule, 'minute'), 'minute', 'hour', opts);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (schedule.shapes.minute === 'range') {
|
|
268
|
+
return minuteRangeLead(schedule.pattern.minute, opts);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// A minute list has segments; an offset/uneven step the core enumerated to a
|
|
272
|
+
// list reads as a stride when its fires form a progression.
|
|
273
|
+
return strideFromSegments(segmentsOf(schedule, 'minute'), 'minute', 'hour',
|
|
274
|
+
opts) ?? listPastThe(segmentWords(segmentsOf(schedule, 'minute'), opts),
|
|
275
|
+
'minute', 'hour', opts);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Assemble the dense form, or null when the pattern is not dense. The calendar
|
|
279
|
+
// anchor leads ("on the last weekday of the month, …"), the cadences run
|
|
280
|
+
// coarse-to-fine (hour, then minute), and the second nests under the minute
|
|
281
|
+
// ("…, and within each of those minutes, every second …"). Each fragment is
|
|
282
|
+
// today's leaf phrasing, reordered and nested but otherwise unchanged.
|
|
283
|
+
function denseCadence(schedule: Schedule,
|
|
284
|
+
opts: NormalizedOptions): string | null {
|
|
285
|
+
if (!isDenseCadence(schedule, opts)) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const hour = denseHourFragment(schedule, opts);
|
|
290
|
+
const minute = denseMinuteFragment(schedule, opts);
|
|
291
|
+
const second = secondsClause(schedule, 'minute', opts);
|
|
292
|
+
const nested = hour + ', ' + minute +
|
|
293
|
+
', and within each of those minutes, ' + second;
|
|
294
|
+
|
|
295
|
+
// A trailing day qualifier (" on the last weekday of the month") leads the
|
|
296
|
+
// dense form instead; with no anchor the hour cadence leads alone.
|
|
297
|
+
const anchor = trailingQualifier(schedule, opts).trim();
|
|
298
|
+
|
|
299
|
+
return anchor ? anchor + ', ' + nested : nested;
|
|
181
300
|
}
|
|
182
301
|
|
|
183
|
-
|
|
302
|
+
// --- Seconds renderers. ---
|
|
303
|
+
|
|
304
|
+
function renderEverySecond(schedule: Schedule, plan: PlanOf<'everySecond'>,
|
|
184
305
|
opts: NormalizedOptions): string {
|
|
185
|
-
|
|
306
|
+
return 'every second' + trailingQualifier(schedule, opts);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function renderStandaloneSeconds(schedule: Schedule,
|
|
310
|
+
plan: PlanOf<'standaloneSeconds'>, opts: NormalizedOptions): string {
|
|
311
|
+
return secondsLeadClause(schedule, opts) + trailingQualifier(schedule, opts);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function renderSecondPastMinute(schedule: Schedule,
|
|
315
|
+
plan: PlanOf<'secondPastMinute'>, opts: NormalizedOptions): string {
|
|
316
|
+
const secondField = schedule.pattern.second;
|
|
186
317
|
|
|
187
318
|
return getNumber(secondField, opts) + ' ' +
|
|
188
319
|
pluralize(secondField, 'second') +
|
|
189
|
-
' past the minute, every minute' + trailingQualifier(
|
|
320
|
+
' past the minute, every minute' + trailingQualifier(schedule, opts);
|
|
190
321
|
}
|
|
191
322
|
|
|
192
323
|
// A meaningful second combined with a single specific minute (and an open
|
|
193
324
|
// hour). A single second folds into the minute anchor ("30 minutes and 15
|
|
194
325
|
// seconds past the hour, every hour"); a list, range, or step leads with
|
|
195
326
|
// its own clause.
|
|
196
|
-
function renderSecondsWithinMinute(
|
|
197
|
-
opts: NormalizedOptions): string {
|
|
198
|
-
const minuteField =
|
|
327
|
+
function renderSecondsWithinMinute(schedule: Schedule,
|
|
328
|
+
plan: PlanOf<'secondsWithinMinute'>, opts: NormalizedOptions): string {
|
|
329
|
+
const minuteField = schedule.pattern.minute;
|
|
199
330
|
const minuteWord = getNumber(minuteField, opts);
|
|
200
331
|
const minuteUnit = pluralize(minuteField, 'minute');
|
|
201
332
|
|
|
202
333
|
if (plan.singleSecond) {
|
|
203
|
-
const secondField =
|
|
334
|
+
const secondField = schedule.pattern.second;
|
|
204
335
|
|
|
205
336
|
return minuteWord + ' ' + minuteUnit + ' and ' +
|
|
206
337
|
getNumber(secondField, opts) + ' ' + pluralize(secondField, 'second') +
|
|
207
|
-
' past the hour, every hour' + trailingQualifier(
|
|
338
|
+
' past the hour, every hour' + trailingQualifier(schedule, opts);
|
|
208
339
|
}
|
|
209
340
|
|
|
210
|
-
return secondsLeadClause(
|
|
341
|
+
return secondsLeadClause(schedule, opts) + ', ' + minuteWord + ' ' +
|
|
211
342
|
minuteUnit + ' past the hour, every hour' +
|
|
212
|
-
trailingQualifier(
|
|
343
|
+
trailingQualifier(schedule, opts);
|
|
213
344
|
}
|
|
214
345
|
|
|
215
346
|
// The hour-cadence rendering of a compose-seconds plan whose clock-time rest
|
|
216
347
|
// would cross-multiply an hour stride under a single pinned minute, or null
|
|
217
348
|
// when that does not apply (a non-clock rest, a multi-valued minute, or an
|
|
218
349
|
// hour that is not a stride).
|
|
219
|
-
function composeHourCadence(
|
|
350
|
+
function composeHourCadence(schedule: Schedule, plan: PlanOf<'composeSeconds'>,
|
|
220
351
|
opts: NormalizedOptions): string | null {
|
|
221
352
|
const clockRest = plan.rest.kind === 'clockTimes' ||
|
|
222
353
|
plan.rest.kind === 'compactClockTimes';
|
|
223
354
|
|
|
224
|
-
if (!clockRest ||
|
|
355
|
+
if (!clockRest || schedule.shapes.minute !== 'single') {
|
|
225
356
|
return null;
|
|
226
357
|
}
|
|
227
358
|
|
|
228
|
-
const minute = +
|
|
359
|
+
const minute = +schedule.pattern.minute;
|
|
229
360
|
|
|
230
|
-
return hourCadence(
|
|
361
|
+
return hourCadence(schedule, minute, opts) ??
|
|
362
|
+
hourRangeCadence(schedule, minute, opts);
|
|
231
363
|
}
|
|
232
364
|
|
|
233
365
|
// A wildcard or stepped second under a fixed minute across one or more specific
|
|
@@ -243,23 +375,24 @@ function composeHourCadence(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
|
243
375
|
// form, never collapsing to the bare hour (which once repeated it, "9 a.m.,
|
|
244
376
|
// 9 a.m."). A non-zero pinned minute is an unambiguous clock time the compact
|
|
245
377
|
// "of 9:05 a.m." form reads as the minute, never the hour.
|
|
246
|
-
function clockTimesConfinement(
|
|
247
|
-
opts: NormalizedOptions): string {
|
|
248
|
-
if (+rest.times[0].minute === 0 &&
|
|
249
|
-
return secondsLeadClause(
|
|
250
|
-
durationHours(
|
|
378
|
+
function clockTimesConfinement(schedule: Schedule,
|
|
379
|
+
rest: PlanOf<'clockTimes'>, opts: NormalizedOptions): string {
|
|
380
|
+
if (+rest.times[0].minute === 0 && schedule.shapes.minute === 'single') {
|
|
381
|
+
return secondsLeadClause(schedule, opts) + ' for one minute at ' +
|
|
382
|
+
durationHours(schedule, rest, opts);
|
|
251
383
|
}
|
|
252
384
|
|
|
253
|
-
return secondsLeadClause(
|
|
385
|
+
return secondsLeadClause(schedule, opts) + ' of ' +
|
|
386
|
+
clockTimesOf(schedule, rest, opts);
|
|
254
387
|
}
|
|
255
388
|
|
|
256
|
-
function renderComposeSeconds(
|
|
257
|
-
opts: NormalizedOptions): string {
|
|
389
|
+
function renderComposeSeconds(schedule: Schedule,
|
|
390
|
+
plan: PlanOf<'composeSeconds'>, opts: NormalizedOptions): string {
|
|
258
391
|
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
259
392
|
// minute is a cadence, not a wall of clock times: speak the second/minute
|
|
260
393
|
// lead, then the hour cadence ("at 30 seconds past the hour, every two
|
|
261
394
|
// hours"). The clock-time rest would otherwise cross-multiply the hours.
|
|
262
|
-
const cadence = composeHourCadence(
|
|
395
|
+
const cadence = composeHourCadence(schedule, plan, opts);
|
|
263
396
|
|
|
264
397
|
if (cadence !== null) {
|
|
265
398
|
return cadence;
|
|
@@ -268,8 +401,9 @@ function renderComposeSeconds(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
|
268
401
|
// A wildcard or stepped second under a fixed minute across one or more
|
|
269
402
|
// specific hours confines the seconds to the clock time(s).
|
|
270
403
|
if (plan.rest.kind === 'clockTimes' &&
|
|
271
|
-
(
|
|
272
|
-
|
|
404
|
+
(schedule.shapes.second === 'wildcard' ||
|
|
405
|
+
schedule.shapes.second === 'step')) {
|
|
406
|
+
return clockTimesConfinement(schedule, plan.rest, opts);
|
|
273
407
|
}
|
|
274
408
|
|
|
275
409
|
// A wildcard second under a */2 minute step with a wildcard hour binds
|
|
@@ -277,12 +411,12 @@ function renderComposeSeconds(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
|
277
411
|
// the natural English for an interval of 2, and "of" joins the two without
|
|
278
412
|
// the ambiguity of a comma, which reads as two independent cadences.
|
|
279
413
|
// Scoped to */2 only; other step sizes keep the comma form.
|
|
280
|
-
if (
|
|
414
|
+
if (schedule.shapes.second === 'wildcard' &&
|
|
281
415
|
plan.rest.kind === 'minuteFrequency' &&
|
|
282
416
|
plan.rest.hours.kind === 'none' &&
|
|
283
|
-
|
|
417
|
+
schedule.pattern.minute === '*/2') {
|
|
284
418
|
return 'every second of every other minute' +
|
|
285
|
-
trailingQualifier(
|
|
419
|
+
trailingQualifier(schedule, opts);
|
|
286
420
|
}
|
|
287
421
|
|
|
288
422
|
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
@@ -290,22 +424,22 @@ function renderComposeSeconds(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
|
290
424
|
// double it. A wildcard or stepped second is not folded there (no
|
|
291
425
|
// clockSecond), so it still leads its own clause here.
|
|
292
426
|
const restOwnsLead = plan.rest.kind === 'compactClockTimes' &&
|
|
293
|
-
|
|
294
|
-
const lead = restOwnsLead ? '' : secondsLeadClause(
|
|
427
|
+
schedule.analyses.clockSecond;
|
|
428
|
+
const lead = restOwnsLead ? '' : secondsLeadClause(schedule, opts) + ', ';
|
|
295
429
|
|
|
296
|
-
return lead + render(
|
|
430
|
+
return lead + render(schedule, plan.rest, opts);
|
|
297
431
|
}
|
|
298
432
|
|
|
299
433
|
// The bare-hour words for a minute-0 duration confinement, joined and followed
|
|
300
434
|
// by the trailing day qualifier: "9 a.m. and 11 a.m., every day", "midnight,
|
|
301
435
|
// 2 a.m., …, every day". The hour reads as its word (noon/midnight included),
|
|
302
436
|
// never "H:00", since the "for one minute" frame already carries the minute.
|
|
303
|
-
function durationHours(
|
|
437
|
+
function durationHours(schedule: Schedule, plan: PlanOf<'clockTimes'>,
|
|
304
438
|
opts: NormalizedOptions): string {
|
|
305
439
|
const hours = plan.times.map(function clock(time) {
|
|
306
440
|
return getTime({hour: time.hour, minute: 0}, opts);
|
|
307
441
|
});
|
|
308
|
-
const trail = dayQualifier(
|
|
442
|
+
const trail = dayQualifier(schedule, leadingWords, opts);
|
|
309
443
|
|
|
310
444
|
return joinList(hours, opts) + (trail && ', ' + trail);
|
|
311
445
|
}
|
|
@@ -313,7 +447,7 @@ function durationHours(ir: IR, plan: PlanOf<'clockTimes'>,
|
|
|
313
447
|
// The clock times for a non-zero pinned-minute compose-seconds rest, joined
|
|
314
448
|
// and followed by the trailing day qualifier: "9:05 a.m. and 11:05 a.m.,
|
|
315
449
|
// every day". The non-zero minute reads as a clock time, never the hour.
|
|
316
|
-
function clockTimesOf(
|
|
450
|
+
function clockTimesOf(schedule: Schedule, plan: PlanOf<'clockTimes'>,
|
|
317
451
|
opts: NormalizedOptions): string {
|
|
318
452
|
const times = plan.times.map(function clock(time) {
|
|
319
453
|
return getTime({
|
|
@@ -323,7 +457,7 @@ function clockTimesOf(ir: IR, plan: PlanOf<'clockTimes'>,
|
|
|
323
457
|
explicit: true
|
|
324
458
|
}, opts);
|
|
325
459
|
});
|
|
326
|
-
const trail = dayQualifier(
|
|
460
|
+
const trail = dayQualifier(schedule, leadingWords, opts);
|
|
327
461
|
|
|
328
462
|
return joinList(times, opts) + (trail && ', ' + trail);
|
|
329
463
|
}
|
|
@@ -331,8 +465,9 @@ function clockTimesOf(ir: IR, plan: PlanOf<'clockTimes'>,
|
|
|
331
465
|
// The leading clause describing a second field relative to the minute,
|
|
332
466
|
// e.g. "at 5 and 10 seconds past the minute" or "every second from zero
|
|
333
467
|
// through 30 past the minute".
|
|
334
|
-
function secondsLeadClause(
|
|
335
|
-
|
|
468
|
+
function secondsLeadClause(schedule: Schedule,
|
|
469
|
+
opts: NormalizedOptions): string {
|
|
470
|
+
return secondsClause(schedule, 'minute', opts);
|
|
336
471
|
}
|
|
337
472
|
|
|
338
473
|
// The second clause counted against an arbitrary anchor. The anchor is
|
|
@@ -340,10 +475,10 @@ function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
|
340
475
|
// pinned minute 0 into the hour and counts the second "past the hour"
|
|
341
476
|
// instead ("at 30 seconds past the hour", "every second from 0 through 10
|
|
342
477
|
// past the hour"), so the minute-0 confinement is stated, not dropped.
|
|
343
|
-
function secondsClause(
|
|
478
|
+
function secondsClause(schedule: Schedule, anchor: string,
|
|
344
479
|
opts: NormalizedOptions): string {
|
|
345
|
-
const secondField =
|
|
346
|
-
const shape =
|
|
480
|
+
const secondField = schedule.pattern.second;
|
|
481
|
+
const shape = schedule.shapes.second;
|
|
347
482
|
|
|
348
483
|
if (secondField === '*') {
|
|
349
484
|
return 'every second';
|
|
@@ -352,7 +487,7 @@ function secondsClause(ir: IR, anchor: string,
|
|
|
352
487
|
if (shape === 'step') {
|
|
353
488
|
// The plan reached this clause only for a stepped second field, whose
|
|
354
489
|
// first segment is always a step segment.
|
|
355
|
-
return stepCycle60(stepSegment(
|
|
490
|
+
return stepCycle60(stepSegment(schedule, 'second'),
|
|
356
491
|
'second', anchor, opts);
|
|
357
492
|
}
|
|
358
493
|
|
|
@@ -372,63 +507,63 @@ function secondsClause(ir: IR, anchor: string,
|
|
|
372
507
|
// A non-wildcard second under the list/step path always has segments. An
|
|
373
508
|
// offset/uneven step the core enumerated to a fire list reads as a stride
|
|
374
509
|
// cadence when those fires form a long-enough progression.
|
|
375
|
-
return strideFromSegments(segmentsOf(
|
|
376
|
-
opts) ?? listPastThe(segmentWords(segmentsOf(
|
|
510
|
+
return strideFromSegments(segmentsOf(schedule, 'second'), 'second', anchor,
|
|
511
|
+
opts) ?? listPastThe(segmentWords(segmentsOf(schedule, 'second'), opts),
|
|
377
512
|
'second', anchor, opts);
|
|
378
513
|
}
|
|
379
514
|
|
|
380
515
|
// --- Minute renderers. ---
|
|
381
516
|
|
|
382
|
-
function renderEveryMinute(
|
|
517
|
+
function renderEveryMinute(schedule: Schedule, plan: PlanOf<'everyMinute'>,
|
|
383
518
|
opts: NormalizedOptions): string {
|
|
384
|
-
return 'every minute' + trailingQualifier(
|
|
519
|
+
return 'every minute' + trailingQualifier(schedule, opts);
|
|
385
520
|
}
|
|
386
521
|
|
|
387
|
-
function renderSingleMinute(
|
|
522
|
+
function renderSingleMinute(schedule: Schedule, plan: PlanOf<'singleMinute'>,
|
|
388
523
|
opts: NormalizedOptions): string {
|
|
389
|
-
const minuteField =
|
|
524
|
+
const minuteField = schedule.pattern.minute;
|
|
390
525
|
|
|
391
526
|
return getNumber(minuteField, opts) + ' ' +
|
|
392
527
|
pluralize(minuteField, 'minute') +
|
|
393
|
-
' past the hour, every hour' + trailingQualifier(
|
|
528
|
+
' past the hour, every hour' + trailingQualifier(schedule, opts);
|
|
394
529
|
}
|
|
395
530
|
|
|
396
|
-
function renderRangeOfMinutes(
|
|
397
|
-
opts: NormalizedOptions): string {
|
|
398
|
-
return minuteRangeLead(
|
|
399
|
-
trailingQualifier(
|
|
531
|
+
function renderRangeOfMinutes(schedule: Schedule,
|
|
532
|
+
plan: PlanOf<'rangeOfMinutes'>, opts: NormalizedOptions): string {
|
|
533
|
+
return minuteRangeLead(schedule.pattern.minute, opts) +
|
|
534
|
+
trailingQualifier(schedule, opts);
|
|
400
535
|
}
|
|
401
536
|
|
|
402
|
-
function renderMultipleMinutes(
|
|
403
|
-
opts: NormalizedOptions): string {
|
|
537
|
+
function renderMultipleMinutes(schedule: Schedule,
|
|
538
|
+
plan: PlanOf<'multipleMinutes'>, opts: NormalizedOptions): string {
|
|
404
539
|
// A multiple-minutes plan is selected only for a minute list, which has
|
|
405
540
|
// segments. An offset/uneven step the core enumerated to this list reads as
|
|
406
541
|
// a stride cadence when the fires form a long-enough progression.
|
|
407
542
|
const stride =
|
|
408
|
-
strideFromSegments(segmentsOf(
|
|
543
|
+
strideFromSegments(segmentsOf(schedule, 'minute'), 'minute', 'hour', opts);
|
|
409
544
|
|
|
410
|
-
return (stride ?? listPastThe(segmentWords(segmentsOf(
|
|
411
|
-
opts), 'minute', 'hour', opts)) + trailingQualifier(
|
|
545
|
+
return (stride ?? listPastThe(segmentWords(segmentsOf(schedule, 'minute'),
|
|
546
|
+
opts), 'minute', 'hour', opts)) + trailingQualifier(schedule, opts);
|
|
412
547
|
}
|
|
413
548
|
|
|
414
549
|
// A repeating minute step, qualified by the active hour window(s).
|
|
415
|
-
function renderMinuteFrequency(
|
|
416
|
-
opts: NormalizedOptions): string {
|
|
550
|
+
function renderMinuteFrequency(schedule: Schedule,
|
|
551
|
+
plan: PlanOf<'minuteFrequency'>, opts: NormalizedOptions): string {
|
|
417
552
|
// A minute-frequency plan is selected only for a stepped minute field,
|
|
418
553
|
// which has segments.
|
|
419
|
-
let phrase = stepCycle60(stepSegment(
|
|
554
|
+
let phrase = stepCycle60(stepSegment(schedule, 'minute'),
|
|
420
555
|
'minute', 'hour', opts);
|
|
421
556
|
|
|
422
557
|
if (plan.hours.kind === 'during') {
|
|
423
558
|
// A uneven hour stride confines the minute cadence to its own bounded hour
|
|
424
559
|
// cadence ("every 15 minutes, every five hours from midnight through 8
|
|
425
560
|
// p.m."); an irregular hour list still names each hour's window.
|
|
426
|
-
const cadence = unevenHourCadence(
|
|
561
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
427
562
|
|
|
428
563
|
phrase += cadence ?
|
|
429
564
|
', ' + cadence :
|
|
430
565
|
' during the ' +
|
|
431
|
-
hourTimesFromPlan(
|
|
566
|
+
hourTimesFromPlan(schedule, plan.hours.times, false, opts) + ' hours';
|
|
432
567
|
}
|
|
433
568
|
else if (plan.hours.kind === 'window') {
|
|
434
569
|
// A minute-frequency cadence ("every 15 minutes") fills the hours from a
|
|
@@ -447,10 +582,10 @@ function renderMinuteFrequency(ir: IR, plan: PlanOf<'minuteFrequency'>,
|
|
|
447
582
|
// which confines the cadence to every Nth hour; a stepped hour field's
|
|
448
583
|
// first segment is a step segment.
|
|
449
584
|
phrase += ' ' +
|
|
450
|
-
everyNthHour(stepSegment(
|
|
585
|
+
everyNthHour(stepSegment(schedule, 'hour'), opts);
|
|
451
586
|
}
|
|
452
587
|
|
|
453
|
-
return phrase + trailingQualifier(
|
|
588
|
+
return phrase + trailingQualifier(schedule, opts);
|
|
454
589
|
}
|
|
455
590
|
|
|
456
591
|
// A minute wildcard or plain range under a single specific hour fires
|
|
@@ -458,43 +593,43 @@ function renderMinuteFrequency(ir: IR, plan: PlanOf<'minuteFrequency'>,
|
|
|
458
593
|
// whole hour, so it reads as that hour itself ("every minute of the 9 a.m.
|
|
459
594
|
// hour") rather than a synthesized "from H:00 through H:59" range the source
|
|
460
595
|
// never stated; a plain range is a real window and keeps "from … through …".
|
|
461
|
-
function renderMinuteSpanInHour(
|
|
462
|
-
opts: NormalizedOptions): string {
|
|
463
|
-
if (
|
|
596
|
+
function renderMinuteSpanInHour(schedule: Schedule,
|
|
597
|
+
plan: PlanOf<'minuteSpanInHour'>, opts: NormalizedOptions): string {
|
|
598
|
+
if (schedule.pattern.minute === '*') {
|
|
464
599
|
return 'every minute of the ' +
|
|
465
600
|
getTime({hour: plan.hour, minute: 0}, opts) + ' hour' +
|
|
466
|
-
trailingQualifier(
|
|
601
|
+
trailingQualifier(schedule, opts);
|
|
467
602
|
}
|
|
468
603
|
|
|
469
604
|
return 'every minute from ' +
|
|
470
605
|
getTime({hour: plan.hour, minute: plan.span[0]}, opts) +
|
|
471
606
|
through(opts) + getTime({hour: plan.hour, minute: plan.span[1]}, opts) +
|
|
472
|
-
trailingQualifier(
|
|
607
|
+
trailingQualifier(schedule, opts);
|
|
473
608
|
}
|
|
474
609
|
|
|
475
610
|
// A minute window combined with discrete hours fires within that window
|
|
476
611
|
// during each hour.
|
|
477
|
-
function renderMinutesAcrossHours(
|
|
478
|
-
opts: NormalizedOptions): string {
|
|
612
|
+
function renderMinutesAcrossHours(schedule: Schedule,
|
|
613
|
+
plan: PlanOf<'minutesAcrossHours'>, opts: NormalizedOptions): string {
|
|
479
614
|
// A uneven hour stride reads as a cadence, not a wall of hour columns: the
|
|
480
615
|
// minute lead, then "every N hours from X through Y".
|
|
481
|
-
const cadence = unevenHourCadence(
|
|
616
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
482
617
|
|
|
483
618
|
if (plan.form === 'wildcard') {
|
|
484
619
|
if (cadence !== null) {
|
|
485
|
-
return 'every minute, ' + cadence + trailingQualifier(
|
|
620
|
+
return 'every minute, ' + cadence + trailingQualifier(schedule, opts);
|
|
486
621
|
}
|
|
487
622
|
|
|
488
623
|
return 'every minute during the ' +
|
|
489
|
-
hourTimesFromPlan(
|
|
490
|
-
trailingQualifier(
|
|
624
|
+
hourTimesFromPlan(schedule, plan.times, false, opts) + ' hours' +
|
|
625
|
+
trailingQualifier(schedule, opts);
|
|
491
626
|
}
|
|
492
627
|
|
|
493
628
|
if (plan.form === 'range') {
|
|
494
|
-
const lead = minuteRangeLead(
|
|
629
|
+
const lead = minuteRangeLead(schedule.pattern.minute, opts);
|
|
495
630
|
|
|
496
631
|
if (cadence !== null) {
|
|
497
|
-
return lead + ', ' + cadence + trailingQualifier(
|
|
632
|
+
return lead + ', ' + cadence + trailingQualifier(schedule, opts);
|
|
498
633
|
}
|
|
499
634
|
|
|
500
635
|
// A plain minute range is a cadence, so an hour list confines it with the
|
|
@@ -505,30 +640,31 @@ function renderMinutesAcrossHours(ir: IR, plan: PlanOf<'minutesAcrossHours'>,
|
|
|
505
640
|
// hour, at 9 a.m."), never the plural "hours" confinement.
|
|
506
641
|
if (singleHourFire(plan.times)) {
|
|
507
642
|
return lead + ', at ' +
|
|
508
|
-
hourTimesFromPlan(
|
|
509
|
-
trailingQualifier(
|
|
643
|
+
hourTimesFromPlan(schedule, plan.times, true, opts) +
|
|
644
|
+
trailingQualifier(schedule, opts);
|
|
510
645
|
}
|
|
511
646
|
|
|
512
647
|
return lead + ' during the ' +
|
|
513
|
-
hourTimesFromPlan(
|
|
514
|
-
trailingQualifier(
|
|
648
|
+
hourTimesFromPlan(schedule, plan.times, false, opts) + ' hours' +
|
|
649
|
+
trailingQualifier(schedule, opts);
|
|
515
650
|
}
|
|
516
651
|
|
|
517
652
|
// The 'list' form is a minute list, which has segments; an offset/uneven
|
|
518
653
|
// step enumerated to that list reads as a stride. A list is a set of
|
|
519
654
|
// discrete fire minutes, not a cadence, so it keeps the "at <times>" frame.
|
|
520
655
|
const lead =
|
|
521
|
-
strideFromSegments(segmentsOf(
|
|
522
|
-
|
|
656
|
+
strideFromSegments(segmentsOf(schedule, 'minute'), 'minute', 'hour',
|
|
657
|
+
opts) ??
|
|
658
|
+
listPastThe(segmentWords(segmentsOf(schedule, 'minute'), opts),
|
|
523
659
|
'minute', 'hour', opts);
|
|
524
660
|
|
|
525
661
|
if (cadence !== null) {
|
|
526
|
-
return lead + ', ' + cadence + trailingQualifier(
|
|
662
|
+
return lead + ', ' + cadence + trailingQualifier(schedule, opts);
|
|
527
663
|
}
|
|
528
664
|
|
|
529
|
-
const times = hourTimesFromPlan(
|
|
665
|
+
const times = hourTimesFromPlan(schedule, plan.times, true, opts);
|
|
530
666
|
|
|
531
|
-
return lead + ', at ' + times + trailingQualifier(
|
|
667
|
+
return lead + ', at ' + times + trailingQualifier(schedule, opts);
|
|
532
668
|
}
|
|
533
669
|
|
|
534
670
|
// Spelled ordinals for "during every Nth hour" — the clean hour-step
|
|
@@ -551,34 +687,35 @@ function everyNthHour(segment: StepSegment, opts: NormalizedOptions): string {
|
|
|
551
687
|
// A minute wildcard or plain range under an hour step. A wildcard minute (a
|
|
552
688
|
// cadence) is reached only for a clean step and is confined to every Nth hour;
|
|
553
689
|
// a plain range is a per-hour window whose recurrence trails as its own clause.
|
|
554
|
-
function renderMinuteSpanAcrossHourStep(
|
|
690
|
+
function renderMinuteSpanAcrossHourStep(schedule: Schedule,
|
|
555
691
|
plan: PlanOf<'minuteSpanAcrossHourStep'>, opts: NormalizedOptions): string {
|
|
556
692
|
// This plan is reached only under a stepped hour field, whose first
|
|
557
693
|
// segment is a step segment.
|
|
558
|
-
const segment = stepSegment(
|
|
694
|
+
const segment = stepSegment(schedule, 'hour');
|
|
559
695
|
|
|
560
696
|
// A wildcard minute over a stepped hour is reached only for a clean stride
|
|
561
697
|
// (a bounded or uneven step routes through minutesAcrossHours instead), so it
|
|
562
698
|
// confines to every Nth hour without a bounded-cadence case here.
|
|
563
699
|
if (plan.form === 'wildcard') {
|
|
564
700
|
return 'every minute ' + everyNthHour(segment, opts) +
|
|
565
|
-
trailingQualifier(
|
|
701
|
+
trailingQualifier(schedule, opts);
|
|
566
702
|
}
|
|
567
703
|
|
|
568
704
|
// A minute list keeps the same cadence clause; only its lead differs. An
|
|
569
705
|
// offset/uneven step the core enumerated to that list reads as a stride.
|
|
570
706
|
const lead = plan.form === 'list' ?
|
|
571
|
-
strideFromSegments(segmentsOf(
|
|
572
|
-
|
|
707
|
+
strideFromSegments(segmentsOf(schedule, 'minute'), 'minute', 'hour',
|
|
708
|
+
opts) ??
|
|
709
|
+
listPastThe(segmentWords(segmentsOf(schedule, 'minute'), opts),
|
|
573
710
|
'minute', 'hour', opts) :
|
|
574
|
-
minuteRangeLead(
|
|
711
|
+
minuteRangeLead(schedule.pattern.minute, opts);
|
|
575
712
|
// A bounded or uneven hour step reads as its endpoint-pinning cadence after
|
|
576
713
|
// the minute lead, not a wall of clock-time columns; an offset-clean step
|
|
577
714
|
// keeps its existing per-step phrasing.
|
|
578
|
-
const cadence = unevenHourCadence(
|
|
715
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
579
716
|
|
|
580
717
|
return lead + ', ' +
|
|
581
|
-
(cadence ?? stepHours(segment, opts)) + trailingQualifier(
|
|
718
|
+
(cadence ?? stepHours(segment, opts)) + trailingQualifier(schedule, opts);
|
|
582
719
|
}
|
|
583
720
|
|
|
584
721
|
// Lead phrase for a plain minute range: "every minute from <a> through <b>
|
|
@@ -594,60 +731,60 @@ function minuteRangeLead(minuteField: string,
|
|
|
594
731
|
|
|
595
732
|
// --- Hour renderers. ---
|
|
596
733
|
|
|
597
|
-
function renderEveryHour(
|
|
734
|
+
function renderEveryHour(schedule: Schedule, plan: PlanOf<'everyHour'>,
|
|
598
735
|
opts: NormalizedOptions): string {
|
|
599
|
-
return 'every hour' + trailingQualifier(
|
|
736
|
+
return 'every hour' + trailingQualifier(schedule, opts);
|
|
600
737
|
}
|
|
601
738
|
|
|
602
739
|
// An hour range fires within a window: on the hour it reads "every hour
|
|
603
740
|
// from 9 a.m. through 5 p.m."; a minute wildcard or range fires every
|
|
604
741
|
// minute; a discrete minute anchors as a lead clause.
|
|
605
|
-
function renderHourRange(
|
|
742
|
+
function renderHourRange(schedule: Schedule, plan: PlanOf<'hourRange'>,
|
|
606
743
|
opts: NormalizedOptions): string {
|
|
607
744
|
const window = hourWindow(boundedWindow(plan), opts);
|
|
608
745
|
|
|
609
746
|
if (plan.minuteForm === 'wildcard') {
|
|
610
|
-
return 'every minute ' + window + trailingQualifier(
|
|
747
|
+
return 'every minute ' + window + trailingQualifier(schedule, opts);
|
|
611
748
|
}
|
|
612
749
|
|
|
613
750
|
if (plan.minuteForm === 'range') {
|
|
614
|
-
return minuteRangeLead(
|
|
615
|
-
trailingQualifier(
|
|
751
|
+
return minuteRangeLead(schedule.pattern.minute, opts) + ', ' + window +
|
|
752
|
+
trailingQualifier(schedule, opts);
|
|
616
753
|
}
|
|
617
754
|
|
|
618
|
-
return rangeMinuteLead(
|
|
619
|
-
trailingQualifier(
|
|
755
|
+
return rangeMinuteLead(schedule, opts) + ' ' + window +
|
|
756
|
+
trailingQualifier(schedule, opts);
|
|
620
757
|
}
|
|
621
758
|
|
|
622
759
|
// Lead phrase for a discrete minute within an hour range: on-the-hour
|
|
623
760
|
// reads "every hour"; otherwise the minute list anchors it.
|
|
624
|
-
function rangeMinuteLead(
|
|
625
|
-
if (
|
|
761
|
+
function rangeMinuteLead(schedule: Schedule, opts: NormalizedOptions): string {
|
|
762
|
+
if (schedule.pattern.minute === '0') {
|
|
626
763
|
return 'every hour';
|
|
627
764
|
}
|
|
628
765
|
|
|
629
766
|
// A non-"0" minute here is a discrete list, which has segments; an
|
|
630
767
|
// offset/uneven step enumerated to that list reads as a stride.
|
|
631
|
-
return strideFromSegments(segmentsOf(
|
|
632
|
-
opts) ?? listPastThe(segmentWords(segmentsOf(
|
|
768
|
+
return strideFromSegments(segmentsOf(schedule, 'minute'), 'minute', 'hour',
|
|
769
|
+
opts) ?? listPastThe(segmentWords(segmentsOf(schedule, 'minute'), opts),
|
|
633
770
|
'minute', 'hour', opts);
|
|
634
771
|
}
|
|
635
772
|
|
|
636
|
-
function renderHourStep(
|
|
773
|
+
function renderHourStep(schedule: Schedule, plan: PlanOf<'hourStep'>,
|
|
637
774
|
opts: NormalizedOptions): string {
|
|
638
775
|
// A bounded or uneven hour step reads as its endpoint-pinning cadence ("every
|
|
639
776
|
// two hours from 9 a.m. through 5 p.m."), the same form the compound paths
|
|
640
777
|
// speak; an offset-clean step keeps its bare or "from M" cadence.
|
|
641
|
-
const cadence = unevenHourCadence(
|
|
778
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
642
779
|
|
|
643
780
|
if (cadence !== null) {
|
|
644
|
-
return cadence + trailingQualifier(
|
|
781
|
+
return cadence + trailingQualifier(schedule, opts);
|
|
645
782
|
}
|
|
646
783
|
|
|
647
784
|
// An hour-step plan is selected only for a stepped hour field, whose
|
|
648
785
|
// first segment is a step segment.
|
|
649
|
-
return stepHours(stepSegment(
|
|
650
|
-
trailingQualifier(
|
|
786
|
+
return stepHours(stepSegment(schedule, 'hour'), opts) +
|
|
787
|
+
trailingQualifier(schedule, opts);
|
|
651
788
|
}
|
|
652
789
|
|
|
653
790
|
// The hour-range plan as a window. The close lands on the top of the final
|
|
@@ -714,15 +851,15 @@ function hourWindow(
|
|
|
714
851
|
|
|
715
852
|
// Expand a discrete set of hours and minutes into clock times prefixed by
|
|
716
853
|
// a day-level qualifier, e.g. "every day at 9 a.m. and 9:30 a.m.".
|
|
717
|
-
function renderClockTimes(
|
|
854
|
+
function renderClockTimes(schedule: Schedule, plan: PlanOf<'clockTimes'>,
|
|
718
855
|
opts: NormalizedOptions): string {
|
|
719
856
|
// An hour step or range (or arithmetic-progression hour list) under a
|
|
720
857
|
// single pinned minute reads as a cadence or window rather than a
|
|
721
858
|
// cross-product of clock times.
|
|
722
|
-
if (
|
|
723
|
-
const minute = +
|
|
724
|
-
const cadence = hourCadence(
|
|
725
|
-
hourRangeCadence(
|
|
859
|
+
if (schedule.shapes.minute === 'single') {
|
|
860
|
+
const minute = +schedule.pattern.minute;
|
|
861
|
+
const cadence = hourCadence(schedule, minute, opts) ??
|
|
862
|
+
hourRangeCadence(schedule, minute, opts);
|
|
726
863
|
|
|
727
864
|
if (cadence !== null) {
|
|
728
865
|
return cadence;
|
|
@@ -739,28 +876,28 @@ function renderClockTimes(ir: IR, plan: PlanOf<'clockTimes'>,
|
|
|
739
876
|
}, opts);
|
|
740
877
|
});
|
|
741
878
|
|
|
742
|
-
return interpretDayQualifier(
|
|
743
|
-
dayUnionTrail(
|
|
879
|
+
return interpretDayQualifier(schedule, opts) + 'at ' + joinList(times, opts) +
|
|
880
|
+
dayUnionTrail(schedule, opts);
|
|
744
881
|
}
|
|
745
882
|
|
|
746
883
|
// The trailing day-union condition for a clock-time form (which leads with its
|
|
747
884
|
// time, not a day qualifier), or an empty string when the pattern is not a day
|
|
748
885
|
// union. The cadence renderers carry this through `trailingQualifier` instead.
|
|
749
|
-
function dayUnionTrail(
|
|
750
|
-
return isDayUnion(
|
|
886
|
+
function dayUnionTrail(schedule: Schedule, opts: NormalizedOptions): string {
|
|
887
|
+
return isDayUnion(schedule, opts) ? dayUnionCondition(schedule, opts) : '';
|
|
751
888
|
}
|
|
752
889
|
|
|
753
890
|
// Compact form for a clock-time set past the enumeration cap. A single
|
|
754
891
|
// minute folds into per-segment hour windows; a minute list leads with its
|
|
755
892
|
// own clause instead of cross-multiplying into a wall of times.
|
|
756
|
-
function renderCompactClockTimes(
|
|
757
|
-
opts: NormalizedOptions): string {
|
|
893
|
+
function renderCompactClockTimes(schedule: Schedule,
|
|
894
|
+
plan: PlanOf<'compactClockTimes'>, opts: NormalizedOptions): string {
|
|
758
895
|
if (plan.fold) {
|
|
759
896
|
// An hour step or range (or arithmetic-progression hour list) under the
|
|
760
897
|
// single pinned minute reads as a cadence or window, not a wall of clock
|
|
761
898
|
// times. (Returns null for an irregular list, which keeps folding below.)
|
|
762
|
-
const cadence = hourCadence(
|
|
763
|
-
hourRangeCadence(
|
|
899
|
+
const cadence = hourCadence(schedule, +plan.minute, opts) ??
|
|
900
|
+
hourRangeCadence(schedule, +plan.minute, opts);
|
|
764
901
|
|
|
765
902
|
if (cadence !== null) {
|
|
766
903
|
return cadence;
|
|
@@ -768,41 +905,45 @@ function renderCompactClockTimes(ir: IR, plan: PlanOf<'compactClockTimes'>,
|
|
|
768
905
|
|
|
769
906
|
// A compact clock-time plan is reached only for discrete hours, which
|
|
770
907
|
// have segments.
|
|
771
|
-
const hasRange = segmentsOf(
|
|
908
|
+
const hasRange = segmentsOf(schedule, 'hour').some(function range(segment) {
|
|
772
909
|
return segment.kind === 'range';
|
|
773
910
|
});
|
|
774
911
|
|
|
775
912
|
// A contiguous hour range reads with the hour-range frame ("every
|
|
776
913
|
// hour from X through Y"), not a clock-time span ("at X through Y").
|
|
777
|
-
if (hasRange && !
|
|
778
|
-
return foldedHourWindows(
|
|
914
|
+
if (hasRange && !schedule.analyses.clockSecond) {
|
|
915
|
+
return foldedHourWindows(schedule, plan, opts) +
|
|
916
|
+
trailingQualifier(schedule, opts);
|
|
779
917
|
}
|
|
780
918
|
|
|
781
|
-
const fold = {minute: plan.minute, second:
|
|
919
|
+
const fold = {minute: plan.minute, second: schedule.analyses.clockSecond};
|
|
782
920
|
|
|
783
|
-
return interpretDayQualifier(
|
|
784
|
-
hourSegmentTimes(
|
|
921
|
+
return interpretDayQualifier(schedule, opts) + 'at ' +
|
|
922
|
+
hourSegmentTimes(schedule, fold, true, opts) +
|
|
923
|
+
dayUnionTrail(schedule, opts);
|
|
785
924
|
}
|
|
786
925
|
|
|
787
926
|
const minuteLead =
|
|
788
927
|
// The non-fold branch is a minute list, which has segments. An
|
|
789
928
|
// offset/uneven step enumerated to that list reads as a stride.
|
|
790
|
-
strideFromSegments(segmentsOf(
|
|
791
|
-
|
|
929
|
+
strideFromSegments(segmentsOf(schedule, 'minute'), 'minute', 'hour',
|
|
930
|
+
opts) ??
|
|
931
|
+
listPastThe(segmentWords(segmentsOf(schedule, 'minute'), opts),
|
|
792
932
|
'minute', 'hour', opts);
|
|
793
933
|
// A uneven hour stride reads as a cadence after the minute lead, not a wall
|
|
794
934
|
// of clock-time columns.
|
|
795
|
-
const cadence = unevenHourCadence(
|
|
935
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
796
936
|
const phrase = cadence ?
|
|
797
|
-
minuteLead + ', ' + cadence + trailingQualifier(
|
|
937
|
+
minuteLead + ', ' + cadence + trailingQualifier(schedule, opts) :
|
|
798
938
|
minuteLead +
|
|
799
|
-
', at ' +
|
|
800
|
-
|
|
939
|
+
', at ' +
|
|
940
|
+
hourSegmentTimes(schedule, {minute: 0, second: null}, true, opts) +
|
|
941
|
+
trailingQualifier(schedule, opts);
|
|
801
942
|
|
|
802
943
|
// A single non-zero second cannot fold into the per-minute clause, so it
|
|
803
944
|
// leads with its own.
|
|
804
|
-
return
|
|
805
|
-
secondsLeadClause(
|
|
945
|
+
return schedule.analyses.clockSecond ?
|
|
946
|
+
secondsLeadClause(schedule, opts) + ', ' + phrase :
|
|
806
947
|
phrase;
|
|
807
948
|
}
|
|
808
949
|
|
|
@@ -810,18 +951,18 @@ function renderCompactClockTimes(ir: IR, plan: PlanOf<'compactClockTimes'>,
|
|
|
810
951
|
// hour-range frame: a shared minute lead ("every hour" / "at 30 minutes
|
|
811
952
|
// past the hour"), each range as a window, and any non-contiguous hour
|
|
812
953
|
// appended by `outlierTail` ("and at Z").
|
|
813
|
-
function foldedHourWindows(
|
|
814
|
-
opts: NormalizedOptions): string {
|
|
954
|
+
function foldedHourWindows(schedule: Schedule,
|
|
955
|
+
plan: PlanOf<'compactClockTimes'>, opts: NormalizedOptions): string {
|
|
815
956
|
const minute = plan.minute;
|
|
816
957
|
const windows: string[] = [];
|
|
817
|
-
const times = collectHourOutliers(
|
|
958
|
+
const times = collectHourOutliers(schedule).map(function time(hour) {
|
|
818
959
|
return getTime({hour, minute}, opts);
|
|
819
960
|
});
|
|
820
961
|
|
|
821
962
|
// Reached only via the fold branch under discrete hours, which have segments.
|
|
822
963
|
// A folded minute is a discrete pin/list, never a wildcard, so the run is not
|
|
823
964
|
// continuous to the top of the next hour: the window is not an until-window.
|
|
824
|
-
segmentsOf(
|
|
965
|
+
segmentsOf(schedule, 'hour').forEach(function classify(segment) {
|
|
825
966
|
if (segment.kind === 'range') {
|
|
826
967
|
windows.push(rangeWindow({
|
|
827
968
|
continuous: false,
|
|
@@ -832,18 +973,19 @@ function foldedHourWindows(ir: IR, plan: PlanOf<'compactClockTimes'>,
|
|
|
832
973
|
}
|
|
833
974
|
});
|
|
834
975
|
|
|
835
|
-
const phrase = rangeMinuteLead(
|
|
976
|
+
const phrase = rangeMinuteLead(schedule, opts) + ' ' +
|
|
977
|
+
joinList(windows, opts);
|
|
836
978
|
|
|
837
979
|
return phrase + outlierTail(times, opts);
|
|
838
980
|
}
|
|
839
981
|
|
|
840
982
|
// The hours outside a contiguous run — every non-range segment's values, with
|
|
841
983
|
// a step contributing its whole fire set.
|
|
842
|
-
function collectHourOutliers(
|
|
984
|
+
function collectHourOutliers(schedule: Schedule): number[] {
|
|
843
985
|
const hours: number[] = [];
|
|
844
986
|
|
|
845
987
|
// Reached only under discrete hours, which carry segments.
|
|
846
|
-
segmentsOf(
|
|
988
|
+
segmentsOf(schedule, 'hour').forEach(function classify(segment) {
|
|
847
989
|
if (segment.kind === 'step') {
|
|
848
990
|
hours.push(...segment.fires);
|
|
849
991
|
}
|
|
@@ -894,19 +1036,19 @@ function isCadenceField(token: string): boolean {
|
|
|
894
1036
|
// the pattern has no cadence lead (the finest restricted field is a clock-point
|
|
895
1037
|
// single/range/list). The seconds lead when restricted as a cadence; otherwise
|
|
896
1038
|
// the minute leads when the second is a plain :00 and the minute is a cadence.
|
|
897
|
-
function leadingCadence(
|
|
1039
|
+
function leadingCadence(schedule: Schedule, opts: NormalizedOptions):
|
|
898
1040
|
{text: string; secondLead: boolean} | null {
|
|
899
|
-
const {second, minute} =
|
|
1041
|
+
const {second, minute} = schedule.pattern;
|
|
900
1042
|
|
|
901
1043
|
if (isCadenceField(second)) {
|
|
902
|
-
return {secondLead: true, text: secondsClause(
|
|
1044
|
+
return {secondLead: true, text: secondsClause(schedule, 'minute', opts)};
|
|
903
1045
|
}
|
|
904
1046
|
|
|
905
1047
|
if (second === '0' && isCadenceField(minute)) {
|
|
906
1048
|
const text = minute === '*' ?
|
|
907
1049
|
'every minute' :
|
|
908
1050
|
// A clean minute step's first segment is a step segment.
|
|
909
|
-
stepCycle60(stepSegment(
|
|
1051
|
+
stepCycle60(stepSegment(schedule, 'minute'),
|
|
910
1052
|
'minute', 'hour', opts);
|
|
911
1053
|
|
|
912
1054
|
return {secondLead: false, text};
|
|
@@ -919,8 +1061,9 @@ function leadingCadence(ir: IR, opts: NormalizedOptions):
|
|
|
919
1061
|
// confinement: "during minute :NN", "during minutes :NN through :MM", "during
|
|
920
1062
|
// minutes :NN and :MM". A clean minute step reads "of every other minute". A
|
|
921
1063
|
// wildcard minute is redundant under the seconds cadence and drops (empty).
|
|
922
|
-
function minuteConfinement(
|
|
923
|
-
|
|
1064
|
+
function minuteConfinement(schedule: Schedule,
|
|
1065
|
+
opts: NormalizedOptions): string {
|
|
1066
|
+
const minute = schedule.pattern.minute;
|
|
924
1067
|
|
|
925
1068
|
if (minute === '*') {
|
|
926
1069
|
return '';
|
|
@@ -935,13 +1078,13 @@ function minuteConfinement(ir: IR, opts: NormalizedOptions): string {
|
|
|
935
1078
|
// A minute single/range/list under the seconds lead. The minute reads as a
|
|
936
1079
|
// ":NN" clock-minute confinement, never "N minutes past the hour" (that is
|
|
937
1080
|
// the minute-lead clock-point form).
|
|
938
|
-
const segments = segmentsOf(
|
|
1081
|
+
const segments = segmentsOf(schedule, 'minute');
|
|
939
1082
|
|
|
940
|
-
if (
|
|
1083
|
+
if (schedule.shapes.minute === 'single') {
|
|
941
1084
|
return ' during minute :' + pad(minute);
|
|
942
1085
|
}
|
|
943
1086
|
|
|
944
|
-
if (
|
|
1087
|
+
if (schedule.shapes.minute === 'range') {
|
|
945
1088
|
const bounds = minute.split('-');
|
|
946
1089
|
|
|
947
1090
|
return ' during minutes :' + pad(bounds[0]) + through(opts) + ':' +
|
|
@@ -962,15 +1105,15 @@ function minuteConfinement(ir: IR, opts: NormalizedOptions): string {
|
|
|
962
1105
|
// ("of the midnight hour"). A clean hour step is "of every other hour"; a range
|
|
963
1106
|
// reuses the until-window; a list or stepped range reads "during the … hours".
|
|
964
1107
|
// A wildcard hour drops (empty).
|
|
965
|
-
function hourConfinement(
|
|
966
|
-
const hour =
|
|
1108
|
+
function hourConfinement(schedule: Schedule, opts: NormalizedOptions): string {
|
|
1109
|
+
const hour = schedule.pattern.hour;
|
|
967
1110
|
|
|
968
1111
|
if (hour === '*') {
|
|
969
1112
|
// A pinned minute confinement ("during minute :00") repeats across every
|
|
970
1113
|
// hour, so the hour is named as the unit of recurrence; a stepped minute
|
|
971
1114
|
// ("of every other minute") or absent minute already implies all hours.
|
|
972
|
-
const minutePinned =
|
|
973
|
-
!isCadenceField(
|
|
1115
|
+
const minutePinned = schedule.pattern.minute !== '*' &&
|
|
1116
|
+
!isCadenceField(schedule.pattern.minute);
|
|
974
1117
|
|
|
975
1118
|
return minutePinned ? ' of every hour' : '';
|
|
976
1119
|
}
|
|
@@ -979,10 +1122,10 @@ function hourConfinement(ir: IR, opts: NormalizedOptions): string {
|
|
|
979
1122
|
return hour === '*/2' ? ' of every other hour' : '';
|
|
980
1123
|
}
|
|
981
1124
|
|
|
982
|
-
if (
|
|
1125
|
+
if (schedule.shapes.hour === 'single') {
|
|
983
1126
|
const h = +hour;
|
|
984
1127
|
|
|
985
|
-
if (
|
|
1128
|
+
if (schedule.shapes.minute === 'step') {
|
|
986
1129
|
return ' from ' + getTime({hour: h, minute: 0}, opts) + ' until ' +
|
|
987
1130
|
getTime({hour: (h + 1) % 24, minute: 0}, opts);
|
|
988
1131
|
}
|
|
@@ -990,21 +1133,22 @@ function hourConfinement(ir: IR, opts: NormalizedOptions): string {
|
|
|
990
1133
|
// A pinned minute confinement already named the minute, so the hour reads
|
|
991
1134
|
// as a plain clock point; a wildcard or absent minute makes the hour the
|
|
992
1135
|
// unit of recurrence ("of the midnight hour").
|
|
993
|
-
if (
|
|
1136
|
+
if (schedule.pattern.minute !== '*' &&
|
|
1137
|
+
!isCadenceField(schedule.pattern.minute)) {
|
|
994
1138
|
return ' at ' + getTime({hour: h, minute: 0}, opts);
|
|
995
1139
|
}
|
|
996
1140
|
|
|
997
1141
|
return ' of the ' + getTime({hour: h, minute: 0}, opts) + ' hour';
|
|
998
1142
|
}
|
|
999
1143
|
|
|
1000
|
-
if (
|
|
1144
|
+
if (schedule.shapes.hour === 'range') {
|
|
1001
1145
|
const bounds = hour.split('-');
|
|
1002
1146
|
|
|
1003
1147
|
// The until-window holds only when the run is continuous to the top of the
|
|
1004
1148
|
// next hour — a wildcard minute fills every minute of the final hour; a
|
|
1005
1149
|
// confined minute (":00", a step) stops within it, reading "through".
|
|
1006
1150
|
return ' ' + rangeWindow({
|
|
1007
|
-
continuous:
|
|
1151
|
+
continuous: schedule.pattern.minute === '*',
|
|
1008
1152
|
from: +bounds[0],
|
|
1009
1153
|
throughMinute: 0,
|
|
1010
1154
|
to: +bounds[1]
|
|
@@ -1013,16 +1157,8 @@ function hourConfinement(ir: IR, opts: NormalizedOptions): string {
|
|
|
1013
1157
|
|
|
1014
1158
|
// An hour list or stepped range reads "during the <times> hours".
|
|
1015
1159
|
return ' during the ' +
|
|
1016
|
-
hourSegmentTimes(
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
// Whether the hour field reads as a contiguous window — a real range whose
|
|
1020
|
-
// close depends on the finer field's last fire. A finer STEP cadence does not
|
|
1021
|
-
// fill the closing hour ("from 9 a.m. until 5:45 p.m."), so that window is left
|
|
1022
|
-
// to the existing windowing renderer rather than the confinement frame, which
|
|
1023
|
-
// closes on the top of the next hour ("until 6 p.m.").
|
|
1024
|
-
function isContiguousHourRange(ir: IR): boolean {
|
|
1025
|
-
return ir.shapes.hour === 'range';
|
|
1160
|
+
hourSegmentTimes(schedule, {minute: 0, second: null}, false, opts) +
|
|
1161
|
+
' hours';
|
|
1026
1162
|
}
|
|
1027
1163
|
|
|
1028
1164
|
// Whether an hour field is confinement-eligible. An OPEN hour stride — a clean
|
|
@@ -1031,26 +1167,27 @@ function isContiguousHourRange(ir: IR): boolean {
|
|
|
1031
1167
|
// idiom ("of every other hour"), so other open steps defer. A BOUNDED stepped
|
|
1032
1168
|
// range (`a-b/n`, e.g. `9-17/2`) is a discrete set of named hours the
|
|
1033
1169
|
// confinement frame speaks as a list ("during the 9 a.m., 11 a.m., … hours").
|
|
1034
|
-
function confinableHour(
|
|
1035
|
-
if (
|
|
1170
|
+
function confinableHour(schedule: Schedule): boolean {
|
|
1171
|
+
if (schedule.shapes.hour !== 'step') {
|
|
1036
1172
|
return true;
|
|
1037
1173
|
}
|
|
1038
1174
|
|
|
1039
1175
|
// Reached only under a stepped hour, whose first segment is a step segment.
|
|
1040
|
-
const segment = stepSegment(
|
|
1176
|
+
const segment = stepSegment(schedule, 'hour');
|
|
1041
1177
|
|
|
1042
|
-
return
|
|
1178
|
+
return schedule.pattern.hour === '*/2' ||
|
|
1179
|
+
segment.startToken.indexOf('-') !== -1;
|
|
1043
1180
|
}
|
|
1044
1181
|
|
|
1045
1182
|
// Whether a minute list is really a stride the existing renderer speaks as a
|
|
1046
1183
|
// cadence ("every two minutes from 3 through 59"): such a progression is not a
|
|
1047
1184
|
// short explicit ":NN" confinement, so it defers.
|
|
1048
|
-
function isMinuteStride(
|
|
1049
|
-
if (
|
|
1185
|
+
function isMinuteStride(schedule: Schedule): boolean {
|
|
1186
|
+
if (schedule.shapes.minute !== 'list') {
|
|
1050
1187
|
return false;
|
|
1051
1188
|
}
|
|
1052
1189
|
|
|
1053
|
-
const values = singleValues(segmentsOf(
|
|
1190
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
1054
1191
|
|
|
1055
1192
|
return values !== null && arithmeticStep(values) !== null;
|
|
1056
1193
|
}
|
|
@@ -1059,13 +1196,13 @@ function isMinuteStride(ir: IR): boolean {
|
|
|
1059
1196
|
// frame covers a finer leading cadence (seconds, or minute under a :00 second)
|
|
1060
1197
|
// with each coarser field as a confinement; shapes outside it defer to the
|
|
1061
1198
|
// existing renderers, which already produce that phrasing for them.
|
|
1062
|
-
function confinementEligible(
|
|
1199
|
+
function confinementEligible(schedule: Schedule,
|
|
1063
1200
|
lead: {secondLead: boolean}): boolean {
|
|
1064
|
-
const {minute, hour} =
|
|
1201
|
+
const {minute, hour} = schedule.pattern;
|
|
1065
1202
|
const minuteStep = isCadenceField(minute) && minute !== '*';
|
|
1066
1203
|
|
|
1067
1204
|
// A non-`*/2` hour stride keeps the existing cadence form.
|
|
1068
|
-
if (!confinableHour(
|
|
1205
|
+
if (!confinableHour(schedule)) {
|
|
1069
1206
|
return false;
|
|
1070
1207
|
}
|
|
1071
1208
|
|
|
@@ -1074,17 +1211,20 @@ function confinementEligible(ir: IR,
|
|
|
1074
1211
|
// and only where it fills the coarser field: a contiguous hour range or a
|
|
1075
1212
|
// single hour both close on the minute's real last fire, which the
|
|
1076
1213
|
// windowing renderer already speaks. The `*/2` step fills both, so it keeps
|
|
1077
|
-
// the "of every other minute" confinement; other steps defer entirely.
|
|
1214
|
+
// the "of every other minute" confinement; other steps defer entirely. A
|
|
1215
|
+
// contiguous hour range (`hour === 'range'`) is left to that windowing
|
|
1216
|
+
// renderer rather than this confinement frame, which closes on the top of
|
|
1217
|
+
// the next hour.
|
|
1078
1218
|
if (minuteStep) {
|
|
1079
|
-
return minute === '*/2' &&
|
|
1219
|
+
return minute === '*/2' && schedule.shapes.hour !== 'range';
|
|
1080
1220
|
}
|
|
1081
1221
|
|
|
1082
1222
|
// A minute list that is really a stride keeps its cadence form; a short
|
|
1083
1223
|
// explicit minute list crossed with a discrete hour LIST is a wall of
|
|
1084
1224
|
// distinct clock times ("9:00 a.m., 9:25 a.m., …"), not a single minute
|
|
1085
1225
|
// confinement. Both stay with the enumerating renderer.
|
|
1086
|
-
if (isMinuteStride(
|
|
1087
|
-
|
|
1226
|
+
if (isMinuteStride(schedule) ||
|
|
1227
|
+
schedule.shapes.minute === 'list' && schedule.shapes.hour === 'list') {
|
|
1088
1228
|
return false;
|
|
1089
1229
|
}
|
|
1090
1230
|
|
|
@@ -1100,14 +1240,15 @@ function confinementEligible(ir: IR,
|
|
|
1100
1240
|
return true;
|
|
1101
1241
|
}
|
|
1102
1242
|
|
|
1103
|
-
return
|
|
1243
|
+
return schedule.shapes.hour === 'single' && minute === '*/2';
|
|
1104
1244
|
}
|
|
1105
1245
|
|
|
1106
1246
|
// Render the pattern with the confinement frame: a finer leading cadence with
|
|
1107
1247
|
// each coarser field as a confinement, or null when it does not apply. Routed
|
|
1108
1248
|
// to from the cadence renderers in place of the older juxtaposed-cadence and
|
|
1109
1249
|
// duration-frame forms.
|
|
1110
|
-
function confinement(
|
|
1250
|
+
function confinement(schedule: Schedule,
|
|
1251
|
+
opts: NormalizedOptions): string | null {
|
|
1111
1252
|
// The confinement frame is scoped to the default (US) dialect, the one that
|
|
1112
1253
|
// carries the until-window; every other dialect and the compact `short` form
|
|
1113
1254
|
// keep their established juxtaposed-cadence / duration-frame phrasing.
|
|
@@ -1118,20 +1259,20 @@ function confinement(ir: IR, opts: NormalizedOptions): string | null {
|
|
|
1118
1259
|
// With nothing coarser to confine (minute and hour both wildcard), the bare
|
|
1119
1260
|
// cadence renderers already speak the pattern ("every second", "every
|
|
1120
1261
|
// minute"); the confinement frame only applies once a coarser field is set.
|
|
1121
|
-
if (
|
|
1262
|
+
if (schedule.pattern.minute === '*' && schedule.pattern.hour === '*') {
|
|
1122
1263
|
return null;
|
|
1123
1264
|
}
|
|
1124
1265
|
|
|
1125
|
-
const lead = leadingCadence(
|
|
1266
|
+
const lead = leadingCadence(schedule, opts);
|
|
1126
1267
|
|
|
1127
|
-
if (!lead || !confinementEligible(
|
|
1268
|
+
if (!lead || !confinementEligible(schedule, lead)) {
|
|
1128
1269
|
return null;
|
|
1129
1270
|
}
|
|
1130
1271
|
|
|
1131
|
-
const minutePart = lead.secondLead ? minuteConfinement(
|
|
1272
|
+
const minutePart = lead.secondLead ? minuteConfinement(schedule, opts) : '';
|
|
1132
1273
|
|
|
1133
|
-
return lead.text + minutePart + hourConfinement(
|
|
1134
|
-
trailingQualifier(
|
|
1274
|
+
return lead.text + minutePart + hourConfinement(schedule, opts) +
|
|
1275
|
+
trailingQualifier(schedule, opts);
|
|
1135
1276
|
}
|
|
1136
1277
|
|
|
1137
1278
|
// The plan dispatch table.
|
|
@@ -1171,31 +1312,30 @@ const renderers = {
|
|
|
1171
1312
|
function renderStride(stride: Stride, opts: NormalizedOptions): string {
|
|
1172
1313
|
const {interval, start, last, cycle, unit, anchor} = stride;
|
|
1173
1314
|
const cadence = 'every ' + getNumber(interval, opts) + ' ' + unit + 's';
|
|
1174
|
-
const tiles = cycle % interval === 0;
|
|
1175
1315
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1316
|
+
return chooseStride({start, interval, cycle}, {
|
|
1317
|
+
bare: () => cadence,
|
|
1179
1318
|
|
|
1180
|
-
if (start < interval && tiles) {
|
|
1181
1319
|
// A clean wrap from a non-zero offset: name the start, no endpoint.
|
|
1182
|
-
|
|
1183
|
-
pluralize(start, unit) + ' past the ' + anchor
|
|
1184
|
-
}
|
|
1320
|
+
offset: () => cadence + ' from ' + getNumber(start, opts) + ' ' +
|
|
1321
|
+
pluralize(start, unit) + ' past the ' + anchor,
|
|
1185
1322
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1323
|
+
// A bounded, non-wrapping set: pin both endpoints. Each bound is a value,
|
|
1324
|
+
// so it reads as a digit, matching the range idiom ("from 0 through 30").
|
|
1325
|
+
bounded: () => {
|
|
1326
|
+
const num = seriesNumber();
|
|
1189
1327
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1328
|
+
return cadence + ' from ' + num(start) + through(opts) + num(last) + ' ' +
|
|
1329
|
+
pluralize(last, unit) + ' past the ' + anchor;
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1192
1332
|
}
|
|
1193
1333
|
|
|
1194
1334
|
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
1195
1335
|
// form an arithmetic progression long enough to beat the list (the core
|
|
1196
|
-
// enumerates an offset/uneven step to this fire list; the
|
|
1197
|
-
// the renderer recognizes the progression). Returns null for a
|
|
1198
|
-
// or a too-short list, leaving the caller to enumerate.
|
|
1336
|
+
// enumerates an offset/uneven step to this fire list; the Schedule is
|
|
1337
|
+
// unchanged, so the renderer recognizes the progression). Returns null for a
|
|
1338
|
+
// non-progression or a too-short list, leaving the caller to enumerate.
|
|
1199
1339
|
function strideFromSegments(segments: Segment[], unit: string, anchor: string,
|
|
1200
1340
|
opts: NormalizedOptions): string | null {
|
|
1201
1341
|
const values = singleValues(segments);
|
|
@@ -1278,18 +1418,14 @@ function hourStrideCadence(stride: {start: number; interval: number;
|
|
|
1278
1418
|
last: number}, opts: NormalizedOptions): string {
|
|
1279
1419
|
const {start, interval, last} = stride;
|
|
1280
1420
|
const cadence = 'every ' + getNumber(interval, opts) + ' hours';
|
|
1281
|
-
const tiles = 24 % interval === 0;
|
|
1282
|
-
|
|
1283
|
-
if (start === 0 && tiles) {
|
|
1284
|
-
return cadence;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
if (start < interval && tiles) {
|
|
1288
|
-
return cadence + ' from ' + getTime({hour: start, minute: 0}, opts);
|
|
1289
|
-
}
|
|
1290
1421
|
|
|
1291
|
-
return
|
|
1292
|
-
|
|
1422
|
+
return chooseStride({start, interval, cycle: 24}, {
|
|
1423
|
+
bare: () => cadence,
|
|
1424
|
+
offset: () => cadence + ' from ' + getTime({hour: start, minute: 0}, opts),
|
|
1425
|
+
bounded: () =>
|
|
1426
|
+
cadence + ' from ' + getTime({hour: start, minute: 0}, opts) +
|
|
1427
|
+
through(opts) + getTime({hour: last, minute: 0}, opts)
|
|
1428
|
+
});
|
|
1293
1429
|
}
|
|
1294
1430
|
|
|
1295
1431
|
// The bounded cadence for an hour stride that pins both clock-time endpoints,
|
|
@@ -1299,8 +1435,9 @@ function hourStrideCadence(stride: {start: number; interval: number;
|
|
|
1299
1435
|
// ("…, every five hours from midnight through 8 p.m.") than as a wall of
|
|
1300
1436
|
// clock-time columns. An offset-clean stride keeps its existing confinement
|
|
1301
1437
|
// form, so only the endpoint-bearing case routes here.
|
|
1302
|
-
function unevenHourCadence(
|
|
1303
|
-
|
|
1438
|
+
function unevenHourCadence(schedule: Schedule,
|
|
1439
|
+
opts: NormalizedOptions): string | null {
|
|
1440
|
+
const stride = hourStride(schedule);
|
|
1304
1441
|
|
|
1305
1442
|
if (!stride || offsetCleanStride(stride)) {
|
|
1306
1443
|
return null;
|
|
@@ -1312,14 +1449,14 @@ function unevenHourCadence(ir: IR, opts: NormalizedOptions): string | null {
|
|
|
1312
1449
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1313
1450
|
// segment yields its {start, interval, last} directly; an all-single hour
|
|
1314
1451
|
// list yields one only when its values form a step progression (so an irregular
|
|
1315
|
-
// list like 9,17 keeps enumerating). The
|
|
1452
|
+
// list like 9,17 keeps enumerating). The Schedule is unchanged — the renderer
|
|
1316
1453
|
// recognizes the stride and speaks it as a cadence instead of the clock-time
|
|
1317
1454
|
// cross-product.
|
|
1318
|
-
function hourStride(
|
|
1455
|
+
function hourStride(schedule: Schedule):
|
|
1319
1456
|
{start: number; interval: number; last: number} | null {
|
|
1320
1457
|
// Reached only from the clock-time paths, which run under discrete hours
|
|
1321
1458
|
// and so always carry hour segments.
|
|
1322
|
-
const segments = segmentsOf(
|
|
1459
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1323
1460
|
|
|
1324
1461
|
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
1325
1462
|
const segment = segments[0];
|
|
@@ -1346,8 +1483,8 @@ function hourStride(ir: IR):
|
|
|
1346
1483
|
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
1347
1484
|
// fills the minute (a "for one minute" frame at minute 0); a single 0 is just
|
|
1348
1485
|
// the top of the minute (no clause); anything else needs its own clause.
|
|
1349
|
-
function subMinuteSecond(
|
|
1350
|
-
return
|
|
1486
|
+
function subMinuteSecond(schedule: Schedule): boolean {
|
|
1487
|
+
return schedule.pattern.second === '*' || schedule.shapes.second === 'step';
|
|
1351
1488
|
}
|
|
1352
1489
|
|
|
1353
1490
|
// The lead clause for an hour-cadence rendering: the second and the pinned
|
|
@@ -1357,14 +1494,14 @@ function subMinuteSecond(ir: IR): boolean {
|
|
|
1357
1494
|
// minute" frame (the whole minute-0 window). A non-zero minute is a real
|
|
1358
1495
|
// clock minute: the second leads with its own "past the minute" clause (if
|
|
1359
1496
|
// any), then the minute reads "M minutes past the hour".
|
|
1360
|
-
function hourCadenceLead(
|
|
1497
|
+
function hourCadenceLead(schedule: Schedule, minute: number,
|
|
1361
1498
|
opts: NormalizedOptions): string {
|
|
1362
1499
|
if (minute === 0) {
|
|
1363
|
-
if (subMinuteSecond(
|
|
1364
|
-
return secondsClause(
|
|
1500
|
+
if (subMinuteSecond(schedule)) {
|
|
1501
|
+
return secondsClause(schedule, 'minute', opts) + ' for one minute';
|
|
1365
1502
|
}
|
|
1366
1503
|
|
|
1367
|
-
return secondsClause(
|
|
1504
|
+
return secondsClause(schedule, 'hour', opts);
|
|
1368
1505
|
}
|
|
1369
1506
|
|
|
1370
1507
|
const minutePhrase = getNumber(minute, opts) + ' ' +
|
|
@@ -1372,11 +1509,11 @@ function hourCadenceLead(ir: IR, minute: number,
|
|
|
1372
1509
|
|
|
1373
1510
|
// A single 0 second is just the top of the minute, so the minute leads
|
|
1374
1511
|
// alone; any other second prefixes its own clause.
|
|
1375
|
-
if (
|
|
1512
|
+
if (schedule.pattern.second === '0') {
|
|
1376
1513
|
return minutePhrase;
|
|
1377
1514
|
}
|
|
1378
1515
|
|
|
1379
|
-
return secondsClause(
|
|
1516
|
+
return secondsClause(schedule, 'minute', opts) + ', ' + minutePhrase;
|
|
1380
1517
|
}
|
|
1381
1518
|
|
|
1382
1519
|
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
@@ -1388,10 +1525,10 @@ function hourCadenceLead(ir: IR, minute: number,
|
|
|
1388
1525
|
// but a plain :00) makes every clock time three digit-groups, so any stride
|
|
1389
1526
|
// is worth compacting; otherwise the stride must exceed the clock-time cap,
|
|
1390
1527
|
// the same point at which the core itself stops enumerating. Renderer-only;
|
|
1391
|
-
// the
|
|
1392
|
-
function hourCadence(
|
|
1528
|
+
// the Schedule is unchanged.
|
|
1529
|
+
function hourCadence(schedule: Schedule, minute: number,
|
|
1393
1530
|
opts: NormalizedOptions): string | null {
|
|
1394
|
-
const stride = hourStride(
|
|
1531
|
+
const stride = hourStride(schedule);
|
|
1395
1532
|
|
|
1396
1533
|
if (!stride) {
|
|
1397
1534
|
return null;
|
|
@@ -1404,7 +1541,7 @@ function hourCadence(ir: IR, minute: number,
|
|
|
1404
1541
|
// or "from M" form is no shorter than the list, so the list reads fine. A
|
|
1405
1542
|
// bounded or uneven stride has no clean wrap, so its endpoint-pinning cadence
|
|
1406
1543
|
// ("every five hours from midnight through 8 p.m.") reads better however few.
|
|
1407
|
-
if (
|
|
1544
|
+
if (schedule.pattern.second === '0' && fires <= maxClockTimes &&
|
|
1408
1545
|
offsetCleanStride(stride)) {
|
|
1409
1546
|
return null;
|
|
1410
1547
|
}
|
|
@@ -1414,33 +1551,33 @@ function hourCadence(ir: IR, minute: number,
|
|
|
1414
1551
|
// minute during every other hour", matching the "every minute during every
|
|
1415
1552
|
// other hour" idiom and keeping it distinct from the bare hour-step form
|
|
1416
1553
|
// ("every two hours") so the minute-0 confinement is never heard as it.
|
|
1417
|
-
const minuteZeroStride = minute === 0 && subMinuteSecond(
|
|
1418
|
-
cleanStrideSegment(
|
|
1554
|
+
const minuteZeroStride = minute === 0 && subMinuteSecond(schedule) &&
|
|
1555
|
+
cleanStrideSegment(schedule);
|
|
1419
1556
|
|
|
1420
1557
|
if (minuteZeroStride) {
|
|
1421
|
-
return secondsClause(
|
|
1422
|
-
everyNthHour(minuteZeroStride, opts) + trailingQualifier(
|
|
1558
|
+
return secondsClause(schedule, 'minute', opts) + ' for one minute ' +
|
|
1559
|
+
everyNthHour(minuteZeroStride, opts) + trailingQualifier(schedule, opts);
|
|
1423
1560
|
}
|
|
1424
1561
|
|
|
1425
1562
|
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
1426
1563
|
// lead clause to fold in, so the bounded cadence stands on its own ("every
|
|
1427
1564
|
// five hours from midnight through 8 p.m."); only a real minute or second
|
|
1428
1565
|
// prefixes its clause.
|
|
1429
|
-
if (minute === 0 &&
|
|
1430
|
-
return hourStrideCadence(stride, opts) + trailingQualifier(
|
|
1566
|
+
if (minute === 0 && schedule.pattern.second === '0') {
|
|
1567
|
+
return hourStrideCadence(stride, opts) + trailingQualifier(schedule, opts);
|
|
1431
1568
|
}
|
|
1432
1569
|
|
|
1433
|
-
return hourCadenceLead(
|
|
1434
|
-
hourStrideCadence(stride, opts) + trailingQualifier(
|
|
1570
|
+
return hourCadenceLead(schedule, minute, opts) + ', ' +
|
|
1571
|
+
hourStrideCadence(stride, opts) + trailingQualifier(schedule, opts);
|
|
1435
1572
|
}
|
|
1436
1573
|
|
|
1437
1574
|
// The hour step segment when the hour is a clean stride with an idiomatic
|
|
1438
1575
|
// ordinal ("every other", "every sixth"), suitable for the "during every Nth
|
|
1439
1576
|
// hour" confinement frame; null otherwise (an uneven stride, a bounded step,
|
|
1440
1577
|
// or an arithmetic-progression list, which keep the bounded cadence form).
|
|
1441
|
-
function cleanStrideSegment(
|
|
1578
|
+
function cleanStrideSegment(schedule: Schedule): StepSegment | null {
|
|
1442
1579
|
// Reached only after hourStride confirmed a stride, so hour segments exist.
|
|
1443
|
-
const segments = segmentsOf(
|
|
1580
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1444
1581
|
const segment = segments.length === 1 && segments[0];
|
|
1445
1582
|
|
|
1446
1583
|
if (!segment || segment.kind !== 'step' ||
|
|
@@ -1457,10 +1594,10 @@ function cleanStrideSegment(ir: IR): StepSegment | null {
|
|
|
1457
1594
|
// A pure single-value list (9,17) has no range to span and still enumerates;
|
|
1458
1595
|
// a step is handled by hourStride/hourCadence, so a field whose only segments
|
|
1459
1596
|
// are steps and singles is left alone here.
|
|
1460
|
-
function hasHourWindow(
|
|
1597
|
+
function hasHourWindow(schedule: Schedule): boolean {
|
|
1461
1598
|
// Reached only from the clock-time paths, which run under discrete hours
|
|
1462
1599
|
// and so always carry hour segments.
|
|
1463
|
-
return segmentsOf(
|
|
1600
|
+
return segmentsOf(schedule, 'hour').some(function range(segment) {
|
|
1464
1601
|
return segment.kind === 'range';
|
|
1465
1602
|
});
|
|
1466
1603
|
}
|
|
@@ -1472,12 +1609,13 @@ function hasHourWindow(ir: IR): boolean {
|
|
|
1472
1609
|
// minute, never a wildcard — so the run is not continuous to the top of the
|
|
1473
1610
|
// next hour and the window keeps "through". Mirrors foldedHourWindows but
|
|
1474
1611
|
// pinned to minute 0.
|
|
1475
|
-
function hourRangeWindowTail(
|
|
1612
|
+
function hourRangeWindowTail(schedule: Schedule,
|
|
1613
|
+
opts: NormalizedOptions): string {
|
|
1476
1614
|
const windows: string[] = [];
|
|
1477
|
-
const outlierHours = collectHourOutliers(
|
|
1615
|
+
const outlierHours = collectHourOutliers(schedule);
|
|
1478
1616
|
|
|
1479
1617
|
// Reached only after hasHourWindow, so hour segments exist.
|
|
1480
|
-
segmentsOf(
|
|
1618
|
+
segmentsOf(schedule, 'hour').forEach(function classify(segment) {
|
|
1481
1619
|
if (segment.kind === 'range') {
|
|
1482
1620
|
windows.push(rangeWindow({
|
|
1483
1621
|
continuous: false,
|
|
@@ -1502,21 +1640,21 @@ function hourRangeWindowTail(ir: IR, opts: NormalizedOptions): string {
|
|
|
1502
1640
|
// the hours into a wall of clock times. Returns null when the hour has no
|
|
1503
1641
|
// range (a pure single-value list, a single hour, or a step, which other
|
|
1504
1642
|
// paths own), or when a plain :00 set is short enough that enumeration is no
|
|
1505
|
-
// longer than the window. Renderer-only; the
|
|
1506
|
-
function hourRangeCadence(
|
|
1643
|
+
// longer than the window. Renderer-only; the Schedule is unchanged.
|
|
1644
|
+
function hourRangeCadence(schedule: Schedule, minute: number,
|
|
1507
1645
|
opts: NormalizedOptions): string | null {
|
|
1508
1646
|
// Scoped to minute 0: the minute folds into the lead and every hour fires
|
|
1509
1647
|
// at the top, so the window closes cleanly on the final hour. A non-zero
|
|
1510
1648
|
// pinned minute is a real clock minute the existing clock-time window form
|
|
1511
1649
|
// already speaks ("9:30:15 a.m. through 8:30:15 p.m."), unchanged.
|
|
1512
|
-
if (minute !== 0 || !hasHourWindow(
|
|
1650
|
+
if (minute !== 0 || !hasHourWindow(schedule)) {
|
|
1513
1651
|
return null;
|
|
1514
1652
|
}
|
|
1515
1653
|
|
|
1516
1654
|
// A plain top-of-minute second (:00) carries no clause: the existing
|
|
1517
1655
|
// hour-range and folded-window renderers already speak that window, so this
|
|
1518
1656
|
// path only forms a window when there is a meaningful second to lead with.
|
|
1519
|
-
if (
|
|
1657
|
+
if (schedule.pattern.second === '0') {
|
|
1520
1658
|
return null;
|
|
1521
1659
|
}
|
|
1522
1660
|
|
|
@@ -1526,14 +1664,15 @@ function hourRangeCadence(ir: IR, minute: number,
|
|
|
1526
1664
|
// uses). This is kept distinct from the bare minute-0 window ("every hour
|
|
1527
1665
|
// from 9 a.m. through 5 p.m.") so the one-minute confinement is never heard
|
|
1528
1666
|
// as it — the hour-range analog of "for one minute during every other hour".
|
|
1529
|
-
if (subMinuteSecond(
|
|
1530
|
-
return secondsClause(
|
|
1531
|
-
|
|
1532
|
-
|
|
1667
|
+
if (subMinuteSecond(schedule)) {
|
|
1668
|
+
return secondsClause(schedule, 'minute', opts) +
|
|
1669
|
+
' for one minute during the ' +
|
|
1670
|
+
hourSegmentTimes(schedule, {minute: 0, second: null}, false, opts) +
|
|
1671
|
+
' hours' + trailingQualifier(schedule, opts);
|
|
1533
1672
|
}
|
|
1534
1673
|
|
|
1535
|
-
return hourCadenceLead(
|
|
1536
|
-
hourRangeWindowTail(
|
|
1674
|
+
return hourCadenceLead(schedule, minute, opts) + ', ' +
|
|
1675
|
+
hourRangeWindowTail(schedule, opts) + trailingQualifier(schedule, opts);
|
|
1537
1676
|
}
|
|
1538
1677
|
|
|
1539
1678
|
// --- List and segment phrasing. ---
|
|
@@ -1649,13 +1788,13 @@ function singleHourFire(times: HourTimesPlan): boolean {
|
|
|
1649
1788
|
// The hour times accompanying a window phrase: enumerated fires up to the
|
|
1650
1789
|
// cap, segment rendering past it (decided by the core). `atContext` marks
|
|
1651
1790
|
// an "at <times>" frame (vs "during the <times> hours").
|
|
1652
|
-
function hourTimesFromPlan(
|
|
1653
|
-
opts: NormalizedOptions): string {
|
|
1791
|
+
function hourTimesFromPlan(schedule: Schedule, times: HourTimesPlan,
|
|
1792
|
+
atContext: boolean, opts: NormalizedOptions): string {
|
|
1654
1793
|
if (times.kind === 'fires') {
|
|
1655
1794
|
return hourTimes(times.fires, opts);
|
|
1656
1795
|
}
|
|
1657
1796
|
|
|
1658
|
-
return hourSegmentTimes(
|
|
1797
|
+
return hourSegmentTimes(schedule, {minute: 0, second: null}, atContext, opts);
|
|
1659
1798
|
}
|
|
1660
1799
|
|
|
1661
1800
|
// The hour values an hour segment covers: a range's bounds, a step's
|
|
@@ -1671,13 +1810,13 @@ function segmentHours(segment: Segment): (string | number)[] {
|
|
|
1671
1810
|
// Clock times for the hour field rendered segment by segment, so ranges
|
|
1672
1811
|
// read as windows ("9:30 a.m. through 8:30 p.m.") rather than an
|
|
1673
1812
|
// enumeration. The minute (and optional second) fold into each time.
|
|
1674
|
-
function hourSegmentTimes(
|
|
1813
|
+
function hourSegmentTimes(schedule: Schedule,
|
|
1675
1814
|
fold: {minute: number | string; second: number | null | undefined},
|
|
1676
1815
|
atContext: boolean, opts: NormalizedOptions): string {
|
|
1677
1816
|
const {minute, second} = fold;
|
|
1678
1817
|
// Hour-segment rendering is reached only under discrete hours, which have
|
|
1679
1818
|
// segments.
|
|
1680
|
-
const segments = segmentsOf(
|
|
1819
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1681
1820
|
const plain = mixedTwelve(segments.flatMap(function entries(segment) {
|
|
1682
1821
|
return segmentHours(segment).map(function entry(hour) {
|
|
1683
1822
|
return {hour: +hour, minute, second};
|
|
@@ -1785,47 +1924,49 @@ const leadingWords: QualifierWords = {
|
|
|
1785
1924
|
|
|
1786
1925
|
// A trailing day-level qualifier for bare frequencies, e.g. " on Monday".
|
|
1787
1926
|
// Returns an empty string when no date, month, or weekday is set.
|
|
1788
|
-
function trailingQualifier(
|
|
1927
|
+
function trailingQualifier(schedule: Schedule,
|
|
1928
|
+
opts: NormalizedOptions): string {
|
|
1789
1929
|
// A day union reframes both day fields as a trailing condition clause; the
|
|
1790
1930
|
// month leads the whole description (applied in `describe`), so it is not
|
|
1791
1931
|
// part of the trailing qualifier here.
|
|
1792
|
-
if (isDayUnion(
|
|
1793
|
-
return dayUnionCondition(
|
|
1932
|
+
if (isDayUnion(schedule, opts)) {
|
|
1933
|
+
return dayUnionCondition(schedule, opts);
|
|
1794
1934
|
}
|
|
1795
1935
|
|
|
1796
|
-
const phrase = dayQualifier(
|
|
1936
|
+
const phrase = dayQualifier(schedule, trailingWords, opts);
|
|
1797
1937
|
|
|
1798
1938
|
return phrase && ' ' + phrase;
|
|
1799
1939
|
}
|
|
1800
1940
|
|
|
1801
1941
|
// Build the day-level qualifier that precedes a specific time, e.g.
|
|
1802
1942
|
// "every day ", "every Friday ", or "on January 13 ".
|
|
1803
|
-
function interpretDayQualifier(
|
|
1943
|
+
function interpretDayQualifier(schedule: Schedule,
|
|
1944
|
+
opts: NormalizedOptions): string {
|
|
1804
1945
|
// A day union puts the time first ("at midnight whenever the day is …"), so
|
|
1805
1946
|
// the leading position contributes no day phrase; the condition clause is
|
|
1806
1947
|
// appended after the time by the clock renderer.
|
|
1807
|
-
if (isDayUnion(
|
|
1948
|
+
if (isDayUnion(schedule, opts)) {
|
|
1808
1949
|
return '';
|
|
1809
1950
|
}
|
|
1810
1951
|
|
|
1811
|
-
return dayQualifier(
|
|
1952
|
+
return dayQualifier(schedule, leadingWords, opts) + ' ';
|
|
1812
1953
|
}
|
|
1813
1954
|
|
|
1814
1955
|
// The day-level qualifier phrase (date, month, and weekday), or
|
|
1815
1956
|
// `words.all` when all three are wildcards. `words` supplies the
|
|
1816
1957
|
// connectives that differ between the trailing and leading positions.
|
|
1817
|
-
function dayQualifier(
|
|
1958
|
+
function dayQualifier(schedule: Schedule, words: QualifierWords,
|
|
1818
1959
|
opts: NormalizedOptions): string {
|
|
1819
|
-
const pattern =
|
|
1960
|
+
const pattern = schedule.pattern;
|
|
1820
1961
|
|
|
1821
1962
|
// Standard cron fires when day-of-month OR day-of-week matches, when
|
|
1822
1963
|
// both are restricted.
|
|
1823
1964
|
if (pattern.date !== '*' && pattern.weekday !== '*') {
|
|
1824
|
-
return dateOrWeekday(
|
|
1965
|
+
return dateOrWeekday(schedule, opts);
|
|
1825
1966
|
}
|
|
1826
1967
|
|
|
1827
1968
|
if (pattern.date !== '*') {
|
|
1828
|
-
return datePhrase(
|
|
1969
|
+
return datePhrase(schedule, words, opts);
|
|
1829
1970
|
}
|
|
1830
1971
|
|
|
1831
1972
|
// A weekday qualifier, optionally scoped to a month ("on Monday in
|
|
@@ -1837,46 +1978,47 @@ function dayQualifier(ir: IR, words: QualifierWords,
|
|
|
1837
1978
|
// the "of the month" recurrence a concrete month makes redundant; a plain
|
|
1838
1979
|
// weekday name takes the ordinary " in <month>" scope.
|
|
1839
1980
|
if (quartzWeekday) {
|
|
1840
|
-
return monthScopeForRecurrence(quartzWeekday,
|
|
1981
|
+
return monthScopeForRecurrence(quartzWeekday, schedule, opts);
|
|
1841
1982
|
}
|
|
1842
1983
|
|
|
1843
1984
|
const weekdays = words.weekday +
|
|
1844
|
-
weekdayPhrase(
|
|
1985
|
+
weekdayPhrase(schedule, words.recurringWeekday, opts);
|
|
1845
1986
|
|
|
1846
|
-
return weekdays + monthScope(
|
|
1987
|
+
return weekdays + monthScope(schedule, opts);
|
|
1847
1988
|
}
|
|
1848
1989
|
|
|
1849
1990
|
if (pattern.month !== '*') {
|
|
1850
|
-
return words.month + monthName(
|
|
1991
|
+
return words.month + monthName(schedule, opts);
|
|
1851
1992
|
}
|
|
1852
1993
|
|
|
1853
1994
|
return words.all;
|
|
1854
1995
|
}
|
|
1855
1996
|
|
|
1856
1997
|
// The date portion of a day qualifier (the weekday is a wildcard).
|
|
1857
|
-
function datePhrase(
|
|
1998
|
+
function datePhrase(schedule: Schedule, words: QualifierWords,
|
|
1858
1999
|
opts: NormalizedOptions): string {
|
|
1859
|
-
const pattern =
|
|
2000
|
+
const pattern = schedule.pattern;
|
|
1860
2001
|
const quartzDate = quartzDatePhrase(pattern.date, opts);
|
|
1861
2002
|
|
|
1862
2003
|
if (quartzDate) {
|
|
1863
|
-
return monthScopeForRecurrence(quartzDate,
|
|
2004
|
+
return monthScopeForRecurrence(quartzDate, schedule, opts);
|
|
1864
2005
|
}
|
|
1865
2006
|
|
|
1866
2007
|
if (isOpenStep(pattern.date)) {
|
|
1867
2008
|
return monthScopeForRecurrence(
|
|
1868
|
-
words.stepDate + stepDates(pattern.date),
|
|
2009
|
+
words.stepDate + stepDates(pattern.date), schedule, opts);
|
|
1869
2010
|
}
|
|
1870
2011
|
|
|
1871
|
-
if (pattern.month !== '*' && !monthFoldsIntoDate(
|
|
1872
|
-
return 'on the ' + dateOrdinals(
|
|
2012
|
+
if (pattern.month !== '*' && !monthFoldsIntoDate(schedule)) {
|
|
2013
|
+
return 'on the ' + dateOrdinals(schedule, opts) +
|
|
2014
|
+
monthScope(schedule, opts);
|
|
1873
2015
|
}
|
|
1874
2016
|
|
|
1875
2017
|
if (pattern.month !== '*') {
|
|
1876
|
-
return 'on ' + monthDatePhrase(
|
|
2018
|
+
return 'on ' + monthDatePhrase(schedule, opts);
|
|
1877
2019
|
}
|
|
1878
2020
|
|
|
1879
|
-
return 'on the ' + dateOrdinals(
|
|
2021
|
+
return 'on the ' + dateOrdinals(schedule, opts);
|
|
1880
2022
|
}
|
|
1881
2023
|
|
|
1882
2024
|
// Whether the month can fold into a calendar date ("on June 1"): flat name
|
|
@@ -1885,10 +2027,10 @@ function datePhrase(ir: IR, words: QualifierWords,
|
|
|
1885
2027
|
// "(June) through (September 1)" — and the "every odd/even-numbered month"
|
|
1886
2028
|
// frequency phrase has no name to place before the date; both scope the date
|
|
1887
2029
|
// instead ("on the 1st in June through September").
|
|
1888
|
-
function monthFoldsIntoDate(
|
|
1889
|
-
return !oddEvenMonth(
|
|
2030
|
+
function monthFoldsIntoDate(schedule: Schedule): boolean {
|
|
2031
|
+
return !oddEvenMonth(schedule.pattern.month) &&
|
|
1890
2032
|
// Reached only with a restricted month, which has segments.
|
|
1891
|
-
segmentsOf(
|
|
2033
|
+
segmentsOf(schedule, 'month').every(function flat(segment) {
|
|
1892
2034
|
return segment.kind !== 'range';
|
|
1893
2035
|
});
|
|
1894
2036
|
}
|
|
@@ -1904,17 +2046,18 @@ function monthFoldsIntoDate(ir: IR): boolean {
|
|
|
1904
2046
|
// and `dayUnionCondition`), not inside the trailing/leading qualifier. Scoped
|
|
1905
2047
|
// to the until-window dialect; every other dialect and the `short` form keep
|
|
1906
2048
|
// the established "on <dom> or on <dow>" phrasing.
|
|
1907
|
-
function isDayUnion(
|
|
1908
|
-
return
|
|
2049
|
+
function isDayUnion(schedule: Schedule, opts: NormalizedOptions): boolean {
|
|
2050
|
+
return schedule.pattern.date !== '*' && schedule.pattern.weekday !== '*' &&
|
|
1909
2051
|
!!opts.style.untilWindow && !opts.short;
|
|
1910
2052
|
}
|
|
1911
2053
|
|
|
1912
2054
|
// The trailing condition clause for a day union, e.g. " whenever the day is
|
|
1913
2055
|
// the 1st or a Friday". The day predicates are flattened into one or-list so
|
|
1914
2056
|
// the union reads as a single set of matching days.
|
|
1915
|
-
function dayUnionCondition(
|
|
1916
|
-
|
|
1917
|
-
|
|
2057
|
+
function dayUnionCondition(schedule: Schedule,
|
|
2058
|
+
opts: NormalizedOptions): string {
|
|
2059
|
+
const pieces = [...dayUnionDatePieces(schedule, opts),
|
|
2060
|
+
...dayUnionWeekdayPieces(schedule, opts)];
|
|
1918
2061
|
|
|
1919
2062
|
return ' whenever the day is ' + joinOr(pieces, opts);
|
|
1920
2063
|
}
|
|
@@ -1922,12 +2065,13 @@ function dayUnionCondition(ir: IR, opts: NormalizedOptions): string {
|
|
|
1922
2065
|
// The leading "in <month> " scope for a day union, or an empty string when the
|
|
1923
2066
|
// month is a wildcard. The month scopes the whole union, so it leads the clause
|
|
1924
2067
|
// rather than attaching to either day half.
|
|
1925
|
-
function dayUnionMonthLead(
|
|
1926
|
-
|
|
2068
|
+
function dayUnionMonthLead(schedule: Schedule,
|
|
2069
|
+
opts: NormalizedOptions): string {
|
|
2070
|
+
if (schedule.pattern.month === '*') {
|
|
1927
2071
|
return '';
|
|
1928
2072
|
}
|
|
1929
2073
|
|
|
1930
|
-
return 'in ' + monthName(
|
|
2074
|
+
return 'in ' + monthName(schedule, opts) + ' ';
|
|
1931
2075
|
}
|
|
1932
2076
|
|
|
1933
2077
|
// The day-of-month half of a union as a flat list of predicate pieces. A
|
|
@@ -1935,8 +2079,9 @@ function dayUnionMonthLead(ir: IR, opts: NormalizedOptions): string {
|
|
|
1935
2079
|
// `*/2`-style step is the parity idiom ("an odd-numbered day"); a plain field
|
|
1936
2080
|
// reads each segment as "the <ordinal>" or "from the <ordinal> through the
|
|
1937
2081
|
// <ordinal>".
|
|
1938
|
-
function dayUnionDatePieces(
|
|
1939
|
-
|
|
2082
|
+
function dayUnionDatePieces(schedule: Schedule,
|
|
2083
|
+
opts: NormalizedOptions): string[] {
|
|
2084
|
+
const dateField = schedule.pattern.date;
|
|
1940
2085
|
const quartz = quartzDatePhrase(dateField, opts);
|
|
1941
2086
|
|
|
1942
2087
|
if (quartz) {
|
|
@@ -1954,7 +2099,7 @@ function dayUnionDatePieces(ir: IR, opts: NormalizedOptions): string[] {
|
|
|
1954
2099
|
// spreads its enumerated fires as separate "the <ordinal>" alternatives.
|
|
1955
2100
|
const pieces: string[] = [];
|
|
1956
2101
|
|
|
1957
|
-
segmentsOf(
|
|
2102
|
+
segmentsOf(schedule, 'date').forEach(function expand(segment) {
|
|
1958
2103
|
if (segment.kind === 'range') {
|
|
1959
2104
|
pieces.push('from the ' + getOrdinal(segment.bounds[0]) + through(opts) +
|
|
1960
2105
|
'the ' + getOrdinal(segment.bounds[1]));
|
|
@@ -1977,8 +2122,9 @@ function dayUnionDatePieces(ir: IR, opts: NormalizedOptions): string[] {
|
|
|
1977
2122
|
// through-Friday range is the "a weekday" idiom; every other weekday names each
|
|
1978
2123
|
// day with the indefinite article ("a Friday", "a Sunday"), so each reads as a
|
|
1979
2124
|
// kind of day the union can match.
|
|
1980
|
-
function dayUnionWeekdayPieces(
|
|
1981
|
-
|
|
2125
|
+
function dayUnionWeekdayPieces(schedule: Schedule,
|
|
2126
|
+
opts: NormalizedOptions): string[] {
|
|
2127
|
+
const weekdayField = schedule.pattern.weekday;
|
|
1982
2128
|
const quartz = quartzWeekdayPhrase(weekdayField, opts);
|
|
1983
2129
|
|
|
1984
2130
|
if (quartz) {
|
|
@@ -1991,7 +2137,7 @@ function dayUnionWeekdayPieces(ir: IR, opts: NormalizedOptions): string[] {
|
|
|
1991
2137
|
// Sunday, a Tuesday, a Thursday, or a Saturday").
|
|
1992
2138
|
const pieces: string[] = [];
|
|
1993
2139
|
|
|
1994
|
-
segmentsOf(
|
|
2140
|
+
segmentsOf(schedule, 'weekday').forEach(function expand(segment) {
|
|
1995
2141
|
if (segment.kind === 'range' &&
|
|
1996
2142
|
segment.bounds[0] === '1' && segment.bounds[1] === '5') {
|
|
1997
2143
|
pieces.push('a weekday');
|
|
@@ -2042,26 +2188,27 @@ function oddEvenDay(dateField: string): string | null {
|
|
|
2042
2188
|
// names itself on the weekday ("or on Friday in June"), keeping both halves
|
|
2043
2189
|
// scoped; otherwise (a Quartz date, an open day step, a month range, or the
|
|
2044
2190
|
// odd/even frequency) it trails the whole or as ", in <month>".
|
|
2045
|
-
function dateOrWeekday(
|
|
2046
|
-
const pattern =
|
|
2191
|
+
function dateOrWeekday(schedule: Schedule, opts: NormalizedOptions): string {
|
|
2192
|
+
const pattern = schedule.pattern;
|
|
2047
2193
|
// The day-of-month-OR-day-of-week union is out of scope for the recurring
|
|
2048
2194
|
// plural (it is reframed elsewhere): the weekday half stays singular here.
|
|
2049
2195
|
const weekdayPart = quartzWeekdayPhrase(pattern.weekday, opts) ||
|
|
2050
|
-
'on ' + weekdayPhrase(
|
|
2196
|
+
'on ' + weekdayPhrase(schedule, false, opts);
|
|
2051
2197
|
|
|
2052
|
-
if (pattern.month !== '*' && monthFoldsIntoDate(
|
|
2198
|
+
if (pattern.month !== '*' && monthFoldsIntoDate(schedule) &&
|
|
2053
2199
|
!quartzDatePhrase(pattern.date, opts) && !isOpenStep(pattern.date)) {
|
|
2054
|
-
return 'on ' + monthDatePhrase(
|
|
2055
|
-
' in ' + monthName(
|
|
2200
|
+
return 'on ' + monthDatePhrase(schedule, opts) + ' or ' + weekdayPart +
|
|
2201
|
+
' in ' + monthName(schedule, opts);
|
|
2056
2202
|
}
|
|
2057
2203
|
|
|
2058
|
-
return datePart(
|
|
2204
|
+
return datePart(schedule, opts) + ' or ' + weekdayPart +
|
|
2205
|
+
orMonthScope(schedule, opts);
|
|
2059
2206
|
}
|
|
2060
2207
|
|
|
2061
2208
|
// The day-of-month half of an or-day phrase, without any month scope (the
|
|
2062
2209
|
// month scopes the whole or, applied by the caller).
|
|
2063
|
-
function datePart(
|
|
2064
|
-
const pattern =
|
|
2210
|
+
function datePart(schedule: Schedule, opts: NormalizedOptions): string {
|
|
2211
|
+
const pattern = schedule.pattern;
|
|
2065
2212
|
const quartzDate = quartzDatePhrase(pattern.date, opts);
|
|
2066
2213
|
|
|
2067
2214
|
if (quartzDate) {
|
|
@@ -2072,18 +2219,18 @@ function datePart(ir: IR, opts: NormalizedOptions): string {
|
|
|
2072
2219
|
return stepDates(pattern.date);
|
|
2073
2220
|
}
|
|
2074
2221
|
|
|
2075
|
-
return 'on the ' + dateOrdinals(
|
|
2222
|
+
return 'on the ' + dateOrdinals(schedule, opts);
|
|
2076
2223
|
}
|
|
2077
2224
|
|
|
2078
2225
|
// A trailing month scope for the whole or, set off by a comma so it reads
|
|
2079
2226
|
// over both day halves ("…or on Friday, in June"); empty when the month is a
|
|
2080
2227
|
// wildcard.
|
|
2081
|
-
function orMonthScope(
|
|
2082
|
-
if (
|
|
2228
|
+
function orMonthScope(schedule: Schedule, opts: NormalizedOptions): string {
|
|
2229
|
+
if (schedule.pattern.month === '*') {
|
|
2083
2230
|
return '';
|
|
2084
2231
|
}
|
|
2085
2232
|
|
|
2086
|
-
return ', in ' + monthName(
|
|
2233
|
+
return ', in ' + monthName(schedule, opts);
|
|
2087
2234
|
}
|
|
2088
2235
|
|
|
2089
2236
|
// The day-qualifier phrase for a Quartz date field (e.g. "on the last day
|
|
@@ -2140,16 +2287,16 @@ function quartzWeekdayPhrase(weekdayField: string,
|
|
|
2140
2287
|
// reads as if the 13 belongs to January alone. The day is reattached to the
|
|
2141
2288
|
// whole list with the possessive "the <ordinal> of <months>", which names the
|
|
2142
2289
|
// same day across every month unambiguously.
|
|
2143
|
-
function monthDatePhrase(
|
|
2144
|
-
const month = monthName(
|
|
2290
|
+
function monthDatePhrase(schedule: Schedule, opts: NormalizedOptions): string {
|
|
2291
|
+
const month = monthName(schedule, opts);
|
|
2145
2292
|
// A month-day phrase is reached only with a restricted date, which has
|
|
2146
2293
|
// segments.
|
|
2147
|
-
const days = renderSegments(segmentsOf(
|
|
2294
|
+
const days = renderSegments(segmentsOf(schedule, 'date'),
|
|
2148
2295
|
opts.style.ordinals ? getOrdinal : cardinalDay, opts);
|
|
2149
2296
|
|
|
2150
|
-
if (opts.style.dayFirst &&
|
|
2151
|
-
|
|
2152
|
-
return 'the ' + getOrdinal(
|
|
2297
|
+
if (opts.style.dayFirst && schedule.shapes.date === 'single' &&
|
|
2298
|
+
schedule.shapes.month !== 'single') {
|
|
2299
|
+
return 'the ' + getOrdinal(schedule.pattern.date) + ' of ' + month;
|
|
2153
2300
|
}
|
|
2154
2301
|
|
|
2155
2302
|
return opts.style.dayFirst ? days + ' ' + month : month + ' ' + days;
|
|
@@ -2162,12 +2309,12 @@ function cardinalDay(value: number | string): string {
|
|
|
2162
2309
|
|
|
2163
2310
|
// A trailing " in <month>" scope, or an empty string when the month is a
|
|
2164
2311
|
// wildcard.
|
|
2165
|
-
function monthScope(
|
|
2166
|
-
if (
|
|
2312
|
+
function monthScope(schedule: Schedule, opts: NormalizedOptions): string {
|
|
2313
|
+
if (schedule.pattern.month === '*') {
|
|
2167
2314
|
return '';
|
|
2168
2315
|
}
|
|
2169
2316
|
|
|
2170
|
-
return ' in ' + monthName(
|
|
2317
|
+
return ' in ' + monthName(schedule, opts);
|
|
2171
2318
|
}
|
|
2172
2319
|
|
|
2173
2320
|
// Scope a phrase that ends in the recurrence "of the month" (the Quartz last-
|
|
@@ -2178,25 +2325,27 @@ function monthScope(ir: IR, opts: NormalizedOptions): string {
|
|
|
2178
2325
|
// distributes the recurrence across the span and keeps it, rephrased as "of
|
|
2179
2326
|
// each month from <first> through <last>". A month list is left as-is (the
|
|
2180
2327
|
// recurrence stays, scoped "in <names>"), and a wildcard month adds nothing.
|
|
2181
|
-
function monthScopeForRecurrence(phrase: string,
|
|
2328
|
+
function monthScopeForRecurrence(phrase: string, schedule: Schedule,
|
|
2182
2329
|
opts: NormalizedOptions): string {
|
|
2183
|
-
if (
|
|
2330
|
+
if (schedule.pattern.month === '*') {
|
|
2184
2331
|
return phrase;
|
|
2185
2332
|
}
|
|
2186
2333
|
|
|
2187
2334
|
const carriesRecurrence = phrase.indexOf(' of the month') !== -1;
|
|
2188
2335
|
|
|
2189
|
-
if (carriesRecurrence &&
|
|
2336
|
+
if (carriesRecurrence && schedule.shapes.month === 'range') {
|
|
2190
2337
|
return phrase.replace(' of the month', ' of each month') + ' from ' +
|
|
2191
|
-
monthName(
|
|
2338
|
+
monthName(schedule, opts);
|
|
2192
2339
|
}
|
|
2193
2340
|
|
|
2194
2341
|
if (carriesRecurrence &&
|
|
2195
|
-
(
|
|
2196
|
-
|
|
2342
|
+
(schedule.shapes.month === 'single' ||
|
|
2343
|
+
schedule.shapes.month === 'step')) {
|
|
2344
|
+
return phrase.replace(' of the month', '') + ' in ' +
|
|
2345
|
+
monthName(schedule, opts);
|
|
2197
2346
|
}
|
|
2198
2347
|
|
|
2199
|
-
return phrase + ' in ' + monthName(
|
|
2348
|
+
return phrase + ' in ' + monthName(schedule, opts);
|
|
2200
2349
|
}
|
|
2201
2350
|
|
|
2202
2351
|
// Frequency phrase for an open day-of-month step, e.g. "every other day of
|
|
@@ -2219,17 +2368,17 @@ function stepDates(dateField: string): string {
|
|
|
2219
2368
|
|
|
2220
2369
|
// Render the date field's segments as suffixed ordinals. Open steps are
|
|
2221
2370
|
// handled separately as a frequency phrase.
|
|
2222
|
-
function dateOrdinals(
|
|
2371
|
+
function dateOrdinals(schedule: Schedule, opts: NormalizedOptions): string {
|
|
2223
2372
|
// Reached only with a restricted date, which has segments.
|
|
2224
|
-
return renderSegments(segmentsOf(
|
|
2373
|
+
return renderSegments(segmentsOf(schedule, 'date'), getOrdinal, opts);
|
|
2225
2374
|
}
|
|
2226
2375
|
|
|
2227
2376
|
// Render the month field as names. There are few, named months, so a step
|
|
2228
2377
|
// enumerates them ("January, April, July, and October") rather than reading as
|
|
2229
2378
|
// a frequency — except interval 2, which reads as "every odd/even-numbered
|
|
2230
2379
|
// month".
|
|
2231
|
-
function monthName(
|
|
2232
|
-
const oddEven = oddEvenMonth(
|
|
2380
|
+
function monthName(schedule: Schedule, opts: NormalizedOptions): string {
|
|
2381
|
+
const oddEven = oddEvenMonth(schedule.pattern.month);
|
|
2233
2382
|
|
|
2234
2383
|
if (oddEven) {
|
|
2235
2384
|
return oddEven;
|
|
@@ -2237,7 +2386,7 @@ function monthName(ir: IR, opts: NormalizedOptions): string {
|
|
|
2237
2386
|
|
|
2238
2387
|
// A restricted month has segments; open steps of interval 3+ enumerate their
|
|
2239
2388
|
// fires here too.
|
|
2240
|
-
return renderSegments(segmentsOf(
|
|
2389
|
+
return renderSegments(segmentsOf(schedule, 'month'), function name(value) {
|
|
2241
2390
|
return getMonth(value, opts);
|
|
2242
2391
|
}, opts);
|
|
2243
2392
|
}
|
|
@@ -2271,12 +2420,12 @@ function oddEvenMonth(monthField: string): string | null {
|
|
|
2271
2420
|
// keeps the singular idiom ("on Monday through Friday") so its through-
|
|
2272
2421
|
// connective stays unmistakable, and a leading time-anchored form ("every
|
|
2273
2422
|
// Monday") is never recurring here.
|
|
2274
|
-
function weekdayPhrase(
|
|
2423
|
+
function weekdayPhrase(schedule: Schedule, recurring: boolean,
|
|
2275
2424
|
opts: NormalizedOptions): string {
|
|
2276
2425
|
// Reached only with a restricted weekday, which has segments. Weekday lists
|
|
2277
|
-
// display Monday-first (Sunday last) so a weekend reads naturally; the
|
|
2278
|
-
// stays canonical (Sunday=0) and ranges keep their form.
|
|
2279
|
-
const segments = orderWeekdaysForDisplay(segmentsOf(
|
|
2426
|
+
// display Monday-first (Sunday last) so a weekend reads naturally; the
|
|
2427
|
+
// Schedule stays canonical (Sunday=0) and ranges keep their form.
|
|
2428
|
+
const segments = orderWeekdaysForDisplay(segmentsOf(schedule, 'weekday'));
|
|
2280
2429
|
const hasRange = segments.some(function range(segment) {
|
|
2281
2430
|
return segment.kind === 'range';
|
|
2282
2431
|
});
|
|
@@ -2332,9 +2481,9 @@ function renderSegments(segments: Segment[],
|
|
|
2332
2481
|
|
|
2333
2482
|
// Append or fold the year field into a finished description. An
|
|
2334
2483
|
// explicitly supplied year is always rendered.
|
|
2335
|
-
function applyYear(description: string,
|
|
2484
|
+
function applyYear(description: string, schedule: Schedule,
|
|
2336
2485
|
opts: NormalizedOptions): string {
|
|
2337
|
-
const yearField =
|
|
2486
|
+
const yearField = schedule.pattern.year;
|
|
2338
2487
|
|
|
2339
2488
|
if (yearField === '*') {
|
|
2340
2489
|
return description;
|
|
@@ -2350,7 +2499,7 @@ function applyYear(description: string, ir: IR,
|
|
|
2350
2499
|
const label = yearLabel(yearField, opts);
|
|
2351
2500
|
|
|
2352
2501
|
if (yearField.indexOf('-') === -1 && yearField.indexOf(',') === -1 &&
|
|
2353
|
-
|
|
2502
|
+
schedule.pattern.date !== '*' && description.indexOf(' at ') !== -1) {
|
|
2354
2503
|
// US dates take a comma before the year ("January 1, 2030"); UK dates
|
|
2355
2504
|
// do not ("1 January 2030").
|
|
2356
2505
|
const yearGlue = opts.style.dayFirst ? ' ' : ', ';
|
|
@@ -2520,7 +2669,7 @@ function getWeekday(d: number | string, opts: NormalizedOptions): string {
|
|
|
2520
2669
|
return (weekday && weekday[opts.short ? 1 : 0]) as string;
|
|
2521
2670
|
}
|
|
2522
2671
|
|
|
2523
|
-
// The English language module: the
|
|
2672
|
+
// The English language module: the Schedule renderer plus the language-owned
|
|
2524
2673
|
// strings and option normalization.
|
|
2525
2674
|
const en: Language = {
|
|
2526
2675
|
describe,
|