cronli5 0.1.2 → 0.1.5
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 +89 -0
- package/cli.js +9 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +280 -64
- package/dist/cronli5.js +280 -64
- package/dist/lang/de.cjs +227 -42
- package/dist/lang/de.js +227 -42
- package/dist/lang/en.cjs +216 -50
- package/dist/lang/en.js +216 -50
- package/dist/lang/es.cjs +259 -54
- package/dist/lang/es.js +259 -54
- package/dist/lang/fi.cjs +230 -69
- package/dist/lang/fi.js +230 -69
- package/dist/lang/zh.cjs +190 -19
- package/dist/lang/zh.js +190 -19
- package/package.json +3 -1
- package/src/core/analyze.ts +7 -0
- package/src/core/ir.ts +1 -1
- package/src/core/normalize.ts +94 -4
- package/src/core/util.ts +31 -1
- package/src/lang/de/index.ts +449 -46
- package/src/lang/en/index.ts +433 -63
- package/src/lang/es/index.ts +505 -63
- package/src/lang/fi/index.ts +455 -89
- package/src/lang/zh/index.ts +393 -30
- package/types/core/ir.d.ts +1 -1
- package/types/core/util.d.ts +6 -1
package/src/lang/es/index.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// lists render as per-hour windows).
|
|
10
10
|
|
|
11
11
|
import {clockDigits, numeral} from '../../core/format.js';
|
|
12
|
+
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
13
|
+
import {arithmeticStep, toFieldNumber} from '../../core/util.js';
|
|
12
14
|
import type {Cronli5Options} from '../../types.js';
|
|
13
15
|
import type {
|
|
14
16
|
Field, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode,
|
|
@@ -25,6 +27,20 @@ type Renderer = (ir: IR, plan: PlanNode, opts: Opts) => string;
|
|
|
25
27
|
// A `step` segment, narrowed from the discriminated `Segment` union.
|
|
26
28
|
type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
27
29
|
|
|
30
|
+
// A step cadence to phrase: the `interval` repeats over a `cycle`-long field
|
|
31
|
+
// (60 for minute/second), running from `start` to `last`. `unit` is the
|
|
32
|
+
// singular noun and `anchor` the larger unit the values count against. When
|
|
33
|
+
// `anchor` is empty the caller supplies its own trailing scope, so the cadence
|
|
34
|
+
// drops the "de cada <anchor>" tail.
|
|
35
|
+
interface Stride {
|
|
36
|
+
interval: number;
|
|
37
|
+
start: number;
|
|
38
|
+
last: number;
|
|
39
|
+
cycle: number;
|
|
40
|
+
unit: string;
|
|
41
|
+
anchor: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
28
44
|
// One end of a clock-time range. The second is optional and may be absent
|
|
29
45
|
// (top-of-hour windows) or a folded clock second.
|
|
30
46
|
type ClockEnd = {hour: number; minute: number; second?: number | null};
|
|
@@ -109,16 +125,6 @@ const weekdayNames = [
|
|
|
109
125
|
'sábado'
|
|
110
126
|
];
|
|
111
127
|
|
|
112
|
-
// Cron token vocabulary (JAN..DEC, SUN..SAT) is part of cron syntax; map
|
|
113
|
-
// it to Spanish names.
|
|
114
|
-
const monthTokens: {[token: string]: number} = {
|
|
115
|
-
JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6,
|
|
116
|
-
JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12
|
|
117
|
-
};
|
|
118
|
-
const weekdayTokens: {[token: string]: number} = {
|
|
119
|
-
SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6
|
|
120
|
-
};
|
|
121
|
-
|
|
122
128
|
// Ordinals for Quartz `#` weekday occurrences (1-5).
|
|
123
129
|
const nthWeekdayNames =
|
|
124
130
|
[null, 'primer', 'segundo', 'tercer', 'cuarto', 'quinto'];
|
|
@@ -214,29 +220,78 @@ function renderSecondsWithinMinute(
|
|
|
214
220
|
' de cada hora' + trailingQualifier(ir, opts);
|
|
215
221
|
}
|
|
216
222
|
|
|
223
|
+
// A seconds list nested into one or more fixed clock times ("..., en los
|
|
224
|
+
// segundos 5 y 30 de las 09:00 y 17:00"). An offset/uneven second step the
|
|
225
|
+
// core enumerated to this list reads as a stride cadence; otherwise the fires
|
|
226
|
+
// are listed. The clock time follows with the genitive "de", so the stride
|
|
227
|
+
// drops its "de cada minuto" anchor.
|
|
228
|
+
function secondsListAtClock(
|
|
229
|
+
ir: IR,
|
|
230
|
+
rest: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
231
|
+
opts: Opts
|
|
232
|
+
): string {
|
|
233
|
+
const clockPhrases = rest.times.map(function clock(time) {
|
|
234
|
+
return atTime(timePhrase(time.hour, time.minute, null, opts));
|
|
235
|
+
});
|
|
236
|
+
const grouped = groupClockTimesByArticle(clockPhrases);
|
|
237
|
+
// Strip the leading "a " prefix from the grouped result so the caller can
|
|
238
|
+
// prepend "de " to produce the genitive form "de las 09:00 y 17:00".
|
|
239
|
+
const clockList = grouped.startsWith('a ') ? grouped.slice(2) : grouped;
|
|
240
|
+
const stride =
|
|
241
|
+
strideFromSegments(fieldSegments(ir, 'second'), 'segundo', '', opts);
|
|
242
|
+
const secondsPhrase = stride ?? 'en los segundos ' +
|
|
243
|
+
joinList(segmentWords(fieldSegments(ir, 'second')));
|
|
244
|
+
const dayFrame = trailingQualifier(ir, opts);
|
|
245
|
+
|
|
246
|
+
return (dayFrame ? dayFrame.trimStart() + ', ' : '') +
|
|
247
|
+
secondsPhrase + ' de ' + clockList;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// The hour-cadence rendering of a compose-seconds plan whose clock-time rest
|
|
251
|
+
// would cross-multiply an hour stride under a single pinned minute, or null
|
|
252
|
+
// when that does not apply (a non-clock rest, a multi-valued minute, or an
|
|
253
|
+
// hour that is not a stride).
|
|
254
|
+
function composeHourCadence(
|
|
255
|
+
ir: IR,
|
|
256
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
257
|
+
opts: Opts
|
|
258
|
+
): string | null {
|
|
259
|
+
const clockRest = plan.rest.kind === 'clockTimes' ||
|
|
260
|
+
plan.rest.kind === 'compactClockTimes';
|
|
261
|
+
|
|
262
|
+
return clockRest && ir.shapes.minute === 'single' ?
|
|
263
|
+
hourCadence(ir, +ir.pattern.minute, opts) :
|
|
264
|
+
null;
|
|
265
|
+
}
|
|
266
|
+
|
|
217
267
|
function renderComposeSeconds(
|
|
218
268
|
ir: IR,
|
|
219
269
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
220
270
|
opts: Opts
|
|
221
271
|
): string {
|
|
272
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
273
|
+
// minute is a cadence, not a wall of clock times: the second/minute lead,
|
|
274
|
+
// then the hour cadence ("en el segundo 30 de cada hora, cada dos horas").
|
|
275
|
+
// The clock-time rest would otherwise cross-multiply the hours.
|
|
276
|
+
const hourCad = composeHourCadence(ir, plan, opts);
|
|
277
|
+
|
|
278
|
+
if (hourCad !== null) {
|
|
279
|
+
return hourCad;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// A wildcard or stepped second with the minute pinned to a single value
|
|
283
|
+
// across one or more specific hours: the seconds confine to the clock time.
|
|
284
|
+
if (plan.rest.kind === 'clockTimes' &&
|
|
285
|
+
(ir.shapes.second === 'wildcard' || ir.shapes.second === 'step')) {
|
|
286
|
+
return pinnedMinuteSeconds(ir, plan.rest, opts);
|
|
287
|
+
}
|
|
288
|
+
|
|
222
289
|
// Seconds list + fixed clock time: nest the seconds into the clock time(s)
|
|
223
290
|
// with genitive "de las HH:MM" instead of "de cada minuto"; the minute is
|
|
224
291
|
// fixed so "de cada minuto" is misleading. Single seconds already fold into
|
|
225
292
|
// the time in the clockTimes renderer; step seconds keep their own clause.
|
|
226
293
|
if (plan.rest.kind === 'clockTimes' && ir.shapes.second === 'list') {
|
|
227
|
-
|
|
228
|
-
return atTime(timePhrase(time.hour, time.minute, null, opts));
|
|
229
|
-
});
|
|
230
|
-
const grouped = groupClockTimesByArticle(clockPhrases);
|
|
231
|
-
// Strip the leading "a " prefix from the grouped result so the caller can
|
|
232
|
-
// prepend "de " to produce the genitive form "de las 09:00 y 17:00".
|
|
233
|
-
const clockList = grouped.startsWith('a ') ? grouped.slice(2) : grouped;
|
|
234
|
-
const secondsPhrase = 'en los segundos ' +
|
|
235
|
-
joinList(segmentWords(fieldSegments(ir, 'second')));
|
|
236
|
-
const dayFrame = trailingQualifier(ir, opts);
|
|
237
|
-
|
|
238
|
-
return (dayFrame ? dayFrame.trimStart() + ', ' : '') +
|
|
239
|
-
secondsPhrase + ' de ' + clockList;
|
|
294
|
+
return secondsListAtClock(ir, plan.rest, opts);
|
|
240
295
|
}
|
|
241
296
|
|
|
242
297
|
// Second-step + fixed minute + hour range + weekday: anchor the cadence to
|
|
@@ -253,11 +308,72 @@ function renderComposeSeconds(
|
|
|
253
308
|
return dayFrame + ', ' + window + ', ' + cadence;
|
|
254
309
|
}
|
|
255
310
|
|
|
311
|
+
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
312
|
+
// cadences that read as contradictory ("cada segundo, cada dos minutos").
|
|
313
|
+
// Bind them with the genitive "de" ("cada segundo de cada dos minutos"),
|
|
314
|
+
// mirroring English. Other strides, a restricted hour, and an hour cadence
|
|
315
|
+
// keep the juxtaposed form.
|
|
316
|
+
if (isEveryOtherMinuteSeconds(ir, plan)) {
|
|
317
|
+
return secondsLeadClause(ir, opts) + ' de ' + render(ir, plan.rest, opts);
|
|
318
|
+
}
|
|
319
|
+
|
|
256
320
|
return secondsLeadClause(ir, opts) + ', ' + render(ir, plan.rest, opts);
|
|
257
321
|
}
|
|
258
322
|
|
|
323
|
+
// A wildcard second over an unoffset minute */2 with a wildcard hour: the two
|
|
324
|
+
// cadences read as contradictory side by side, so they bind into one.
|
|
325
|
+
function isEveryOtherMinuteSeconds(
|
|
326
|
+
ir: IR,
|
|
327
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
328
|
+
): boolean {
|
|
329
|
+
if (plan.rest.kind !== 'minuteFrequency' ||
|
|
330
|
+
ir.shapes.second !== 'wildcard' || ir.shapes.hour !== 'wildcard') {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const minuteStep = stepSegment(ir.analyses.segments.minute);
|
|
335
|
+
|
|
336
|
+
return minuteStep.startToken === '*' && minuteStep.interval === 2;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// A wildcard or stepped second under a single pinned minute and specific
|
|
340
|
+
// hour(s). The clock-time rest folds the minute into the hour, and on the
|
|
341
|
+
// 12-hour clock a pinned minute-0 drops the :00 entirely ("a las 9 de la
|
|
342
|
+
// mañana") — and even "a las 9" reads aloud as the whole hour, hiding the
|
|
343
|
+
// one-minute confinement (60 fires in :00, not 3,600 across the hour). Minute
|
|
344
|
+
// 0 is the one-minute window at the top of each named hour: a duration frame
|
|
345
|
+
// ("durante un minuto a las 9") states the confinement outright, with the hour
|
|
346
|
+
// as a bare hour so it cannot be heard as the whole hour. A non-zero pinned
|
|
347
|
+
// minute is an unambiguous clock time, so the genitive "de las 09:05" form
|
|
348
|
+
// reads it as the minute, never the hour.
|
|
349
|
+
function pinnedMinuteSeconds(
|
|
350
|
+
ir: IR,
|
|
351
|
+
rest: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
352
|
+
opts: Opts
|
|
353
|
+
): string {
|
|
354
|
+
const dayTrail = leadingQualifier(ir, opts).trimEnd();
|
|
355
|
+
const trail = dayTrail ? ', ' + dayTrail : '';
|
|
356
|
+
|
|
357
|
+
if (+rest.times[0].minute === 0) {
|
|
358
|
+
return secondsLeadClause(ir, opts) + ' durante un minuto ' +
|
|
359
|
+
durationHourList(rest.times, opts) + trail;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return secondsLeadClause(ir, opts) + ' de ' +
|
|
363
|
+
explicitClockList(rest.times, opts) + trail;
|
|
364
|
+
}
|
|
365
|
+
|
|
259
366
|
// The leading clause describing a second field relative to the minute.
|
|
260
367
|
function secondsLeadClause(ir: IR, opts: Opts): string {
|
|
368
|
+
return secondsClause(ir, 'minuto', opts);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// The second clause counted against an arbitrary anchor. The anchor is
|
|
372
|
+
// "minuto" in the standalone seconds path; the hour-cadence path folds a
|
|
373
|
+
// pinned minute 0 into the hour and counts the second "de cada hora" instead
|
|
374
|
+
// ("en el segundo 30 de cada hora"), so the minute-0 confinement is stated,
|
|
375
|
+
// not dropped.
|
|
376
|
+
function secondsClause(ir: IR, anchor: string, opts: Opts): string {
|
|
261
377
|
const secondField = ir.pattern.second;
|
|
262
378
|
const shape = ir.shapes.second;
|
|
263
379
|
|
|
@@ -267,23 +383,24 @@ function secondsLeadClause(ir: IR, opts: Opts): string {
|
|
|
267
383
|
|
|
268
384
|
if (shape === 'step') {
|
|
269
385
|
return stepCycle60(stepSegment(ir.analyses.segments.second), 'segundo',
|
|
270
|
-
|
|
386
|
+
anchor, opts);
|
|
271
387
|
}
|
|
272
388
|
|
|
273
389
|
if (shape === 'range') {
|
|
274
390
|
const bounds = secondField.split('-');
|
|
275
391
|
|
|
276
392
|
return 'cada segundo del ' + bounds[0] + ' al ' + bounds[1] +
|
|
277
|
-
' de cada
|
|
393
|
+
' de cada ' + anchor;
|
|
278
394
|
}
|
|
279
395
|
|
|
280
396
|
if (shape === 'single') {
|
|
281
|
-
return 'en el segundo ' + secondField + ' de cada
|
|
397
|
+
return 'en el segundo ' + secondField + ' de cada ' + anchor;
|
|
282
398
|
}
|
|
283
399
|
|
|
284
|
-
return '
|
|
400
|
+
return strideFromSegments(fieldSegments(ir, 'second'), 'segundo', anchor,
|
|
401
|
+
opts) ?? 'en los segundos ' +
|
|
285
402
|
joinList(segmentWords(fieldSegments(ir, 'second'))) +
|
|
286
|
-
' de cada
|
|
403
|
+
' de cada ' + anchor;
|
|
287
404
|
}
|
|
288
405
|
|
|
289
406
|
// --- Minute renderers. ---
|
|
@@ -319,12 +436,15 @@ function renderMultipleMinutes(
|
|
|
319
436
|
plan: Extract<PlanNode, {kind: 'multipleMinutes'}>,
|
|
320
437
|
opts: Opts
|
|
321
438
|
): string {
|
|
322
|
-
return minutesList(ir) + trailingQualifier(ir, opts);
|
|
439
|
+
return minutesList(ir, opts) + trailingQualifier(ir, opts);
|
|
323
440
|
}
|
|
324
441
|
|
|
325
|
-
// "en los minutos 5, 10 y 30 de cada hora".
|
|
326
|
-
|
|
327
|
-
|
|
442
|
+
// "en los minutos 5, 10 y 30 de cada hora". An offset/uneven step the core
|
|
443
|
+
// enumerated to this list reads as a stride cadence when the fires form a
|
|
444
|
+
// long-enough progression.
|
|
445
|
+
function minutesList(ir: IR, opts: Opts): string {
|
|
446
|
+
return strideFromSegments(fieldSegments(ir, 'minute'), 'minuto', 'hora',
|
|
447
|
+
opts) ?? 'en los minutos ' +
|
|
328
448
|
joinList(segmentWords(fieldSegments(ir, 'minute'))) + ' de cada hora';
|
|
329
449
|
}
|
|
330
450
|
|
|
@@ -453,12 +573,21 @@ function renderMinuteFrequency(
|
|
|
453
573
|
return phrase + trailingQualifier(ir, opts);
|
|
454
574
|
}
|
|
455
575
|
|
|
456
|
-
// "cada minuto de las 9:00 a las 9:29 de la mañana".
|
|
576
|
+
// "cada minuto de las 9:00 a las 9:29 de la mañana". A wildcard minute is the
|
|
577
|
+
// whole hour, so it reads as that hour itself ("cada minuto de la hora de las
|
|
578
|
+
// 09:00") rather than a synthesized "de las HH:00 a las HH:59" range the
|
|
579
|
+
// source never stated; a plain range is a real window and keeps "de … a …".
|
|
457
580
|
function renderMinuteSpanInHour(
|
|
458
581
|
ir: IR,
|
|
459
582
|
plan: Extract<PlanNode, {kind: 'minuteSpanInHour'}>,
|
|
460
583
|
opts: Opts
|
|
461
584
|
): string {
|
|
585
|
+
if (ir.pattern.minute === '*') {
|
|
586
|
+
return 'cada minuto de la hora ' +
|
|
587
|
+
fromTime(timePhrase(plan.hour, 0, null, opts)) +
|
|
588
|
+
trailingQualifier(ir, opts);
|
|
589
|
+
}
|
|
590
|
+
|
|
462
591
|
return 'cada minuto ' +
|
|
463
592
|
timeRange({hour: plan.hour, minute: plan.span[0]},
|
|
464
593
|
{hour: plan.hour, minute: plan.span[1]}, opts) +
|
|
@@ -486,7 +615,7 @@ function renderMinutesAcrossHours(
|
|
|
486
615
|
|
|
487
616
|
const lead = plan.form === 'range' ?
|
|
488
617
|
minuteRangeLead(ir.pattern.minute) :
|
|
489
|
-
minutesList(ir);
|
|
618
|
+
minutesList(ir, opts);
|
|
490
619
|
|
|
491
620
|
return lead + ', ' + atHourTimes(ir, plan.times, opts) +
|
|
492
621
|
trailingQualifier(ir, opts);
|
|
@@ -506,8 +635,14 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
506
635
|
trailingQualifier(ir, opts);
|
|
507
636
|
}
|
|
508
637
|
|
|
509
|
-
|
|
510
|
-
|
|
638
|
+
// A minute list keeps the same cadence clause as the range; only its lead
|
|
639
|
+
// differs ("en los minutos 5 y 30 de cada hora" vs "cada minuto del 0 al
|
|
640
|
+
// 30").
|
|
641
|
+
const lead = plan.form === 'list' ?
|
|
642
|
+
minutesList(ir, opts) :
|
|
643
|
+
minuteRangeLead(ir.pattern.minute);
|
|
644
|
+
|
|
645
|
+
return lead + ', ' + stepHours(segment, opts) + trailingQualifier(ir, opts);
|
|
511
646
|
}
|
|
512
647
|
|
|
513
648
|
// --- Hour renderers. ---
|
|
@@ -544,7 +679,7 @@ function renderHourRange(
|
|
|
544
679
|
|
|
545
680
|
const lead = ir.shapes.minute === 'single' ?
|
|
546
681
|
'en el minuto ' + ir.pattern.minute + ' de cada hora' :
|
|
547
|
-
minutesList(ir);
|
|
682
|
+
minutesList(ir, opts);
|
|
548
683
|
|
|
549
684
|
return lead + ', ' + window + trailingQualifier(ir, opts);
|
|
550
685
|
}
|
|
@@ -682,6 +817,16 @@ function renderClockTimes(
|
|
|
682
817
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
683
818
|
opts: Opts
|
|
684
819
|
): string {
|
|
820
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
821
|
+
// minute reads as a cadence rather than a cross-product of clock times.
|
|
822
|
+
if (ir.shapes.minute === 'single') {
|
|
823
|
+
const cadence = hourCadence(ir, +ir.pattern.minute, opts);
|
|
824
|
+
|
|
825
|
+
if (cadence !== null) {
|
|
826
|
+
return cadence;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
685
830
|
const phrases = plan.times.map(function clock(time) {
|
|
686
831
|
return atTime(timePhrase(time.hour, time.minute, time.second, opts));
|
|
687
832
|
});
|
|
@@ -689,6 +834,82 @@ function renderClockTimes(
|
|
|
689
834
|
return leadingQualifier(ir, opts) + groupClockTimes(phrases);
|
|
690
835
|
}
|
|
691
836
|
|
|
837
|
+
// The genitive clock-time list for a minute-0 compose-seconds confinement:
|
|
838
|
+
// each time with its minute forced visible ("las 09:00"), grouped as usual,
|
|
839
|
+
// then reframed from "a …" to the genitive "de …" the caller prepends. So a
|
|
840
|
+
// pinned minute-0 reads "de las 09:00", never the bare hour.
|
|
841
|
+
function explicitClockList(
|
|
842
|
+
times: {hour: number; minute: number; second?: number | null}[],
|
|
843
|
+
opts: Opts
|
|
844
|
+
): string {
|
|
845
|
+
const phrases = times.map(function clock(time) {
|
|
846
|
+
return atTime(explicitTimePhrase(time.hour, time.minute, opts));
|
|
847
|
+
});
|
|
848
|
+
const grouped = groupClockTimes(phrases);
|
|
849
|
+
|
|
850
|
+
// Strip the leading "a " so the caller's "de " produces the genitive form.
|
|
851
|
+
return grouped.startsWith('a ') ? grouped.slice(2) : grouped;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// The bare-hour list for a minute-0 duration confinement, keeping the "a …"
|
|
855
|
+
// frame the caller embeds after "durante un minuto": "a las 9",
|
|
856
|
+
// "a medianoche", "a las 9, 10, 11 y 12". The hour reads as a bare hour
|
|
857
|
+
// (no minutes), since the "durante un minuto" frame already carries the
|
|
858
|
+
// one-minute window — never "las 09:00", which would read as the whole hour.
|
|
859
|
+
function durationHourList(
|
|
860
|
+
times: {hour: number; minute: number; second?: number | null}[],
|
|
861
|
+
opts: Opts
|
|
862
|
+
): string {
|
|
863
|
+
const phrases = times.map(function clock(time) {
|
|
864
|
+
return atTime(bareHourPhrase(time.hour, opts));
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
return groupClockTimes(phrases);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// A bare hour with its article, no minutes: "las 9" / "la 1" / "mediodía" /
|
|
871
|
+
// "medianoche" on the 24-hour clock, or the 12-hour day-period form
|
|
872
|
+
// ("las 9 de la mañana"). Used by the minute-0 duration frame, where the
|
|
873
|
+
// minute is already stated and the clock minute would only mislead.
|
|
874
|
+
function bareHourPhrase(hour: number, opts: Opts): string {
|
|
875
|
+
if (opts.ampm) {
|
|
876
|
+
return timePhrase(hour, 0, null, opts);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (+hour === 0) {
|
|
880
|
+
return 'medianoche';
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (+hour === 12) {
|
|
884
|
+
return 'mediodía';
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return (+hour === 1 ? 'la ' : 'las ') + hour;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// A clock time with its minute forced visible and the noon/midnight words
|
|
891
|
+
// suppressed: "las 09:00", "las 9:00 de la mañana", "las 12:00 de la tarde".
|
|
892
|
+
// So a pinned minute-0 confinement always shows its ":00".
|
|
893
|
+
function explicitTimePhrase(hour: number, minute: number, opts: Opts): string {
|
|
894
|
+
if (!opts.ampm) {
|
|
895
|
+
const article = +hour === 1 ? 'la ' : 'las ';
|
|
896
|
+
const suffix = opts.style.hSuffix ? ' h' : '';
|
|
897
|
+
|
|
898
|
+
return article +
|
|
899
|
+
clockDigits({hour, minute, second: 0},
|
|
900
|
+
{pad: true, sep: opts.style.sep}) + suffix;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const display = hour % 12 || 12;
|
|
904
|
+
const time = (display === 1 ? 'la ' : 'las ') +
|
|
905
|
+
clockDigits({hour: display, minute, second: 0}, {sep: opts.style.sep});
|
|
906
|
+
const period = opts.style.meridiem === 'english' ?
|
|
907
|
+
meridiemMark(hour) :
|
|
908
|
+
dayPeriod(hour, opts);
|
|
909
|
+
|
|
910
|
+
return time + ' ' + period;
|
|
911
|
+
}
|
|
912
|
+
|
|
692
913
|
// Group a chronological run of "a la(s) …" clock phrases. The 12-hour clock
|
|
693
914
|
// carries day periods ("de la <period>"), which group chronologically by
|
|
694
915
|
// period; the 24-hour clock has none, so it falls through to article-grouping.
|
|
@@ -941,6 +1162,15 @@ function renderCompactClockTimes(
|
|
|
941
1162
|
opts: Opts
|
|
942
1163
|
): string {
|
|
943
1164
|
if (plan.fold) {
|
|
1165
|
+
// An hour step (or arithmetic-progression hour list) under the single
|
|
1166
|
+
// pinned minute reads as a cadence, not a wall of clock times. (Returns
|
|
1167
|
+
// null for an irregular list or a range, which keep folding below.)
|
|
1168
|
+
const cadence = hourCadence(ir, plan.minute, opts);
|
|
1169
|
+
|
|
1170
|
+
if (cadence !== null) {
|
|
1171
|
+
return cadence;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
944
1174
|
const ranged = hourSegments(ir).some(function range(segment) {
|
|
945
1175
|
return segment.kind === 'range';
|
|
946
1176
|
});
|
|
@@ -957,7 +1187,7 @@ function renderCompactClockTimes(
|
|
|
957
1187
|
hourSegmentTimes(ir, plan.minute, ir.analyses.clockSecond, opts);
|
|
958
1188
|
}
|
|
959
1189
|
|
|
960
|
-
const phrase = minutesList(ir) + ', ' +
|
|
1190
|
+
const phrase = minutesList(ir, opts) + ', ' +
|
|
961
1191
|
hourSegmentTimes(ir, 0, null, opts) + trailingQualifier(ir, opts);
|
|
962
1192
|
|
|
963
1193
|
return ir.analyses.clockSecond ?
|
|
@@ -989,8 +1219,42 @@ const renderers = {
|
|
|
989
1219
|
|
|
990
1220
|
// --- Step phrases. ---
|
|
991
1221
|
|
|
1222
|
+
// Speak a step cadence over a `cycle`-long field (60 for minute/second). A
|
|
1223
|
+
// clean stride from the top of the cycle is the bare cadence ("cada quince
|
|
1224
|
+
// minutos"); a uniform offset (start within the first interval, the interval
|
|
1225
|
+
// still dividing the cycle) names only its start, since it wraps cleanly with
|
|
1226
|
+
// no distinct endpoint ("cada seis minutos a partir del minuto 5 de cada
|
|
1227
|
+
// hora"); a non-uniform stride (start >= interval, or an interval that does
|
|
1228
|
+
// not divide the cycle) pins both endpoints so the bounded, non-wrapping set
|
|
1229
|
+
// reads unambiguously ("cada dos minutos del minuto 3 al 59 de cada hora").
|
|
1230
|
+
// This is the one phrasing for every step the renderer speaks, whether the
|
|
1231
|
+
// core kept it a step shape (a clean cadence) or enumerated it to a fire list
|
|
1232
|
+
// (an offset/uneven set the list path recognizes as a progression).
|
|
1233
|
+
function renderStride(stride: Stride, opts: Opts): string {
|
|
1234
|
+
const {interval, start, last, cycle, unit, anchor} = stride;
|
|
1235
|
+
const cadence = 'cada ' + numero(interval, opts) + ' ' + unit + 's';
|
|
1236
|
+
const tiles = cycle % interval === 0;
|
|
1237
|
+
|
|
1238
|
+
if (start === 0 && tiles) {
|
|
1239
|
+
return cadence;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// A context that supplies its own trailing scope passes an empty anchor, so
|
|
1243
|
+
// the cadence keeps its endpoints but drops the "de cada <anchor>" tail.
|
|
1244
|
+
const tail = anchor ? ' de cada ' + anchor : '';
|
|
1245
|
+
|
|
1246
|
+
if (start < interval && tiles) {
|
|
1247
|
+
return cadence + ' a partir del ' + unit + ' ' + start + tail;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return cadence + ' del ' + unit + ' ' + start + ' al ' + last + tail;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
992
1253
|
// "cada 15 minutos", "en los minutos 5, 20 y 35 de cada hora", or
|
|
993
|
-
// "cada 15 minutos a partir del minuto 5 de cada hora".
|
|
1254
|
+
// "cada 15 minutos a partir del minuto 5 de cada hora". A step shape only
|
|
1255
|
+
// reaches here as a clean cadence (the interval divides 60), so the stride
|
|
1256
|
+
// collapses to the bare or uniform-offset form; an offset/uneven set arrives
|
|
1257
|
+
// as a fire list and is recognized by the list path instead.
|
|
994
1258
|
function stepCycle60(
|
|
995
1259
|
segment: StepSegment,
|
|
996
1260
|
unit: string,
|
|
@@ -1003,21 +1267,57 @@ function stepCycle60(
|
|
|
1003
1267
|
}
|
|
1004
1268
|
|
|
1005
1269
|
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
1006
|
-
const interval = segment.interval;
|
|
1007
1270
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1271
|
+
// A short offset cadence still lists its fires; the stride phrasing names
|
|
1272
|
+
// the interval and offset only once there are enough fires to beat the list.
|
|
1273
|
+
if (start !== 0 && segment.fires.length <= 3) {
|
|
1274
|
+
return 'en los ' + unit + 's ' + joinList(wordList(segment.fires)) +
|
|
1275
|
+
' de cada ' + anchor;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
return renderStride({
|
|
1279
|
+
interval: segment.interval,
|
|
1280
|
+
start,
|
|
1281
|
+
last: segment.fires[segment.fires.length - 1],
|
|
1282
|
+
cycle: 60,
|
|
1283
|
+
unit,
|
|
1284
|
+
anchor
|
|
1285
|
+
}, opts);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
1289
|
+
// form an arithmetic progression long enough to beat the list (the core
|
|
1290
|
+
// enumerates an offset/uneven step to this fire list; the IR is unchanged, so
|
|
1291
|
+
// the renderer recognizes the progression). Returns null for a non-progression
|
|
1292
|
+
// or a too-short list, leaving the caller to enumerate.
|
|
1293
|
+
function strideFromSegments(
|
|
1294
|
+
segments: Segment[],
|
|
1295
|
+
unit: string,
|
|
1296
|
+
anchor: string,
|
|
1297
|
+
opts: Opts
|
|
1298
|
+
): string | null {
|
|
1299
|
+
const values = singleValues(segments);
|
|
1300
|
+
const step = values && arithmeticStep(values);
|
|
1301
|
+
|
|
1302
|
+
return step ?
|
|
1303
|
+
renderStride({...step, cycle: 60, unit, anchor}, opts) :
|
|
1304
|
+
null;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// The sorted numeric values a field's segments cover, or null if any segment
|
|
1308
|
+
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
1309
|
+
function singleValues(segments: Segment[]): number[] | null {
|
|
1310
|
+
const values: number[] = [];
|
|
1311
|
+
|
|
1312
|
+
for (const segment of segments) {
|
|
1313
|
+
if (segment.kind !== 'single') {
|
|
1314
|
+
return null;
|
|
1012
1315
|
}
|
|
1013
1316
|
|
|
1014
|
-
|
|
1015
|
-
unit + ' ' + start + ' de cada ' + anchor;
|
|
1317
|
+
values.push(+segment.value);
|
|
1016
1318
|
}
|
|
1017
1319
|
|
|
1018
|
-
|
|
1019
|
-
// stride is rewritten to its fires upstream and never reaches here.)
|
|
1020
|
-
return 'cada ' + numero(interval, opts) + ' ' + unit + 's';
|
|
1320
|
+
return values;
|
|
1021
1321
|
}
|
|
1022
1322
|
|
|
1023
1323
|
// "cada seis horas", "a las 9:00, a las 11:00 y a la 1:00", or "cada
|
|
@@ -1044,6 +1344,150 @@ function stepHours(segment: StepSegment, opts: Opts): string {
|
|
|
1044
1344
|
timePhrase(start, 0, null, opts);
|
|
1045
1345
|
}
|
|
1046
1346
|
|
|
1347
|
+
// --- Hour-step cadence (the 24-cycle analog of renderStride). ---
|
|
1348
|
+
|
|
1349
|
+
// Speak an hour stride as a cadence with clock-time bounds: a clean stride
|
|
1350
|
+
// from midnight is the bare cadence ("cada dos horas"); a clean offset names
|
|
1351
|
+
// only its start ("cada seis horas a partir de las 02:00"); a bounded or
|
|
1352
|
+
// non-tiling stride pins both clock-time endpoints ("cada dos horas de las
|
|
1353
|
+
// 09:00 a las 17:00") so the bounded set reads unambiguously. Used wherever an
|
|
1354
|
+
// hour step (or arithmetic-progression hour list) would otherwise be
|
|
1355
|
+
// cross-multiplied into a wall of clock times.
|
|
1356
|
+
function hourStrideCadence(
|
|
1357
|
+
stride: {start: number; interval: number; last: number},
|
|
1358
|
+
opts: Opts
|
|
1359
|
+
): string {
|
|
1360
|
+
const {start, interval, last} = stride;
|
|
1361
|
+
const cadence = 'cada ' + numero(interval, opts) + ' horas';
|
|
1362
|
+
const tiles = 24 % interval === 0;
|
|
1363
|
+
|
|
1364
|
+
if (start === 0 && tiles) {
|
|
1365
|
+
return cadence;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (start < interval && tiles) {
|
|
1369
|
+
return cadence + ' a partir de ' + timePhrase(start, 0, null, opts);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return cadence + ' de ' + timePhrase(start, 0, null, opts) + ' a ' +
|
|
1373
|
+
timePhrase(last, 0, null, opts);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1377
|
+
// segment yields its {start, interval, last} directly; an all-single hour
|
|
1378
|
+
// list yields one only when its values form a long-enough arithmetic
|
|
1379
|
+
// progression (so an irregular list like 9,17 keeps enumerating). The IR is
|
|
1380
|
+
// unchanged — the renderer recognizes the stride and speaks it as a cadence
|
|
1381
|
+
// instead of the clock-time cross-product.
|
|
1382
|
+
function hourStride(
|
|
1383
|
+
ir: IR
|
|
1384
|
+
): {start: number; interval: number; last: number} | null {
|
|
1385
|
+
const segments = fieldSegments(ir, 'hour');
|
|
1386
|
+
|
|
1387
|
+
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
1388
|
+
const segment = segments[0];
|
|
1389
|
+
const start = segment.startToken === '*' ?
|
|
1390
|
+
0 :
|
|
1391
|
+
+segment.startToken.split('-')[0];
|
|
1392
|
+
|
|
1393
|
+
return {interval: segment.interval, last: segment.fires[
|
|
1394
|
+
segment.fires.length - 1], start};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const values = singleValues(segments);
|
|
1398
|
+
const step = values && arithmeticStep(values);
|
|
1399
|
+
|
|
1400
|
+
return step || null;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
1404
|
+
// fills the minute (a "durante un minuto" frame at minute 0); a single 0 is
|
|
1405
|
+
// just the top of the minute (no clause); anything else needs its own clause.
|
|
1406
|
+
function subMinuteSecond(ir: IR): boolean {
|
|
1407
|
+
return ir.pattern.second === '*' || ir.shapes.second === 'step';
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// The lead clause for an hour-cadence rendering: the second and the pinned
|
|
1411
|
+
// minute, before the hour cadence. A pinned minute 0 folds in — a single,
|
|
1412
|
+
// list, or range second is counted "de cada hora" (the minute-0 is the top of
|
|
1413
|
+
// the hour), and a wildcard or sub-minute step second takes a "durante un
|
|
1414
|
+
// minuto" frame (the whole minute-0 window). A non-zero minute is a real clock
|
|
1415
|
+
// minute: the second leads with its own clause (if any), then the minute reads
|
|
1416
|
+
// "en el minuto M".
|
|
1417
|
+
function hourCadenceLead(ir: IR, minute: number, opts: Opts): string {
|
|
1418
|
+
if (minute === 0) {
|
|
1419
|
+
if (subMinuteSecond(ir)) {
|
|
1420
|
+
return secondsClause(ir, 'minuto', opts) + ' durante un minuto';
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
return secondsClause(ir, 'hora', opts);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
const minutePhrase = 'en el minuto ' + minute;
|
|
1427
|
+
|
|
1428
|
+
// A single 0 second is just the top of the minute, so the minute leads
|
|
1429
|
+
// alone; any other second prefixes its own clause.
|
|
1430
|
+
if (ir.pattern.second === '0') {
|
|
1431
|
+
return minutePhrase;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
return secondsClause(ir, 'minuto', opts) + ', ' + minutePhrase;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
1438
|
+
// pinned minute and a second as a cadence — the lead clause, then the hour
|
|
1439
|
+
// cadence — instead of cross-multiplying the hours into a wall of clock times.
|
|
1440
|
+
// Returns null when the hour is not a stride (an irregular list, a single
|
|
1441
|
+
// hour, or a range), or when the cross-product is short enough that
|
|
1442
|
+
// enumeration is no longer than the cadence: a meaningful second makes every
|
|
1443
|
+
// clock time three digit-groups, so any stride is worth compacting; otherwise
|
|
1444
|
+
// the stride must exceed the clock-time cap, the same point at which the core
|
|
1445
|
+
// itself stops enumerating. Renderer-only; the IR is unchanged.
|
|
1446
|
+
function hourCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
1447
|
+
const stride = hourStride(ir);
|
|
1448
|
+
|
|
1449
|
+
if (!stride) {
|
|
1450
|
+
return null;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const fires = (stride.last - stride.start) / stride.interval + 1;
|
|
1454
|
+
|
|
1455
|
+
if (ir.pattern.second === '0' && fires <= maxClockTimes) {
|
|
1456
|
+
return null;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// A wildcard or sub-minute step second confined to minute 0 of a clean hour
|
|
1460
|
+
// stride is a confinement, not a juxtaposed cadence: it reads "durante un
|
|
1461
|
+
// minuto, durante las horas pares", reusing the hour-step confinement idiom
|
|
1462
|
+
// so the minute-0 window is never heard as the bare hour cadence.
|
|
1463
|
+
const confinement = minute === 0 && subMinuteSecond(ir) &&
|
|
1464
|
+
cleanStrideSegment(ir);
|
|
1465
|
+
|
|
1466
|
+
if (confinement) {
|
|
1467
|
+
return secondsClause(ir, 'minuto', opts) + ' durante un minuto, ' +
|
|
1468
|
+
stepHourSpan(confinement, opts) + trailingQualifier(ir, opts);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
return hourCadenceLead(ir, minute, opts) + ', ' +
|
|
1472
|
+
hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// The hour step segment when the hour is a clean stride es renders as a
|
|
1476
|
+
// confinement phrase ("durante las horas pares"); null otherwise (an offset or
|
|
1477
|
+
// bounded step, an uneven stride, or an arithmetic-progression list, which
|
|
1478
|
+
// keep the bounded cadence form).
|
|
1479
|
+
function cleanStrideSegment(ir: IR): StepSegment | null {
|
|
1480
|
+
const segments = fieldSegments(ir, 'hour');
|
|
1481
|
+
const segment = segments.length === 1 && segments[0];
|
|
1482
|
+
|
|
1483
|
+
if (!segment || segment.kind !== 'step' ||
|
|
1484
|
+
segment.startToken.indexOf('-') !== -1) {
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
return segment;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1047
1491
|
// --- Hour-time phrasing. ---
|
|
1048
1492
|
|
|
1049
1493
|
// "a las 9:00" / "a la 1:00" / "al mediodía" for each fire hour.
|
|
@@ -1689,16 +2133,13 @@ function numero(n: number, opts: Opts): string | number {
|
|
|
1689
2133
|
return numeral(n, numeros, opts);
|
|
1690
2134
|
}
|
|
1691
2135
|
|
|
1692
|
-
// A weekday name from a number or a
|
|
2136
|
+
// A weekday name from a canonical number, or from a Quartz stem (`5L`,
|
|
2137
|
+
// `MON#2`), which the core does not number-canonicalize: resolve any name
|
|
2138
|
+
// via the core's index and fold the Sunday alias 7 to 0.
|
|
1693
2139
|
function weekdayName(token: NameToken): string {
|
|
1694
|
-
|
|
1695
|
-
return weekdayNames[0];
|
|
1696
|
-
}
|
|
2140
|
+
const number = toFieldNumber('' + token, weekdayNumbers);
|
|
1697
2141
|
|
|
1698
|
-
|
|
1699
|
-
// the numeric path indexes the name array, the name path the token map.
|
|
1700
|
-
return weekdayNames[token as number] ||
|
|
1701
|
-
weekdayNames[weekdayTokens[token as string]];
|
|
2142
|
+
return weekdayNames[number === 7 ? 0 : number];
|
|
1702
2143
|
}
|
|
1703
2144
|
|
|
1704
2145
|
// The plural weekday form: días ending in -s are invariant ("los lunes");
|
|
@@ -1709,12 +2150,10 @@ function pluralWeekday(token: NameToken): string {
|
|
|
1709
2150
|
return name.endsWith('s') ? name : name + 's';
|
|
1710
2151
|
}
|
|
1711
2152
|
|
|
1712
|
-
// A month name from a number
|
|
2153
|
+
// A month name from a canonical month number. The name array has a leading
|
|
2154
|
+
// null hole for the 1-based index.
|
|
1713
2155
|
function monthName(token: NameToken): string {
|
|
1714
|
-
|
|
1715
|
-
// token map. The name array has a leading null hole for the 1-based index.
|
|
1716
|
-
return (monthNames[token as number] ||
|
|
1717
|
-
monthNames[monthTokens[token as string]]) as string;
|
|
2156
|
+
return monthNames[+token] as string;
|
|
1718
2157
|
}
|
|
1719
2158
|
|
|
1720
2159
|
// Whether a canonical field value is an open step (`*/n` or `a/n`).
|
|
@@ -1730,7 +2169,10 @@ const es: Language<SpanishStyle> = {
|
|
|
1730
2169
|
fallback: 'un patrón cron irreconocible',
|
|
1731
2170
|
options: normalizeOptions,
|
|
1732
2171
|
reboot: 'al arrancar el sistema',
|
|
1733
|
-
|
|
2172
|
+
// A description ending in a period already carries it, so closing the
|
|
2173
|
+
// sentence must not double it.
|
|
2174
|
+
sentence: (description) =>
|
|
2175
|
+
'Se ejecuta ' + description + (description.endsWith('.') ? '' : '.')
|
|
1734
2176
|
};
|
|
1735
2177
|
|
|
1736
2178
|
export default es;
|