cronli5 0.2.1 → 0.3.4
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 +109 -0
- package/README.md +4 -4
- 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 +320 -240
- package/dist/lang/zh.js +320 -240
- package/package.json +6 -6
- 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 +386 -245
- 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/es/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// The Spanish language module: renders an analyzed cron pattern (the
|
|
1
|
+
// The Spanish language module: renders an analyzed cron pattern (the Schedule
|
|
2
2
|
// produced by core `analyze`) as natural Spanish. Anchored to RAE/DPD and
|
|
3
3
|
// FundéuRAE conventions; see notes.md for the decisions and trade-offs.
|
|
4
4
|
//
|
|
5
5
|
// Spanish is the pilot language for the i18n architecture
|
|
6
|
-
// (docs/i18n-design.md §7): it consumes only the
|
|
6
|
+
// (docs/i18n-design.md §7): it consumes only the Schedule, owns all of its
|
|
7
7
|
// words, and is free to re-plan where Spanish grammar prefers a
|
|
8
8
|
// different shape than the plan hint (e.g. wildcard minutes over hour
|
|
9
9
|
// lists render as per-hour windows).
|
|
@@ -12,21 +12,23 @@ import {clockDigits, numeral} from '../../core/format.js';
|
|
|
12
12
|
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
13
13
|
import {isOpenStep} from '../../core/shapes.js';
|
|
14
14
|
import {
|
|
15
|
-
arithmeticStep, hourListStride, offsetCleanStride,
|
|
16
|
-
segmentsOf, singleValues, stepSegment
|
|
17
|
-
} from '../../core/
|
|
15
|
+
arithmeticStep, hourListStride, offsetCleanStride,
|
|
16
|
+
renderStride as chooseStride, segmentsOf, singleValues, stepSegment
|
|
17
|
+
} from '../../core/cadence.js';
|
|
18
|
+
import {orderWeekdaysForDisplay} from '../../core/weekday.js';
|
|
19
|
+
import {toFieldNumber} from '../../core/util.js';
|
|
18
20
|
import type {Cronli5Options} from '../../types.js';
|
|
19
21
|
import type {
|
|
20
|
-
HourTimesPlan,
|
|
22
|
+
HourTimesPlan, Schedule, Language, NormalizedOptions, PlanNode,
|
|
21
23
|
Segment
|
|
22
|
-
} from '../../core/
|
|
24
|
+
} from '../../core/schedule.js';
|
|
23
25
|
import {resolveDialect, type SpanishStyle} from './dialects.js';
|
|
24
26
|
|
|
25
27
|
// Normalized options carrying Spanish's own style shape.
|
|
26
28
|
type Opts = NormalizedOptions<SpanishStyle>;
|
|
27
29
|
|
|
28
30
|
// The erased renderer signature the dispatch table maps to.
|
|
29
|
-
type Renderer = (
|
|
31
|
+
type Renderer = (schedule: Schedule, plan: PlanNode, opts: Opts) => string;
|
|
30
32
|
|
|
31
33
|
// A `step` segment, narrowed from the discriminated `Segment` union.
|
|
32
34
|
type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
@@ -130,22 +132,22 @@ function normalizeOptions(options?: Cronli5Options): Opts {
|
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
// Render an analyzed cron pattern (the
|
|
134
|
-
function describe(
|
|
135
|
-
return applyYear(render(
|
|
135
|
+
// Render an analyzed cron pattern (the Schedule) as Spanish.
|
|
136
|
+
function describe(schedule: Schedule, opts: Opts): string {
|
|
137
|
+
return applyYear(render(schedule, schedule.plan, opts), schedule, opts);
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
// Render one plan node. `composeSeconds` recurses with its `rest` plan.
|
|
139
141
|
// When BOTH date and weekday are restricted (a date-OR-weekday union), the
|
|
140
142
|
// result is wrapped in the unified `[month] [time], ya sea <DOM> o <DOW>`
|
|
141
143
|
// frame regardless of arm shapes or month type.
|
|
142
|
-
function render(
|
|
144
|
+
function render(schedule: Schedule, plan: PlanNode, opts: Opts): string {
|
|
143
145
|
// Each renderer narrows `plan` to its own `kind`; the dispatch table is
|
|
144
146
|
// keyed by that discriminant, so the union-to-specific match is sound but
|
|
145
147
|
// not expressible without a cast.
|
|
146
|
-
const phrase = (renderers[plan.kind] as Renderer)(
|
|
148
|
+
const phrase = (renderers[plan.kind] as Renderer)(schedule, plan, opts);
|
|
147
149
|
|
|
148
|
-
if (!isDateWeekdayUnion(
|
|
150
|
+
if (!isDateWeekdayUnion(schedule)) {
|
|
149
151
|
return phrase;
|
|
150
152
|
}
|
|
151
153
|
|
|
@@ -153,55 +155,56 @@ function render(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
153
155
|
// (leadingQualifier and trailingQualifier both return '' for union patterns).
|
|
154
156
|
// Front the shared month (possibly with a trailing comma for enumerations),
|
|
155
157
|
// then append the union correlative last.
|
|
156
|
-
const lead = unionMonthLeadFull(
|
|
158
|
+
const lead = unionMonthLeadFull(schedule);
|
|
157
159
|
|
|
158
|
-
return (lead ? lead + ' ' : '') + phrase + unionYaseaSuffix(
|
|
160
|
+
return (lead ? lead + ' ' : '') + phrase + unionYaseaSuffix(schedule, opts);
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
// --- Seconds renderers. ---
|
|
162
164
|
|
|
163
165
|
function renderEverySecond(
|
|
164
|
-
|
|
166
|
+
schedule: Schedule,
|
|
165
167
|
plan: Extract<PlanNode, {kind: 'everySecond'}>,
|
|
166
168
|
opts: Opts
|
|
167
169
|
): string {
|
|
168
|
-
return 'cada segundo' + trailingQualifier(
|
|
170
|
+
return 'cada segundo' + trailingQualifier(schedule, opts);
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
function renderStandaloneSeconds(
|
|
172
|
-
|
|
174
|
+
schedule: Schedule,
|
|
173
175
|
plan: Extract<PlanNode, {kind: 'standaloneSeconds'}>,
|
|
174
176
|
opts: Opts
|
|
175
177
|
): string {
|
|
176
|
-
return secondsLeadClause(
|
|
178
|
+
return secondsLeadClause(schedule, opts) + trailingQualifier(schedule, opts);
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
function renderSecondPastMinute(
|
|
180
|
-
|
|
182
|
+
schedule: Schedule,
|
|
181
183
|
plan: Extract<PlanNode, {kind: 'secondPastMinute'}>,
|
|
182
184
|
opts: Opts
|
|
183
185
|
): string {
|
|
184
|
-
return 'en el segundo ' +
|
|
185
|
-
trailingQualifier(
|
|
186
|
+
return 'en el segundo ' + schedule.pattern.second + ' de cada minuto' +
|
|
187
|
+
trailingQualifier(schedule, opts);
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
// A meaningful second combined with a single specific minute (and an open
|
|
189
191
|
// hour): a single second folds into the minute anchor; a list, range, or
|
|
190
192
|
// step leads with its own clause.
|
|
191
193
|
function renderSecondsWithinMinute(
|
|
192
|
-
|
|
194
|
+
schedule: Schedule,
|
|
193
195
|
plan: Extract<PlanNode, {kind: 'secondsWithinMinute'}>,
|
|
194
196
|
opts: Opts
|
|
195
197
|
): string {
|
|
196
|
-
const minuteField =
|
|
198
|
+
const minuteField = schedule.pattern.minute;
|
|
197
199
|
|
|
198
200
|
if (plan.singleSecond) {
|
|
199
201
|
return 'en el minuto ' + minuteField + ' y el segundo ' +
|
|
200
|
-
|
|
202
|
+
schedule.pattern.second + ' de cada hora' +
|
|
203
|
+
trailingQualifier(schedule, opts);
|
|
201
204
|
}
|
|
202
205
|
|
|
203
|
-
return secondsLeadClause(
|
|
204
|
-
' de cada hora' + trailingQualifier(
|
|
206
|
+
return secondsLeadClause(schedule, opts) + ', en el minuto ' + minuteField +
|
|
207
|
+
' de cada hora' + trailingQualifier(schedule, opts);
|
|
205
208
|
}
|
|
206
209
|
|
|
207
210
|
// A seconds list nested into one or more fixed clock times ("..., en los
|
|
@@ -210,7 +213,7 @@ function renderSecondsWithinMinute(
|
|
|
210
213
|
// are listed. The clock time follows with the genitive "de", so the stride
|
|
211
214
|
// drops its "de cada minuto" anchor.
|
|
212
215
|
function secondsListAtClock(
|
|
213
|
-
|
|
216
|
+
schedule: Schedule,
|
|
214
217
|
rest: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
215
218
|
opts: Opts
|
|
216
219
|
): string {
|
|
@@ -222,10 +225,10 @@ function secondsListAtClock(
|
|
|
222
225
|
// prepend "de " to produce the genitive form "de las 09:00 y 17:00".
|
|
223
226
|
const clockList = grouped.startsWith('a ') ? grouped.slice(2) : grouped;
|
|
224
227
|
const stride =
|
|
225
|
-
strideFromSegments(segmentsOf(
|
|
228
|
+
strideFromSegments(segmentsOf(schedule, 'second'), 'segundo', '', opts);
|
|
226
229
|
const secondsPhrase = stride ?? 'en los segundos ' +
|
|
227
|
-
joinList(segmentWords(segmentsOf(
|
|
228
|
-
const dayFrame = trailingQualifier(
|
|
230
|
+
joinList(segmentWords(segmentsOf(schedule, 'second')));
|
|
231
|
+
const dayFrame = trailingQualifier(schedule, opts);
|
|
229
232
|
|
|
230
233
|
return (dayFrame ? dayFrame.trimStart() + ', ' : '') +
|
|
231
234
|
secondsPhrase + ' de ' + clockList;
|
|
@@ -236,35 +239,37 @@ function secondsListAtClock(
|
|
|
236
239
|
// when that does not apply (a non-clock rest, a multi-valued minute, or an
|
|
237
240
|
// hour that is not a stride).
|
|
238
241
|
function composeHourCadence(
|
|
239
|
-
|
|
242
|
+
schedule: Schedule,
|
|
240
243
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
241
244
|
opts: Opts
|
|
242
245
|
): string | null {
|
|
243
246
|
const clockRest = plan.rest.kind === 'clockTimes' ||
|
|
244
247
|
plan.rest.kind === 'compactClockTimes';
|
|
245
248
|
|
|
246
|
-
if (!clockRest ||
|
|
249
|
+
if (!clockRest || schedule.shapes.minute !== 'single') {
|
|
247
250
|
return null;
|
|
248
251
|
}
|
|
249
252
|
|
|
250
|
-
const minute = +
|
|
253
|
+
const minute = +schedule.pattern.minute;
|
|
251
254
|
|
|
252
|
-
return hourCadence(
|
|
255
|
+
return hourCadence(schedule, minute, opts) ??
|
|
256
|
+
hourRangeCadence(schedule, minute, opts);
|
|
253
257
|
}
|
|
254
258
|
|
|
255
259
|
// A wildcard or stepped second with a fixed minute across one or more specific
|
|
256
260
|
// hours: the seconds confine to the clock time(s), each minute named.
|
|
257
261
|
function isPinnedMinuteSeconds(
|
|
258
|
-
|
|
262
|
+
schedule: Schedule,
|
|
259
263
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
260
264
|
): plan is Extract<PlanNode, {kind: 'composeSeconds'}> &
|
|
261
265
|
{rest: Extract<PlanNode, {kind: 'clockTimes'}>} {
|
|
262
266
|
return plan.rest.kind === 'clockTimes' &&
|
|
263
|
-
(
|
|
267
|
+
(schedule.shapes.second === 'wildcard' ||
|
|
268
|
+
schedule.shapes.second === 'step');
|
|
264
269
|
}
|
|
265
270
|
|
|
266
271
|
function renderComposeSeconds(
|
|
267
|
-
|
|
272
|
+
schedule: Schedule,
|
|
268
273
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
269
274
|
opts: Opts
|
|
270
275
|
): string {
|
|
@@ -272,7 +277,7 @@ function renderComposeSeconds(
|
|
|
272
277
|
// minute is a cadence, not a wall of clock times: the second/minute lead,
|
|
273
278
|
// then the hour cadence ("en el segundo 30 de cada hora, cada dos horas").
|
|
274
279
|
// The clock-time rest would otherwise cross-multiply the hours.
|
|
275
|
-
const hourCad = composeHourCadence(
|
|
280
|
+
const hourCad = composeHourCadence(schedule, plan, opts);
|
|
276
281
|
|
|
277
282
|
if (hourCad !== null) {
|
|
278
283
|
return hourCad;
|
|
@@ -280,28 +285,28 @@ function renderComposeSeconds(
|
|
|
280
285
|
|
|
281
286
|
// A wildcard or stepped second with the minute pinned to a single value
|
|
282
287
|
// across one or more specific hours: the seconds confine to the clock time.
|
|
283
|
-
if (isPinnedMinuteSeconds(
|
|
284
|
-
return pinnedMinuteSeconds(
|
|
288
|
+
if (isPinnedMinuteSeconds(schedule, plan)) {
|
|
289
|
+
return pinnedMinuteSeconds(schedule, plan.rest, opts);
|
|
285
290
|
}
|
|
286
291
|
|
|
287
292
|
// Seconds list + fixed clock time: nest the seconds into the clock time(s)
|
|
288
293
|
// with genitive "de las HH:MM" instead of "de cada minuto"; the minute is
|
|
289
294
|
// fixed so "de cada minuto" is misleading. Single seconds already fold into
|
|
290
295
|
// the time in the clockTimes renderer; step seconds keep their own clause.
|
|
291
|
-
if (plan.rest.kind === 'clockTimes' &&
|
|
292
|
-
return secondsListAtClock(
|
|
296
|
+
if (plan.rest.kind === 'clockTimes' && schedule.shapes.second === 'list') {
|
|
297
|
+
return secondsListAtClock(schedule, plan.rest, opts);
|
|
293
298
|
}
|
|
294
299
|
|
|
295
300
|
// Second-step + fixed minute + hour range + weekday: anchor the cadence to
|
|
296
301
|
// the minute after the weekday + hour-range frame.
|
|
297
|
-
if (plan.rest.kind === 'hourRange' &&
|
|
298
|
-
|
|
302
|
+
if (plan.rest.kind === 'hourRange' && schedule.shapes.second === 'step' &&
|
|
303
|
+
schedule.pattern.weekday !== '*') {
|
|
299
304
|
const restNode = plan.rest;
|
|
300
305
|
const window = hourWindow(boundedWindow(restNode), opts);
|
|
301
|
-
const dayFrame = weekdayQualifier(
|
|
306
|
+
const dayFrame = weekdayQualifier(schedule) + monthScope(schedule);
|
|
302
307
|
const cadence = 'cada ' +
|
|
303
|
-
numero(stepSegment(
|
|
304
|
-
' segundos del minuto ' +
|
|
308
|
+
numero(stepSegment(schedule, 'second').interval, opts) +
|
|
309
|
+
' segundos del minuto ' + schedule.pattern.minute;
|
|
305
310
|
|
|
306
311
|
return dayFrame + ', ' + window + ', ' + cadence;
|
|
307
312
|
}
|
|
@@ -311,8 +316,9 @@ function renderComposeSeconds(
|
|
|
311
316
|
// Bind them with the genitive "de" ("cada segundo de cada dos minutos"),
|
|
312
317
|
// mirroring English. Other strides, a restricted hour, and an hour cadence
|
|
313
318
|
// keep the juxtaposed form.
|
|
314
|
-
if (isEveryOtherMinuteSeconds(
|
|
315
|
-
return secondsLeadClause(
|
|
319
|
+
if (isEveryOtherMinuteSeconds(schedule, plan)) {
|
|
320
|
+
return secondsLeadClause(schedule, opts) + ' de ' +
|
|
321
|
+
render(schedule, plan.rest, opts);
|
|
316
322
|
}
|
|
317
323
|
|
|
318
324
|
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
@@ -320,24 +326,25 @@ function renderComposeSeconds(
|
|
|
320
326
|
// double it. A wildcard or stepped second is not folded there (no
|
|
321
327
|
// clockSecond), so it still leads its own clause here.
|
|
322
328
|
const restOwnsLead = plan.rest.kind === 'compactClockTimes' &&
|
|
323
|
-
|
|
324
|
-
const lead = restOwnsLead ? '' : secondsLeadClause(
|
|
329
|
+
schedule.analyses.clockSecond;
|
|
330
|
+
const lead = restOwnsLead ? '' : secondsLeadClause(schedule, opts) + ', ';
|
|
325
331
|
|
|
326
|
-
return lead + render(
|
|
332
|
+
return lead + render(schedule, plan.rest, opts);
|
|
327
333
|
}
|
|
328
334
|
|
|
329
335
|
// A wildcard second over an unoffset minute */2 with a wildcard hour: the two
|
|
330
336
|
// cadences read as contradictory side by side, so they bind into one.
|
|
331
337
|
function isEveryOtherMinuteSeconds(
|
|
332
|
-
|
|
338
|
+
schedule: Schedule,
|
|
333
339
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
334
340
|
): boolean {
|
|
335
341
|
if (plan.rest.kind !== 'minuteFrequency' ||
|
|
336
|
-
|
|
342
|
+
schedule.shapes.second !== 'wildcard' ||
|
|
343
|
+
schedule.shapes.hour !== 'wildcard') {
|
|
337
344
|
return false;
|
|
338
345
|
}
|
|
339
346
|
|
|
340
|
-
const minuteStep = stepSegment(
|
|
347
|
+
const minuteStep = stepSegment(schedule, 'minute');
|
|
341
348
|
|
|
342
349
|
return minuteStep.startToken === '*' && minuteStep.interval === 2;
|
|
343
350
|
}
|
|
@@ -353,11 +360,11 @@ function isEveryOtherMinuteSeconds(
|
|
|
353
360
|
// minute is an unambiguous clock time, so the genitive "de las 09:05" form
|
|
354
361
|
// reads it as the minute, never the hour.
|
|
355
362
|
function pinnedMinuteSeconds(
|
|
356
|
-
|
|
363
|
+
schedule: Schedule,
|
|
357
364
|
rest: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
358
365
|
opts: Opts
|
|
359
366
|
): string {
|
|
360
|
-
const dayTrail = leadingQualifier(
|
|
367
|
+
const dayTrail = leadingQualifier(schedule, opts).trimEnd();
|
|
361
368
|
const trail = dayTrail ? ', ' + dayTrail : '';
|
|
362
369
|
|
|
363
370
|
// The "durante un minuto a las 9" duration form drops the clock minute, so it
|
|
@@ -365,18 +372,18 @@ function pinnedMinuteSeconds(
|
|
|
365
372
|
// minute LIST whose first value is 0 (e.g. */45 → :00, :45) must name each
|
|
366
373
|
// minute, never collapse to the bare hour (which once repeated it, "a las 9 y
|
|
367
374
|
// 9"), so it takes the explicit clock list.
|
|
368
|
-
if (+rest.times[0].minute === 0 &&
|
|
369
|
-
return secondsLeadClause(
|
|
375
|
+
if (+rest.times[0].minute === 0 && schedule.shapes.minute === 'single') {
|
|
376
|
+
return secondsLeadClause(schedule, opts) + ' durante un minuto ' +
|
|
370
377
|
durationHourList(rest.times, opts) + trail;
|
|
371
378
|
}
|
|
372
379
|
|
|
373
|
-
return secondsLeadClause(
|
|
380
|
+
return secondsLeadClause(schedule, opts) + ' de ' +
|
|
374
381
|
explicitClockList(rest.times, opts) + trail;
|
|
375
382
|
}
|
|
376
383
|
|
|
377
384
|
// The leading clause describing a second field relative to the minute.
|
|
378
|
-
function secondsLeadClause(
|
|
379
|
-
return secondsClause(
|
|
385
|
+
function secondsLeadClause(schedule: Schedule, opts: Opts): string {
|
|
386
|
+
return secondsClause(schedule, 'minuto', opts);
|
|
380
387
|
}
|
|
381
388
|
|
|
382
389
|
// The second clause counted against an arbitrary anchor. The anchor is
|
|
@@ -384,16 +391,16 @@ function secondsLeadClause(ir: IR, opts: Opts): string {
|
|
|
384
391
|
// pinned minute 0 into the hour and counts the second "de cada hora" instead
|
|
385
392
|
// ("en el segundo 30 de cada hora"), so the minute-0 confinement is stated,
|
|
386
393
|
// not dropped.
|
|
387
|
-
function secondsClause(
|
|
388
|
-
const secondField =
|
|
389
|
-
const shape =
|
|
394
|
+
function secondsClause(schedule: Schedule, anchor: string, opts: Opts): string {
|
|
395
|
+
const secondField = schedule.pattern.second;
|
|
396
|
+
const shape = schedule.shapes.second;
|
|
390
397
|
|
|
391
398
|
if (secondField === '*') {
|
|
392
399
|
return 'cada segundo';
|
|
393
400
|
}
|
|
394
401
|
|
|
395
402
|
if (shape === 'step') {
|
|
396
|
-
return stepCycle60(stepSegment(
|
|
403
|
+
return stepCycle60(stepSegment(schedule, 'second'), 'segundo',
|
|
397
404
|
anchor, opts);
|
|
398
405
|
}
|
|
399
406
|
|
|
@@ -408,55 +415,55 @@ function secondsClause(ir: IR, anchor: string, opts: Opts): string {
|
|
|
408
415
|
return 'en el segundo ' + secondField + ' de cada ' + anchor;
|
|
409
416
|
}
|
|
410
417
|
|
|
411
|
-
return strideFromSegments(segmentsOf(
|
|
418
|
+
return strideFromSegments(segmentsOf(schedule, 'second'), 'segundo', anchor,
|
|
412
419
|
opts) ?? 'en los segundos ' +
|
|
413
|
-
joinList(segmentWords(segmentsOf(
|
|
420
|
+
joinList(segmentWords(segmentsOf(schedule, 'second'))) +
|
|
414
421
|
' de cada ' + anchor;
|
|
415
422
|
}
|
|
416
423
|
|
|
417
424
|
// --- Minute renderers. ---
|
|
418
425
|
|
|
419
426
|
function renderEveryMinute(
|
|
420
|
-
|
|
427
|
+
schedule: Schedule,
|
|
421
428
|
plan: Extract<PlanNode, {kind: 'everyMinute'}>,
|
|
422
429
|
opts: Opts
|
|
423
430
|
): string {
|
|
424
|
-
return 'cada minuto' + trailingQualifier(
|
|
431
|
+
return 'cada minuto' + trailingQualifier(schedule, opts);
|
|
425
432
|
}
|
|
426
433
|
|
|
427
434
|
function renderSingleMinute(
|
|
428
|
-
|
|
435
|
+
schedule: Schedule,
|
|
429
436
|
plan: Extract<PlanNode, {kind: 'singleMinute'}>,
|
|
430
437
|
opts: Opts
|
|
431
438
|
): string {
|
|
432
|
-
return 'en el minuto ' +
|
|
433
|
-
trailingQualifier(
|
|
439
|
+
return 'en el minuto ' + schedule.pattern.minute + ' de cada hora' +
|
|
440
|
+
trailingQualifier(schedule, opts);
|
|
434
441
|
}
|
|
435
442
|
|
|
436
443
|
function renderRangeOfMinutes(
|
|
437
|
-
|
|
444
|
+
schedule: Schedule,
|
|
438
445
|
plan: Extract<PlanNode, {kind: 'rangeOfMinutes'}>,
|
|
439
446
|
opts: Opts
|
|
440
447
|
): string {
|
|
441
|
-
return minuteRangeLead(
|
|
442
|
-
trailingQualifier(
|
|
448
|
+
return minuteRangeLead(schedule.pattern.minute) + ' de cada hora' +
|
|
449
|
+
trailingQualifier(schedule, opts);
|
|
443
450
|
}
|
|
444
451
|
|
|
445
452
|
function renderMultipleMinutes(
|
|
446
|
-
|
|
453
|
+
schedule: Schedule,
|
|
447
454
|
plan: Extract<PlanNode, {kind: 'multipleMinutes'}>,
|
|
448
455
|
opts: Opts
|
|
449
456
|
): string {
|
|
450
|
-
return minutesList(
|
|
457
|
+
return minutesList(schedule, opts) + trailingQualifier(schedule, opts);
|
|
451
458
|
}
|
|
452
459
|
|
|
453
460
|
// "en los minutos 5, 10 y 30 de cada hora". An offset/uneven step the core
|
|
454
461
|
// enumerated to this list reads as a stride cadence when the fires form a
|
|
455
462
|
// long-enough progression.
|
|
456
|
-
function minutesList(
|
|
457
|
-
return strideFromSegments(segmentsOf(
|
|
463
|
+
function minutesList(schedule: Schedule, opts: Opts): string {
|
|
464
|
+
return strideFromSegments(segmentsOf(schedule, 'minute'), 'minuto', 'hora',
|
|
458
465
|
opts) ?? 'en los minutos ' +
|
|
459
|
-
joinList(segmentWords(segmentsOf(
|
|
466
|
+
joinList(segmentWords(segmentsOf(schedule, 'minute'))) + ' de cada hora';
|
|
460
467
|
}
|
|
461
468
|
|
|
462
469
|
// "cada minuto del 0 al 30". The standalone renderer adds "de cada hora";
|
|
@@ -558,17 +565,17 @@ function spanHours(hours: number[]): string {
|
|
|
558
565
|
|
|
559
566
|
// A repeating minute step, qualified by the active hour window(s).
|
|
560
567
|
function renderMinuteFrequency(
|
|
561
|
-
|
|
568
|
+
schedule: Schedule,
|
|
562
569
|
plan: Extract<PlanNode, {kind: 'minuteFrequency'}>,
|
|
563
570
|
opts: Opts
|
|
564
571
|
): string {
|
|
565
|
-
let phrase = stepCycle60(stepSegment(
|
|
572
|
+
let phrase = stepCycle60(stepSegment(schedule, 'minute'), 'minuto',
|
|
566
573
|
'hora', opts);
|
|
567
574
|
|
|
568
575
|
if (plan.hours.kind === 'during') {
|
|
569
576
|
// A uneven hour stride confines the minute cadence to its own bounded hour
|
|
570
577
|
// cadence ("cada 15 minutos, cada cinco horas de las 00:00 a las 20:00").
|
|
571
|
-
const cadence = unevenHourCadence(
|
|
578
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
572
579
|
|
|
573
580
|
if (cadence) {
|
|
574
581
|
phrase += ', ' + cadence;
|
|
@@ -576,9 +583,9 @@ function renderMinuteFrequency(
|
|
|
576
583
|
else {
|
|
577
584
|
// An offset step (e.g. 1/2) arrives here; a single step reads as a
|
|
578
585
|
// confinement, not the verbose window list.
|
|
579
|
-
phrase += singleHourStep(
|
|
580
|
-
', ' + stepHourSpan(stepSegment(
|
|
581
|
-
' ' + hourSpanFromTimes(
|
|
586
|
+
phrase += singleHourStep(schedule.analyses.segments.hour) ?
|
|
587
|
+
', ' + stepHourSpan(stepSegment(schedule, 'hour'), opts) :
|
|
588
|
+
' ' + hourSpanFromTimes(schedule, plan.hours.times, opts);
|
|
582
589
|
}
|
|
583
590
|
}
|
|
584
591
|
else if (plan.hours.kind === 'window') {
|
|
@@ -587,10 +594,10 @@ function renderMinuteFrequency(
|
|
|
587
594
|
else if (plan.hours.kind === 'step') {
|
|
588
595
|
// A clean stride is a confinement ("las horas pares", or the active-hour
|
|
589
596
|
// list), never a juxtaposed cadence ("cada dos horas").
|
|
590
|
-
phrase += ', ' + stepHourSpan(stepSegment(
|
|
597
|
+
phrase += ', ' + stepHourSpan(stepSegment(schedule, 'hour'), opts);
|
|
591
598
|
}
|
|
592
599
|
|
|
593
|
-
return phrase + trailingQualifier(
|
|
600
|
+
return phrase + trailingQualifier(schedule, opts);
|
|
594
601
|
}
|
|
595
602
|
|
|
596
603
|
// "cada minuto de las 9:00 a las 9:29 de la mañana". A wildcard minute is the
|
|
@@ -598,144 +605,144 @@ function renderMinuteFrequency(
|
|
|
598
605
|
// 09:00") rather than a synthesized "de las HH:00 a las HH:59" range the
|
|
599
606
|
// source never stated; a plain range is a real window and keeps "de … a …".
|
|
600
607
|
function renderMinuteSpanInHour(
|
|
601
|
-
|
|
608
|
+
schedule: Schedule,
|
|
602
609
|
plan: Extract<PlanNode, {kind: 'minuteSpanInHour'}>,
|
|
603
610
|
opts: Opts
|
|
604
611
|
): string {
|
|
605
|
-
if (
|
|
612
|
+
if (schedule.pattern.minute === '*') {
|
|
606
613
|
return 'cada minuto de la hora ' +
|
|
607
614
|
fromTime(timePhrase(plan.hour, 0, null, opts)) +
|
|
608
|
-
trailingQualifier(
|
|
615
|
+
trailingQualifier(schedule, opts);
|
|
609
616
|
}
|
|
610
617
|
|
|
611
618
|
return 'cada minuto ' +
|
|
612
619
|
timeRange({hour: plan.hour, minute: plan.span[0]},
|
|
613
620
|
{hour: plan.hour, minute: plan.span[1]}, opts) +
|
|
614
|
-
trailingQualifier(
|
|
621
|
+
trailingQualifier(schedule, opts);
|
|
615
622
|
}
|
|
616
623
|
|
|
617
624
|
// A minute window under discrete hours. Spanish re-plans the
|
|
618
625
|
// wildcard form: rather than "during the X hours", each hour reads as its
|
|
619
626
|
// own window ("de las 9:00 a las 9:59").
|
|
620
627
|
function renderMinutesAcrossHours(
|
|
621
|
-
|
|
628
|
+
schedule: Schedule,
|
|
622
629
|
plan: Extract<PlanNode, {kind: 'minutesAcrossHours'}>,
|
|
623
630
|
opts: Opts
|
|
624
631
|
): string {
|
|
625
632
|
// A uneven hour stride reads as a cadence, not a wall of hour columns: the
|
|
626
633
|
// minute lead, then "cada N horas de las X a las Y".
|
|
627
|
-
const cadence = unevenHourCadence(
|
|
634
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
628
635
|
|
|
629
636
|
if (plan.form === 'wildcard') {
|
|
630
637
|
if (cadence !== null) {
|
|
631
|
-
return 'cada minuto, ' + cadence + trailingQualifier(
|
|
638
|
+
return 'cada minuto, ' + cadence + trailingQualifier(schedule, opts);
|
|
632
639
|
}
|
|
633
640
|
|
|
634
|
-
if (singleHourStep(
|
|
641
|
+
if (singleHourStep(schedule.analyses.segments.hour)) {
|
|
635
642
|
return 'cada minuto, ' +
|
|
636
|
-
stepHourSpan(stepSegment(
|
|
637
|
-
trailingQualifier(
|
|
643
|
+
stepHourSpan(stepSegment(schedule, 'hour'), opts) +
|
|
644
|
+
trailingQualifier(schedule, opts);
|
|
638
645
|
}
|
|
639
646
|
|
|
640
|
-
return 'cada minuto ' + hourSpanFromTimes(
|
|
641
|
-
trailingQualifier(
|
|
647
|
+
return 'cada minuto ' + hourSpanFromTimes(schedule, plan.times, opts) +
|
|
648
|
+
trailingQualifier(schedule, opts);
|
|
642
649
|
}
|
|
643
650
|
|
|
644
651
|
const lead = plan.form === 'range' ?
|
|
645
|
-
minuteRangeLead(
|
|
646
|
-
minutesList(
|
|
652
|
+
minuteRangeLead(schedule.pattern.minute) :
|
|
653
|
+
minutesList(schedule, opts);
|
|
647
654
|
|
|
648
655
|
if (cadence !== null) {
|
|
649
|
-
return lead + ', ' + cadence + trailingQualifier(
|
|
656
|
+
return lead + ', ' + cadence + trailingQualifier(schedule, opts);
|
|
650
657
|
}
|
|
651
658
|
|
|
652
|
-
return lead + ', ' + atHourTimes(
|
|
653
|
-
trailingQualifier(
|
|
659
|
+
return lead + ', ' + atHourTimes(schedule, plan.times, opts) +
|
|
660
|
+
trailingQualifier(schedule, opts);
|
|
654
661
|
}
|
|
655
662
|
|
|
656
663
|
function renderMinuteSpanAcrossHourStep(
|
|
657
|
-
|
|
664
|
+
schedule: Schedule,
|
|
658
665
|
plan: Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>,
|
|
659
666
|
opts: Opts
|
|
660
667
|
): string {
|
|
661
|
-
const segment = stepSegment(
|
|
668
|
+
const segment = stepSegment(schedule, 'hour');
|
|
662
669
|
// A bounded or uneven hour step reads as its endpoint-pinning cadence; an
|
|
663
670
|
// offset-clean step keeps its confinement / per-step phrasing.
|
|
664
|
-
const cadence = unevenHourCadence(
|
|
671
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
665
672
|
|
|
666
673
|
// A wildcard minute (a cadence) is reached only for a clean stride (a bounded
|
|
667
674
|
// or uneven step routes through minutesAcrossHours instead) and is confined.
|
|
668
675
|
if (plan.form === 'wildcard') {
|
|
669
676
|
return 'cada minuto, ' + stepHourSpan(segment, opts) +
|
|
670
|
-
trailingQualifier(
|
|
677
|
+
trailingQualifier(schedule, opts);
|
|
671
678
|
}
|
|
672
679
|
|
|
673
680
|
// A minute list keeps the same cadence clause as the range; only its lead
|
|
674
681
|
// differs ("en los minutos 5 y 30 de cada hora" vs "cada minuto del 0 al
|
|
675
682
|
// 30").
|
|
676
683
|
const lead = plan.form === 'list' ?
|
|
677
|
-
minutesList(
|
|
678
|
-
minuteRangeLead(
|
|
684
|
+
minutesList(schedule, opts) :
|
|
685
|
+
minuteRangeLead(schedule.pattern.minute);
|
|
679
686
|
|
|
680
687
|
return lead + ', ' +
|
|
681
|
-
(cadence ?? stepHours(segment, opts)) + trailingQualifier(
|
|
688
|
+
(cadence ?? stepHours(segment, opts)) + trailingQualifier(schedule, opts);
|
|
682
689
|
}
|
|
683
690
|
|
|
684
691
|
// --- Hour renderers. ---
|
|
685
692
|
|
|
686
693
|
function renderEveryHour(
|
|
687
|
-
|
|
694
|
+
schedule: Schedule,
|
|
688
695
|
plan: Extract<PlanNode, {kind: 'everyHour'}>,
|
|
689
696
|
opts: Opts
|
|
690
697
|
): string {
|
|
691
|
-
return 'cada hora' + trailingQualifier(
|
|
698
|
+
return 'cada hora' + trailingQualifier(schedule, opts);
|
|
692
699
|
}
|
|
693
700
|
|
|
694
701
|
function renderHourRange(
|
|
695
|
-
|
|
702
|
+
schedule: Schedule,
|
|
696
703
|
plan: Extract<PlanNode, {kind: 'hourRange'}>,
|
|
697
704
|
opts: Opts
|
|
698
705
|
): string {
|
|
699
706
|
const window = hourWindow(boundedWindow(plan), opts);
|
|
700
707
|
|
|
701
708
|
if (plan.minuteForm === 'wildcard') {
|
|
702
|
-
return 'cada minuto ' + window + trailingQualifier(
|
|
709
|
+
return 'cada minuto ' + window + trailingQualifier(schedule, opts);
|
|
703
710
|
}
|
|
704
711
|
|
|
705
712
|
if (plan.minuteForm === 'range') {
|
|
706
|
-
return minuteRangeLead(
|
|
707
|
-
trailingQualifier(
|
|
713
|
+
return minuteRangeLead(schedule.pattern.minute) + ', ' + window +
|
|
714
|
+
trailingQualifier(schedule, opts);
|
|
708
715
|
}
|
|
709
716
|
|
|
710
717
|
// On the hour the window joins directly ("cada hora de las 9:00 a las
|
|
711
718
|
// 17:00"); a discrete minute anchors its own clause first.
|
|
712
|
-
if (
|
|
713
|
-
return 'cada hora ' + window + trailingQualifier(
|
|
719
|
+
if (schedule.pattern.minute === '0') {
|
|
720
|
+
return 'cada hora ' + window + trailingQualifier(schedule, opts);
|
|
714
721
|
}
|
|
715
722
|
|
|
716
|
-
const lead =
|
|
717
|
-
'en el minuto ' +
|
|
718
|
-
minutesList(
|
|
723
|
+
const lead = schedule.shapes.minute === 'single' ?
|
|
724
|
+
'en el minuto ' + schedule.pattern.minute + ' de cada hora' :
|
|
725
|
+
minutesList(schedule, opts);
|
|
719
726
|
|
|
720
|
-
return lead + ', ' + window + trailingQualifier(
|
|
727
|
+
return lead + ', ' + window + trailingQualifier(schedule, opts);
|
|
721
728
|
}
|
|
722
729
|
|
|
723
730
|
function renderHourStep(
|
|
724
|
-
|
|
731
|
+
schedule: Schedule,
|
|
725
732
|
plan: Extract<PlanNode, {kind: 'hourStep'}>,
|
|
726
733
|
opts: Opts
|
|
727
734
|
): string {
|
|
728
735
|
// A bounded or uneven hour step reads as its endpoint-pinning cadence ("cada
|
|
729
736
|
// dos horas de las 09:00 a las 17:00"); an offset-clean step keeps its bare
|
|
730
737
|
// or "a partir de" cadence.
|
|
731
|
-
const cadence = unevenHourCadence(
|
|
738
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
732
739
|
|
|
733
740
|
if (cadence !== null) {
|
|
734
|
-
return cadence + trailingQualifier(
|
|
741
|
+
return cadence + trailingQualifier(schedule, opts);
|
|
735
742
|
}
|
|
736
743
|
|
|
737
|
-
return stepHours(stepSegment(
|
|
738
|
-
trailingQualifier(
|
|
744
|
+
return stepHours(stepSegment(schedule, 'hour'), opts) +
|
|
745
|
+
trailingQualifier(schedule, opts);
|
|
739
746
|
}
|
|
740
747
|
|
|
741
748
|
// The hour-range plan as a window. The close lands on the top of the final
|
|
@@ -764,8 +771,8 @@ function hourWindow(
|
|
|
764
771
|
|
|
765
772
|
// Whether BOTH the date and weekday fields are restricted (not '*'): cron
|
|
766
773
|
// fires when either condition matches, making this a date-OR-weekday union.
|
|
767
|
-
function isDateWeekdayUnion(
|
|
768
|
-
return
|
|
774
|
+
function isDateWeekdayUnion(schedule: Schedule): boolean {
|
|
775
|
+
return schedule.pattern.date !== '*' && schedule.pattern.weekday !== '*';
|
|
769
776
|
}
|
|
770
777
|
|
|
771
778
|
// The month lead for the unified union frame, with a trailing comma appended
|
|
@@ -773,14 +780,14 @@ function isDateWeekdayUnion(ir: IR): boolean {
|
|
|
773
780
|
// Single month → `en enero`; range → `de enero a marzo`;
|
|
774
781
|
// step/enumeration (≥2 flattened singles) → `en enero, marzo, …, y noviembre,`.
|
|
775
782
|
// Wildcard month → '' (omit; frame starts with the time).
|
|
776
|
-
function unionMonthLeadFull(
|
|
777
|
-
if (
|
|
783
|
+
function unionMonthLeadFull(schedule: Schedule): string {
|
|
784
|
+
if (schedule.pattern.month === '*') {
|
|
778
785
|
return '';
|
|
779
786
|
}
|
|
780
787
|
|
|
781
|
-
const lead = monthPhrase(
|
|
782
|
-
const segments = flattenSteps(segmentsOf(
|
|
783
|
-
const isEnumeration = !monthRanged(
|
|
788
|
+
const lead = monthPhrase(schedule, monthRanged(schedule) ? 'de ' : 'en ');
|
|
789
|
+
const segments = flattenSteps(segmentsOf(schedule, 'month'));
|
|
790
|
+
const isEnumeration = !monthRanged(schedule) && segments.length >= 2;
|
|
784
791
|
|
|
785
792
|
return isEnumeration ? lead + ',' : lead;
|
|
786
793
|
}
|
|
@@ -789,19 +796,30 @@ function unionMonthLeadFull(ir: IR): string {
|
|
|
789
796
|
// Quartz and open-step forms are self-contained; ranges use `del N al M del
|
|
790
797
|
// mes`; a single date reads `el día N` under a restricted month (month is in
|
|
791
798
|
// the lead) or `el N de cada mes` under a wildcard month.
|
|
792
|
-
function domArm(
|
|
793
|
-
const date =
|
|
799
|
+
function domArm(schedule: Schedule, opts: Opts): string {
|
|
800
|
+
const date = schedule.pattern.date;
|
|
794
801
|
const quartz = quartzDatePhrase(date);
|
|
795
802
|
|
|
796
803
|
if (quartz) {
|
|
797
804
|
return quartz;
|
|
798
805
|
}
|
|
799
806
|
|
|
807
|
+
// In the union the `*/2` day-of-month is a parity predicate over the days of
|
|
808
|
+
// the month ("un día impar del mes" = 1, 3, …, 31, resetting each month),
|
|
809
|
+
// not the durative "cada dos días del mes" the standalone form uses. A bare
|
|
810
|
+
// "cada dos días" would mis-imply a continuous every-other-day cadence with
|
|
811
|
+
// no monthly anchor, so the reader could not reconstruct the odd days.
|
|
812
|
+
const parity = parityDayPredicate(date);
|
|
813
|
+
|
|
814
|
+
if (parity) {
|
|
815
|
+
return parity;
|
|
816
|
+
}
|
|
817
|
+
|
|
800
818
|
if (isOpenStep(date)) {
|
|
801
819
|
return stepDates(date, opts);
|
|
802
820
|
}
|
|
803
821
|
|
|
804
|
-
const segments = segmentsOf(
|
|
822
|
+
const segments = segmentsOf(schedule, 'date');
|
|
805
823
|
|
|
806
824
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
807
825
|
return 'del ' + segments[0].bounds[0] + ' al ' +
|
|
@@ -809,7 +827,7 @@ function domArm(ir: IR, opts: Opts): string {
|
|
|
809
827
|
}
|
|
810
828
|
|
|
811
829
|
if (segments.length === 1 && segments[0].kind === 'single') {
|
|
812
|
-
return
|
|
830
|
+
return schedule.pattern.month === '*' ?
|
|
813
831
|
'el ' + segments[0].value + ' de cada mes' :
|
|
814
832
|
'el día ' + segments[0].value;
|
|
815
833
|
}
|
|
@@ -821,16 +839,16 @@ function domArm(ir: IR, opts: Opts): string {
|
|
|
821
839
|
// Quartz forms are self-contained; a single weekday reads `cualquier <name>`;
|
|
822
840
|
// all other forms use the same phrasing as the standalone weekday qualifier
|
|
823
841
|
// (range → `de lunes a viernes`; list/step → `los domingos, …`).
|
|
824
|
-
function dowArm(
|
|
825
|
-
const quartz = quartzWeekdayPhrase(
|
|
842
|
+
function dowArm(schedule: Schedule): string {
|
|
843
|
+
const quartz = quartzWeekdayPhrase(schedule.pattern.weekday);
|
|
826
844
|
|
|
827
845
|
if (quartz) {
|
|
828
846
|
return quartz;
|
|
829
847
|
}
|
|
830
848
|
|
|
831
849
|
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
832
|
-
// form. The
|
|
833
|
-
const segments = orderWeekdaysForDisplay(segmentsOf(
|
|
850
|
+
// form. The Schedule stays canonical (Sunday=0). The helper flattens steps.
|
|
851
|
+
const segments = orderWeekdaysForDisplay(segmentsOf(schedule, 'weekday'));
|
|
834
852
|
const allSingles = segments.every(function single(segment) {
|
|
835
853
|
return segment.kind === 'single';
|
|
836
854
|
});
|
|
@@ -847,8 +865,12 @@ function dowArm(ir: IR): string {
|
|
|
847
865
|
}));
|
|
848
866
|
}
|
|
849
867
|
|
|
868
|
+
// A lone weekday range reads "cualquier día de lunes a viernes" in the union:
|
|
869
|
+
// the leading "cualquier día" makes it a day predicate parallel to the
|
|
870
|
+
// date arm ("el 1 de cada mes o cualquier día de lunes a viernes"), so the
|
|
871
|
+
// union "o" plainly joins two independent day conditions.
|
|
850
872
|
if (segments.length === 1) {
|
|
851
|
-
return weekdayRange(segments[0] as RangeNameSegment);
|
|
873
|
+
return 'cualquier día ' + weekdayRange(segments[0] as RangeNameSegment);
|
|
852
874
|
}
|
|
853
875
|
|
|
854
876
|
return joinList(segments.map(function name(segment) {
|
|
@@ -859,23 +881,23 @@ function dowArm(ir: IR): string {
|
|
|
859
881
|
}
|
|
860
882
|
|
|
861
883
|
// The `, ya sea <DOM> o <DOW>` correlative suffix for the union frame.
|
|
862
|
-
function unionYaseaSuffix(
|
|
863
|
-
return ', ya sea ' + domArm(
|
|
884
|
+
function unionYaseaSuffix(schedule: Schedule, opts: Opts): string {
|
|
885
|
+
return ', ya sea ' + domArm(schedule, opts) + ' o ' + dowArm(schedule);
|
|
864
886
|
}
|
|
865
887
|
|
|
866
888
|
// "todos los días a las 9:30 y a las 17:00".
|
|
867
889
|
function renderClockTimes(
|
|
868
|
-
|
|
890
|
+
schedule: Schedule,
|
|
869
891
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
870
892
|
opts: Opts
|
|
871
893
|
): string {
|
|
872
894
|
// An hour step or range (or arithmetic-progression hour list) under a single
|
|
873
895
|
// pinned minute reads as a cadence or window rather than a cross-product of
|
|
874
896
|
// clock times.
|
|
875
|
-
if (
|
|
876
|
-
const minute = +
|
|
877
|
-
const cadence = hourCadence(
|
|
878
|
-
hourRangeCadence(
|
|
897
|
+
if (schedule.shapes.minute === 'single') {
|
|
898
|
+
const minute = +schedule.pattern.minute;
|
|
899
|
+
const cadence = hourCadence(schedule, minute, opts) ??
|
|
900
|
+
hourRangeCadence(schedule, minute, opts);
|
|
879
901
|
|
|
880
902
|
if (cadence !== null) {
|
|
881
903
|
return cadence;
|
|
@@ -886,7 +908,7 @@ function renderClockTimes(
|
|
|
886
908
|
return atTime(timePhrase(time.hour, time.minute, time.second, opts));
|
|
887
909
|
});
|
|
888
910
|
|
|
889
|
-
return leadingQualifier(
|
|
911
|
+
return leadingQualifier(schedule, opts) + groupClockTimes(phrases);
|
|
890
912
|
}
|
|
891
913
|
|
|
892
914
|
// The genitive clock-time list for a minute-0 compose-seconds confinement:
|
|
@@ -1212,7 +1234,7 @@ function groupClockTimesByArticle(phrases: string[]): string {
|
|
|
1212
1234
|
// Compact form past the enumeration cap: a single minute folds into
|
|
1213
1235
|
// per-segment hour windows; a minute list leads with its own clause.
|
|
1214
1236
|
function renderCompactClockTimes(
|
|
1215
|
-
|
|
1237
|
+
schedule: Schedule,
|
|
1216
1238
|
plan: Extract<PlanNode, {kind: 'compactClockTimes'}>,
|
|
1217
1239
|
opts: Opts
|
|
1218
1240
|
): string {
|
|
@@ -1220,39 +1242,44 @@ function renderCompactClockTimes(
|
|
|
1220
1242
|
// An hour step or range (or arithmetic-progression hour list) under the
|
|
1221
1243
|
// single pinned minute reads as a cadence or window, not a wall of clock
|
|
1222
1244
|
// times. (Returns null for an irregular list, which keeps folding below.)
|
|
1223
|
-
const cadence = hourCadence(
|
|
1224
|
-
hourRangeCadence(
|
|
1245
|
+
const cadence = hourCadence(schedule, plan.minute, opts) ??
|
|
1246
|
+
hourRangeCadence(schedule, plan.minute, opts);
|
|
1225
1247
|
|
|
1226
1248
|
if (cadence !== null) {
|
|
1227
1249
|
return cadence;
|
|
1228
1250
|
}
|
|
1229
1251
|
|
|
1230
|
-
const ranged = segmentsOf(
|
|
1252
|
+
const ranged = segmentsOf(schedule, 'hour').some(function range(segment) {
|
|
1231
1253
|
return segment.kind === 'range';
|
|
1232
1254
|
});
|
|
1233
1255
|
|
|
1234
1256
|
// A folded contiguous hour range reads with the hourly cadence ("cada
|
|
1235
1257
|
// hora de las 9:00 a las 20:00 y a las 22:00"), not "todos los días".
|
|
1236
|
-
if (ranged && !
|
|
1258
|
+
if (ranged && !schedule.analyses.clockSecond) {
|
|
1237
1259
|
return 'cada hora ' +
|
|
1238
|
-
hourSegmentTimes(
|
|
1239
|
-
|
|
1260
|
+
hourSegmentTimes(
|
|
1261
|
+
schedule, plan.minute, schedule.analyses.clockSecond, opts
|
|
1262
|
+
) +
|
|
1263
|
+
trailingQualifier(schedule, opts);
|
|
1240
1264
|
}
|
|
1241
1265
|
|
|
1242
|
-
return leadingQualifier(
|
|
1243
|
-
hourSegmentTimes(
|
|
1266
|
+
return leadingQualifier(schedule, opts) +
|
|
1267
|
+
hourSegmentTimes(
|
|
1268
|
+
schedule, plan.minute, schedule.analyses.clockSecond, opts
|
|
1269
|
+
);
|
|
1244
1270
|
}
|
|
1245
1271
|
|
|
1246
1272
|
// A uneven hour stride reads as a cadence after the minute lead, not a wall
|
|
1247
1273
|
// of clock-time columns.
|
|
1248
|
-
const cadence = unevenHourCadence(
|
|
1274
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
1249
1275
|
const phrase = cadence ?
|
|
1250
|
-
minutesList(
|
|
1251
|
-
|
|
1252
|
-
|
|
1276
|
+
minutesList(schedule, opts) + ', ' + cadence +
|
|
1277
|
+
trailingQualifier(schedule, opts) :
|
|
1278
|
+
minutesList(schedule, opts) + ', ' +
|
|
1279
|
+
hourContextTimes(schedule, opts) + trailingQualifier(schedule, opts);
|
|
1253
1280
|
|
|
1254
|
-
return
|
|
1255
|
-
secondsLeadClause(
|
|
1281
|
+
return schedule.analyses.clockSecond ?
|
|
1282
|
+
secondsLeadClause(schedule, opts) + ', ' + phrase :
|
|
1256
1283
|
phrase;
|
|
1257
1284
|
}
|
|
1258
1285
|
|
|
@@ -1294,21 +1321,17 @@ const renderers = {
|
|
|
1294
1321
|
function renderStride(stride: Stride, opts: Opts): string {
|
|
1295
1322
|
const {interval, start, last, cycle, unit, anchor} = stride;
|
|
1296
1323
|
const cadence = 'cada ' + numero(interval, opts) + ' ' + unit + 's';
|
|
1297
|
-
const tiles = cycle % interval === 0;
|
|
1298
|
-
|
|
1299
|
-
if (start === 0 && tiles) {
|
|
1300
|
-
return cadence;
|
|
1301
|
-
}
|
|
1302
1324
|
|
|
1303
1325
|
// A context that supplies its own trailing scope passes an empty anchor, so
|
|
1304
1326
|
// the cadence keeps its endpoints but drops the "de cada <anchor>" tail.
|
|
1305
1327
|
const tail = anchor ? ' de cada ' + anchor : '';
|
|
1306
1328
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1329
|
+
return chooseStride({start, interval, cycle}, {
|
|
1330
|
+
bare: () => cadence,
|
|
1331
|
+
offset: () => cadence + ' a partir del ' + unit + ' ' + start + tail,
|
|
1332
|
+
bounded: () =>
|
|
1333
|
+
cadence + ' del ' + unit + ' ' + start + ' al ' + last + tail
|
|
1334
|
+
});
|
|
1312
1335
|
}
|
|
1313
1336
|
|
|
1314
1337
|
// "cada 15 minutos", "en los minutos 5, 20 y 35 de cada hora", or
|
|
@@ -1348,9 +1371,9 @@ function stepCycle60(
|
|
|
1348
1371
|
|
|
1349
1372
|
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
1350
1373
|
// form an arithmetic progression long enough to beat the list (the core
|
|
1351
|
-
// enumerates an offset/uneven step to this fire list; the
|
|
1352
|
-
// the renderer recognizes the progression). Returns null for a
|
|
1353
|
-
// or a too-short list, leaving the caller to enumerate.
|
|
1374
|
+
// enumerates an offset/uneven step to this fire list; the Schedule is
|
|
1375
|
+
// unchanged, so the renderer recognizes the progression). Returns null for a
|
|
1376
|
+
// non-progression or a too-short list, leaving the caller to enumerate.
|
|
1354
1377
|
function strideFromSegments(
|
|
1355
1378
|
segments: Segment[],
|
|
1356
1379
|
unit: string,
|
|
@@ -1405,18 +1428,13 @@ function hourStrideCadence(
|
|
|
1405
1428
|
): string {
|
|
1406
1429
|
const {start, interval, last} = stride;
|
|
1407
1430
|
const cadence = 'cada ' + numero(interval, opts) + ' horas';
|
|
1408
|
-
const tiles = 24 % interval === 0;
|
|
1409
|
-
|
|
1410
|
-
if (start === 0 && tiles) {
|
|
1411
|
-
return cadence;
|
|
1412
|
-
}
|
|
1413
1431
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1432
|
+
return chooseStride({start, interval, cycle: 24}, {
|
|
1433
|
+
bare: () => cadence,
|
|
1434
|
+
offset: () => cadence + ' a partir de ' + timePhrase(start, 0, null, opts),
|
|
1435
|
+
bounded: () => cadence + ' de ' + timePhrase(start, 0, null, opts) + ' a ' +
|
|
1436
|
+
timePhrase(last, 0, null, opts)
|
|
1437
|
+
});
|
|
1420
1438
|
}
|
|
1421
1439
|
|
|
1422
1440
|
// The bounded cadence for an hour stride that pins both clock-time endpoints,
|
|
@@ -1426,8 +1444,8 @@ function hourStrideCadence(
|
|
|
1426
1444
|
// ("…, cada cinco horas de las 00:00 a las 20:00") than as a wall of clock
|
|
1427
1445
|
// times. An offset-clean stride keeps its existing confinement form, so only
|
|
1428
1446
|
// the endpoint-bearing case routes here.
|
|
1429
|
-
function unevenHourCadence(
|
|
1430
|
-
const stride = hourStride(
|
|
1447
|
+
function unevenHourCadence(schedule: Schedule, opts: Opts): string | null {
|
|
1448
|
+
const stride = hourStride(schedule);
|
|
1431
1449
|
|
|
1432
1450
|
if (!stride || offsetCleanStride(stride)) {
|
|
1433
1451
|
return null;
|
|
@@ -1439,13 +1457,13 @@ function unevenHourCadence(ir: IR, opts: Opts): string | null {
|
|
|
1439
1457
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1440
1458
|
// segment yields its {start, interval, last} directly; an all-single hour
|
|
1441
1459
|
// list yields one only when its values form a step progression (so an irregular
|
|
1442
|
-
// list like 9,17 keeps enumerating). The
|
|
1460
|
+
// list like 9,17 keeps enumerating). The Schedule is unchanged — the renderer
|
|
1443
1461
|
// recognizes the stride and speaks it as a cadence instead of the clock-time
|
|
1444
1462
|
// cross-product.
|
|
1445
1463
|
function hourStride(
|
|
1446
|
-
|
|
1464
|
+
schedule: Schedule
|
|
1447
1465
|
): {start: number; interval: number; last: number} | null {
|
|
1448
|
-
const segments = segmentsOf(
|
|
1466
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1449
1467
|
|
|
1450
1468
|
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
1451
1469
|
const segment = segments[0];
|
|
@@ -1472,8 +1490,8 @@ function hourStride(
|
|
|
1472
1490
|
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
1473
1491
|
// fills the minute (a "durante un minuto" frame at minute 0); a single 0 is
|
|
1474
1492
|
// just the top of the minute (no clause); anything else needs its own clause.
|
|
1475
|
-
function subMinuteSecond(
|
|
1476
|
-
return
|
|
1493
|
+
function subMinuteSecond(schedule: Schedule): boolean {
|
|
1494
|
+
return schedule.pattern.second === '*' || schedule.shapes.second === 'step';
|
|
1477
1495
|
}
|
|
1478
1496
|
|
|
1479
1497
|
// The lead clause for an hour-cadence rendering: the second and the pinned
|
|
@@ -1483,24 +1501,26 @@ function subMinuteSecond(ir: IR): boolean {
|
|
|
1483
1501
|
// minuto" frame (the whole minute-0 window). A non-zero minute is a real clock
|
|
1484
1502
|
// minute: the second leads with its own clause (if any), then the minute reads
|
|
1485
1503
|
// "en el minuto M".
|
|
1486
|
-
function hourCadenceLead(
|
|
1504
|
+
function hourCadenceLead(
|
|
1505
|
+
schedule: Schedule, minute: number, opts: Opts
|
|
1506
|
+
): string {
|
|
1487
1507
|
if (minute === 0) {
|
|
1488
|
-
if (subMinuteSecond(
|
|
1489
|
-
return secondsClause(
|
|
1508
|
+
if (subMinuteSecond(schedule)) {
|
|
1509
|
+
return secondsClause(schedule, 'minuto', opts) + ' durante un minuto';
|
|
1490
1510
|
}
|
|
1491
1511
|
|
|
1492
|
-
return secondsClause(
|
|
1512
|
+
return secondsClause(schedule, 'hora', opts);
|
|
1493
1513
|
}
|
|
1494
1514
|
|
|
1495
1515
|
const minutePhrase = 'en el minuto ' + minute;
|
|
1496
1516
|
|
|
1497
1517
|
// A single 0 second is just the top of the minute, so the minute leads
|
|
1498
1518
|
// alone; any other second prefixes its own clause.
|
|
1499
|
-
if (
|
|
1519
|
+
if (schedule.pattern.second === '0') {
|
|
1500
1520
|
return minutePhrase;
|
|
1501
1521
|
}
|
|
1502
1522
|
|
|
1503
|
-
return secondsClause(
|
|
1523
|
+
return secondsClause(schedule, 'minuto', opts) + ', ' + minutePhrase;
|
|
1504
1524
|
}
|
|
1505
1525
|
|
|
1506
1526
|
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
@@ -1511,9 +1531,11 @@ function hourCadenceLead(ir: IR, minute: number, opts: Opts): string {
|
|
|
1511
1531
|
// enumeration is no longer than the cadence: a meaningful second makes every
|
|
1512
1532
|
// clock time three digit-groups, so any stride is worth compacting; otherwise
|
|
1513
1533
|
// the stride must exceed the clock-time cap, the same point at which the core
|
|
1514
|
-
// itself stops enumerating. Renderer-only; the
|
|
1515
|
-
function hourCadence(
|
|
1516
|
-
|
|
1534
|
+
// itself stops enumerating. Renderer-only; the Schedule is unchanged.
|
|
1535
|
+
function hourCadence(
|
|
1536
|
+
schedule: Schedule, minute: number, opts: Opts
|
|
1537
|
+
): string | null {
|
|
1538
|
+
const stride = hourStride(schedule);
|
|
1517
1539
|
|
|
1518
1540
|
if (!stride) {
|
|
1519
1541
|
return null;
|
|
@@ -1526,7 +1548,7 @@ function hourCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
|
1526
1548
|
// or "a partir de" form is no shorter than the list. A bounded or uneven
|
|
1527
1549
|
// stride has no clean wrap, so its endpoint-pinning cadence ("cada cinco
|
|
1528
1550
|
// horas de las 00:00 a las 20:00") reads better however short.
|
|
1529
|
-
if (
|
|
1551
|
+
if (schedule.pattern.second === '0' && fires <= maxClockTimes &&
|
|
1530
1552
|
offsetCleanStride(stride)) {
|
|
1531
1553
|
return null;
|
|
1532
1554
|
}
|
|
@@ -1535,31 +1557,31 @@ function hourCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
|
1535
1557
|
// stride is a confinement, not a juxtaposed cadence: it reads "durante un
|
|
1536
1558
|
// minuto, durante las horas pares", reusing the hour-step confinement idiom
|
|
1537
1559
|
// so the minute-0 window is never heard as the bare hour cadence.
|
|
1538
|
-
const confinement = minute === 0 && subMinuteSecond(
|
|
1539
|
-
cleanStrideSegment(
|
|
1560
|
+
const confinement = minute === 0 && subMinuteSecond(schedule) &&
|
|
1561
|
+
cleanStrideSegment(schedule);
|
|
1540
1562
|
|
|
1541
1563
|
if (confinement) {
|
|
1542
|
-
return secondsClause(
|
|
1543
|
-
stepHourSpan(confinement, opts) + trailingQualifier(
|
|
1564
|
+
return secondsClause(schedule, 'minuto', opts) + ' durante un minuto, ' +
|
|
1565
|
+
stepHourSpan(confinement, opts) + trailingQualifier(schedule, opts);
|
|
1544
1566
|
}
|
|
1545
1567
|
|
|
1546
1568
|
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
1547
1569
|
// lead clause to fold in, so the bounded cadence stands on its own ("cada
|
|
1548
1570
|
// cinco horas de las 00:00 a las 20:00").
|
|
1549
|
-
if (minute === 0 &&
|
|
1550
|
-
return hourStrideCadence(stride, opts) + trailingQualifier(
|
|
1571
|
+
if (minute === 0 && schedule.pattern.second === '0') {
|
|
1572
|
+
return hourStrideCadence(stride, opts) + trailingQualifier(schedule, opts);
|
|
1551
1573
|
}
|
|
1552
1574
|
|
|
1553
|
-
return hourCadenceLead(
|
|
1554
|
-
hourStrideCadence(stride, opts) + trailingQualifier(
|
|
1575
|
+
return hourCadenceLead(schedule, minute, opts) + ', ' +
|
|
1576
|
+
hourStrideCadence(stride, opts) + trailingQualifier(schedule, opts);
|
|
1555
1577
|
}
|
|
1556
1578
|
|
|
1557
1579
|
// The hour step segment when the hour is a clean stride es renders as a
|
|
1558
1580
|
// confinement phrase ("durante las horas pares"); null otherwise (an offset or
|
|
1559
1581
|
// bounded step, an uneven stride, or an arithmetic-progression list, which
|
|
1560
1582
|
// keep the bounded cadence form).
|
|
1561
|
-
function cleanStrideSegment(
|
|
1562
|
-
const segments = segmentsOf(
|
|
1583
|
+
function cleanStrideSegment(schedule: Schedule): StepSegment | null {
|
|
1584
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1563
1585
|
const segment = segments.length === 1 && segments[0];
|
|
1564
1586
|
|
|
1565
1587
|
if (!segment || segment.kind !== 'step' ||
|
|
@@ -1574,8 +1596,8 @@ function cleanStrideSegment(ir: IR): StepSegment | null {
|
|
|
1574
1596
|
// range — and so forms a window rather than a cross-product of clock times.
|
|
1575
1597
|
// A pure single-value list (9,17) has no range to span and still enumerates;
|
|
1576
1598
|
// a step is handled by hourStride/hourCadence.
|
|
1577
|
-
function hasHourWindow(
|
|
1578
|
-
return segmentsOf(
|
|
1599
|
+
function hasHourWindow(schedule: Schedule): boolean {
|
|
1600
|
+
return segmentsOf(schedule, 'hour').some(function range(segment) {
|
|
1579
1601
|
return segment.kind === 'range';
|
|
1580
1602
|
});
|
|
1581
1603
|
}
|
|
@@ -1587,9 +1609,12 @@ function hasHourWindow(ir: IR): boolean {
|
|
|
1587
1609
|
// times. The hour-RANGE analog of hourCadence. Returns null when the hour has
|
|
1588
1610
|
// no range, when the minute is non-zero (a real clock minute the existing
|
|
1589
1611
|
// window form already speaks), or when a plain :00 set carries no clause.
|
|
1590
|
-
// Renderer-only; the
|
|
1591
|
-
function hourRangeCadence(
|
|
1592
|
-
|
|
1612
|
+
// Renderer-only; the Schedule is unchanged.
|
|
1613
|
+
function hourRangeCadence(
|
|
1614
|
+
schedule: Schedule, minute: number, opts: Opts
|
|
1615
|
+
): string | null {
|
|
1616
|
+
if (minute !== 0 || !hasHourWindow(schedule) ||
|
|
1617
|
+
schedule.pattern.second === '0') {
|
|
1593
1618
|
return null;
|
|
1594
1619
|
}
|
|
1595
1620
|
|
|
@@ -1599,14 +1624,15 @@ function hourRangeCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
|
1599
1624
|
// ("cada hora de las 09:00 a las 17:00") so the confinement is never heard
|
|
1600
1625
|
// as it — the hour-range analog of "durante un minuto, durante las horas
|
|
1601
1626
|
// pares".
|
|
1602
|
-
if (subMinuteSecond(
|
|
1603
|
-
return secondsClause(
|
|
1604
|
-
'durante las horas ' + hourSegmentTimes(
|
|
1605
|
-
trailingQualifier(
|
|
1627
|
+
if (subMinuteSecond(schedule)) {
|
|
1628
|
+
return secondsClause(schedule, 'minuto', opts) + ' durante un minuto, ' +
|
|
1629
|
+
'durante las horas ' + hourSegmentTimes(schedule, 0, null, opts) +
|
|
1630
|
+
trailingQualifier(schedule, opts);
|
|
1606
1631
|
}
|
|
1607
1632
|
|
|
1608
|
-
return hourCadenceLead(
|
|
1609
|
-
hourSegmentTimes(
|
|
1633
|
+
return hourCadenceLead(schedule, minute, opts) + ', ' +
|
|
1634
|
+
hourSegmentTimes(schedule, 0, null, opts) +
|
|
1635
|
+
trailingQualifier(schedule, opts);
|
|
1610
1636
|
}
|
|
1611
1637
|
|
|
1612
1638
|
// --- Hour-time phrasing. ---
|
|
@@ -1617,8 +1643,8 @@ function hourRangeCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
|
1617
1643
|
// hour "de la hora de las HH:00" (the idiom a wildcard minute already uses).
|
|
1618
1644
|
// Used by the compact-clock non-fold path, where the minute is a step or list
|
|
1619
1645
|
// (a single-value minute keeps its real "a las HH:MM" clock time elsewhere).
|
|
1620
|
-
function hourContextTimes(
|
|
1621
|
-
const segments = segmentsOf(
|
|
1646
|
+
function hourContextTimes(schedule: Schedule, opts: Opts): string {
|
|
1647
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1622
1648
|
|
|
1623
1649
|
// Collect the point hours (singles and step fires) — a range stays a window.
|
|
1624
1650
|
const points: number[] = [];
|
|
@@ -1686,7 +1712,7 @@ function atTimes(hours: number[], opts: Opts): string[] {
|
|
|
1686
1712
|
// The hour times accompanying a lead clause: "a las 9:00 y a las 17:00",
|
|
1687
1713
|
// with long expansions rendered segment by segment.
|
|
1688
1714
|
function atHourTimes(
|
|
1689
|
-
|
|
1715
|
+
schedule: Schedule,
|
|
1690
1716
|
times: HourTimesPlan,
|
|
1691
1717
|
opts: Opts
|
|
1692
1718
|
): string {
|
|
@@ -1694,25 +1720,27 @@ function atHourTimes(
|
|
|
1694
1720
|
return groupClockTimesByArticle(atTimes(times.fires, opts));
|
|
1695
1721
|
}
|
|
1696
1722
|
|
|
1697
|
-
return hourSegmentTimes(
|
|
1723
|
+
return hourSegmentTimes(schedule, 0, null, opts);
|
|
1698
1724
|
}
|
|
1699
1725
|
|
|
1700
1726
|
// The active hours of a confined cadence: a few hours read as windows; many
|
|
1701
1727
|
// read better as a compact list ("durante las horas de las 9, 11, 13, 15 y
|
|
1702
1728
|
// 17") than as a sprawl of windows.
|
|
1703
|
-
function hourSpanFromTimes(
|
|
1729
|
+
function hourSpanFromTimes(
|
|
1730
|
+
schedule: Schedule, times: HourTimesPlan, opts: Opts
|
|
1731
|
+
): string {
|
|
1704
1732
|
if (times.kind === 'fires' && times.fires.length > 3) {
|
|
1705
1733
|
return 'durante las horas ' + hourSpanList(times.fires, opts);
|
|
1706
1734
|
}
|
|
1707
1735
|
|
|
1708
|
-
return hourWindowsFromTimes(
|
|
1736
|
+
return hourWindowsFromTimes(schedule, times, opts);
|
|
1709
1737
|
}
|
|
1710
1738
|
|
|
1711
1739
|
// Each fire hour as its own one-hour window: "de las 9:00 a las 9:59 y de
|
|
1712
1740
|
// las 17:00 a las 17:59". Spanish prefers this to the English "during the
|
|
1713
1741
|
// 9 a.m. and 5 p.m. hours" shape.
|
|
1714
1742
|
function hourWindowsFromTimes(
|
|
1715
|
-
|
|
1743
|
+
schedule: Schedule,
|
|
1716
1744
|
times: HourTimesPlan,
|
|
1717
1745
|
opts: Opts
|
|
1718
1746
|
): string {
|
|
@@ -1722,7 +1750,7 @@ function hourWindowsFromTimes(
|
|
|
1722
1750
|
}));
|
|
1723
1751
|
}
|
|
1724
1752
|
|
|
1725
|
-
return joinList(segmentsOf(
|
|
1753
|
+
return joinList(segmentsOf(schedule, 'hour').map(function window(segment) {
|
|
1726
1754
|
if (segment.kind === 'range') {
|
|
1727
1755
|
return timeRange({hour: +segment.bounds[0], minute: 0},
|
|
1728
1756
|
{hour: +segment.bounds[1], minute: 59}, opts);
|
|
@@ -1742,7 +1770,7 @@ function hourWindowsFromTimes(
|
|
|
1742
1770
|
// (and optional second) folded into each: "de las 9:30 a las 20:30 y también
|
|
1743
1771
|
// a las 22:30" when an isolated point-time follows a range.
|
|
1744
1772
|
function hourSegmentTimes(
|
|
1745
|
-
|
|
1773
|
+
schedule: Schedule,
|
|
1746
1774
|
minute: number,
|
|
1747
1775
|
second: number | null | undefined,
|
|
1748
1776
|
opts: Opts
|
|
@@ -1751,7 +1779,7 @@ function hourSegmentTimes(
|
|
|
1751
1779
|
const pieces: string[] = [];
|
|
1752
1780
|
const fromRange: boolean[] = [];
|
|
1753
1781
|
|
|
1754
|
-
segmentsOf(
|
|
1782
|
+
segmentsOf(schedule, 'hour').forEach(function clock(segment) {
|
|
1755
1783
|
if (segment.kind === 'step') {
|
|
1756
1784
|
segment.fires.forEach(function each(hour) {
|
|
1757
1785
|
pieces.push(atTime(timePhrase(hour, minute, second, opts)));
|
|
@@ -1942,23 +1970,23 @@ function dayPeriod(hour: number, opts: Opts): string {
|
|
|
1942
1970
|
// lunes ", "el 13 de cada mes ", "de lunes a viernes ".
|
|
1943
1971
|
// Date-OR-weekday unions skip this entirely — the unified frame in `render`
|
|
1944
1972
|
// handles the month lead and day-level suffix.
|
|
1945
|
-
function leadingQualifier(
|
|
1946
|
-
const pattern =
|
|
1973
|
+
function leadingQualifier(schedule: Schedule, opts: Opts): string {
|
|
1974
|
+
const pattern = schedule.pattern;
|
|
1947
1975
|
|
|
1948
1976
|
if (pattern.date !== '*' && pattern.weekday !== '*') {
|
|
1949
1977
|
return '';
|
|
1950
1978
|
}
|
|
1951
1979
|
|
|
1952
1980
|
if (pattern.date !== '*') {
|
|
1953
|
-
return datePhrase(
|
|
1981
|
+
return datePhrase(schedule, opts) + ' ';
|
|
1954
1982
|
}
|
|
1955
1983
|
|
|
1956
1984
|
if (pattern.weekday !== '*') {
|
|
1957
|
-
return weekdayQualifier(
|
|
1985
|
+
return weekdayQualifier(schedule) + monthScope(schedule) + ' ';
|
|
1958
1986
|
}
|
|
1959
1987
|
|
|
1960
1988
|
if (pattern.month !== '*') {
|
|
1961
|
-
return 'todos los días ' + monthPhrase(
|
|
1989
|
+
return 'todos los días ' + monthPhrase(schedule, 'de ') + ' ';
|
|
1962
1990
|
}
|
|
1963
1991
|
|
|
1964
1992
|
return 'todos los días ';
|
|
@@ -1968,23 +1996,23 @@ function leadingQualifier(ir: IR, opts: Opts): string {
|
|
|
1968
1996
|
// de cada mes". Empty when no day-level field is set.
|
|
1969
1997
|
// Date-OR-weekday unions skip this entirely — the unified frame in `render`
|
|
1970
1998
|
// handles the month lead and day-level suffix.
|
|
1971
|
-
function trailingQualifier(
|
|
1972
|
-
const pattern =
|
|
1999
|
+
function trailingQualifier(schedule: Schedule, opts: Opts): string {
|
|
2000
|
+
const pattern = schedule.pattern;
|
|
1973
2001
|
|
|
1974
2002
|
if (pattern.date !== '*' && pattern.weekday !== '*') {
|
|
1975
2003
|
return '';
|
|
1976
2004
|
}
|
|
1977
2005
|
|
|
1978
2006
|
if (pattern.date !== '*') {
|
|
1979
|
-
return ' ' + datePhrase(
|
|
2007
|
+
return ' ' + datePhrase(schedule, opts);
|
|
1980
2008
|
}
|
|
1981
2009
|
|
|
1982
2010
|
if (pattern.weekday !== '*') {
|
|
1983
|
-
return ' ' + weekdayQualifier(
|
|
2011
|
+
return ' ' + weekdayQualifier(schedule) + monthScope(schedule);
|
|
1984
2012
|
}
|
|
1985
2013
|
|
|
1986
2014
|
if (pattern.month !== '*') {
|
|
1987
|
-
return ' ' + monthPhrase(
|
|
2015
|
+
return ' ' + monthPhrase(schedule, 'en ');
|
|
1988
2016
|
}
|
|
1989
2017
|
|
|
1990
2018
|
return '';
|
|
@@ -1993,24 +2021,24 @@ function trailingQualifier(ir: IR, opts: Opts): string {
|
|
|
1993
2021
|
// The date qualifier: "el 13 de junio", "los días 1 y 15 de cada mes",
|
|
1994
2022
|
// "del 1 al 15 de cada mes", or a Quartz phrase. A foldable single year
|
|
1995
2023
|
// joins the date ("el 25 de diciembre de 2030").
|
|
1996
|
-
function datePhrase(
|
|
1997
|
-
const pattern =
|
|
2024
|
+
function datePhrase(schedule: Schedule, opts: Opts): string {
|
|
2025
|
+
const pattern = schedule.pattern;
|
|
1998
2026
|
|
|
1999
2027
|
if (quartzDatePhrase(pattern.date) || isOpenStep(pattern.date)) {
|
|
2000
|
-
return dateClause(
|
|
2028
|
+
return dateClause(schedule, '', opts) + monthScope(schedule);
|
|
2001
2029
|
}
|
|
2002
2030
|
|
|
2003
|
-
return dateClause(
|
|
2031
|
+
return dateClause(schedule, dateMonthPart(schedule), opts);
|
|
2004
2032
|
}
|
|
2005
2033
|
|
|
2006
2034
|
// The date words with a caller-chosen month part. Quartz phrases and open
|
|
2007
2035
|
// steps are self-contained and ignore the month part.
|
|
2008
2036
|
function dateClause(
|
|
2009
|
-
|
|
2037
|
+
schedule: Schedule,
|
|
2010
2038
|
monthPart: string,
|
|
2011
2039
|
opts: Opts
|
|
2012
2040
|
): string {
|
|
2013
|
-
const pattern =
|
|
2041
|
+
const pattern = schedule.pattern;
|
|
2014
2042
|
const quartz = quartzDatePhrase(pattern.date);
|
|
2015
2043
|
|
|
2016
2044
|
if (quartz) {
|
|
@@ -2021,25 +2049,25 @@ function dateClause(
|
|
|
2021
2049
|
return stepDates(pattern.date, opts);
|
|
2022
2050
|
}
|
|
2023
2051
|
|
|
2024
|
-
const segments = segmentsOf(
|
|
2052
|
+
const segments = segmentsOf(schedule, 'date');
|
|
2025
2053
|
|
|
2026
2054
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
2027
2055
|
return 'del ' + segments[0].bounds[0] + ' al ' +
|
|
2028
|
-
segments[0].bounds[1] + monthPart + foldedYear(
|
|
2056
|
+
segments[0].bounds[1] + monthPart + foldedYear(schedule);
|
|
2029
2057
|
}
|
|
2030
2058
|
|
|
2031
2059
|
if (segments.length === 1 && segments[0].kind === 'single') {
|
|
2032
|
-
return 'el ' + segments[0].value + monthPart + foldedYear(
|
|
2060
|
+
return 'el ' + segments[0].value + monthPart + foldedYear(schedule);
|
|
2033
2061
|
}
|
|
2034
2062
|
|
|
2035
2063
|
return 'los días ' + joinList(segmentWords(segments)) + monthPart +
|
|
2036
|
-
foldedYear(
|
|
2064
|
+
foldedYear(schedule);
|
|
2037
2065
|
}
|
|
2038
2066
|
|
|
2039
2067
|
// Whether the month field contains a range segment.
|
|
2040
|
-
function monthRanged(
|
|
2041
|
-
return
|
|
2042
|
-
segmentsOf(
|
|
2068
|
+
function monthRanged(schedule: Schedule): boolean {
|
|
2069
|
+
return schedule.pattern.month !== '*' &&
|
|
2070
|
+
segmentsOf(schedule, 'month').some(function range(segment) {
|
|
2043
2071
|
return segment.kind === 'range';
|
|
2044
2072
|
});
|
|
2045
2073
|
}
|
|
@@ -2049,21 +2077,21 @@ function monthRanged(ir: IR): boolean {
|
|
|
2049
2077
|
// "el 1 de junio a septiembre" parses as "(el 1 de junio) a septiembre" —
|
|
2050
2078
|
// so it scopes the date instead ("el 1 de cada mes, de junio a
|
|
2051
2079
|
// septiembre").
|
|
2052
|
-
function dateMonthPart(
|
|
2053
|
-
if (
|
|
2080
|
+
function dateMonthPart(schedule: Schedule): string {
|
|
2081
|
+
if (schedule.pattern.month === '*') {
|
|
2054
2082
|
return ' de cada mes';
|
|
2055
2083
|
}
|
|
2056
2084
|
|
|
2057
|
-
if (monthRanged(
|
|
2058
|
-
return ' de cada mes, ' + monthPhrase(
|
|
2085
|
+
if (monthRanged(schedule)) {
|
|
2086
|
+
return ' de cada mes, ' + monthPhrase(schedule, 'de ');
|
|
2059
2087
|
}
|
|
2060
2088
|
|
|
2061
|
-
return ' ' + monthPhrase(
|
|
2089
|
+
return ' ' + monthPhrase(schedule, 'de ');
|
|
2062
2090
|
}
|
|
2063
2091
|
|
|
2064
2092
|
// "de 2030" when a single year can fold into a calendar date.
|
|
2065
|
-
function foldedYear(
|
|
2066
|
-
const yearField =
|
|
2093
|
+
function foldedYear(schedule: Schedule): string {
|
|
2094
|
+
const yearField = schedule.pattern.year;
|
|
2067
2095
|
|
|
2068
2096
|
if (yearField === '*' || yearField.indexOf('/') !== -1 ||
|
|
2069
2097
|
yearField.indexOf('-') !== -1 || yearField.indexOf(',') !== -1) {
|
|
@@ -2119,16 +2147,16 @@ function quartzWeekdayPhrase(weekdayField: string): string | undefined {
|
|
|
2119
2147
|
// miércoles y viernes". No "todos" prefix: the plural definite article
|
|
2120
2148
|
// ("los lunes") already conveys "every Monday" in Spanish, unlike "todos
|
|
2121
2149
|
// los días", where "los días" alone does not mean "every day".
|
|
2122
|
-
function weekdayQualifier(
|
|
2123
|
-
const quartz = quartzWeekdayPhrase(
|
|
2150
|
+
function weekdayQualifier(schedule: Schedule): string {
|
|
2151
|
+
const quartz = quartzWeekdayPhrase(schedule.pattern.weekday);
|
|
2124
2152
|
|
|
2125
2153
|
if (quartz) {
|
|
2126
2154
|
return quartz;
|
|
2127
2155
|
}
|
|
2128
2156
|
|
|
2129
2157
|
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
2130
|
-
// form. The
|
|
2131
|
-
const segments = orderWeekdaysForDisplay(segmentsOf(
|
|
2158
|
+
// form. The Schedule stays canonical (Sunday=0). The helper flattens steps.
|
|
2159
|
+
const segments = orderWeekdaysForDisplay(segmentsOf(schedule, 'weekday'));
|
|
2132
2160
|
const allSingles = segments.every(function single(segment) {
|
|
2133
2161
|
return segment.kind === 'single';
|
|
2134
2162
|
});
|
|
@@ -2180,8 +2208,8 @@ function flattenSteps(segments: Segment[]): NameSegment[] {
|
|
|
2180
2208
|
// as one unit, so in mixed lists every piece repeats its preposition
|
|
2181
2209
|
// ("en enero y de marzo a junio") — a bare "enero y marzo a junio" parses
|
|
2182
2210
|
// as "(enero y marzo) a junio".
|
|
2183
|
-
function monthPhrase(
|
|
2184
|
-
const segments = flattenSteps(segmentsOf(
|
|
2211
|
+
function monthPhrase(schedule: Schedule, lead: string): string {
|
|
2212
|
+
const segments = flattenSteps(segmentsOf(schedule, 'month'));
|
|
2185
2213
|
const ranged = segments.some(function range(segment) {
|
|
2186
2214
|
return segment.kind === 'range';
|
|
2187
2215
|
});
|
|
@@ -2207,15 +2235,39 @@ function monthPhrase(ir: IR, lead: string): string {
|
|
|
2207
2235
|
// junio"). A ranged scope sets off with a comma ("el último día del mes,
|
|
2208
2236
|
// de junio a septiembre") — gluing "de junio" after "del mes"
|
|
2209
2237
|
// garden-paths.
|
|
2210
|
-
function monthScope(
|
|
2211
|
-
if (
|
|
2238
|
+
function monthScope(schedule: Schedule): string {
|
|
2239
|
+
if (schedule.pattern.month === '*') {
|
|
2212
2240
|
return '';
|
|
2213
2241
|
}
|
|
2214
2242
|
|
|
2215
|
-
return (monthRanged(
|
|
2243
|
+
return (monthRanged(schedule) ? ', ' : ' ') + monthPhrase(schedule, 'de ');
|
|
2216
2244
|
}
|
|
2217
2245
|
|
|
2218
2246
|
// Open day-of-month steps: "cada 2 días del mes (desde el 5)".
|
|
2247
|
+
// The parity predicate for a `*/2`-style day-of-month step, used only inside
|
|
2248
|
+
// the OR union frame (see domArm). `*/2` and `1/2` fire on the odd days
|
|
2249
|
+
// (1, 3, …, 31); `2/2` fires on the even days. Any other open step has no
|
|
2250
|
+
// parity reading, so the caller falls back to stepDates.
|
|
2251
|
+
function parityDayPredicate(dateField: string): string | undefined {
|
|
2252
|
+
if (!isOpenStep(dateField)) {
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
const [start, step] = dateField.split('/');
|
|
2257
|
+
|
|
2258
|
+
if (+step !== 2) {
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
if (start === '*' || start === '1') {
|
|
2263
|
+
return 'un día impar del mes';
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
if (start === '2') {
|
|
2267
|
+
return 'un día par del mes';
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2219
2271
|
function stepDates(dateField: string, opts: Opts): string {
|
|
2220
2272
|
const parts = dateField.split('/');
|
|
2221
2273
|
let phrase = 'cada ' + numero(+parts[1], opts) + ' días del mes';
|
|
@@ -2233,10 +2285,10 @@ function stepDates(dateField: string, opts: Opts): string {
|
|
|
2233
2285
|
// "en 2030, 2031 y 2032", "cada dos años desde 2030".
|
|
2234
2286
|
function applyYear(
|
|
2235
2287
|
description: string,
|
|
2236
|
-
|
|
2288
|
+
schedule: Schedule,
|
|
2237
2289
|
opts: Opts
|
|
2238
2290
|
): string {
|
|
2239
|
-
const yearField =
|
|
2291
|
+
const yearField = schedule.pattern.year;
|
|
2240
2292
|
|
|
2241
2293
|
if (yearField === '*') {
|
|
2242
2294
|
return description;
|
|
@@ -2247,7 +2299,7 @@ function applyYear(
|
|
|
2247
2299
|
}
|
|
2248
2300
|
|
|
2249
2301
|
// A foldable single year already joined its date in datePhrase.
|
|
2250
|
-
if (foldedYear(
|
|
2302
|
+
if (foldedYear(schedule) && schedule.pattern.date !== '*') {
|
|
2251
2303
|
return description;
|
|
2252
2304
|
}
|
|
2253
2305
|
|
|
@@ -2345,7 +2397,7 @@ function monthName(token: NameToken): string {
|
|
|
2345
2397
|
}
|
|
2346
2398
|
|
|
2347
2399
|
|
|
2348
|
-
// The Spanish language module: the
|
|
2400
|
+
// The Spanish language module: the Schedule renderer plus the language-owned
|
|
2349
2401
|
// strings and option normalization.
|
|
2350
2402
|
const es: Language<SpanishStyle> = {
|
|
2351
2403
|
describe,
|