cronli5 0.2.0 → 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 +90 -0
- package/README.md +4 -4
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +514 -407
- package/dist/cronli5.js +514 -407
- package/dist/lang/de.cjs +296 -225
- package/dist/lang/de.js +296 -225
- package/dist/lang/en.cjs +471 -364
- package/dist/lang/en.js +471 -364
- package/dist/lang/es.cjs +318 -281
- package/dist/lang/es.js +318 -281
- package/dist/lang/fi.cjs +326 -276
- package/dist/lang/fi.js +326 -276
- package/dist/lang/zh.cjs +308 -236
- package/dist/lang/zh.js +308 -236
- package/package.json +1 -1
- package/src/core/analyze.ts +22 -21
- 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} +23 -24
- package/src/core/shapes.ts +8 -1
- package/src/core/specs.ts +1 -1
- package/src/core/util.ts +4 -83
- package/src/core/validate.ts +2 -2
- package/src/core/weekday.ts +54 -0
- package/src/cronli5.ts +7 -7
- package/src/lang/de/index.ts +329 -288
- package/src/lang/en/dialects.ts +1 -1
- package/src/lang/en/index.ts +640 -516
- package/src/lang/es/index.ts +342 -374
- package/src/lang/es/notes.md +1 -1
- package/src/lang/fi/dialects.ts +1 -1
- package/src/lang/fi/index.ts +367 -372
- package/src/lang/fi/notes.md +23 -8
- package/src/lang/fi/status.json +1 -1
- package/src/lang/zh/index.ts +344 -262
- package/src/types.ts +6 -6
- package/types/core/analyze.d.ts +4 -4
- 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} +16 -21
- package/types/core/shapes.d.ts +2 -1
- package/types/core/specs.d.ts +1 -1
- package/types/core/util.d.ts +1 -15
- 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,30 +1,34 @@
|
|
|
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
|
|
7
|
-
// words, and is free to re-
|
|
6
|
+
// (docs/i18n-design.md §7): it consumes only the Schedule, owns all of its
|
|
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).
|
|
10
10
|
|
|
11
11
|
import {clockDigits, numeral} from '../../core/format.js';
|
|
12
12
|
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
13
|
+
import {isOpenStep} from '../../core/shapes.js';
|
|
13
14
|
import {
|
|
14
|
-
arithmeticStep,
|
|
15
|
-
|
|
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';
|
|
16
20
|
import type {Cronli5Options} from '../../types.js';
|
|
17
21
|
import type {
|
|
18
|
-
|
|
22
|
+
HourTimesPlan, Schedule, Language, NormalizedOptions, PlanNode,
|
|
19
23
|
Segment
|
|
20
|
-
} from '../../core/
|
|
24
|
+
} from '../../core/schedule.js';
|
|
21
25
|
import {resolveDialect, type SpanishStyle} from './dialects.js';
|
|
22
26
|
|
|
23
27
|
// Normalized options carrying Spanish's own style shape.
|
|
24
28
|
type Opts = NormalizedOptions<SpanishStyle>;
|
|
25
29
|
|
|
26
30
|
// The erased renderer signature the dispatch table maps to.
|
|
27
|
-
type Renderer = (
|
|
31
|
+
type Renderer = (schedule: Schedule, plan: PlanNode, opts: Opts) => string;
|
|
28
32
|
|
|
29
33
|
// A `step` segment, narrowed from the discriminated `Segment` union.
|
|
30
34
|
type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
@@ -62,26 +66,6 @@ type NameSegment =
|
|
|
62
66
|
type RangeNameSegment = Extract<NameSegment, {kind: 'range'}>;
|
|
63
67
|
type SingleNameSegment = Extract<NameSegment, {kind: 'single'}>;
|
|
64
68
|
|
|
65
|
-
// The first (and only) segment of a step field. The plan only routes here
|
|
66
|
-
// for step shapes, whose segments list is present and step-kinded; this
|
|
67
|
-
// asserts what the analysis guarantees but the type cannot express.
|
|
68
|
-
function stepSegment(segments: Segment[] | null): StepSegment {
|
|
69
|
-
return (segments as Segment[])[0] as StepSegment;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// The hour field's classified segments. Callers reach here only for hour
|
|
73
|
-
// shapes the analysis segmented, so the list is present; the type permits
|
|
74
|
-
// null (wildcard/quartz) that these paths never carry.
|
|
75
|
-
function hourSegments(ir: IR): Segment[] {
|
|
76
|
-
return ir.analyses.segments.hour as Segment[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// A field's classified segments. Callers reach a segment list only when the
|
|
80
|
-
// field is non-wildcard and non-quartz, where the analysis always produced
|
|
81
|
-
// one; the type's null (those two shapes) is unreachable on these paths.
|
|
82
|
-
function fieldSegments(ir: IR, field: Field): Segment[] {
|
|
83
|
-
return ir.analyses.segments[field] as Segment[];
|
|
84
|
-
}
|
|
85
69
|
|
|
86
70
|
// Spanish number names for the integers zero through ten.
|
|
87
71
|
const numeros = [
|
|
@@ -148,22 +132,22 @@ function normalizeOptions(options?: Cronli5Options): Opts {
|
|
|
148
132
|
};
|
|
149
133
|
}
|
|
150
134
|
|
|
151
|
-
// Render an analyzed cron pattern (the
|
|
152
|
-
function describe(
|
|
153
|
-
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);
|
|
154
138
|
}
|
|
155
139
|
|
|
156
140
|
// Render one plan node. `composeSeconds` recurses with its `rest` plan.
|
|
157
141
|
// When BOTH date and weekday are restricted (a date-OR-weekday union), the
|
|
158
142
|
// result is wrapped in the unified `[month] [time], ya sea <DOM> o <DOW>`
|
|
159
143
|
// frame regardless of arm shapes or month type.
|
|
160
|
-
function render(
|
|
144
|
+
function render(schedule: Schedule, plan: PlanNode, opts: Opts): string {
|
|
161
145
|
// Each renderer narrows `plan` to its own `kind`; the dispatch table is
|
|
162
146
|
// keyed by that discriminant, so the union-to-specific match is sound but
|
|
163
147
|
// not expressible without a cast.
|
|
164
|
-
const phrase = (renderers[plan.kind] as Renderer)(
|
|
148
|
+
const phrase = (renderers[plan.kind] as Renderer)(schedule, plan, opts);
|
|
165
149
|
|
|
166
|
-
if (!isDateWeekdayUnion(
|
|
150
|
+
if (!isDateWeekdayUnion(schedule)) {
|
|
167
151
|
return phrase;
|
|
168
152
|
}
|
|
169
153
|
|
|
@@ -171,55 +155,56 @@ function render(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
171
155
|
// (leadingQualifier and trailingQualifier both return '' for union patterns).
|
|
172
156
|
// Front the shared month (possibly with a trailing comma for enumerations),
|
|
173
157
|
// then append the union correlative last.
|
|
174
|
-
const lead = unionMonthLeadFull(
|
|
158
|
+
const lead = unionMonthLeadFull(schedule);
|
|
175
159
|
|
|
176
|
-
return (lead ? lead + ' ' : '') + phrase + unionYaseaSuffix(
|
|
160
|
+
return (lead ? lead + ' ' : '') + phrase + unionYaseaSuffix(schedule, opts);
|
|
177
161
|
}
|
|
178
162
|
|
|
179
163
|
// --- Seconds renderers. ---
|
|
180
164
|
|
|
181
165
|
function renderEverySecond(
|
|
182
|
-
|
|
166
|
+
schedule: Schedule,
|
|
183
167
|
plan: Extract<PlanNode, {kind: 'everySecond'}>,
|
|
184
168
|
opts: Opts
|
|
185
169
|
): string {
|
|
186
|
-
return 'cada segundo' + trailingQualifier(
|
|
170
|
+
return 'cada segundo' + trailingQualifier(schedule, opts);
|
|
187
171
|
}
|
|
188
172
|
|
|
189
173
|
function renderStandaloneSeconds(
|
|
190
|
-
|
|
174
|
+
schedule: Schedule,
|
|
191
175
|
plan: Extract<PlanNode, {kind: 'standaloneSeconds'}>,
|
|
192
176
|
opts: Opts
|
|
193
177
|
): string {
|
|
194
|
-
return secondsLeadClause(
|
|
178
|
+
return secondsLeadClause(schedule, opts) + trailingQualifier(schedule, opts);
|
|
195
179
|
}
|
|
196
180
|
|
|
197
181
|
function renderSecondPastMinute(
|
|
198
|
-
|
|
182
|
+
schedule: Schedule,
|
|
199
183
|
plan: Extract<PlanNode, {kind: 'secondPastMinute'}>,
|
|
200
184
|
opts: Opts
|
|
201
185
|
): string {
|
|
202
|
-
return 'en el segundo ' +
|
|
203
|
-
trailingQualifier(
|
|
186
|
+
return 'en el segundo ' + schedule.pattern.second + ' de cada minuto' +
|
|
187
|
+
trailingQualifier(schedule, opts);
|
|
204
188
|
}
|
|
205
189
|
|
|
206
190
|
// A meaningful second combined with a single specific minute (and an open
|
|
207
191
|
// hour): a single second folds into the minute anchor; a list, range, or
|
|
208
192
|
// step leads with its own clause.
|
|
209
193
|
function renderSecondsWithinMinute(
|
|
210
|
-
|
|
194
|
+
schedule: Schedule,
|
|
211
195
|
plan: Extract<PlanNode, {kind: 'secondsWithinMinute'}>,
|
|
212
196
|
opts: Opts
|
|
213
197
|
): string {
|
|
214
|
-
const minuteField =
|
|
198
|
+
const minuteField = schedule.pattern.minute;
|
|
215
199
|
|
|
216
200
|
if (plan.singleSecond) {
|
|
217
201
|
return 'en el minuto ' + minuteField + ' y el segundo ' +
|
|
218
|
-
|
|
202
|
+
schedule.pattern.second + ' de cada hora' +
|
|
203
|
+
trailingQualifier(schedule, opts);
|
|
219
204
|
}
|
|
220
205
|
|
|
221
|
-
return secondsLeadClause(
|
|
222
|
-
' de cada hora' + trailingQualifier(
|
|
206
|
+
return secondsLeadClause(schedule, opts) + ', en el minuto ' + minuteField +
|
|
207
|
+
' de cada hora' + trailingQualifier(schedule, opts);
|
|
223
208
|
}
|
|
224
209
|
|
|
225
210
|
// A seconds list nested into one or more fixed clock times ("..., en los
|
|
@@ -228,7 +213,7 @@ function renderSecondsWithinMinute(
|
|
|
228
213
|
// are listed. The clock time follows with the genitive "de", so the stride
|
|
229
214
|
// drops its "de cada minuto" anchor.
|
|
230
215
|
function secondsListAtClock(
|
|
231
|
-
|
|
216
|
+
schedule: Schedule,
|
|
232
217
|
rest: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
233
218
|
opts: Opts
|
|
234
219
|
): string {
|
|
@@ -240,10 +225,10 @@ function secondsListAtClock(
|
|
|
240
225
|
// prepend "de " to produce the genitive form "de las 09:00 y 17:00".
|
|
241
226
|
const clockList = grouped.startsWith('a ') ? grouped.slice(2) : grouped;
|
|
242
227
|
const stride =
|
|
243
|
-
strideFromSegments(
|
|
228
|
+
strideFromSegments(segmentsOf(schedule, 'second'), 'segundo', '', opts);
|
|
244
229
|
const secondsPhrase = stride ?? 'en los segundos ' +
|
|
245
|
-
joinList(segmentWords(
|
|
246
|
-
const dayFrame = trailingQualifier(
|
|
230
|
+
joinList(segmentWords(segmentsOf(schedule, 'second')));
|
|
231
|
+
const dayFrame = trailingQualifier(schedule, opts);
|
|
247
232
|
|
|
248
233
|
return (dayFrame ? dayFrame.trimStart() + ', ' : '') +
|
|
249
234
|
secondsPhrase + ' de ' + clockList;
|
|
@@ -254,35 +239,37 @@ function secondsListAtClock(
|
|
|
254
239
|
// when that does not apply (a non-clock rest, a multi-valued minute, or an
|
|
255
240
|
// hour that is not a stride).
|
|
256
241
|
function composeHourCadence(
|
|
257
|
-
|
|
242
|
+
schedule: Schedule,
|
|
258
243
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
259
244
|
opts: Opts
|
|
260
245
|
): string | null {
|
|
261
246
|
const clockRest = plan.rest.kind === 'clockTimes' ||
|
|
262
247
|
plan.rest.kind === 'compactClockTimes';
|
|
263
248
|
|
|
264
|
-
if (!clockRest ||
|
|
249
|
+
if (!clockRest || schedule.shapes.minute !== 'single') {
|
|
265
250
|
return null;
|
|
266
251
|
}
|
|
267
252
|
|
|
268
|
-
const minute = +
|
|
253
|
+
const minute = +schedule.pattern.minute;
|
|
269
254
|
|
|
270
|
-
return hourCadence(
|
|
255
|
+
return hourCadence(schedule, minute, opts) ??
|
|
256
|
+
hourRangeCadence(schedule, minute, opts);
|
|
271
257
|
}
|
|
272
258
|
|
|
273
259
|
// A wildcard or stepped second with a fixed minute across one or more specific
|
|
274
260
|
// hours: the seconds confine to the clock time(s), each minute named.
|
|
275
261
|
function isPinnedMinuteSeconds(
|
|
276
|
-
|
|
262
|
+
schedule: Schedule,
|
|
277
263
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
278
264
|
): plan is Extract<PlanNode, {kind: 'composeSeconds'}> &
|
|
279
265
|
{rest: Extract<PlanNode, {kind: 'clockTimes'}>} {
|
|
280
266
|
return plan.rest.kind === 'clockTimes' &&
|
|
281
|
-
(
|
|
267
|
+
(schedule.shapes.second === 'wildcard' ||
|
|
268
|
+
schedule.shapes.second === 'step');
|
|
282
269
|
}
|
|
283
270
|
|
|
284
271
|
function renderComposeSeconds(
|
|
285
|
-
|
|
272
|
+
schedule: Schedule,
|
|
286
273
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
287
274
|
opts: Opts
|
|
288
275
|
): string {
|
|
@@ -290,7 +277,7 @@ function renderComposeSeconds(
|
|
|
290
277
|
// minute is a cadence, not a wall of clock times: the second/minute lead,
|
|
291
278
|
// then the hour cadence ("en el segundo 30 de cada hora, cada dos horas").
|
|
292
279
|
// The clock-time rest would otherwise cross-multiply the hours.
|
|
293
|
-
const hourCad = composeHourCadence(
|
|
280
|
+
const hourCad = composeHourCadence(schedule, plan, opts);
|
|
294
281
|
|
|
295
282
|
if (hourCad !== null) {
|
|
296
283
|
return hourCad;
|
|
@@ -298,28 +285,28 @@ function renderComposeSeconds(
|
|
|
298
285
|
|
|
299
286
|
// A wildcard or stepped second with the minute pinned to a single value
|
|
300
287
|
// across one or more specific hours: the seconds confine to the clock time.
|
|
301
|
-
if (isPinnedMinuteSeconds(
|
|
302
|
-
return pinnedMinuteSeconds(
|
|
288
|
+
if (isPinnedMinuteSeconds(schedule, plan)) {
|
|
289
|
+
return pinnedMinuteSeconds(schedule, plan.rest, opts);
|
|
303
290
|
}
|
|
304
291
|
|
|
305
292
|
// Seconds list + fixed clock time: nest the seconds into the clock time(s)
|
|
306
293
|
// with genitive "de las HH:MM" instead of "de cada minuto"; the minute is
|
|
307
294
|
// fixed so "de cada minuto" is misleading. Single seconds already fold into
|
|
308
295
|
// the time in the clockTimes renderer; step seconds keep their own clause.
|
|
309
|
-
if (plan.rest.kind === 'clockTimes' &&
|
|
310
|
-
return secondsListAtClock(
|
|
296
|
+
if (plan.rest.kind === 'clockTimes' && schedule.shapes.second === 'list') {
|
|
297
|
+
return secondsListAtClock(schedule, plan.rest, opts);
|
|
311
298
|
}
|
|
312
299
|
|
|
313
300
|
// Second-step + fixed minute + hour range + weekday: anchor the cadence to
|
|
314
301
|
// the minute after the weekday + hour-range frame.
|
|
315
|
-
if (plan.rest.kind === 'hourRange' &&
|
|
316
|
-
|
|
302
|
+
if (plan.rest.kind === 'hourRange' && schedule.shapes.second === 'step' &&
|
|
303
|
+
schedule.pattern.weekday !== '*') {
|
|
317
304
|
const restNode = plan.rest;
|
|
318
305
|
const window = hourWindow(boundedWindow(restNode), opts);
|
|
319
|
-
const dayFrame = weekdayQualifier(
|
|
306
|
+
const dayFrame = weekdayQualifier(schedule) + monthScope(schedule);
|
|
320
307
|
const cadence = 'cada ' +
|
|
321
|
-
numero(stepSegment(
|
|
322
|
-
' segundos del minuto ' +
|
|
308
|
+
numero(stepSegment(schedule, 'second').interval, opts) +
|
|
309
|
+
' segundos del minuto ' + schedule.pattern.minute;
|
|
323
310
|
|
|
324
311
|
return dayFrame + ', ' + window + ', ' + cadence;
|
|
325
312
|
}
|
|
@@ -329,8 +316,9 @@ function renderComposeSeconds(
|
|
|
329
316
|
// Bind them with the genitive "de" ("cada segundo de cada dos minutos"),
|
|
330
317
|
// mirroring English. Other strides, a restricted hour, and an hour cadence
|
|
331
318
|
// keep the juxtaposed form.
|
|
332
|
-
if (isEveryOtherMinuteSeconds(
|
|
333
|
-
return secondsLeadClause(
|
|
319
|
+
if (isEveryOtherMinuteSeconds(schedule, plan)) {
|
|
320
|
+
return secondsLeadClause(schedule, opts) + ' de ' +
|
|
321
|
+
render(schedule, plan.rest, opts);
|
|
334
322
|
}
|
|
335
323
|
|
|
336
324
|
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
@@ -338,24 +326,25 @@ function renderComposeSeconds(
|
|
|
338
326
|
// double it. A wildcard or stepped second is not folded there (no
|
|
339
327
|
// clockSecond), so it still leads its own clause here.
|
|
340
328
|
const restOwnsLead = plan.rest.kind === 'compactClockTimes' &&
|
|
341
|
-
|
|
342
|
-
const lead = restOwnsLead ? '' : secondsLeadClause(
|
|
329
|
+
schedule.analyses.clockSecond;
|
|
330
|
+
const lead = restOwnsLead ? '' : secondsLeadClause(schedule, opts) + ', ';
|
|
343
331
|
|
|
344
|
-
return lead + render(
|
|
332
|
+
return lead + render(schedule, plan.rest, opts);
|
|
345
333
|
}
|
|
346
334
|
|
|
347
335
|
// A wildcard second over an unoffset minute */2 with a wildcard hour: the two
|
|
348
336
|
// cadences read as contradictory side by side, so they bind into one.
|
|
349
337
|
function isEveryOtherMinuteSeconds(
|
|
350
|
-
|
|
338
|
+
schedule: Schedule,
|
|
351
339
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
352
340
|
): boolean {
|
|
353
341
|
if (plan.rest.kind !== 'minuteFrequency' ||
|
|
354
|
-
|
|
342
|
+
schedule.shapes.second !== 'wildcard' ||
|
|
343
|
+
schedule.shapes.hour !== 'wildcard') {
|
|
355
344
|
return false;
|
|
356
345
|
}
|
|
357
346
|
|
|
358
|
-
const minuteStep = stepSegment(
|
|
347
|
+
const minuteStep = stepSegment(schedule, 'minute');
|
|
359
348
|
|
|
360
349
|
return minuteStep.startToken === '*' && minuteStep.interval === 2;
|
|
361
350
|
}
|
|
@@ -371,11 +360,11 @@ function isEveryOtherMinuteSeconds(
|
|
|
371
360
|
// minute is an unambiguous clock time, so the genitive "de las 09:05" form
|
|
372
361
|
// reads it as the minute, never the hour.
|
|
373
362
|
function pinnedMinuteSeconds(
|
|
374
|
-
|
|
363
|
+
schedule: Schedule,
|
|
375
364
|
rest: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
376
365
|
opts: Opts
|
|
377
366
|
): string {
|
|
378
|
-
const dayTrail = leadingQualifier(
|
|
367
|
+
const dayTrail = leadingQualifier(schedule, opts).trimEnd();
|
|
379
368
|
const trail = dayTrail ? ', ' + dayTrail : '';
|
|
380
369
|
|
|
381
370
|
// The "durante un minuto a las 9" duration form drops the clock minute, so it
|
|
@@ -383,18 +372,18 @@ function pinnedMinuteSeconds(
|
|
|
383
372
|
// minute LIST whose first value is 0 (e.g. */45 → :00, :45) must name each
|
|
384
373
|
// minute, never collapse to the bare hour (which once repeated it, "a las 9 y
|
|
385
374
|
// 9"), so it takes the explicit clock list.
|
|
386
|
-
if (+rest.times[0].minute === 0 &&
|
|
387
|
-
return secondsLeadClause(
|
|
375
|
+
if (+rest.times[0].minute === 0 && schedule.shapes.minute === 'single') {
|
|
376
|
+
return secondsLeadClause(schedule, opts) + ' durante un minuto ' +
|
|
388
377
|
durationHourList(rest.times, opts) + trail;
|
|
389
378
|
}
|
|
390
379
|
|
|
391
|
-
return secondsLeadClause(
|
|
380
|
+
return secondsLeadClause(schedule, opts) + ' de ' +
|
|
392
381
|
explicitClockList(rest.times, opts) + trail;
|
|
393
382
|
}
|
|
394
383
|
|
|
395
384
|
// The leading clause describing a second field relative to the minute.
|
|
396
|
-
function secondsLeadClause(
|
|
397
|
-
return secondsClause(
|
|
385
|
+
function secondsLeadClause(schedule: Schedule, opts: Opts): string {
|
|
386
|
+
return secondsClause(schedule, 'minuto', opts);
|
|
398
387
|
}
|
|
399
388
|
|
|
400
389
|
// The second clause counted against an arbitrary anchor. The anchor is
|
|
@@ -402,16 +391,16 @@ function secondsLeadClause(ir: IR, opts: Opts): string {
|
|
|
402
391
|
// pinned minute 0 into the hour and counts the second "de cada hora" instead
|
|
403
392
|
// ("en el segundo 30 de cada hora"), so the minute-0 confinement is stated,
|
|
404
393
|
// not dropped.
|
|
405
|
-
function secondsClause(
|
|
406
|
-
const secondField =
|
|
407
|
-
const shape =
|
|
394
|
+
function secondsClause(schedule: Schedule, anchor: string, opts: Opts): string {
|
|
395
|
+
const secondField = schedule.pattern.second;
|
|
396
|
+
const shape = schedule.shapes.second;
|
|
408
397
|
|
|
409
398
|
if (secondField === '*') {
|
|
410
399
|
return 'cada segundo';
|
|
411
400
|
}
|
|
412
401
|
|
|
413
402
|
if (shape === 'step') {
|
|
414
|
-
return stepCycle60(stepSegment(
|
|
403
|
+
return stepCycle60(stepSegment(schedule, 'second'), 'segundo',
|
|
415
404
|
anchor, opts);
|
|
416
405
|
}
|
|
417
406
|
|
|
@@ -426,55 +415,55 @@ function secondsClause(ir: IR, anchor: string, opts: Opts): string {
|
|
|
426
415
|
return 'en el segundo ' + secondField + ' de cada ' + anchor;
|
|
427
416
|
}
|
|
428
417
|
|
|
429
|
-
return strideFromSegments(
|
|
418
|
+
return strideFromSegments(segmentsOf(schedule, 'second'), 'segundo', anchor,
|
|
430
419
|
opts) ?? 'en los segundos ' +
|
|
431
|
-
joinList(segmentWords(
|
|
420
|
+
joinList(segmentWords(segmentsOf(schedule, 'second'))) +
|
|
432
421
|
' de cada ' + anchor;
|
|
433
422
|
}
|
|
434
423
|
|
|
435
424
|
// --- Minute renderers. ---
|
|
436
425
|
|
|
437
426
|
function renderEveryMinute(
|
|
438
|
-
|
|
427
|
+
schedule: Schedule,
|
|
439
428
|
plan: Extract<PlanNode, {kind: 'everyMinute'}>,
|
|
440
429
|
opts: Opts
|
|
441
430
|
): string {
|
|
442
|
-
return 'cada minuto' + trailingQualifier(
|
|
431
|
+
return 'cada minuto' + trailingQualifier(schedule, opts);
|
|
443
432
|
}
|
|
444
433
|
|
|
445
434
|
function renderSingleMinute(
|
|
446
|
-
|
|
435
|
+
schedule: Schedule,
|
|
447
436
|
plan: Extract<PlanNode, {kind: 'singleMinute'}>,
|
|
448
437
|
opts: Opts
|
|
449
438
|
): string {
|
|
450
|
-
return 'en el minuto ' +
|
|
451
|
-
trailingQualifier(
|
|
439
|
+
return 'en el minuto ' + schedule.pattern.minute + ' de cada hora' +
|
|
440
|
+
trailingQualifier(schedule, opts);
|
|
452
441
|
}
|
|
453
442
|
|
|
454
443
|
function renderRangeOfMinutes(
|
|
455
|
-
|
|
444
|
+
schedule: Schedule,
|
|
456
445
|
plan: Extract<PlanNode, {kind: 'rangeOfMinutes'}>,
|
|
457
446
|
opts: Opts
|
|
458
447
|
): string {
|
|
459
|
-
return minuteRangeLead(
|
|
460
|
-
trailingQualifier(
|
|
448
|
+
return minuteRangeLead(schedule.pattern.minute) + ' de cada hora' +
|
|
449
|
+
trailingQualifier(schedule, opts);
|
|
461
450
|
}
|
|
462
451
|
|
|
463
452
|
function renderMultipleMinutes(
|
|
464
|
-
|
|
453
|
+
schedule: Schedule,
|
|
465
454
|
plan: Extract<PlanNode, {kind: 'multipleMinutes'}>,
|
|
466
455
|
opts: Opts
|
|
467
456
|
): string {
|
|
468
|
-
return minutesList(
|
|
457
|
+
return minutesList(schedule, opts) + trailingQualifier(schedule, opts);
|
|
469
458
|
}
|
|
470
459
|
|
|
471
460
|
// "en los minutos 5, 10 y 30 de cada hora". An offset/uneven step the core
|
|
472
461
|
// enumerated to this list reads as a stride cadence when the fires form a
|
|
473
462
|
// long-enough progression.
|
|
474
|
-
function minutesList(
|
|
475
|
-
return strideFromSegments(
|
|
463
|
+
function minutesList(schedule: Schedule, opts: Opts): string {
|
|
464
|
+
return strideFromSegments(segmentsOf(schedule, 'minute'), 'minuto', 'hora',
|
|
476
465
|
opts) ?? 'en los minutos ' +
|
|
477
|
-
joinList(segmentWords(
|
|
466
|
+
joinList(segmentWords(segmentsOf(schedule, 'minute'))) + ' de cada hora';
|
|
478
467
|
}
|
|
479
468
|
|
|
480
469
|
// "cada minuto del 0 al 30". The standalone renderer adds "de cada hora";
|
|
@@ -496,7 +485,7 @@ function singleHourStep(segments: Segment[] | null): boolean {
|
|
|
496
485
|
// A single hour step as a confinement. A stride of two over the whole day
|
|
497
486
|
// reads idiomatically as the even ("las horas pares") or odd ("impares")
|
|
498
487
|
// hours; any other step names its active hours, which pins the schedule
|
|
499
|
-
// precisely (
|
|
488
|
+
// precisely (ordinal/colloquial forms would be imprecise here).
|
|
500
489
|
function stepHourSpan(segment: StepSegment, opts: Opts): string {
|
|
501
490
|
const bounded = segment.startToken.indexOf('-') !== -1;
|
|
502
491
|
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
@@ -576,17 +565,17 @@ function spanHours(hours: number[]): string {
|
|
|
576
565
|
|
|
577
566
|
// A repeating minute step, qualified by the active hour window(s).
|
|
578
567
|
function renderMinuteFrequency(
|
|
579
|
-
|
|
568
|
+
schedule: Schedule,
|
|
580
569
|
plan: Extract<PlanNode, {kind: 'minuteFrequency'}>,
|
|
581
570
|
opts: Opts
|
|
582
571
|
): string {
|
|
583
|
-
let phrase = stepCycle60(stepSegment(
|
|
572
|
+
let phrase = stepCycle60(stepSegment(schedule, 'minute'), 'minuto',
|
|
584
573
|
'hora', opts);
|
|
585
574
|
|
|
586
575
|
if (plan.hours.kind === 'during') {
|
|
587
576
|
// A uneven hour stride confines the minute cadence to its own bounded hour
|
|
588
577
|
// cadence ("cada 15 minutos, cada cinco horas de las 00:00 a las 20:00").
|
|
589
|
-
const cadence = unevenHourCadence(
|
|
578
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
590
579
|
|
|
591
580
|
if (cadence) {
|
|
592
581
|
phrase += ', ' + cadence;
|
|
@@ -594,9 +583,9 @@ function renderMinuteFrequency(
|
|
|
594
583
|
else {
|
|
595
584
|
// An offset step (e.g. 1/2) arrives here; a single step reads as a
|
|
596
585
|
// confinement, not the verbose window list.
|
|
597
|
-
phrase += singleHourStep(
|
|
598
|
-
', ' + stepHourSpan(stepSegment(
|
|
599
|
-
' ' + hourSpanFromTimes(
|
|
586
|
+
phrase += singleHourStep(schedule.analyses.segments.hour) ?
|
|
587
|
+
', ' + stepHourSpan(stepSegment(schedule, 'hour'), opts) :
|
|
588
|
+
' ' + hourSpanFromTimes(schedule, plan.hours.times, opts);
|
|
600
589
|
}
|
|
601
590
|
}
|
|
602
591
|
else if (plan.hours.kind === 'window') {
|
|
@@ -605,10 +594,10 @@ function renderMinuteFrequency(
|
|
|
605
594
|
else if (plan.hours.kind === 'step') {
|
|
606
595
|
// A clean stride is a confinement ("las horas pares", or the active-hour
|
|
607
596
|
// list), never a juxtaposed cadence ("cada dos horas").
|
|
608
|
-
phrase += ', ' + stepHourSpan(stepSegment(
|
|
597
|
+
phrase += ', ' + stepHourSpan(stepSegment(schedule, 'hour'), opts);
|
|
609
598
|
}
|
|
610
599
|
|
|
611
|
-
return phrase + trailingQualifier(
|
|
600
|
+
return phrase + trailingQualifier(schedule, opts);
|
|
612
601
|
}
|
|
613
602
|
|
|
614
603
|
// "cada minuto de las 9:00 a las 9:29 de la mañana". A wildcard minute is the
|
|
@@ -616,144 +605,144 @@ function renderMinuteFrequency(
|
|
|
616
605
|
// 09:00") rather than a synthesized "de las HH:00 a las HH:59" range the
|
|
617
606
|
// source never stated; a plain range is a real window and keeps "de … a …".
|
|
618
607
|
function renderMinuteSpanInHour(
|
|
619
|
-
|
|
608
|
+
schedule: Schedule,
|
|
620
609
|
plan: Extract<PlanNode, {kind: 'minuteSpanInHour'}>,
|
|
621
610
|
opts: Opts
|
|
622
611
|
): string {
|
|
623
|
-
if (
|
|
612
|
+
if (schedule.pattern.minute === '*') {
|
|
624
613
|
return 'cada minuto de la hora ' +
|
|
625
614
|
fromTime(timePhrase(plan.hour, 0, null, opts)) +
|
|
626
|
-
trailingQualifier(
|
|
615
|
+
trailingQualifier(schedule, opts);
|
|
627
616
|
}
|
|
628
617
|
|
|
629
618
|
return 'cada minuto ' +
|
|
630
619
|
timeRange({hour: plan.hour, minute: plan.span[0]},
|
|
631
620
|
{hour: plan.hour, minute: plan.span[1]}, opts) +
|
|
632
|
-
trailingQualifier(
|
|
621
|
+
trailingQualifier(schedule, opts);
|
|
633
622
|
}
|
|
634
623
|
|
|
635
|
-
// A minute window under discrete hours. Spanish re-
|
|
624
|
+
// A minute window under discrete hours. Spanish re-plans the
|
|
636
625
|
// wildcard form: rather than "during the X hours", each hour reads as its
|
|
637
626
|
// own window ("de las 9:00 a las 9:59").
|
|
638
627
|
function renderMinutesAcrossHours(
|
|
639
|
-
|
|
628
|
+
schedule: Schedule,
|
|
640
629
|
plan: Extract<PlanNode, {kind: 'minutesAcrossHours'}>,
|
|
641
630
|
opts: Opts
|
|
642
631
|
): string {
|
|
643
632
|
// A uneven hour stride reads as a cadence, not a wall of hour columns: the
|
|
644
633
|
// minute lead, then "cada N horas de las X a las Y".
|
|
645
|
-
const cadence = unevenHourCadence(
|
|
634
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
646
635
|
|
|
647
636
|
if (plan.form === 'wildcard') {
|
|
648
637
|
if (cadence !== null) {
|
|
649
|
-
return 'cada minuto, ' + cadence + trailingQualifier(
|
|
638
|
+
return 'cada minuto, ' + cadence + trailingQualifier(schedule, opts);
|
|
650
639
|
}
|
|
651
640
|
|
|
652
|
-
if (singleHourStep(
|
|
641
|
+
if (singleHourStep(schedule.analyses.segments.hour)) {
|
|
653
642
|
return 'cada minuto, ' +
|
|
654
|
-
stepHourSpan(stepSegment(
|
|
655
|
-
trailingQualifier(
|
|
643
|
+
stepHourSpan(stepSegment(schedule, 'hour'), opts) +
|
|
644
|
+
trailingQualifier(schedule, opts);
|
|
656
645
|
}
|
|
657
646
|
|
|
658
|
-
return 'cada minuto ' + hourSpanFromTimes(
|
|
659
|
-
trailingQualifier(
|
|
647
|
+
return 'cada minuto ' + hourSpanFromTimes(schedule, plan.times, opts) +
|
|
648
|
+
trailingQualifier(schedule, opts);
|
|
660
649
|
}
|
|
661
650
|
|
|
662
651
|
const lead = plan.form === 'range' ?
|
|
663
|
-
minuteRangeLead(
|
|
664
|
-
minutesList(
|
|
652
|
+
minuteRangeLead(schedule.pattern.minute) :
|
|
653
|
+
minutesList(schedule, opts);
|
|
665
654
|
|
|
666
655
|
if (cadence !== null) {
|
|
667
|
-
return lead + ', ' + cadence + trailingQualifier(
|
|
656
|
+
return lead + ', ' + cadence + trailingQualifier(schedule, opts);
|
|
668
657
|
}
|
|
669
658
|
|
|
670
|
-
return lead + ', ' + atHourTimes(
|
|
671
|
-
trailingQualifier(
|
|
659
|
+
return lead + ', ' + atHourTimes(schedule, plan.times, opts) +
|
|
660
|
+
trailingQualifier(schedule, opts);
|
|
672
661
|
}
|
|
673
662
|
|
|
674
663
|
function renderMinuteSpanAcrossHourStep(
|
|
675
|
-
|
|
664
|
+
schedule: Schedule,
|
|
676
665
|
plan: Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>,
|
|
677
666
|
opts: Opts
|
|
678
667
|
): string {
|
|
679
|
-
const segment = stepSegment(
|
|
668
|
+
const segment = stepSegment(schedule, 'hour');
|
|
680
669
|
// A bounded or uneven hour step reads as its endpoint-pinning cadence; an
|
|
681
670
|
// offset-clean step keeps its confinement / per-step phrasing.
|
|
682
|
-
const cadence = unevenHourCadence(
|
|
671
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
683
672
|
|
|
684
673
|
// A wildcard minute (a cadence) is reached only for a clean stride (a bounded
|
|
685
674
|
// or uneven step routes through minutesAcrossHours instead) and is confined.
|
|
686
675
|
if (plan.form === 'wildcard') {
|
|
687
676
|
return 'cada minuto, ' + stepHourSpan(segment, opts) +
|
|
688
|
-
trailingQualifier(
|
|
677
|
+
trailingQualifier(schedule, opts);
|
|
689
678
|
}
|
|
690
679
|
|
|
691
680
|
// A minute list keeps the same cadence clause as the range; only its lead
|
|
692
681
|
// differs ("en los minutos 5 y 30 de cada hora" vs "cada minuto del 0 al
|
|
693
682
|
// 30").
|
|
694
683
|
const lead = plan.form === 'list' ?
|
|
695
|
-
minutesList(
|
|
696
|
-
minuteRangeLead(
|
|
684
|
+
minutesList(schedule, opts) :
|
|
685
|
+
minuteRangeLead(schedule.pattern.minute);
|
|
697
686
|
|
|
698
687
|
return lead + ', ' +
|
|
699
|
-
(cadence ?? stepHours(segment, opts)) + trailingQualifier(
|
|
688
|
+
(cadence ?? stepHours(segment, opts)) + trailingQualifier(schedule, opts);
|
|
700
689
|
}
|
|
701
690
|
|
|
702
691
|
// --- Hour renderers. ---
|
|
703
692
|
|
|
704
693
|
function renderEveryHour(
|
|
705
|
-
|
|
694
|
+
schedule: Schedule,
|
|
706
695
|
plan: Extract<PlanNode, {kind: 'everyHour'}>,
|
|
707
696
|
opts: Opts
|
|
708
697
|
): string {
|
|
709
|
-
return 'cada hora' + trailingQualifier(
|
|
698
|
+
return 'cada hora' + trailingQualifier(schedule, opts);
|
|
710
699
|
}
|
|
711
700
|
|
|
712
701
|
function renderHourRange(
|
|
713
|
-
|
|
702
|
+
schedule: Schedule,
|
|
714
703
|
plan: Extract<PlanNode, {kind: 'hourRange'}>,
|
|
715
704
|
opts: Opts
|
|
716
705
|
): string {
|
|
717
706
|
const window = hourWindow(boundedWindow(plan), opts);
|
|
718
707
|
|
|
719
708
|
if (plan.minuteForm === 'wildcard') {
|
|
720
|
-
return 'cada minuto ' + window + trailingQualifier(
|
|
709
|
+
return 'cada minuto ' + window + trailingQualifier(schedule, opts);
|
|
721
710
|
}
|
|
722
711
|
|
|
723
712
|
if (plan.minuteForm === 'range') {
|
|
724
|
-
return minuteRangeLead(
|
|
725
|
-
trailingQualifier(
|
|
713
|
+
return minuteRangeLead(schedule.pattern.minute) + ', ' + window +
|
|
714
|
+
trailingQualifier(schedule, opts);
|
|
726
715
|
}
|
|
727
716
|
|
|
728
717
|
// On the hour the window joins directly ("cada hora de las 9:00 a las
|
|
729
718
|
// 17:00"); a discrete minute anchors its own clause first.
|
|
730
|
-
if (
|
|
731
|
-
return 'cada hora ' + window + trailingQualifier(
|
|
719
|
+
if (schedule.pattern.minute === '0') {
|
|
720
|
+
return 'cada hora ' + window + trailingQualifier(schedule, opts);
|
|
732
721
|
}
|
|
733
722
|
|
|
734
|
-
const lead =
|
|
735
|
-
'en el minuto ' +
|
|
736
|
-
minutesList(
|
|
723
|
+
const lead = schedule.shapes.minute === 'single' ?
|
|
724
|
+
'en el minuto ' + schedule.pattern.minute + ' de cada hora' :
|
|
725
|
+
minutesList(schedule, opts);
|
|
737
726
|
|
|
738
|
-
return lead + ', ' + window + trailingQualifier(
|
|
727
|
+
return lead + ', ' + window + trailingQualifier(schedule, opts);
|
|
739
728
|
}
|
|
740
729
|
|
|
741
730
|
function renderHourStep(
|
|
742
|
-
|
|
731
|
+
schedule: Schedule,
|
|
743
732
|
plan: Extract<PlanNode, {kind: 'hourStep'}>,
|
|
744
733
|
opts: Opts
|
|
745
734
|
): string {
|
|
746
735
|
// A bounded or uneven hour step reads as its endpoint-pinning cadence ("cada
|
|
747
736
|
// dos horas de las 09:00 a las 17:00"); an offset-clean step keeps its bare
|
|
748
737
|
// or "a partir de" cadence.
|
|
749
|
-
const cadence = unevenHourCadence(
|
|
738
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
750
739
|
|
|
751
740
|
if (cadence !== null) {
|
|
752
|
-
return cadence + trailingQualifier(
|
|
741
|
+
return cadence + trailingQualifier(schedule, opts);
|
|
753
742
|
}
|
|
754
743
|
|
|
755
|
-
return stepHours(stepSegment(
|
|
756
|
-
trailingQualifier(
|
|
744
|
+
return stepHours(stepSegment(schedule, 'hour'), opts) +
|
|
745
|
+
trailingQualifier(schedule, opts);
|
|
757
746
|
}
|
|
758
747
|
|
|
759
748
|
// The hour-range plan as a window. The close lands on the top of the final
|
|
@@ -782,8 +771,8 @@ function hourWindow(
|
|
|
782
771
|
|
|
783
772
|
// Whether BOTH the date and weekday fields are restricted (not '*'): cron
|
|
784
773
|
// fires when either condition matches, making this a date-OR-weekday union.
|
|
785
|
-
function isDateWeekdayUnion(
|
|
786
|
-
return
|
|
774
|
+
function isDateWeekdayUnion(schedule: Schedule): boolean {
|
|
775
|
+
return schedule.pattern.date !== '*' && schedule.pattern.weekday !== '*';
|
|
787
776
|
}
|
|
788
777
|
|
|
789
778
|
// The month lead for the unified union frame, with a trailing comma appended
|
|
@@ -791,14 +780,14 @@ function isDateWeekdayUnion(ir: IR): boolean {
|
|
|
791
780
|
// Single month → `en enero`; range → `de enero a marzo`;
|
|
792
781
|
// step/enumeration (≥2 flattened singles) → `en enero, marzo, …, y noviembre,`.
|
|
793
782
|
// Wildcard month → '' (omit; frame starts with the time).
|
|
794
|
-
function unionMonthLeadFull(
|
|
795
|
-
if (
|
|
783
|
+
function unionMonthLeadFull(schedule: Schedule): string {
|
|
784
|
+
if (schedule.pattern.month === '*') {
|
|
796
785
|
return '';
|
|
797
786
|
}
|
|
798
787
|
|
|
799
|
-
const lead = monthPhrase(
|
|
800
|
-
const segments = flattenSteps(
|
|
801
|
-
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;
|
|
802
791
|
|
|
803
792
|
return isEnumeration ? lead + ',' : lead;
|
|
804
793
|
}
|
|
@@ -807,19 +796,30 @@ function unionMonthLeadFull(ir: IR): string {
|
|
|
807
796
|
// Quartz and open-step forms are self-contained; ranges use `del N al M del
|
|
808
797
|
// mes`; a single date reads `el día N` under a restricted month (month is in
|
|
809
798
|
// the lead) or `el N de cada mes` under a wildcard month.
|
|
810
|
-
function domArm(
|
|
811
|
-
const date =
|
|
799
|
+
function domArm(schedule: Schedule, opts: Opts): string {
|
|
800
|
+
const date = schedule.pattern.date;
|
|
812
801
|
const quartz = quartzDatePhrase(date);
|
|
813
802
|
|
|
814
803
|
if (quartz) {
|
|
815
804
|
return quartz;
|
|
816
805
|
}
|
|
817
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
|
+
|
|
818
818
|
if (isOpenStep(date)) {
|
|
819
819
|
return stepDates(date, opts);
|
|
820
820
|
}
|
|
821
821
|
|
|
822
|
-
const segments =
|
|
822
|
+
const segments = segmentsOf(schedule, 'date');
|
|
823
823
|
|
|
824
824
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
825
825
|
return 'del ' + segments[0].bounds[0] + ' al ' +
|
|
@@ -827,7 +827,7 @@ function domArm(ir: IR, opts: Opts): string {
|
|
|
827
827
|
}
|
|
828
828
|
|
|
829
829
|
if (segments.length === 1 && segments[0].kind === 'single') {
|
|
830
|
-
return
|
|
830
|
+
return schedule.pattern.month === '*' ?
|
|
831
831
|
'el ' + segments[0].value + ' de cada mes' :
|
|
832
832
|
'el día ' + segments[0].value;
|
|
833
833
|
}
|
|
@@ -839,16 +839,16 @@ function domArm(ir: IR, opts: Opts): string {
|
|
|
839
839
|
// Quartz forms are self-contained; a single weekday reads `cualquier <name>`;
|
|
840
840
|
// all other forms use the same phrasing as the standalone weekday qualifier
|
|
841
841
|
// (range → `de lunes a viernes`; list/step → `los domingos, …`).
|
|
842
|
-
function dowArm(
|
|
843
|
-
const quartz = quartzWeekdayPhrase(
|
|
842
|
+
function dowArm(schedule: Schedule): string {
|
|
843
|
+
const quartz = quartzWeekdayPhrase(schedule.pattern.weekday);
|
|
844
844
|
|
|
845
845
|
if (quartz) {
|
|
846
846
|
return quartz;
|
|
847
847
|
}
|
|
848
848
|
|
|
849
849
|
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
850
|
-
// form. The
|
|
851
|
-
const segments = orderWeekdaysForDisplay(
|
|
850
|
+
// form. The Schedule stays canonical (Sunday=0). The helper flattens steps.
|
|
851
|
+
const segments = orderWeekdaysForDisplay(segmentsOf(schedule, 'weekday'));
|
|
852
852
|
const allSingles = segments.every(function single(segment) {
|
|
853
853
|
return segment.kind === 'single';
|
|
854
854
|
});
|
|
@@ -865,8 +865,12 @@ function dowArm(ir: IR): string {
|
|
|
865
865
|
}));
|
|
866
866
|
}
|
|
867
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.
|
|
868
872
|
if (segments.length === 1) {
|
|
869
|
-
return weekdayRange(segments[0] as RangeNameSegment);
|
|
873
|
+
return 'cualquier día ' + weekdayRange(segments[0] as RangeNameSegment);
|
|
870
874
|
}
|
|
871
875
|
|
|
872
876
|
return joinList(segments.map(function name(segment) {
|
|
@@ -877,23 +881,23 @@ function dowArm(ir: IR): string {
|
|
|
877
881
|
}
|
|
878
882
|
|
|
879
883
|
// The `, ya sea <DOM> o <DOW>` correlative suffix for the union frame.
|
|
880
|
-
function unionYaseaSuffix(
|
|
881
|
-
return ', ya sea ' + domArm(
|
|
884
|
+
function unionYaseaSuffix(schedule: Schedule, opts: Opts): string {
|
|
885
|
+
return ', ya sea ' + domArm(schedule, opts) + ' o ' + dowArm(schedule);
|
|
882
886
|
}
|
|
883
887
|
|
|
884
888
|
// "todos los días a las 9:30 y a las 17:00".
|
|
885
889
|
function renderClockTimes(
|
|
886
|
-
|
|
890
|
+
schedule: Schedule,
|
|
887
891
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
888
892
|
opts: Opts
|
|
889
893
|
): string {
|
|
890
894
|
// An hour step or range (or arithmetic-progression hour list) under a single
|
|
891
895
|
// pinned minute reads as a cadence or window rather than a cross-product of
|
|
892
896
|
// clock times.
|
|
893
|
-
if (
|
|
894
|
-
const minute = +
|
|
895
|
-
const cadence = hourCadence(
|
|
896
|
-
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);
|
|
897
901
|
|
|
898
902
|
if (cadence !== null) {
|
|
899
903
|
return cadence;
|
|
@@ -904,7 +908,7 @@ function renderClockTimes(
|
|
|
904
908
|
return atTime(timePhrase(time.hour, time.minute, time.second, opts));
|
|
905
909
|
});
|
|
906
910
|
|
|
907
|
-
return leadingQualifier(
|
|
911
|
+
return leadingQualifier(schedule, opts) + groupClockTimes(phrases);
|
|
908
912
|
}
|
|
909
913
|
|
|
910
914
|
// The genitive clock-time list for a minute-0 compose-seconds confinement:
|
|
@@ -1230,7 +1234,7 @@ function groupClockTimesByArticle(phrases: string[]): string {
|
|
|
1230
1234
|
// Compact form past the enumeration cap: a single minute folds into
|
|
1231
1235
|
// per-segment hour windows; a minute list leads with its own clause.
|
|
1232
1236
|
function renderCompactClockTimes(
|
|
1233
|
-
|
|
1237
|
+
schedule: Schedule,
|
|
1234
1238
|
plan: Extract<PlanNode, {kind: 'compactClockTimes'}>,
|
|
1235
1239
|
opts: Opts
|
|
1236
1240
|
): string {
|
|
@@ -1238,39 +1242,44 @@ function renderCompactClockTimes(
|
|
|
1238
1242
|
// An hour step or range (or arithmetic-progression hour list) under the
|
|
1239
1243
|
// single pinned minute reads as a cadence or window, not a wall of clock
|
|
1240
1244
|
// times. (Returns null for an irregular list, which keeps folding below.)
|
|
1241
|
-
const cadence = hourCadence(
|
|
1242
|
-
hourRangeCadence(
|
|
1245
|
+
const cadence = hourCadence(schedule, plan.minute, opts) ??
|
|
1246
|
+
hourRangeCadence(schedule, plan.minute, opts);
|
|
1243
1247
|
|
|
1244
1248
|
if (cadence !== null) {
|
|
1245
1249
|
return cadence;
|
|
1246
1250
|
}
|
|
1247
1251
|
|
|
1248
|
-
const ranged =
|
|
1252
|
+
const ranged = segmentsOf(schedule, 'hour').some(function range(segment) {
|
|
1249
1253
|
return segment.kind === 'range';
|
|
1250
1254
|
});
|
|
1251
1255
|
|
|
1252
1256
|
// A folded contiguous hour range reads with the hourly cadence ("cada
|
|
1253
1257
|
// hora de las 9:00 a las 20:00 y a las 22:00"), not "todos los días".
|
|
1254
|
-
if (ranged && !
|
|
1258
|
+
if (ranged && !schedule.analyses.clockSecond) {
|
|
1255
1259
|
return 'cada hora ' +
|
|
1256
|
-
hourSegmentTimes(
|
|
1257
|
-
|
|
1260
|
+
hourSegmentTimes(
|
|
1261
|
+
schedule, plan.minute, schedule.analyses.clockSecond, opts
|
|
1262
|
+
) +
|
|
1263
|
+
trailingQualifier(schedule, opts);
|
|
1258
1264
|
}
|
|
1259
1265
|
|
|
1260
|
-
return leadingQualifier(
|
|
1261
|
-
hourSegmentTimes(
|
|
1266
|
+
return leadingQualifier(schedule, opts) +
|
|
1267
|
+
hourSegmentTimes(
|
|
1268
|
+
schedule, plan.minute, schedule.analyses.clockSecond, opts
|
|
1269
|
+
);
|
|
1262
1270
|
}
|
|
1263
1271
|
|
|
1264
1272
|
// A uneven hour stride reads as a cadence after the minute lead, not a wall
|
|
1265
1273
|
// of clock-time columns.
|
|
1266
|
-
const cadence = unevenHourCadence(
|
|
1274
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
1267
1275
|
const phrase = cadence ?
|
|
1268
|
-
minutesList(
|
|
1269
|
-
|
|
1270
|
-
|
|
1276
|
+
minutesList(schedule, opts) + ', ' + cadence +
|
|
1277
|
+
trailingQualifier(schedule, opts) :
|
|
1278
|
+
minutesList(schedule, opts) + ', ' +
|
|
1279
|
+
hourContextTimes(schedule, opts) + trailingQualifier(schedule, opts);
|
|
1271
1280
|
|
|
1272
|
-
return
|
|
1273
|
-
secondsLeadClause(
|
|
1281
|
+
return schedule.analyses.clockSecond ?
|
|
1282
|
+
secondsLeadClause(schedule, opts) + ', ' + phrase :
|
|
1274
1283
|
phrase;
|
|
1275
1284
|
}
|
|
1276
1285
|
|
|
@@ -1312,21 +1321,17 @@ const renderers = {
|
|
|
1312
1321
|
function renderStride(stride: Stride, opts: Opts): string {
|
|
1313
1322
|
const {interval, start, last, cycle, unit, anchor} = stride;
|
|
1314
1323
|
const cadence = 'cada ' + numero(interval, opts) + ' ' + unit + 's';
|
|
1315
|
-
const tiles = cycle % interval === 0;
|
|
1316
|
-
|
|
1317
|
-
if (start === 0 && tiles) {
|
|
1318
|
-
return cadence;
|
|
1319
|
-
}
|
|
1320
1324
|
|
|
1321
1325
|
// A context that supplies its own trailing scope passes an empty anchor, so
|
|
1322
1326
|
// the cadence keeps its endpoints but drops the "de cada <anchor>" tail.
|
|
1323
1327
|
const tail = anchor ? ' de cada ' + anchor : '';
|
|
1324
1328
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
+
});
|
|
1330
1335
|
}
|
|
1331
1336
|
|
|
1332
1337
|
// "cada 15 minutos", "en los minutos 5, 20 y 35 de cada hora", or
|
|
@@ -1366,9 +1371,9 @@ function stepCycle60(
|
|
|
1366
1371
|
|
|
1367
1372
|
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
1368
1373
|
// form an arithmetic progression long enough to beat the list (the core
|
|
1369
|
-
// enumerates an offset/uneven step to this fire list; the
|
|
1370
|
-
// the renderer recognizes the progression). Returns null for a
|
|
1371
|
-
// 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.
|
|
1372
1377
|
function strideFromSegments(
|
|
1373
1378
|
segments: Segment[],
|
|
1374
1379
|
unit: string,
|
|
@@ -1383,21 +1388,6 @@ function strideFromSegments(
|
|
|
1383
1388
|
null;
|
|
1384
1389
|
}
|
|
1385
1390
|
|
|
1386
|
-
// The sorted numeric values a field's segments cover, or null if any segment
|
|
1387
|
-
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
1388
|
-
function singleValues(segments: Segment[]): number[] | null {
|
|
1389
|
-
const values: number[] = [];
|
|
1390
|
-
|
|
1391
|
-
for (const segment of segments) {
|
|
1392
|
-
if (segment.kind !== 'single') {
|
|
1393
|
-
return null;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
values.push(+segment.value);
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
return values;
|
|
1400
|
-
}
|
|
1401
1391
|
|
|
1402
1392
|
// "cada seis horas", "a las 9:00, a las 11:00 y a la 1:00", or "cada
|
|
1403
1393
|
// cinco horas a partir de las 2:00".
|
|
@@ -1438,29 +1428,13 @@ function hourStrideCadence(
|
|
|
1438
1428
|
): string {
|
|
1439
1429
|
const {start, interval, last} = stride;
|
|
1440
1430
|
const cadence = 'cada ' + numero(interval, opts) + ' horas';
|
|
1441
|
-
const tiles = 24 % interval === 0;
|
|
1442
|
-
|
|
1443
|
-
if (start === 0 && tiles) {
|
|
1444
|
-
return cadence;
|
|
1445
|
-
}
|
|
1446
1431
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// Whether an hour stride wraps the day cleanly from within its first interval
|
|
1456
|
-
// (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
|
|
1457
|
-
// stride has no distinct endpoint and keeps its bare or "a partir de" cadence.
|
|
1458
|
-
// Every other stride — a uneven interval, or one starting at or past its
|
|
1459
|
-
// interval (a bounded `a-b/n`) — is a bounded set the cadence pins the ends of.
|
|
1460
|
-
function offsetCleanStride(
|
|
1461
|
-
stride: {start: number; interval: number}
|
|
1462
|
-
): boolean {
|
|
1463
|
-
return stride.start < stride.interval && 24 % stride.interval === 0;
|
|
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
|
+
});
|
|
1464
1438
|
}
|
|
1465
1439
|
|
|
1466
1440
|
// The bounded cadence for an hour stride that pins both clock-time endpoints,
|
|
@@ -1470,8 +1444,8 @@ function offsetCleanStride(
|
|
|
1470
1444
|
// ("…, cada cinco horas de las 00:00 a las 20:00") than as a wall of clock
|
|
1471
1445
|
// times. An offset-clean stride keeps its existing confinement form, so only
|
|
1472
1446
|
// the endpoint-bearing case routes here.
|
|
1473
|
-
function unevenHourCadence(
|
|
1474
|
-
const stride = hourStride(
|
|
1447
|
+
function unevenHourCadence(schedule: Schedule, opts: Opts): string | null {
|
|
1448
|
+
const stride = hourStride(schedule);
|
|
1475
1449
|
|
|
1476
1450
|
if (!stride || offsetCleanStride(stride)) {
|
|
1477
1451
|
return null;
|
|
@@ -1480,51 +1454,16 @@ function unevenHourCadence(ir: IR, opts: Opts): string | null {
|
|
|
1480
1454
|
return hourStrideCadence(stride, opts);
|
|
1481
1455
|
}
|
|
1482
1456
|
|
|
1483
|
-
// An hour list's arithmetic progression, or null when its values are not a
|
|
1484
|
-
// step the renderer should speak as a cadence. The core rewrites a uneven hour
|
|
1485
|
-
// step (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its
|
|
1486
|
-
// literal fire list, indistinguishable in the IR from a hand-written list; the
|
|
1487
|
-
// renderer recovers the cadence from the values. A progression starting at zero
|
|
1488
|
-
// is a `*/n` step however short (0,7,14,21 is `*/7`); a non-zero progression is
|
|
1489
|
-
// only a step when it is too long to be a deliberate clock-time list (e.g. 9,17
|
|
1490
|
-
// is two named times, not a cadence). Interval one is a plain range, never a
|
|
1491
|
-
// step.
|
|
1492
|
-
function hourListStride(
|
|
1493
|
-
values: number[]
|
|
1494
|
-
): {start: number; interval: number; last: number} | null {
|
|
1495
|
-
if (values.length < 2) {
|
|
1496
|
-
return null;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
const interval = values[1] - values[0];
|
|
1500
|
-
|
|
1501
|
-
if (interval < 2) {
|
|
1502
|
-
return null;
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
for (let i = 2; i < values.length; i += 1) {
|
|
1506
|
-
if (values[i] - values[i - 1] !== interval) {
|
|
1507
|
-
return null;
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
if (values[0] !== 0 && values.length < 5) {
|
|
1512
|
-
return null;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
return {interval, last: values[values.length - 1], start: values[0]};
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
1457
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1519
1458
|
// segment yields its {start, interval, last} directly; an all-single hour
|
|
1520
1459
|
// list yields one only when its values form a step progression (so an irregular
|
|
1521
|
-
// list like 9,17 keeps enumerating). The
|
|
1460
|
+
// list like 9,17 keeps enumerating). The Schedule is unchanged — the renderer
|
|
1522
1461
|
// recognizes the stride and speaks it as a cadence instead of the clock-time
|
|
1523
1462
|
// cross-product.
|
|
1524
1463
|
function hourStride(
|
|
1525
|
-
|
|
1464
|
+
schedule: Schedule
|
|
1526
1465
|
): {start: number; interval: number; last: number} | null {
|
|
1527
|
-
const segments =
|
|
1466
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1528
1467
|
|
|
1529
1468
|
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
1530
1469
|
const segment = segments[0];
|
|
@@ -1551,8 +1490,8 @@ function hourStride(
|
|
|
1551
1490
|
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
1552
1491
|
// fills the minute (a "durante un minuto" frame at minute 0); a single 0 is
|
|
1553
1492
|
// just the top of the minute (no clause); anything else needs its own clause.
|
|
1554
|
-
function subMinuteSecond(
|
|
1555
|
-
return
|
|
1493
|
+
function subMinuteSecond(schedule: Schedule): boolean {
|
|
1494
|
+
return schedule.pattern.second === '*' || schedule.shapes.second === 'step';
|
|
1556
1495
|
}
|
|
1557
1496
|
|
|
1558
1497
|
// The lead clause for an hour-cadence rendering: the second and the pinned
|
|
@@ -1562,24 +1501,26 @@ function subMinuteSecond(ir: IR): boolean {
|
|
|
1562
1501
|
// minuto" frame (the whole minute-0 window). A non-zero minute is a real clock
|
|
1563
1502
|
// minute: the second leads with its own clause (if any), then the minute reads
|
|
1564
1503
|
// "en el minuto M".
|
|
1565
|
-
function hourCadenceLead(
|
|
1504
|
+
function hourCadenceLead(
|
|
1505
|
+
schedule: Schedule, minute: number, opts: Opts
|
|
1506
|
+
): string {
|
|
1566
1507
|
if (minute === 0) {
|
|
1567
|
-
if (subMinuteSecond(
|
|
1568
|
-
return secondsClause(
|
|
1508
|
+
if (subMinuteSecond(schedule)) {
|
|
1509
|
+
return secondsClause(schedule, 'minuto', opts) + ' durante un minuto';
|
|
1569
1510
|
}
|
|
1570
1511
|
|
|
1571
|
-
return secondsClause(
|
|
1512
|
+
return secondsClause(schedule, 'hora', opts);
|
|
1572
1513
|
}
|
|
1573
1514
|
|
|
1574
1515
|
const minutePhrase = 'en el minuto ' + minute;
|
|
1575
1516
|
|
|
1576
1517
|
// A single 0 second is just the top of the minute, so the minute leads
|
|
1577
1518
|
// alone; any other second prefixes its own clause.
|
|
1578
|
-
if (
|
|
1519
|
+
if (schedule.pattern.second === '0') {
|
|
1579
1520
|
return minutePhrase;
|
|
1580
1521
|
}
|
|
1581
1522
|
|
|
1582
|
-
return secondsClause(
|
|
1523
|
+
return secondsClause(schedule, 'minuto', opts) + ', ' + minutePhrase;
|
|
1583
1524
|
}
|
|
1584
1525
|
|
|
1585
1526
|
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
@@ -1590,9 +1531,11 @@ function hourCadenceLead(ir: IR, minute: number, opts: Opts): string {
|
|
|
1590
1531
|
// enumeration is no longer than the cadence: a meaningful second makes every
|
|
1591
1532
|
// clock time three digit-groups, so any stride is worth compacting; otherwise
|
|
1592
1533
|
// the stride must exceed the clock-time cap, the same point at which the core
|
|
1593
|
-
// itself stops enumerating. Renderer-only; the
|
|
1594
|
-
function hourCadence(
|
|
1595
|
-
|
|
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);
|
|
1596
1539
|
|
|
1597
1540
|
if (!stride) {
|
|
1598
1541
|
return null;
|
|
@@ -1605,7 +1548,7 @@ function hourCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
|
1605
1548
|
// or "a partir de" form is no shorter than the list. A bounded or uneven
|
|
1606
1549
|
// stride has no clean wrap, so its endpoint-pinning cadence ("cada cinco
|
|
1607
1550
|
// horas de las 00:00 a las 20:00") reads better however short.
|
|
1608
|
-
if (
|
|
1551
|
+
if (schedule.pattern.second === '0' && fires <= maxClockTimes &&
|
|
1609
1552
|
offsetCleanStride(stride)) {
|
|
1610
1553
|
return null;
|
|
1611
1554
|
}
|
|
@@ -1614,31 +1557,31 @@ function hourCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
|
1614
1557
|
// stride is a confinement, not a juxtaposed cadence: it reads "durante un
|
|
1615
1558
|
// minuto, durante las horas pares", reusing the hour-step confinement idiom
|
|
1616
1559
|
// so the minute-0 window is never heard as the bare hour cadence.
|
|
1617
|
-
const confinement = minute === 0 && subMinuteSecond(
|
|
1618
|
-
cleanStrideSegment(
|
|
1560
|
+
const confinement = minute === 0 && subMinuteSecond(schedule) &&
|
|
1561
|
+
cleanStrideSegment(schedule);
|
|
1619
1562
|
|
|
1620
1563
|
if (confinement) {
|
|
1621
|
-
return secondsClause(
|
|
1622
|
-
stepHourSpan(confinement, opts) + trailingQualifier(
|
|
1564
|
+
return secondsClause(schedule, 'minuto', opts) + ' durante un minuto, ' +
|
|
1565
|
+
stepHourSpan(confinement, opts) + trailingQualifier(schedule, opts);
|
|
1623
1566
|
}
|
|
1624
1567
|
|
|
1625
1568
|
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
1626
1569
|
// lead clause to fold in, so the bounded cadence stands on its own ("cada
|
|
1627
1570
|
// cinco horas de las 00:00 a las 20:00").
|
|
1628
|
-
if (minute === 0 &&
|
|
1629
|
-
return hourStrideCadence(stride, opts) + trailingQualifier(
|
|
1571
|
+
if (minute === 0 && schedule.pattern.second === '0') {
|
|
1572
|
+
return hourStrideCadence(stride, opts) + trailingQualifier(schedule, opts);
|
|
1630
1573
|
}
|
|
1631
1574
|
|
|
1632
|
-
return hourCadenceLead(
|
|
1633
|
-
hourStrideCadence(stride, opts) + trailingQualifier(
|
|
1575
|
+
return hourCadenceLead(schedule, minute, opts) + ', ' +
|
|
1576
|
+
hourStrideCadence(stride, opts) + trailingQualifier(schedule, opts);
|
|
1634
1577
|
}
|
|
1635
1578
|
|
|
1636
1579
|
// The hour step segment when the hour is a clean stride es renders as a
|
|
1637
1580
|
// confinement phrase ("durante las horas pares"); null otherwise (an offset or
|
|
1638
1581
|
// bounded step, an uneven stride, or an arithmetic-progression list, which
|
|
1639
1582
|
// keep the bounded cadence form).
|
|
1640
|
-
function cleanStrideSegment(
|
|
1641
|
-
const segments =
|
|
1583
|
+
function cleanStrideSegment(schedule: Schedule): StepSegment | null {
|
|
1584
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1642
1585
|
const segment = segments.length === 1 && segments[0];
|
|
1643
1586
|
|
|
1644
1587
|
if (!segment || segment.kind !== 'step' ||
|
|
@@ -1653,8 +1596,8 @@ function cleanStrideSegment(ir: IR): StepSegment | null {
|
|
|
1653
1596
|
// range — and so forms a window rather than a cross-product of clock times.
|
|
1654
1597
|
// A pure single-value list (9,17) has no range to span and still enumerates;
|
|
1655
1598
|
// a step is handled by hourStride/hourCadence.
|
|
1656
|
-
function hasHourWindow(
|
|
1657
|
-
return
|
|
1599
|
+
function hasHourWindow(schedule: Schedule): boolean {
|
|
1600
|
+
return segmentsOf(schedule, 'hour').some(function range(segment) {
|
|
1658
1601
|
return segment.kind === 'range';
|
|
1659
1602
|
});
|
|
1660
1603
|
}
|
|
@@ -1666,9 +1609,12 @@ function hasHourWindow(ir: IR): boolean {
|
|
|
1666
1609
|
// times. The hour-RANGE analog of hourCadence. Returns null when the hour has
|
|
1667
1610
|
// no range, when the minute is non-zero (a real clock minute the existing
|
|
1668
1611
|
// window form already speaks), or when a plain :00 set carries no clause.
|
|
1669
|
-
// Renderer-only; the
|
|
1670
|
-
function hourRangeCadence(
|
|
1671
|
-
|
|
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') {
|
|
1672
1618
|
return null;
|
|
1673
1619
|
}
|
|
1674
1620
|
|
|
@@ -1678,14 +1624,15 @@ function hourRangeCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
|
1678
1624
|
// ("cada hora de las 09:00 a las 17:00") so the confinement is never heard
|
|
1679
1625
|
// as it — the hour-range analog of "durante un minuto, durante las horas
|
|
1680
1626
|
// pares".
|
|
1681
|
-
if (subMinuteSecond(
|
|
1682
|
-
return secondsClause(
|
|
1683
|
-
'durante las horas ' + hourSegmentTimes(
|
|
1684
|
-
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);
|
|
1685
1631
|
}
|
|
1686
1632
|
|
|
1687
|
-
return hourCadenceLead(
|
|
1688
|
-
hourSegmentTimes(
|
|
1633
|
+
return hourCadenceLead(schedule, minute, opts) + ', ' +
|
|
1634
|
+
hourSegmentTimes(schedule, 0, null, opts) +
|
|
1635
|
+
trailingQualifier(schedule, opts);
|
|
1689
1636
|
}
|
|
1690
1637
|
|
|
1691
1638
|
// --- Hour-time phrasing. ---
|
|
@@ -1696,8 +1643,8 @@ function hourRangeCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
|
1696
1643
|
// hour "de la hora de las HH:00" (the idiom a wildcard minute already uses).
|
|
1697
1644
|
// Used by the compact-clock non-fold path, where the minute is a step or list
|
|
1698
1645
|
// (a single-value minute keeps its real "a las HH:MM" clock time elsewhere).
|
|
1699
|
-
function hourContextTimes(
|
|
1700
|
-
const segments =
|
|
1646
|
+
function hourContextTimes(schedule: Schedule, opts: Opts): string {
|
|
1647
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1701
1648
|
|
|
1702
1649
|
// Collect the point hours (singles and step fires) — a range stays a window.
|
|
1703
1650
|
const points: number[] = [];
|
|
@@ -1765,7 +1712,7 @@ function atTimes(hours: number[], opts: Opts): string[] {
|
|
|
1765
1712
|
// The hour times accompanying a lead clause: "a las 9:00 y a las 17:00",
|
|
1766
1713
|
// with long expansions rendered segment by segment.
|
|
1767
1714
|
function atHourTimes(
|
|
1768
|
-
|
|
1715
|
+
schedule: Schedule,
|
|
1769
1716
|
times: HourTimesPlan,
|
|
1770
1717
|
opts: Opts
|
|
1771
1718
|
): string {
|
|
@@ -1773,25 +1720,27 @@ function atHourTimes(
|
|
|
1773
1720
|
return groupClockTimesByArticle(atTimes(times.fires, opts));
|
|
1774
1721
|
}
|
|
1775
1722
|
|
|
1776
|
-
return hourSegmentTimes(
|
|
1723
|
+
return hourSegmentTimes(schedule, 0, null, opts);
|
|
1777
1724
|
}
|
|
1778
1725
|
|
|
1779
1726
|
// The active hours of a confined cadence: a few hours read as windows; many
|
|
1780
1727
|
// read better as a compact list ("durante las horas de las 9, 11, 13, 15 y
|
|
1781
1728
|
// 17") than as a sprawl of windows.
|
|
1782
|
-
function hourSpanFromTimes(
|
|
1729
|
+
function hourSpanFromTimes(
|
|
1730
|
+
schedule: Schedule, times: HourTimesPlan, opts: Opts
|
|
1731
|
+
): string {
|
|
1783
1732
|
if (times.kind === 'fires' && times.fires.length > 3) {
|
|
1784
1733
|
return 'durante las horas ' + hourSpanList(times.fires, opts);
|
|
1785
1734
|
}
|
|
1786
1735
|
|
|
1787
|
-
return hourWindowsFromTimes(
|
|
1736
|
+
return hourWindowsFromTimes(schedule, times, opts);
|
|
1788
1737
|
}
|
|
1789
1738
|
|
|
1790
1739
|
// Each fire hour as its own one-hour window: "de las 9:00 a las 9:59 y de
|
|
1791
1740
|
// las 17:00 a las 17:59". Spanish prefers this to the English "during the
|
|
1792
1741
|
// 9 a.m. and 5 p.m. hours" shape.
|
|
1793
1742
|
function hourWindowsFromTimes(
|
|
1794
|
-
|
|
1743
|
+
schedule: Schedule,
|
|
1795
1744
|
times: HourTimesPlan,
|
|
1796
1745
|
opts: Opts
|
|
1797
1746
|
): string {
|
|
@@ -1801,7 +1750,7 @@ function hourWindowsFromTimes(
|
|
|
1801
1750
|
}));
|
|
1802
1751
|
}
|
|
1803
1752
|
|
|
1804
|
-
return joinList(
|
|
1753
|
+
return joinList(segmentsOf(schedule, 'hour').map(function window(segment) {
|
|
1805
1754
|
if (segment.kind === 'range') {
|
|
1806
1755
|
return timeRange({hour: +segment.bounds[0], minute: 0},
|
|
1807
1756
|
{hour: +segment.bounds[1], minute: 59}, opts);
|
|
@@ -1821,7 +1770,7 @@ function hourWindowsFromTimes(
|
|
|
1821
1770
|
// (and optional second) folded into each: "de las 9:30 a las 20:30 y también
|
|
1822
1771
|
// a las 22:30" when an isolated point-time follows a range.
|
|
1823
1772
|
function hourSegmentTimes(
|
|
1824
|
-
|
|
1773
|
+
schedule: Schedule,
|
|
1825
1774
|
minute: number,
|
|
1826
1775
|
second: number | null | undefined,
|
|
1827
1776
|
opts: Opts
|
|
@@ -1830,7 +1779,7 @@ function hourSegmentTimes(
|
|
|
1830
1779
|
const pieces: string[] = [];
|
|
1831
1780
|
const fromRange: boolean[] = [];
|
|
1832
1781
|
|
|
1833
|
-
|
|
1782
|
+
segmentsOf(schedule, 'hour').forEach(function clock(segment) {
|
|
1834
1783
|
if (segment.kind === 'step') {
|
|
1835
1784
|
segment.fires.forEach(function each(hour) {
|
|
1836
1785
|
pieces.push(atTime(timePhrase(hour, minute, second, opts)));
|
|
@@ -2021,23 +1970,23 @@ function dayPeriod(hour: number, opts: Opts): string {
|
|
|
2021
1970
|
// lunes ", "el 13 de cada mes ", "de lunes a viernes ".
|
|
2022
1971
|
// Date-OR-weekday unions skip this entirely — the unified frame in `render`
|
|
2023
1972
|
// handles the month lead and day-level suffix.
|
|
2024
|
-
function leadingQualifier(
|
|
2025
|
-
const pattern =
|
|
1973
|
+
function leadingQualifier(schedule: Schedule, opts: Opts): string {
|
|
1974
|
+
const pattern = schedule.pattern;
|
|
2026
1975
|
|
|
2027
1976
|
if (pattern.date !== '*' && pattern.weekday !== '*') {
|
|
2028
1977
|
return '';
|
|
2029
1978
|
}
|
|
2030
1979
|
|
|
2031
1980
|
if (pattern.date !== '*') {
|
|
2032
|
-
return datePhrase(
|
|
1981
|
+
return datePhrase(schedule, opts) + ' ';
|
|
2033
1982
|
}
|
|
2034
1983
|
|
|
2035
1984
|
if (pattern.weekday !== '*') {
|
|
2036
|
-
return weekdayQualifier(
|
|
1985
|
+
return weekdayQualifier(schedule) + monthScope(schedule) + ' ';
|
|
2037
1986
|
}
|
|
2038
1987
|
|
|
2039
1988
|
if (pattern.month !== '*') {
|
|
2040
|
-
return 'todos los días ' + monthPhrase(
|
|
1989
|
+
return 'todos los días ' + monthPhrase(schedule, 'de ') + ' ';
|
|
2041
1990
|
}
|
|
2042
1991
|
|
|
2043
1992
|
return 'todos los días ';
|
|
@@ -2047,23 +1996,23 @@ function leadingQualifier(ir: IR, opts: Opts): string {
|
|
|
2047
1996
|
// de cada mes". Empty when no day-level field is set.
|
|
2048
1997
|
// Date-OR-weekday unions skip this entirely — the unified frame in `render`
|
|
2049
1998
|
// handles the month lead and day-level suffix.
|
|
2050
|
-
function trailingQualifier(
|
|
2051
|
-
const pattern =
|
|
1999
|
+
function trailingQualifier(schedule: Schedule, opts: Opts): string {
|
|
2000
|
+
const pattern = schedule.pattern;
|
|
2052
2001
|
|
|
2053
2002
|
if (pattern.date !== '*' && pattern.weekday !== '*') {
|
|
2054
2003
|
return '';
|
|
2055
2004
|
}
|
|
2056
2005
|
|
|
2057
2006
|
if (pattern.date !== '*') {
|
|
2058
|
-
return ' ' + datePhrase(
|
|
2007
|
+
return ' ' + datePhrase(schedule, opts);
|
|
2059
2008
|
}
|
|
2060
2009
|
|
|
2061
2010
|
if (pattern.weekday !== '*') {
|
|
2062
|
-
return ' ' + weekdayQualifier(
|
|
2011
|
+
return ' ' + weekdayQualifier(schedule) + monthScope(schedule);
|
|
2063
2012
|
}
|
|
2064
2013
|
|
|
2065
2014
|
if (pattern.month !== '*') {
|
|
2066
|
-
return ' ' + monthPhrase(
|
|
2015
|
+
return ' ' + monthPhrase(schedule, 'en ');
|
|
2067
2016
|
}
|
|
2068
2017
|
|
|
2069
2018
|
return '';
|
|
@@ -2072,24 +2021,24 @@ function trailingQualifier(ir: IR, opts: Opts): string {
|
|
|
2072
2021
|
// The date qualifier: "el 13 de junio", "los días 1 y 15 de cada mes",
|
|
2073
2022
|
// "del 1 al 15 de cada mes", or a Quartz phrase. A foldable single year
|
|
2074
2023
|
// joins the date ("el 25 de diciembre de 2030").
|
|
2075
|
-
function datePhrase(
|
|
2076
|
-
const pattern =
|
|
2024
|
+
function datePhrase(schedule: Schedule, opts: Opts): string {
|
|
2025
|
+
const pattern = schedule.pattern;
|
|
2077
2026
|
|
|
2078
2027
|
if (quartzDatePhrase(pattern.date) || isOpenStep(pattern.date)) {
|
|
2079
|
-
return dateClause(
|
|
2028
|
+
return dateClause(schedule, '', opts) + monthScope(schedule);
|
|
2080
2029
|
}
|
|
2081
2030
|
|
|
2082
|
-
return dateClause(
|
|
2031
|
+
return dateClause(schedule, dateMonthPart(schedule), opts);
|
|
2083
2032
|
}
|
|
2084
2033
|
|
|
2085
2034
|
// The date words with a caller-chosen month part. Quartz phrases and open
|
|
2086
2035
|
// steps are self-contained and ignore the month part.
|
|
2087
2036
|
function dateClause(
|
|
2088
|
-
|
|
2037
|
+
schedule: Schedule,
|
|
2089
2038
|
monthPart: string,
|
|
2090
2039
|
opts: Opts
|
|
2091
2040
|
): string {
|
|
2092
|
-
const pattern =
|
|
2041
|
+
const pattern = schedule.pattern;
|
|
2093
2042
|
const quartz = quartzDatePhrase(pattern.date);
|
|
2094
2043
|
|
|
2095
2044
|
if (quartz) {
|
|
@@ -2100,25 +2049,25 @@ function dateClause(
|
|
|
2100
2049
|
return stepDates(pattern.date, opts);
|
|
2101
2050
|
}
|
|
2102
2051
|
|
|
2103
|
-
const segments =
|
|
2052
|
+
const segments = segmentsOf(schedule, 'date');
|
|
2104
2053
|
|
|
2105
2054
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
2106
2055
|
return 'del ' + segments[0].bounds[0] + ' al ' +
|
|
2107
|
-
segments[0].bounds[1] + monthPart + foldedYear(
|
|
2056
|
+
segments[0].bounds[1] + monthPart + foldedYear(schedule);
|
|
2108
2057
|
}
|
|
2109
2058
|
|
|
2110
2059
|
if (segments.length === 1 && segments[0].kind === 'single') {
|
|
2111
|
-
return 'el ' + segments[0].value + monthPart + foldedYear(
|
|
2060
|
+
return 'el ' + segments[0].value + monthPart + foldedYear(schedule);
|
|
2112
2061
|
}
|
|
2113
2062
|
|
|
2114
2063
|
return 'los días ' + joinList(segmentWords(segments)) + monthPart +
|
|
2115
|
-
foldedYear(
|
|
2064
|
+
foldedYear(schedule);
|
|
2116
2065
|
}
|
|
2117
2066
|
|
|
2118
2067
|
// Whether the month field contains a range segment.
|
|
2119
|
-
function monthRanged(
|
|
2120
|
-
return
|
|
2121
|
-
|
|
2068
|
+
function monthRanged(schedule: Schedule): boolean {
|
|
2069
|
+
return schedule.pattern.month !== '*' &&
|
|
2070
|
+
segmentsOf(schedule, 'month').some(function range(segment) {
|
|
2122
2071
|
return segment.kind === 'range';
|
|
2123
2072
|
});
|
|
2124
2073
|
}
|
|
@@ -2128,21 +2077,21 @@ function monthRanged(ir: IR): boolean {
|
|
|
2128
2077
|
// "el 1 de junio a septiembre" parses as "(el 1 de junio) a septiembre" —
|
|
2129
2078
|
// so it scopes the date instead ("el 1 de cada mes, de junio a
|
|
2130
2079
|
// septiembre").
|
|
2131
|
-
function dateMonthPart(
|
|
2132
|
-
if (
|
|
2080
|
+
function dateMonthPart(schedule: Schedule): string {
|
|
2081
|
+
if (schedule.pattern.month === '*') {
|
|
2133
2082
|
return ' de cada mes';
|
|
2134
2083
|
}
|
|
2135
2084
|
|
|
2136
|
-
if (monthRanged(
|
|
2137
|
-
return ' de cada mes, ' + monthPhrase(
|
|
2085
|
+
if (monthRanged(schedule)) {
|
|
2086
|
+
return ' de cada mes, ' + monthPhrase(schedule, 'de ');
|
|
2138
2087
|
}
|
|
2139
2088
|
|
|
2140
|
-
return ' ' + monthPhrase(
|
|
2089
|
+
return ' ' + monthPhrase(schedule, 'de ');
|
|
2141
2090
|
}
|
|
2142
2091
|
|
|
2143
2092
|
// "de 2030" when a single year can fold into a calendar date.
|
|
2144
|
-
function foldedYear(
|
|
2145
|
-
const yearField =
|
|
2093
|
+
function foldedYear(schedule: Schedule): string {
|
|
2094
|
+
const yearField = schedule.pattern.year;
|
|
2146
2095
|
|
|
2147
2096
|
if (yearField === '*' || yearField.indexOf('/') !== -1 ||
|
|
2148
2097
|
yearField.indexOf('-') !== -1 || yearField.indexOf(',') !== -1) {
|
|
@@ -2198,16 +2147,16 @@ function quartzWeekdayPhrase(weekdayField: string): string | undefined {
|
|
|
2198
2147
|
// miércoles y viernes". No "todos" prefix: the plural definite article
|
|
2199
2148
|
// ("los lunes") already conveys "every Monday" in Spanish, unlike "todos
|
|
2200
2149
|
// los días", where "los días" alone does not mean "every day".
|
|
2201
|
-
function weekdayQualifier(
|
|
2202
|
-
const quartz = quartzWeekdayPhrase(
|
|
2150
|
+
function weekdayQualifier(schedule: Schedule): string {
|
|
2151
|
+
const quartz = quartzWeekdayPhrase(schedule.pattern.weekday);
|
|
2203
2152
|
|
|
2204
2153
|
if (quartz) {
|
|
2205
2154
|
return quartz;
|
|
2206
2155
|
}
|
|
2207
2156
|
|
|
2208
2157
|
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
2209
|
-
// form. The
|
|
2210
|
-
const segments = orderWeekdaysForDisplay(
|
|
2158
|
+
// form. The Schedule stays canonical (Sunday=0). The helper flattens steps.
|
|
2159
|
+
const segments = orderWeekdaysForDisplay(segmentsOf(schedule, 'weekday'));
|
|
2211
2160
|
const allSingles = segments.every(function single(segment) {
|
|
2212
2161
|
return segment.kind === 'single';
|
|
2213
2162
|
});
|
|
@@ -2259,8 +2208,8 @@ function flattenSteps(segments: Segment[]): NameSegment[] {
|
|
|
2259
2208
|
// as one unit, so in mixed lists every piece repeats its preposition
|
|
2260
2209
|
// ("en enero y de marzo a junio") — a bare "enero y marzo a junio" parses
|
|
2261
2210
|
// as "(enero y marzo) a junio".
|
|
2262
|
-
function monthPhrase(
|
|
2263
|
-
const segments = flattenSteps(
|
|
2211
|
+
function monthPhrase(schedule: Schedule, lead: string): string {
|
|
2212
|
+
const segments = flattenSteps(segmentsOf(schedule, 'month'));
|
|
2264
2213
|
const ranged = segments.some(function range(segment) {
|
|
2265
2214
|
return segment.kind === 'range';
|
|
2266
2215
|
});
|
|
@@ -2286,15 +2235,39 @@ function monthPhrase(ir: IR, lead: string): string {
|
|
|
2286
2235
|
// junio"). A ranged scope sets off with a comma ("el último día del mes,
|
|
2287
2236
|
// de junio a septiembre") — gluing "de junio" after "del mes"
|
|
2288
2237
|
// garden-paths.
|
|
2289
|
-
function monthScope(
|
|
2290
|
-
if (
|
|
2238
|
+
function monthScope(schedule: Schedule): string {
|
|
2239
|
+
if (schedule.pattern.month === '*') {
|
|
2291
2240
|
return '';
|
|
2292
2241
|
}
|
|
2293
2242
|
|
|
2294
|
-
return (monthRanged(
|
|
2243
|
+
return (monthRanged(schedule) ? ', ' : ' ') + monthPhrase(schedule, 'de ');
|
|
2295
2244
|
}
|
|
2296
2245
|
|
|
2297
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
|
+
|
|
2298
2271
|
function stepDates(dateField: string, opts: Opts): string {
|
|
2299
2272
|
const parts = dateField.split('/');
|
|
2300
2273
|
let phrase = 'cada ' + numero(+parts[1], opts) + ' días del mes';
|
|
@@ -2312,10 +2285,10 @@ function stepDates(dateField: string, opts: Opts): string {
|
|
|
2312
2285
|
// "en 2030, 2031 y 2032", "cada dos años desde 2030".
|
|
2313
2286
|
function applyYear(
|
|
2314
2287
|
description: string,
|
|
2315
|
-
|
|
2288
|
+
schedule: Schedule,
|
|
2316
2289
|
opts: Opts
|
|
2317
2290
|
): string {
|
|
2318
|
-
const yearField =
|
|
2291
|
+
const yearField = schedule.pattern.year;
|
|
2319
2292
|
|
|
2320
2293
|
if (yearField === '*') {
|
|
2321
2294
|
return description;
|
|
@@ -2326,7 +2299,7 @@ function applyYear(
|
|
|
2326
2299
|
}
|
|
2327
2300
|
|
|
2328
2301
|
// A foldable single year already joined its date in datePhrase.
|
|
2329
|
-
if (foldedYear(
|
|
2302
|
+
if (foldedYear(schedule) && schedule.pattern.date !== '*') {
|
|
2330
2303
|
return description;
|
|
2331
2304
|
}
|
|
2332
2305
|
|
|
@@ -2423,13 +2396,8 @@ function monthName(token: NameToken): string {
|
|
|
2423
2396
|
return monthNames[+token] as string;
|
|
2424
2397
|
}
|
|
2425
2398
|
|
|
2426
|
-
// Whether a canonical field value is an open step (`*/n` or `a/n`).
|
|
2427
|
-
function isOpenStep(field: string): boolean {
|
|
2428
|
-
return field.indexOf('/') !== -1 && field.indexOf('-') === -1 &&
|
|
2429
|
-
field.indexOf(',') === -1;
|
|
2430
|
-
}
|
|
2431
2399
|
|
|
2432
|
-
// The Spanish language module: the
|
|
2400
|
+
// The Spanish language module: the Schedule renderer plus the language-owned
|
|
2433
2401
|
// strings and option normalization.
|
|
2434
2402
|
const es: Language<SpanishStyle> = {
|
|
2435
2403
|
describe,
|