cronli5 0.1.4 → 0.1.6
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 +53 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +286 -45
- package/dist/cronli5.js +286 -45
- package/dist/lang/de.cjs +252 -13
- package/dist/lang/de.js +252 -13
- package/dist/lang/en.cjs +281 -38
- package/dist/lang/en.js +281 -38
- package/dist/lang/es.cjs +259 -29
- package/dist/lang/es.js +259 -29
- package/dist/lang/fi.cjs +285 -49
- package/dist/lang/fi.js +285 -49
- package/dist/lang/zh.cjs +225 -42
- package/dist/lang/zh.js +225 -42
- package/package.json +3 -2
- package/src/core/analyze.ts +7 -0
- package/src/core/ir.ts +1 -1
- package/src/core/util.ts +31 -1
- package/src/lang/de/index.ts +561 -30
- package/src/lang/en/index.ts +593 -59
- package/src/lang/es/index.ts +576 -52
- package/src/lang/fi/index.ts +633 -95
- package/src/lang/zh/index.ts +484 -77
- package/types/core/ir.d.ts +1 -1
- package/types/core/util.d.ts +6 -1
package/src/lang/es/index.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// lists render as per-hour windows).
|
|
10
10
|
|
|
11
11
|
import {clockDigits, numeral} from '../../core/format.js';
|
|
12
|
-
import {weekdayNumbers} from '../../core/specs.js';
|
|
13
|
-
import {toFieldNumber} from '../../core/util.js';
|
|
12
|
+
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
13
|
+
import {arithmeticStep, toFieldNumber} from '../../core/util.js';
|
|
14
14
|
import type {Cronli5Options} from '../../types.js';
|
|
15
15
|
import type {
|
|
16
16
|
Field, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode,
|
|
@@ -27,6 +27,20 @@ type Renderer = (ir: IR, plan: PlanNode, opts: Opts) => string;
|
|
|
27
27
|
// A `step` segment, narrowed from the discriminated `Segment` union.
|
|
28
28
|
type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
29
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
|
+
|
|
30
44
|
// One end of a clock-time range. The second is optional and may be absent
|
|
31
45
|
// (top-of-hour windows) or a folded clock second.
|
|
32
46
|
type ClockEnd = {hour: number; minute: number; second?: number | null};
|
|
@@ -206,15 +220,83 @@ function renderSecondsWithinMinute(
|
|
|
206
220
|
' de cada hora' + trailingQualifier(ir, opts);
|
|
207
221
|
}
|
|
208
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
|
+
if (!clockRest || ir.shapes.minute !== 'single') {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const minute = +ir.pattern.minute;
|
|
267
|
+
|
|
268
|
+
return hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// A wildcard or stepped second with a fixed minute across one or more specific
|
|
272
|
+
// hours: the seconds confine to the clock time(s), each minute named.
|
|
273
|
+
function isPinnedMinuteSeconds(
|
|
274
|
+
ir: IR,
|
|
275
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
276
|
+
): plan is Extract<PlanNode, {kind: 'composeSeconds'}> &
|
|
277
|
+
{rest: Extract<PlanNode, {kind: 'clockTimes'}>} {
|
|
278
|
+
return plan.rest.kind === 'clockTimes' &&
|
|
279
|
+
(ir.shapes.second === 'wildcard' || ir.shapes.second === 'step');
|
|
280
|
+
}
|
|
281
|
+
|
|
209
282
|
function renderComposeSeconds(
|
|
210
283
|
ir: IR,
|
|
211
284
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
212
285
|
opts: Opts
|
|
213
286
|
): string {
|
|
287
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
288
|
+
// minute is a cadence, not a wall of clock times: the second/minute lead,
|
|
289
|
+
// then the hour cadence ("en el segundo 30 de cada hora, cada dos horas").
|
|
290
|
+
// The clock-time rest would otherwise cross-multiply the hours.
|
|
291
|
+
const hourCad = composeHourCadence(ir, plan, opts);
|
|
292
|
+
|
|
293
|
+
if (hourCad !== null) {
|
|
294
|
+
return hourCad;
|
|
295
|
+
}
|
|
296
|
+
|
|
214
297
|
// A wildcard or stepped second with the minute pinned to a single value
|
|
215
298
|
// across one or more specific hours: the seconds confine to the clock time.
|
|
216
|
-
if (plan
|
|
217
|
-
(ir.shapes.second === 'wildcard' || ir.shapes.second === 'step')) {
|
|
299
|
+
if (isPinnedMinuteSeconds(ir, plan)) {
|
|
218
300
|
return pinnedMinuteSeconds(ir, plan.rest, opts);
|
|
219
301
|
}
|
|
220
302
|
|
|
@@ -223,19 +305,7 @@ function renderComposeSeconds(
|
|
|
223
305
|
// fixed so "de cada minuto" is misleading. Single seconds already fold into
|
|
224
306
|
// the time in the clockTimes renderer; step seconds keep their own clause.
|
|
225
307
|
if (plan.rest.kind === 'clockTimes' && ir.shapes.second === 'list') {
|
|
226
|
-
|
|
227
|
-
return atTime(timePhrase(time.hour, time.minute, null, opts));
|
|
228
|
-
});
|
|
229
|
-
const grouped = groupClockTimesByArticle(clockPhrases);
|
|
230
|
-
// Strip the leading "a " prefix from the grouped result so the caller can
|
|
231
|
-
// prepend "de " to produce the genitive form "de las 09:00 y 17:00".
|
|
232
|
-
const clockList = grouped.startsWith('a ') ? grouped.slice(2) : grouped;
|
|
233
|
-
const secondsPhrase = 'en los segundos ' +
|
|
234
|
-
joinList(segmentWords(fieldSegments(ir, 'second')));
|
|
235
|
-
const dayFrame = trailingQualifier(ir, opts);
|
|
236
|
-
|
|
237
|
-
return (dayFrame ? dayFrame.trimStart() + ', ' : '') +
|
|
238
|
-
secondsPhrase + ' de ' + clockList;
|
|
308
|
+
return secondsListAtClock(ir, plan.rest, opts);
|
|
239
309
|
}
|
|
240
310
|
|
|
241
311
|
// Second-step + fixed minute + hour range + weekday: anchor the cadence to
|
|
@@ -252,7 +322,40 @@ function renderComposeSeconds(
|
|
|
252
322
|
return dayFrame + ', ' + window + ', ' + cadence;
|
|
253
323
|
}
|
|
254
324
|
|
|
255
|
-
|
|
325
|
+
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
326
|
+
// cadences that read as contradictory ("cada segundo, cada dos minutos").
|
|
327
|
+
// Bind them with the genitive "de" ("cada segundo de cada dos minutos"),
|
|
328
|
+
// mirroring English. Other strides, a restricted hour, and an hour cadence
|
|
329
|
+
// keep the juxtaposed form.
|
|
330
|
+
if (isEveryOtherMinuteSeconds(ir, plan)) {
|
|
331
|
+
return secondsLeadClause(ir, opts) + ' de ' + render(ir, plan.rest, opts);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
335
|
+
// leading clause, so the composer must not prepend a second lead that would
|
|
336
|
+
// double it. A wildcard or stepped second is not folded there (no
|
|
337
|
+
// clockSecond), so it still leads its own clause here.
|
|
338
|
+
const restOwnsLead = plan.rest.kind === 'compactClockTimes' &&
|
|
339
|
+
ir.analyses.clockSecond;
|
|
340
|
+
const lead = restOwnsLead ? '' : secondsLeadClause(ir, opts) + ', ';
|
|
341
|
+
|
|
342
|
+
return lead + render(ir, plan.rest, opts);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// A wildcard second over an unoffset minute */2 with a wildcard hour: the two
|
|
346
|
+
// cadences read as contradictory side by side, so they bind into one.
|
|
347
|
+
function isEveryOtherMinuteSeconds(
|
|
348
|
+
ir: IR,
|
|
349
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
350
|
+
): boolean {
|
|
351
|
+
if (plan.rest.kind !== 'minuteFrequency' ||
|
|
352
|
+
ir.shapes.second !== 'wildcard' || ir.shapes.hour !== 'wildcard') {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const minuteStep = stepSegment(ir.analyses.segments.minute);
|
|
357
|
+
|
|
358
|
+
return minuteStep.startToken === '*' && minuteStep.interval === 2;
|
|
256
359
|
}
|
|
257
360
|
|
|
258
361
|
// A wildcard or stepped second under a single pinned minute and specific
|
|
@@ -273,7 +376,12 @@ function pinnedMinuteSeconds(
|
|
|
273
376
|
const dayTrail = leadingQualifier(ir, opts).trimEnd();
|
|
274
377
|
const trail = dayTrail ? ', ' + dayTrail : '';
|
|
275
378
|
|
|
276
|
-
|
|
379
|
+
// The "durante un minuto a las 9" duration form drops the clock minute, so it
|
|
380
|
+
// is correct only when the minute is a SINGLE 0 — every clock time at :00. A
|
|
381
|
+
// minute LIST whose first value is 0 (e.g. */45 → :00, :45) must name each
|
|
382
|
+
// minute, never collapse to the bare hour (which once repeated it, "a las 9 y
|
|
383
|
+
// 9"), so it takes the explicit clock list.
|
|
384
|
+
if (+rest.times[0].minute === 0 && ir.shapes.minute === 'single') {
|
|
277
385
|
return secondsLeadClause(ir, opts) + ' durante un minuto ' +
|
|
278
386
|
durationHourList(rest.times, opts) + trail;
|
|
279
387
|
}
|
|
@@ -284,6 +392,15 @@ function pinnedMinuteSeconds(
|
|
|
284
392
|
|
|
285
393
|
// The leading clause describing a second field relative to the minute.
|
|
286
394
|
function secondsLeadClause(ir: IR, opts: Opts): string {
|
|
395
|
+
return secondsClause(ir, 'minuto', opts);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// The second clause counted against an arbitrary anchor. The anchor is
|
|
399
|
+
// "minuto" in the standalone seconds path; the hour-cadence path folds a
|
|
400
|
+
// pinned minute 0 into the hour and counts the second "de cada hora" instead
|
|
401
|
+
// ("en el segundo 30 de cada hora"), so the minute-0 confinement is stated,
|
|
402
|
+
// not dropped.
|
|
403
|
+
function secondsClause(ir: IR, anchor: string, opts: Opts): string {
|
|
287
404
|
const secondField = ir.pattern.second;
|
|
288
405
|
const shape = ir.shapes.second;
|
|
289
406
|
|
|
@@ -293,23 +410,24 @@ function secondsLeadClause(ir: IR, opts: Opts): string {
|
|
|
293
410
|
|
|
294
411
|
if (shape === 'step') {
|
|
295
412
|
return stepCycle60(stepSegment(ir.analyses.segments.second), 'segundo',
|
|
296
|
-
|
|
413
|
+
anchor, opts);
|
|
297
414
|
}
|
|
298
415
|
|
|
299
416
|
if (shape === 'range') {
|
|
300
417
|
const bounds = secondField.split('-');
|
|
301
418
|
|
|
302
419
|
return 'cada segundo del ' + bounds[0] + ' al ' + bounds[1] +
|
|
303
|
-
' de cada
|
|
420
|
+
' de cada ' + anchor;
|
|
304
421
|
}
|
|
305
422
|
|
|
306
423
|
if (shape === 'single') {
|
|
307
|
-
return 'en el segundo ' + secondField + ' de cada
|
|
424
|
+
return 'en el segundo ' + secondField + ' de cada ' + anchor;
|
|
308
425
|
}
|
|
309
426
|
|
|
310
|
-
return '
|
|
427
|
+
return strideFromSegments(fieldSegments(ir, 'second'), 'segundo', anchor,
|
|
428
|
+
opts) ?? 'en los segundos ' +
|
|
311
429
|
joinList(segmentWords(fieldSegments(ir, 'second'))) +
|
|
312
|
-
' de cada
|
|
430
|
+
' de cada ' + anchor;
|
|
313
431
|
}
|
|
314
432
|
|
|
315
433
|
// --- Minute renderers. ---
|
|
@@ -345,12 +463,15 @@ function renderMultipleMinutes(
|
|
|
345
463
|
plan: Extract<PlanNode, {kind: 'multipleMinutes'}>,
|
|
346
464
|
opts: Opts
|
|
347
465
|
): string {
|
|
348
|
-
return minutesList(ir) + trailingQualifier(ir, opts);
|
|
466
|
+
return minutesList(ir, opts) + trailingQualifier(ir, opts);
|
|
349
467
|
}
|
|
350
468
|
|
|
351
|
-
// "en los minutos 5, 10 y 30 de cada hora".
|
|
352
|
-
|
|
353
|
-
|
|
469
|
+
// "en los minutos 5, 10 y 30 de cada hora". An offset/uneven step the core
|
|
470
|
+
// enumerated to this list reads as a stride cadence when the fires form a
|
|
471
|
+
// long-enough progression.
|
|
472
|
+
function minutesList(ir: IR, opts: Opts): string {
|
|
473
|
+
return strideFromSegments(fieldSegments(ir, 'minute'), 'minuto', 'hora',
|
|
474
|
+
opts) ?? 'en los minutos ' +
|
|
354
475
|
joinList(segmentWords(fieldSegments(ir, 'minute'))) + ' de cada hora';
|
|
355
476
|
}
|
|
356
477
|
|
|
@@ -461,11 +582,20 @@ function renderMinuteFrequency(
|
|
|
461
582
|
'hora', opts);
|
|
462
583
|
|
|
463
584
|
if (plan.hours.kind === 'during') {
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
585
|
+
// A uneven hour stride confines the minute cadence to its own bounded hour
|
|
586
|
+
// cadence ("cada 15 minutos, cada cinco horas de las 00:00 a las 20:00").
|
|
587
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
588
|
+
|
|
589
|
+
if (cadence) {
|
|
590
|
+
phrase += ', ' + cadence;
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
// An offset step (e.g. 1/2) arrives here; a single step reads as a
|
|
594
|
+
// confinement, not the verbose window list.
|
|
595
|
+
phrase += singleHourStep(ir.analyses.segments.hour) ?
|
|
596
|
+
', ' + stepHourSpan(stepSegment(ir.analyses.segments.hour), opts) :
|
|
597
|
+
' ' + hourSpanFromTimes(ir, plan.hours.times, opts);
|
|
598
|
+
}
|
|
469
599
|
}
|
|
470
600
|
else if (plan.hours.kind === 'window') {
|
|
471
601
|
phrase += ' ' + hourWindow(plan.hours, opts);
|
|
@@ -508,7 +638,15 @@ function renderMinutesAcrossHours(
|
|
|
508
638
|
plan: Extract<PlanNode, {kind: 'minutesAcrossHours'}>,
|
|
509
639
|
opts: Opts
|
|
510
640
|
): string {
|
|
641
|
+
// A uneven hour stride reads as a cadence, not a wall of hour columns: the
|
|
642
|
+
// minute lead, then "cada N horas de las X a las Y".
|
|
643
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
644
|
+
|
|
511
645
|
if (plan.form === 'wildcard') {
|
|
646
|
+
if (cadence !== null) {
|
|
647
|
+
return 'cada minuto, ' + cadence + trailingQualifier(ir, opts);
|
|
648
|
+
}
|
|
649
|
+
|
|
512
650
|
if (singleHourStep(ir.analyses.segments.hour)) {
|
|
513
651
|
return 'cada minuto, ' +
|
|
514
652
|
stepHourSpan(stepSegment(ir.analyses.segments.hour), opts) +
|
|
@@ -521,7 +659,11 @@ function renderMinutesAcrossHours(
|
|
|
521
659
|
|
|
522
660
|
const lead = plan.form === 'range' ?
|
|
523
661
|
minuteRangeLead(ir.pattern.minute) :
|
|
524
|
-
minutesList(ir);
|
|
662
|
+
minutesList(ir, opts);
|
|
663
|
+
|
|
664
|
+
if (cadence !== null) {
|
|
665
|
+
return lead + ', ' + cadence + trailingQualifier(ir, opts);
|
|
666
|
+
}
|
|
525
667
|
|
|
526
668
|
return lead + ', ' + atHourTimes(ir, plan.times, opts) +
|
|
527
669
|
trailingQualifier(ir, opts);
|
|
@@ -533,16 +675,26 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
533
675
|
opts: Opts
|
|
534
676
|
): string {
|
|
535
677
|
const segment = stepSegment(ir.analyses.segments.hour);
|
|
678
|
+
// A bounded or uneven hour step reads as its endpoint-pinning cadence; an
|
|
679
|
+
// offset-clean step keeps its confinement / per-step phrasing.
|
|
680
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
536
681
|
|
|
537
|
-
// A wildcard minute (a cadence) is reached only for a clean stride
|
|
538
|
-
//
|
|
682
|
+
// A wildcard minute (a cadence) is reached only for a clean stride (a bounded
|
|
683
|
+
// or uneven step routes through minutesAcrossHours instead) and is confined.
|
|
539
684
|
if (plan.form === 'wildcard') {
|
|
540
685
|
return 'cada minuto, ' + stepHourSpan(segment, opts) +
|
|
541
686
|
trailingQualifier(ir, opts);
|
|
542
687
|
}
|
|
543
688
|
|
|
544
|
-
|
|
545
|
-
|
|
689
|
+
// A minute list keeps the same cadence clause as the range; only its lead
|
|
690
|
+
// differs ("en los minutos 5 y 30 de cada hora" vs "cada minuto del 0 al
|
|
691
|
+
// 30").
|
|
692
|
+
const lead = plan.form === 'list' ?
|
|
693
|
+
minutesList(ir, opts) :
|
|
694
|
+
minuteRangeLead(ir.pattern.minute);
|
|
695
|
+
|
|
696
|
+
return lead + ', ' +
|
|
697
|
+
(cadence ?? stepHours(segment, opts)) + trailingQualifier(ir, opts);
|
|
546
698
|
}
|
|
547
699
|
|
|
548
700
|
// --- Hour renderers. ---
|
|
@@ -579,7 +731,7 @@ function renderHourRange(
|
|
|
579
731
|
|
|
580
732
|
const lead = ir.shapes.minute === 'single' ?
|
|
581
733
|
'en el minuto ' + ir.pattern.minute + ' de cada hora' :
|
|
582
|
-
minutesList(ir);
|
|
734
|
+
minutesList(ir, opts);
|
|
583
735
|
|
|
584
736
|
return lead + ', ' + window + trailingQualifier(ir, opts);
|
|
585
737
|
}
|
|
@@ -589,6 +741,15 @@ function renderHourStep(
|
|
|
589
741
|
plan: Extract<PlanNode, {kind: 'hourStep'}>,
|
|
590
742
|
opts: Opts
|
|
591
743
|
): string {
|
|
744
|
+
// A bounded or uneven hour step reads as its endpoint-pinning cadence ("cada
|
|
745
|
+
// dos horas de las 09:00 a las 17:00"); an offset-clean step keeps its bare
|
|
746
|
+
// or "a partir de" cadence.
|
|
747
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
748
|
+
|
|
749
|
+
if (cadence !== null) {
|
|
750
|
+
return cadence + trailingQualifier(ir, opts);
|
|
751
|
+
}
|
|
752
|
+
|
|
592
753
|
return stepHours(stepSegment(ir.analyses.segments.hour), opts) +
|
|
593
754
|
trailingQualifier(ir, opts);
|
|
594
755
|
}
|
|
@@ -717,6 +878,19 @@ function renderClockTimes(
|
|
|
717
878
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
718
879
|
opts: Opts
|
|
719
880
|
): string {
|
|
881
|
+
// An hour step or range (or arithmetic-progression hour list) under a single
|
|
882
|
+
// pinned minute reads as a cadence or window rather than a cross-product of
|
|
883
|
+
// clock times.
|
|
884
|
+
if (ir.shapes.minute === 'single') {
|
|
885
|
+
const minute = +ir.pattern.minute;
|
|
886
|
+
const cadence = hourCadence(ir, minute, opts) ??
|
|
887
|
+
hourRangeCadence(ir, minute, opts);
|
|
888
|
+
|
|
889
|
+
if (cadence !== null) {
|
|
890
|
+
return cadence;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
720
894
|
const phrases = plan.times.map(function clock(time) {
|
|
721
895
|
return atTime(timePhrase(time.hour, time.minute, time.second, opts));
|
|
722
896
|
});
|
|
@@ -1052,6 +1226,16 @@ function renderCompactClockTimes(
|
|
|
1052
1226
|
opts: Opts
|
|
1053
1227
|
): string {
|
|
1054
1228
|
if (plan.fold) {
|
|
1229
|
+
// An hour step or range (or arithmetic-progression hour list) under the
|
|
1230
|
+
// single pinned minute reads as a cadence or window, not a wall of clock
|
|
1231
|
+
// times. (Returns null for an irregular list, which keeps folding below.)
|
|
1232
|
+
const cadence = hourCadence(ir, plan.minute, opts) ??
|
|
1233
|
+
hourRangeCadence(ir, plan.minute, opts);
|
|
1234
|
+
|
|
1235
|
+
if (cadence !== null) {
|
|
1236
|
+
return cadence;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1055
1239
|
const ranged = hourSegments(ir).some(function range(segment) {
|
|
1056
1240
|
return segment.kind === 'range';
|
|
1057
1241
|
});
|
|
@@ -1068,8 +1252,13 @@ function renderCompactClockTimes(
|
|
|
1068
1252
|
hourSegmentTimes(ir, plan.minute, ir.analyses.clockSecond, opts);
|
|
1069
1253
|
}
|
|
1070
1254
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1255
|
+
// A uneven hour stride reads as a cadence after the minute lead, not a wall
|
|
1256
|
+
// of clock-time columns.
|
|
1257
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
1258
|
+
const phrase = cadence ?
|
|
1259
|
+
minutesList(ir, opts) + ', ' + cadence + trailingQualifier(ir, opts) :
|
|
1260
|
+
minutesList(ir, opts) + ', ' +
|
|
1261
|
+
hourSegmentTimes(ir, 0, null, opts) + trailingQualifier(ir, opts);
|
|
1073
1262
|
|
|
1074
1263
|
return ir.analyses.clockSecond ?
|
|
1075
1264
|
secondsLeadClause(ir, opts) + ', ' + phrase :
|
|
@@ -1100,8 +1289,42 @@ const renderers = {
|
|
|
1100
1289
|
|
|
1101
1290
|
// --- Step phrases. ---
|
|
1102
1291
|
|
|
1292
|
+
// Speak a step cadence over a `cycle`-long field (60 for minute/second). A
|
|
1293
|
+
// clean stride from the top of the cycle is the bare cadence ("cada quince
|
|
1294
|
+
// minutos"); a uniform offset (start within the first interval, the interval
|
|
1295
|
+
// still dividing the cycle) names only its start, since it wraps cleanly with
|
|
1296
|
+
// no distinct endpoint ("cada seis minutos a partir del minuto 5 de cada
|
|
1297
|
+
// hora"); a non-uniform stride (start >= interval, or an interval that does
|
|
1298
|
+
// not divide the cycle) pins both endpoints so the bounded, non-wrapping set
|
|
1299
|
+
// reads unambiguously ("cada dos minutos del minuto 3 al 59 de cada hora").
|
|
1300
|
+
// This is the one phrasing for every step the renderer speaks, whether the
|
|
1301
|
+
// core kept it a step shape (a clean cadence) or enumerated it to a fire list
|
|
1302
|
+
// (an offset/uneven set the list path recognizes as a progression).
|
|
1303
|
+
function renderStride(stride: Stride, opts: Opts): string {
|
|
1304
|
+
const {interval, start, last, cycle, unit, anchor} = stride;
|
|
1305
|
+
const cadence = 'cada ' + numero(interval, opts) + ' ' + unit + 's';
|
|
1306
|
+
const tiles = cycle % interval === 0;
|
|
1307
|
+
|
|
1308
|
+
if (start === 0 && tiles) {
|
|
1309
|
+
return cadence;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// A context that supplies its own trailing scope passes an empty anchor, so
|
|
1313
|
+
// the cadence keeps its endpoints but drops the "de cada <anchor>" tail.
|
|
1314
|
+
const tail = anchor ? ' de cada ' + anchor : '';
|
|
1315
|
+
|
|
1316
|
+
if (start < interval && tiles) {
|
|
1317
|
+
return cadence + ' a partir del ' + unit + ' ' + start + tail;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
return cadence + ' del ' + unit + ' ' + start + ' al ' + last + tail;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1103
1323
|
// "cada 15 minutos", "en los minutos 5, 20 y 35 de cada hora", or
|
|
1104
|
-
// "cada 15 minutos a partir del minuto 5 de cada hora".
|
|
1324
|
+
// "cada 15 minutos a partir del minuto 5 de cada hora". A step shape only
|
|
1325
|
+
// reaches here as a clean cadence (the interval divides 60), so the stride
|
|
1326
|
+
// collapses to the bare or uniform-offset form; an offset/uneven set arrives
|
|
1327
|
+
// as a fire list and is recognized by the list path instead.
|
|
1105
1328
|
function stepCycle60(
|
|
1106
1329
|
segment: StepSegment,
|
|
1107
1330
|
unit: string,
|
|
@@ -1114,21 +1337,57 @@ function stepCycle60(
|
|
|
1114
1337
|
}
|
|
1115
1338
|
|
|
1116
1339
|
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
1117
|
-
const interval = segment.interval;
|
|
1118
1340
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1341
|
+
// A short offset cadence still lists its fires; the stride phrasing names
|
|
1342
|
+
// the interval and offset only once there are enough fires to beat the list.
|
|
1343
|
+
if (start !== 0 && segment.fires.length <= 3) {
|
|
1344
|
+
return 'en los ' + unit + 's ' + joinList(wordList(segment.fires)) +
|
|
1345
|
+
' de cada ' + anchor;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
return renderStride({
|
|
1349
|
+
interval: segment.interval,
|
|
1350
|
+
start,
|
|
1351
|
+
last: segment.fires[segment.fires.length - 1],
|
|
1352
|
+
cycle: 60,
|
|
1353
|
+
unit,
|
|
1354
|
+
anchor
|
|
1355
|
+
}, opts);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
1359
|
+
// form an arithmetic progression long enough to beat the list (the core
|
|
1360
|
+
// enumerates an offset/uneven step to this fire list; the IR is unchanged, so
|
|
1361
|
+
// the renderer recognizes the progression). Returns null for a non-progression
|
|
1362
|
+
// or a too-short list, leaving the caller to enumerate.
|
|
1363
|
+
function strideFromSegments(
|
|
1364
|
+
segments: Segment[],
|
|
1365
|
+
unit: string,
|
|
1366
|
+
anchor: string,
|
|
1367
|
+
opts: Opts
|
|
1368
|
+
): string | null {
|
|
1369
|
+
const values = singleValues(segments);
|
|
1370
|
+
const step = values && arithmeticStep(values);
|
|
1371
|
+
|
|
1372
|
+
return step ?
|
|
1373
|
+
renderStride({...step, cycle: 60, unit, anchor}, opts) :
|
|
1374
|
+
null;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// The sorted numeric values a field's segments cover, or null if any segment
|
|
1378
|
+
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
1379
|
+
function singleValues(segments: Segment[]): number[] | null {
|
|
1380
|
+
const values: number[] = [];
|
|
1381
|
+
|
|
1382
|
+
for (const segment of segments) {
|
|
1383
|
+
if (segment.kind !== 'single') {
|
|
1384
|
+
return null;
|
|
1123
1385
|
}
|
|
1124
1386
|
|
|
1125
|
-
|
|
1126
|
-
unit + ' ' + start + ' de cada ' + anchor;
|
|
1387
|
+
values.push(+segment.value);
|
|
1127
1388
|
}
|
|
1128
1389
|
|
|
1129
|
-
|
|
1130
|
-
// stride is rewritten to its fires upstream and never reaches here.)
|
|
1131
|
-
return 'cada ' + numero(interval, opts) + ' ' + unit + 's';
|
|
1390
|
+
return values;
|
|
1132
1391
|
}
|
|
1133
1392
|
|
|
1134
1393
|
// "cada seis horas", "a las 9:00, a las 11:00 y a la 1:00", or "cada
|
|
@@ -1155,6 +1414,271 @@ function stepHours(segment: StepSegment, opts: Opts): string {
|
|
|
1155
1414
|
timePhrase(start, 0, null, opts);
|
|
1156
1415
|
}
|
|
1157
1416
|
|
|
1417
|
+
// --- Hour-step cadence (the 24-cycle analog of renderStride). ---
|
|
1418
|
+
|
|
1419
|
+
// Speak an hour stride as a cadence with clock-time bounds: a clean stride
|
|
1420
|
+
// from midnight is the bare cadence ("cada dos horas"); a clean offset names
|
|
1421
|
+
// only its start ("cada seis horas a partir de las 02:00"); a bounded or
|
|
1422
|
+
// non-tiling stride pins both clock-time endpoints ("cada dos horas de las
|
|
1423
|
+
// 09:00 a las 17:00") so the bounded set reads unambiguously. Used wherever an
|
|
1424
|
+
// hour step (or arithmetic-progression hour list) would otherwise be
|
|
1425
|
+
// cross-multiplied into a wall of clock times.
|
|
1426
|
+
function hourStrideCadence(
|
|
1427
|
+
stride: {start: number; interval: number; last: number},
|
|
1428
|
+
opts: Opts
|
|
1429
|
+
): string {
|
|
1430
|
+
const {start, interval, last} = stride;
|
|
1431
|
+
const cadence = 'cada ' + numero(interval, opts) + ' horas';
|
|
1432
|
+
const tiles = 24 % interval === 0;
|
|
1433
|
+
|
|
1434
|
+
if (start === 0 && tiles) {
|
|
1435
|
+
return cadence;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (start < interval && tiles) {
|
|
1439
|
+
return cadence + ' a partir de ' + timePhrase(start, 0, null, opts);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
return cadence + ' de ' + timePhrase(start, 0, null, opts) + ' a ' +
|
|
1443
|
+
timePhrase(last, 0, null, opts);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// Whether an hour stride wraps the day cleanly from within its first interval
|
|
1447
|
+
// (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
|
|
1448
|
+
// stride has no distinct endpoint and keeps its bare or "a partir de" cadence.
|
|
1449
|
+
// Every other stride — a uneven interval, or one starting at or past its
|
|
1450
|
+
// interval (a bounded `a-b/n`) — is a bounded set the cadence pins the ends of.
|
|
1451
|
+
function offsetCleanStride(
|
|
1452
|
+
stride: {start: number; interval: number}
|
|
1453
|
+
): boolean {
|
|
1454
|
+
return stride.start < stride.interval && 24 % stride.interval === 0;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// The bounded cadence for an hour stride that pins both clock-time endpoints,
|
|
1458
|
+
// or null when the hour is not such a stride. The core rewrites a uneven step
|
|
1459
|
+
// to its fire list, so a minute window/list/step crossed with it lands in the
|
|
1460
|
+
// enumerating list paths; there the bounded hour reads better as its cadence
|
|
1461
|
+
// ("…, cada cinco horas de las 00:00 a las 20:00") than as a wall of clock
|
|
1462
|
+
// times. An offset-clean stride keeps its existing confinement form, so only
|
|
1463
|
+
// the endpoint-bearing case routes here.
|
|
1464
|
+
function unevenHourCadence(ir: IR, opts: Opts): string | null {
|
|
1465
|
+
const stride = hourStride(ir);
|
|
1466
|
+
|
|
1467
|
+
if (!stride || offsetCleanStride(stride)) {
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
return hourStrideCadence(stride, opts);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// An hour list's arithmetic progression, or null when its values are not a
|
|
1475
|
+
// step the renderer should speak as a cadence. The core rewrites a uneven hour
|
|
1476
|
+
// step (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its
|
|
1477
|
+
// literal fire list, indistinguishable in the IR from a hand-written list; the
|
|
1478
|
+
// renderer recovers the cadence from the values. A progression starting at zero
|
|
1479
|
+
// is a `*/n` step however short (0,7,14,21 is `*/7`); a non-zero progression is
|
|
1480
|
+
// only a step when it is too long to be a deliberate clock-time list (e.g. 9,17
|
|
1481
|
+
// is two named times, not a cadence). Interval one is a plain range, never a
|
|
1482
|
+
// step.
|
|
1483
|
+
function hourListStride(
|
|
1484
|
+
values: number[]
|
|
1485
|
+
): {start: number; interval: number; last: number} | null {
|
|
1486
|
+
if (values.length < 2) {
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const interval = values[1] - values[0];
|
|
1491
|
+
|
|
1492
|
+
if (interval < 2) {
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
for (let i = 2; i < values.length; i += 1) {
|
|
1497
|
+
if (values[i] - values[i - 1] !== interval) {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (values[0] !== 0 && values.length < 5) {
|
|
1503
|
+
return null;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
return {interval, last: values[values.length - 1], start: values[0]};
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1510
|
+
// segment yields its {start, interval, last} directly; an all-single hour
|
|
1511
|
+
// list yields one only when its values form a step progression (so an irregular
|
|
1512
|
+
// list like 9,17 keeps enumerating). The IR is unchanged — the renderer
|
|
1513
|
+
// recognizes the stride and speaks it as a cadence instead of the clock-time
|
|
1514
|
+
// cross-product.
|
|
1515
|
+
function hourStride(
|
|
1516
|
+
ir: IR
|
|
1517
|
+
): {start: number; interval: number; last: number} | null {
|
|
1518
|
+
const segments = fieldSegments(ir, 'hour');
|
|
1519
|
+
|
|
1520
|
+
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
1521
|
+
const segment = segments[0];
|
|
1522
|
+
|
|
1523
|
+
// A bounded step that fires only once (e.g. `9-10/5` -> just 9) is a single
|
|
1524
|
+
// value, not a stride: it has no interval to speak and no endpoint to pin.
|
|
1525
|
+
if (segment.fires.length < 2) {
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const start = segment.startToken === '*' ?
|
|
1530
|
+
0 :
|
|
1531
|
+
+segment.startToken.split('-')[0];
|
|
1532
|
+
|
|
1533
|
+
return {interval: segment.interval, last: segment.fires[
|
|
1534
|
+
segment.fires.length - 1], start};
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const values = singleValues(segments);
|
|
1538
|
+
|
|
1539
|
+
return values && hourListStride(values);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
1543
|
+
// fills the minute (a "durante un minuto" frame at minute 0); a single 0 is
|
|
1544
|
+
// just the top of the minute (no clause); anything else needs its own clause.
|
|
1545
|
+
function subMinuteSecond(ir: IR): boolean {
|
|
1546
|
+
return ir.pattern.second === '*' || ir.shapes.second === 'step';
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// The lead clause for an hour-cadence rendering: the second and the pinned
|
|
1550
|
+
// minute, before the hour cadence. A pinned minute 0 folds in — a single,
|
|
1551
|
+
// list, or range second is counted "de cada hora" (the minute-0 is the top of
|
|
1552
|
+
// the hour), and a wildcard or sub-minute step second takes a "durante un
|
|
1553
|
+
// minuto" frame (the whole minute-0 window). A non-zero minute is a real clock
|
|
1554
|
+
// minute: the second leads with its own clause (if any), then the minute reads
|
|
1555
|
+
// "en el minuto M".
|
|
1556
|
+
function hourCadenceLead(ir: IR, minute: number, opts: Opts): string {
|
|
1557
|
+
if (minute === 0) {
|
|
1558
|
+
if (subMinuteSecond(ir)) {
|
|
1559
|
+
return secondsClause(ir, 'minuto', opts) + ' durante un minuto';
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
return secondsClause(ir, 'hora', opts);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const minutePhrase = 'en el minuto ' + minute;
|
|
1566
|
+
|
|
1567
|
+
// A single 0 second is just the top of the minute, so the minute leads
|
|
1568
|
+
// alone; any other second prefixes its own clause.
|
|
1569
|
+
if (ir.pattern.second === '0') {
|
|
1570
|
+
return minutePhrase;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
return secondsClause(ir, 'minuto', opts) + ', ' + minutePhrase;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
1577
|
+
// pinned minute and a second as a cadence — the lead clause, then the hour
|
|
1578
|
+
// cadence — instead of cross-multiplying the hours into a wall of clock times.
|
|
1579
|
+
// Returns null when the hour is not a stride (an irregular list, a single
|
|
1580
|
+
// hour, or a range), or when the cross-product is short enough that
|
|
1581
|
+
// enumeration is no longer than the cadence: a meaningful second makes every
|
|
1582
|
+
// clock time three digit-groups, so any stride is worth compacting; otherwise
|
|
1583
|
+
// the stride must exceed the clock-time cap, the same point at which the core
|
|
1584
|
+
// itself stops enumerating. Renderer-only; the IR is unchanged.
|
|
1585
|
+
function hourCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
1586
|
+
const stride = hourStride(ir);
|
|
1587
|
+
|
|
1588
|
+
if (!stride) {
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const fires = (stride.last - stride.start) / stride.interval + 1;
|
|
1593
|
+
|
|
1594
|
+
// A short stride that spells out as few clock times stays an enumeration only
|
|
1595
|
+
// when it wraps cleanly (an offset-clean stride with no endpoint): the bare
|
|
1596
|
+
// or "a partir de" form is no shorter than the list. A bounded or uneven
|
|
1597
|
+
// stride has no clean wrap, so its endpoint-pinning cadence ("cada cinco
|
|
1598
|
+
// horas de las 00:00 a las 20:00") reads better however short.
|
|
1599
|
+
if (ir.pattern.second === '0' && fires <= maxClockTimes &&
|
|
1600
|
+
offsetCleanStride(stride)) {
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// A wildcard or sub-minute step second confined to minute 0 of a clean hour
|
|
1605
|
+
// stride is a confinement, not a juxtaposed cadence: it reads "durante un
|
|
1606
|
+
// minuto, durante las horas pares", reusing the hour-step confinement idiom
|
|
1607
|
+
// so the minute-0 window is never heard as the bare hour cadence.
|
|
1608
|
+
const confinement = minute === 0 && subMinuteSecond(ir) &&
|
|
1609
|
+
cleanStrideSegment(ir);
|
|
1610
|
+
|
|
1611
|
+
if (confinement) {
|
|
1612
|
+
return secondsClause(ir, 'minuto', opts) + ' durante un minuto, ' +
|
|
1613
|
+
stepHourSpan(confinement, opts) + trailingQualifier(ir, opts);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
1617
|
+
// lead clause to fold in, so the bounded cadence stands on its own ("cada
|
|
1618
|
+
// cinco horas de las 00:00 a las 20:00").
|
|
1619
|
+
if (minute === 0 && ir.pattern.second === '0') {
|
|
1620
|
+
return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
return hourCadenceLead(ir, minute, opts) + ', ' +
|
|
1624
|
+
hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// The hour step segment when the hour is a clean stride es renders as a
|
|
1628
|
+
// confinement phrase ("durante las horas pares"); null otherwise (an offset or
|
|
1629
|
+
// bounded step, an uneven stride, or an arithmetic-progression list, which
|
|
1630
|
+
// keep the bounded cadence form).
|
|
1631
|
+
function cleanStrideSegment(ir: IR): StepSegment | null {
|
|
1632
|
+
const segments = fieldSegments(ir, 'hour');
|
|
1633
|
+
const segment = segments.length === 1 && segments[0];
|
|
1634
|
+
|
|
1635
|
+
if (!segment || segment.kind !== 'step' ||
|
|
1636
|
+
segment.startToken.indexOf('-') !== -1) {
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
return segment;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// Whether the hour field is a range — or a list whose segments include a
|
|
1644
|
+
// range — and so forms a window rather than a cross-product of clock times.
|
|
1645
|
+
// A pure single-value list (9,17) has no range to span and still enumerates;
|
|
1646
|
+
// a step is handled by hourStride/hourCadence.
|
|
1647
|
+
function hasHourWindow(ir: IR): boolean {
|
|
1648
|
+
return hourSegments(ir).some(function range(segment) {
|
|
1649
|
+
return segment.kind === 'range';
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Render an hour range (or a list whose segments include a range) under
|
|
1654
|
+
// minute 0 and a meaningful second as the hour-range window — the lead clause,
|
|
1655
|
+
// then "de las 09:00 a las 17:00" (and any non-contiguous hour joined with
|
|
1656
|
+
// "y también") — instead of cross-multiplying the hours into a wall of clock
|
|
1657
|
+
// times. The hour-RANGE analog of hourCadence. Returns null when the hour has
|
|
1658
|
+
// no range, when the minute is non-zero (a real clock minute the existing
|
|
1659
|
+
// window form already speaks), or when a plain :00 set carries no clause.
|
|
1660
|
+
// Renderer-only; the IR is unchanged.
|
|
1661
|
+
function hourRangeCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
1662
|
+
if (minute !== 0 || !hasHourWindow(ir) || ir.pattern.second === '0') {
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// A wildcard or sub-minute step second confined to minute 0 is the whole
|
|
1667
|
+
// minute-0 window ("durante un minuto"), confined to the hour range with the
|
|
1668
|
+
// "durante las horas …" idiom — kept distinct from the bare minute-0 window
|
|
1669
|
+
// ("cada hora de las 09:00 a las 17:00") so the confinement is never heard
|
|
1670
|
+
// as it — the hour-range analog of "durante un minuto, durante las horas
|
|
1671
|
+
// pares".
|
|
1672
|
+
if (subMinuteSecond(ir)) {
|
|
1673
|
+
return secondsClause(ir, 'minuto', opts) + ' durante un minuto, ' +
|
|
1674
|
+
'durante las horas ' + hourSegmentTimes(ir, 0, null, opts) +
|
|
1675
|
+
trailingQualifier(ir, opts);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
return hourCadenceLead(ir, minute, opts) + ', ' +
|
|
1679
|
+
hourSegmentTimes(ir, 0, null, opts) + trailingQualifier(ir, opts);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1158
1682
|
// --- Hour-time phrasing. ---
|
|
1159
1683
|
|
|
1160
1684
|
// "a las 9:00" / "a la 1:00" / "al mediodía" for each fire hour.
|