cronli5 0.1.5 → 0.1.7
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 +71 -0
- package/README.md +2 -2
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +156 -27
- package/dist/cronli5.js +156 -27
- package/dist/lang/de.cjs +157 -36
- package/dist/lang/de.js +157 -36
- package/dist/lang/en.cjs +156 -27
- package/dist/lang/en.js +156 -27
- package/dist/lang/es.cjs +156 -18
- package/dist/lang/es.js +156 -18
- package/dist/lang/fi.cjs +128 -20
- package/dist/lang/fi.js +128 -20
- package/dist/lang/zh.cjs +126 -58
- package/dist/lang/zh.js +126 -58
- package/package.json +2 -2
- package/src/core/util.ts +52 -1
- package/src/lang/de/index.ts +331 -74
- package/src/lang/en/index.ts +327 -62
- package/src/lang/es/index.ts +306 -39
- package/src/lang/fi/index.ts +251 -41
- package/src/lang/zh/index.ts +246 -105
- package/types/core/util.d.ts +10 -1
package/src/lang/es/index.ts
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import {clockDigits, numeral} from '../../core/format.js';
|
|
12
12
|
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
arithmeticStep, orderWeekdaysForDisplay, toFieldNumber
|
|
15
|
+
} from '../../core/util.js';
|
|
14
16
|
import type {Cronli5Options} from '../../types.js';
|
|
15
17
|
import type {
|
|
16
18
|
Field, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode,
|
|
@@ -259,9 +261,24 @@ function composeHourCadence(
|
|
|
259
261
|
const clockRest = plan.rest.kind === 'clockTimes' ||
|
|
260
262
|
plan.rest.kind === 'compactClockTimes';
|
|
261
263
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
if (!clockRest || ir.shapes.minute !== 'single') {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const minute = +ir.pattern.minute;
|
|
269
|
+
|
|
270
|
+
return hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// A wildcard or stepped second with a fixed minute across one or more specific
|
|
274
|
+
// hours: the seconds confine to the clock time(s), each minute named.
|
|
275
|
+
function isPinnedMinuteSeconds(
|
|
276
|
+
ir: IR,
|
|
277
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
278
|
+
): plan is Extract<PlanNode, {kind: 'composeSeconds'}> &
|
|
279
|
+
{rest: Extract<PlanNode, {kind: 'clockTimes'}>} {
|
|
280
|
+
return plan.rest.kind === 'clockTimes' &&
|
|
281
|
+
(ir.shapes.second === 'wildcard' || ir.shapes.second === 'step');
|
|
265
282
|
}
|
|
266
283
|
|
|
267
284
|
function renderComposeSeconds(
|
|
@@ -281,8 +298,7 @@ function renderComposeSeconds(
|
|
|
281
298
|
|
|
282
299
|
// A wildcard or stepped second with the minute pinned to a single value
|
|
283
300
|
// across one or more specific hours: the seconds confine to the clock time.
|
|
284
|
-
if (plan
|
|
285
|
-
(ir.shapes.second === 'wildcard' || ir.shapes.second === 'step')) {
|
|
301
|
+
if (isPinnedMinuteSeconds(ir, plan)) {
|
|
286
302
|
return pinnedMinuteSeconds(ir, plan.rest, opts);
|
|
287
303
|
}
|
|
288
304
|
|
|
@@ -299,7 +315,7 @@ function renderComposeSeconds(
|
|
|
299
315
|
if (plan.rest.kind === 'hourRange' && ir.shapes.second === 'step' &&
|
|
300
316
|
ir.pattern.weekday !== '*') {
|
|
301
317
|
const restNode = plan.rest;
|
|
302
|
-
const window = hourWindow(restNode, opts);
|
|
318
|
+
const window = hourWindow(boundedWindow(restNode), opts);
|
|
303
319
|
const dayFrame = weekdayQualifier(ir) + monthScope(ir);
|
|
304
320
|
const cadence = 'cada ' +
|
|
305
321
|
numero(stepSegment(ir.analyses.segments.second).interval, opts) +
|
|
@@ -317,7 +333,15 @@ function renderComposeSeconds(
|
|
|
317
333
|
return secondsLeadClause(ir, opts) + ' de ' + render(ir, plan.rest, opts);
|
|
318
334
|
}
|
|
319
335
|
|
|
320
|
-
|
|
336
|
+
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
337
|
+
// leading clause, so the composer must not prepend a second lead that would
|
|
338
|
+
// double it. A wildcard or stepped second is not folded there (no
|
|
339
|
+
// clockSecond), so it still leads its own clause here.
|
|
340
|
+
const restOwnsLead = plan.rest.kind === 'compactClockTimes' &&
|
|
341
|
+
ir.analyses.clockSecond;
|
|
342
|
+
const lead = restOwnsLead ? '' : secondsLeadClause(ir, opts) + ', ';
|
|
343
|
+
|
|
344
|
+
return lead + render(ir, plan.rest, opts);
|
|
321
345
|
}
|
|
322
346
|
|
|
323
347
|
// A wildcard second over an unoffset minute */2 with a wildcard hour: the two
|
|
@@ -354,7 +378,12 @@ function pinnedMinuteSeconds(
|
|
|
354
378
|
const dayTrail = leadingQualifier(ir, opts).trimEnd();
|
|
355
379
|
const trail = dayTrail ? ', ' + dayTrail : '';
|
|
356
380
|
|
|
357
|
-
|
|
381
|
+
// The "durante un minuto a las 9" duration form drops the clock minute, so it
|
|
382
|
+
// is correct only when the minute is a SINGLE 0 — every clock time at :00. A
|
|
383
|
+
// minute LIST whose first value is 0 (e.g. */45 → :00, :45) must name each
|
|
384
|
+
// minute, never collapse to the bare hour (which once repeated it, "a las 9 y
|
|
385
|
+
// 9"), so it takes the explicit clock list.
|
|
386
|
+
if (+rest.times[0].minute === 0 && ir.shapes.minute === 'single') {
|
|
358
387
|
return secondsLeadClause(ir, opts) + ' durante un minuto ' +
|
|
359
388
|
durationHourList(rest.times, opts) + trail;
|
|
360
389
|
}
|
|
@@ -555,11 +584,20 @@ function renderMinuteFrequency(
|
|
|
555
584
|
'hora', opts);
|
|
556
585
|
|
|
557
586
|
if (plan.hours.kind === 'during') {
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
587
|
+
// A uneven hour stride confines the minute cadence to its own bounded hour
|
|
588
|
+
// cadence ("cada 15 minutos, cada cinco horas de las 00:00 a las 20:00").
|
|
589
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
590
|
+
|
|
591
|
+
if (cadence) {
|
|
592
|
+
phrase += ', ' + cadence;
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
// An offset step (e.g. 1/2) arrives here; a single step reads as a
|
|
596
|
+
// confinement, not the verbose window list.
|
|
597
|
+
phrase += singleHourStep(ir.analyses.segments.hour) ?
|
|
598
|
+
', ' + stepHourSpan(stepSegment(ir.analyses.segments.hour), opts) :
|
|
599
|
+
' ' + hourSpanFromTimes(ir, plan.hours.times, opts);
|
|
600
|
+
}
|
|
563
601
|
}
|
|
564
602
|
else if (plan.hours.kind === 'window') {
|
|
565
603
|
phrase += ' ' + hourWindow(plan.hours, opts);
|
|
@@ -602,7 +640,15 @@ function renderMinutesAcrossHours(
|
|
|
602
640
|
plan: Extract<PlanNode, {kind: 'minutesAcrossHours'}>,
|
|
603
641
|
opts: Opts
|
|
604
642
|
): string {
|
|
643
|
+
// A uneven hour stride reads as a cadence, not a wall of hour columns: the
|
|
644
|
+
// minute lead, then "cada N horas de las X a las Y".
|
|
645
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
646
|
+
|
|
605
647
|
if (plan.form === 'wildcard') {
|
|
648
|
+
if (cadence !== null) {
|
|
649
|
+
return 'cada minuto, ' + cadence + trailingQualifier(ir, opts);
|
|
650
|
+
}
|
|
651
|
+
|
|
606
652
|
if (singleHourStep(ir.analyses.segments.hour)) {
|
|
607
653
|
return 'cada minuto, ' +
|
|
608
654
|
stepHourSpan(stepSegment(ir.analyses.segments.hour), opts) +
|
|
@@ -617,6 +663,10 @@ function renderMinutesAcrossHours(
|
|
|
617
663
|
minuteRangeLead(ir.pattern.minute) :
|
|
618
664
|
minutesList(ir, opts);
|
|
619
665
|
|
|
666
|
+
if (cadence !== null) {
|
|
667
|
+
return lead + ', ' + cadence + trailingQualifier(ir, opts);
|
|
668
|
+
}
|
|
669
|
+
|
|
620
670
|
return lead + ', ' + atHourTimes(ir, plan.times, opts) +
|
|
621
671
|
trailingQualifier(ir, opts);
|
|
622
672
|
}
|
|
@@ -627,9 +677,12 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
627
677
|
opts: Opts
|
|
628
678
|
): string {
|
|
629
679
|
const segment = stepSegment(ir.analyses.segments.hour);
|
|
680
|
+
// A bounded or uneven hour step reads as its endpoint-pinning cadence; an
|
|
681
|
+
// offset-clean step keeps its confinement / per-step phrasing.
|
|
682
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
630
683
|
|
|
631
|
-
// A wildcard minute (a cadence) is reached only for a clean stride
|
|
632
|
-
//
|
|
684
|
+
// A wildcard minute (a cadence) is reached only for a clean stride (a bounded
|
|
685
|
+
// or uneven step routes through minutesAcrossHours instead) and is confined.
|
|
633
686
|
if (plan.form === 'wildcard') {
|
|
634
687
|
return 'cada minuto, ' + stepHourSpan(segment, opts) +
|
|
635
688
|
trailingQualifier(ir, opts);
|
|
@@ -642,7 +695,8 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
642
695
|
minutesList(ir, opts) :
|
|
643
696
|
minuteRangeLead(ir.pattern.minute);
|
|
644
697
|
|
|
645
|
-
return lead + ', ' +
|
|
698
|
+
return lead + ', ' +
|
|
699
|
+
(cadence ?? stepHours(segment, opts)) + trailingQualifier(ir, opts);
|
|
646
700
|
}
|
|
647
701
|
|
|
648
702
|
// --- Hour renderers. ---
|
|
@@ -689,17 +743,31 @@ function renderHourStep(
|
|
|
689
743
|
plan: Extract<PlanNode, {kind: 'hourStep'}>,
|
|
690
744
|
opts: Opts
|
|
691
745
|
): string {
|
|
746
|
+
// A bounded or uneven hour step reads as its endpoint-pinning cadence ("cada
|
|
747
|
+
// dos horas de las 09:00 a las 17:00"); an offset-clean step keeps its bare
|
|
748
|
+
// or "a partir de" cadence.
|
|
749
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
750
|
+
|
|
751
|
+
if (cadence !== null) {
|
|
752
|
+
return cadence + trailingQualifier(ir, opts);
|
|
753
|
+
}
|
|
754
|
+
|
|
692
755
|
return stepHours(stepSegment(ir.analyses.segments.hour), opts) +
|
|
693
756
|
trailingQualifier(ir, opts);
|
|
694
757
|
}
|
|
695
758
|
|
|
696
|
-
// The hour-range plan as a window
|
|
697
|
-
//
|
|
698
|
-
//
|
|
759
|
+
// The hour-range plan as a window. The close lands on the top of the final
|
|
760
|
+
// hour (minute 0) unless the minute genuinely runs to the end of that hour —
|
|
761
|
+
// i.e. a wildcard minute, which fills every minute and states no separate
|
|
762
|
+
// clause. A pinned/listed/ranged minute is named in its own lead clause, so
|
|
763
|
+
// folding it into the close too would read as a span ("a las 17:05") that
|
|
764
|
+
// contradicts the minute clause; the window stays bare ("a las 17:00").
|
|
699
765
|
function boundedWindow(
|
|
700
766
|
plan: Extract<PlanNode, {kind: 'hourRange'}>
|
|
701
767
|
): {from: number; to: number; last: number} {
|
|
702
|
-
|
|
768
|
+
const last = plan.minuteForm === 'wildcard' ? plan.boundMinute ?? 0 : 0;
|
|
769
|
+
|
|
770
|
+
return {from: plan.from, last, to: plan.to};
|
|
703
771
|
}
|
|
704
772
|
|
|
705
773
|
// "de las 9:00 a las 17:45": a window from the top of the first hour to
|
|
@@ -778,7 +846,9 @@ function dowArm(ir: IR): string {
|
|
|
778
846
|
return quartz;
|
|
779
847
|
}
|
|
780
848
|
|
|
781
|
-
|
|
849
|
+
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
850
|
+
// form. The IR stays canonical (Sunday=0). The helper flattens steps.
|
|
851
|
+
const segments = orderWeekdaysForDisplay(fieldSegments(ir, 'weekday'));
|
|
782
852
|
const allSingles = segments.every(function single(segment) {
|
|
783
853
|
return segment.kind === 'single';
|
|
784
854
|
});
|
|
@@ -817,10 +887,13 @@ function renderClockTimes(
|
|
|
817
887
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
818
888
|
opts: Opts
|
|
819
889
|
): string {
|
|
820
|
-
// An hour step (or arithmetic-progression hour list) under a single
|
|
821
|
-
// minute reads as a cadence rather than a cross-product of
|
|
890
|
+
// An hour step or range (or arithmetic-progression hour list) under a single
|
|
891
|
+
// pinned minute reads as a cadence or window rather than a cross-product of
|
|
892
|
+
// clock times.
|
|
822
893
|
if (ir.shapes.minute === 'single') {
|
|
823
|
-
const
|
|
894
|
+
const minute = +ir.pattern.minute;
|
|
895
|
+
const cadence = hourCadence(ir, minute, opts) ??
|
|
896
|
+
hourRangeCadence(ir, minute, opts);
|
|
824
897
|
|
|
825
898
|
if (cadence !== null) {
|
|
826
899
|
return cadence;
|
|
@@ -1162,10 +1235,11 @@ function renderCompactClockTimes(
|
|
|
1162
1235
|
opts: Opts
|
|
1163
1236
|
): string {
|
|
1164
1237
|
if (plan.fold) {
|
|
1165
|
-
// An hour step (or arithmetic-progression hour list) under the
|
|
1166
|
-
// pinned minute reads as a cadence, not a wall of clock
|
|
1167
|
-
// null for an irregular list
|
|
1168
|
-
const cadence = hourCadence(ir, plan.minute, opts)
|
|
1238
|
+
// An hour step or range (or arithmetic-progression hour list) under the
|
|
1239
|
+
// single pinned minute reads as a cadence or window, not a wall of clock
|
|
1240
|
+
// times. (Returns null for an irregular list, which keeps folding below.)
|
|
1241
|
+
const cadence = hourCadence(ir, plan.minute, opts) ??
|
|
1242
|
+
hourRangeCadence(ir, plan.minute, opts);
|
|
1169
1243
|
|
|
1170
1244
|
if (cadence !== null) {
|
|
1171
1245
|
return cadence;
|
|
@@ -1187,8 +1261,13 @@ function renderCompactClockTimes(
|
|
|
1187
1261
|
hourSegmentTimes(ir, plan.minute, ir.analyses.clockSecond, opts);
|
|
1188
1262
|
}
|
|
1189
1263
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1264
|
+
// A uneven hour stride reads as a cadence after the minute lead, not a wall
|
|
1265
|
+
// of clock-time columns.
|
|
1266
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
1267
|
+
const phrase = cadence ?
|
|
1268
|
+
minutesList(ir, opts) + ', ' + cadence + trailingQualifier(ir, opts) :
|
|
1269
|
+
minutesList(ir, opts) + ', ' +
|
|
1270
|
+
hourContextTimes(ir, opts) + trailingQualifier(ir, opts);
|
|
1192
1271
|
|
|
1193
1272
|
return ir.analyses.clockSecond ?
|
|
1194
1273
|
secondsLeadClause(ir, opts) + ', ' + phrase :
|
|
@@ -1373,12 +1452,75 @@ function hourStrideCadence(
|
|
|
1373
1452
|
timePhrase(last, 0, null, opts);
|
|
1374
1453
|
}
|
|
1375
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;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// The bounded cadence for an hour stride that pins both clock-time endpoints,
|
|
1467
|
+
// or null when the hour is not such a stride. The core rewrites a uneven step
|
|
1468
|
+
// to its fire list, so a minute window/list/step crossed with it lands in the
|
|
1469
|
+
// enumerating list paths; there the bounded hour reads better as its cadence
|
|
1470
|
+
// ("…, cada cinco horas de las 00:00 a las 20:00") than as a wall of clock
|
|
1471
|
+
// times. An offset-clean stride keeps its existing confinement form, so only
|
|
1472
|
+
// the endpoint-bearing case routes here.
|
|
1473
|
+
function unevenHourCadence(ir: IR, opts: Opts): string | null {
|
|
1474
|
+
const stride = hourStride(ir);
|
|
1475
|
+
|
|
1476
|
+
if (!stride || offsetCleanStride(stride)) {
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
return hourStrideCadence(stride, opts);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
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
|
+
|
|
1376
1518
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1377
1519
|
// segment yields its {start, interval, last} directly; an all-single hour
|
|
1378
|
-
// list yields one only when its values form a
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1381
|
-
//
|
|
1520
|
+
// list yields one only when its values form a step progression (so an irregular
|
|
1521
|
+
// list like 9,17 keeps enumerating). The IR is unchanged — the renderer
|
|
1522
|
+
// recognizes the stride and speaks it as a cadence instead of the clock-time
|
|
1523
|
+
// cross-product.
|
|
1382
1524
|
function hourStride(
|
|
1383
1525
|
ir: IR
|
|
1384
1526
|
): {start: number; interval: number; last: number} | null {
|
|
@@ -1386,6 +1528,13 @@ function hourStride(
|
|
|
1386
1528
|
|
|
1387
1529
|
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
1388
1530
|
const segment = segments[0];
|
|
1531
|
+
|
|
1532
|
+
// A bounded step that fires only once (e.g. `9-10/5` -> just 9) is a single
|
|
1533
|
+
// value, not a stride: it has no interval to speak and no endpoint to pin.
|
|
1534
|
+
if (segment.fires.length < 2) {
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1389
1538
|
const start = segment.startToken === '*' ?
|
|
1390
1539
|
0 :
|
|
1391
1540
|
+segment.startToken.split('-')[0];
|
|
@@ -1395,9 +1544,8 @@ function hourStride(
|
|
|
1395
1544
|
}
|
|
1396
1545
|
|
|
1397
1546
|
const values = singleValues(segments);
|
|
1398
|
-
const step = values && arithmeticStep(values);
|
|
1399
1547
|
|
|
1400
|
-
return
|
|
1548
|
+
return values && hourListStride(values);
|
|
1401
1549
|
}
|
|
1402
1550
|
|
|
1403
1551
|
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
@@ -1452,7 +1600,13 @@ function hourCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
|
1452
1600
|
|
|
1453
1601
|
const fires = (stride.last - stride.start) / stride.interval + 1;
|
|
1454
1602
|
|
|
1455
|
-
|
|
1603
|
+
// A short stride that spells out as few clock times stays an enumeration only
|
|
1604
|
+
// when it wraps cleanly (an offset-clean stride with no endpoint): the bare
|
|
1605
|
+
// or "a partir de" form is no shorter than the list. A bounded or uneven
|
|
1606
|
+
// stride has no clean wrap, so its endpoint-pinning cadence ("cada cinco
|
|
1607
|
+
// horas de las 00:00 a las 20:00") reads better however short.
|
|
1608
|
+
if (ir.pattern.second === '0' && fires <= maxClockTimes &&
|
|
1609
|
+
offsetCleanStride(stride)) {
|
|
1456
1610
|
return null;
|
|
1457
1611
|
}
|
|
1458
1612
|
|
|
@@ -1468,6 +1622,13 @@ function hourCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
|
1468
1622
|
stepHourSpan(confinement, opts) + trailingQualifier(ir, opts);
|
|
1469
1623
|
}
|
|
1470
1624
|
|
|
1625
|
+
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
1626
|
+
// lead clause to fold in, so the bounded cadence stands on its own ("cada
|
|
1627
|
+
// cinco horas de las 00:00 a las 20:00").
|
|
1628
|
+
if (minute === 0 && ir.pattern.second === '0') {
|
|
1629
|
+
return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1471
1632
|
return hourCadenceLead(ir, minute, opts) + ', ' +
|
|
1472
1633
|
hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
|
|
1473
1634
|
}
|
|
@@ -1488,8 +1649,112 @@ function cleanStrideSegment(ir: IR): StepSegment | null {
|
|
|
1488
1649
|
return segment;
|
|
1489
1650
|
}
|
|
1490
1651
|
|
|
1652
|
+
// Whether the hour field is a range — or a list whose segments include a
|
|
1653
|
+
// range — and so forms a window rather than a cross-product of clock times.
|
|
1654
|
+
// A pure single-value list (9,17) has no range to span and still enumerates;
|
|
1655
|
+
// a step is handled by hourStride/hourCadence.
|
|
1656
|
+
function hasHourWindow(ir: IR): boolean {
|
|
1657
|
+
return hourSegments(ir).some(function range(segment) {
|
|
1658
|
+
return segment.kind === 'range';
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Render an hour range (or a list whose segments include a range) under
|
|
1663
|
+
// minute 0 and a meaningful second as the hour-range window — the lead clause,
|
|
1664
|
+
// then "de las 09:00 a las 17:00" (and any non-contiguous hour joined with
|
|
1665
|
+
// "y también") — instead of cross-multiplying the hours into a wall of clock
|
|
1666
|
+
// times. The hour-RANGE analog of hourCadence. Returns null when the hour has
|
|
1667
|
+
// no range, when the minute is non-zero (a real clock minute the existing
|
|
1668
|
+
// window form already speaks), or when a plain :00 set carries no clause.
|
|
1669
|
+
// Renderer-only; the IR is unchanged.
|
|
1670
|
+
function hourRangeCadence(ir: IR, minute: number, opts: Opts): string | null {
|
|
1671
|
+
if (minute !== 0 || !hasHourWindow(ir) || ir.pattern.second === '0') {
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// A wildcard or sub-minute step second confined to minute 0 is the whole
|
|
1676
|
+
// minute-0 window ("durante un minuto"), confined to the hour range with the
|
|
1677
|
+
// "durante las horas …" idiom — kept distinct from the bare minute-0 window
|
|
1678
|
+
// ("cada hora de las 09:00 a las 17:00") so the confinement is never heard
|
|
1679
|
+
// as it — the hour-range analog of "durante un minuto, durante las horas
|
|
1680
|
+
// pares".
|
|
1681
|
+
if (subMinuteSecond(ir)) {
|
|
1682
|
+
return secondsClause(ir, 'minuto', opts) + ' durante un minuto, ' +
|
|
1683
|
+
'durante las horas ' + hourSegmentTimes(ir, 0, null, opts) +
|
|
1684
|
+
trailingQualifier(ir, opts);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
return hourCadenceLead(ir, minute, opts) + ', ' +
|
|
1688
|
+
hourSegmentTimes(ir, 0, null, opts) + trailingQualifier(ir, opts);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1491
1691
|
// --- Hour-time phrasing. ---
|
|
1492
1692
|
|
|
1693
|
+
// The fixed hour(s) of a stepped/listed minute, named as the HOUR rather than a
|
|
1694
|
+
// "a las HH:00" clock instant the minute never fires at: noon and midnight read
|
|
1695
|
+
// as the hour word ("al mediodía"/"a medianoche"), any other hour as the whole
|
|
1696
|
+
// hour "de la hora de las HH:00" (the idiom a wildcard minute already uses).
|
|
1697
|
+
// Used by the compact-clock non-fold path, where the minute is a step or list
|
|
1698
|
+
// (a single-value minute keeps its real "a las HH:MM" clock time elsewhere).
|
|
1699
|
+
function hourContextTimes(ir: IR, opts: Opts): string {
|
|
1700
|
+
const segments = hourSegments(ir);
|
|
1701
|
+
|
|
1702
|
+
// Collect the point hours (singles and step fires) — a range stays a window.
|
|
1703
|
+
const points: number[] = [];
|
|
1704
|
+
const hasRange = segments.some(function range(segment) {
|
|
1705
|
+
return segment.kind === 'range';
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
segments.forEach(function collect(segment) {
|
|
1709
|
+
if (segment.kind === 'step') {
|
|
1710
|
+
points.push(...segment.fires);
|
|
1711
|
+
}
|
|
1712
|
+
else if (segment.kind === 'single') {
|
|
1713
|
+
points.push(+segment.value);
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
// All point hours, all noon/midnight: stand alone as their own words ("a
|
|
1718
|
+
// medianoche y al mediodía").
|
|
1719
|
+
function isWord(hour: number): boolean {
|
|
1720
|
+
return !opts.ampm && (hour === 0 || hour === 12);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
if (!hasRange && points.every(isWord)) {
|
|
1724
|
+
return joinList(points.map(function each(hour) {
|
|
1725
|
+
return atTime(bareHourPhrase(hour, opts));
|
|
1726
|
+
}));
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// A point hour as the whole hour: "de la hora de las HH:00".
|
|
1730
|
+
function wholeHour(hour: number): string {
|
|
1731
|
+
return 'de la hora ' + fromTime(explicitTimePhrase(hour, 0, opts));
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// Otherwise each whole hour reads as a window ("de las HH:00 a las HH:00" for
|
|
1735
|
+
// a range, "de la hora de las HH:00" for a point), never a false "a las
|
|
1736
|
+
// HH:00" clock instant the stepped minute never fires at.
|
|
1737
|
+
const pieces: string[] = [];
|
|
1738
|
+
|
|
1739
|
+
segments.forEach(function place(segment) {
|
|
1740
|
+
if (segment.kind === 'range') {
|
|
1741
|
+
pieces.push(timeRange(
|
|
1742
|
+
{hour: +segment.bounds[0], minute: 0},
|
|
1743
|
+
{hour: +segment.bounds[1], minute: 0}, opts));
|
|
1744
|
+
}
|
|
1745
|
+
else if (segment.kind === 'step') {
|
|
1746
|
+
segment.fires.forEach(function each(hour) {
|
|
1747
|
+
pieces.push(wholeHour(hour));
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
pieces.push(wholeHour(+segment.value));
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
return joinList(pieces);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1493
1758
|
// "a las 9:00" / "a la 1:00" / "al mediodía" for each fire hour.
|
|
1494
1759
|
function atTimes(hours: number[], opts: Opts): string[] {
|
|
1495
1760
|
return hours.map(function each(hour) {
|
|
@@ -1940,7 +2205,9 @@ function weekdayQualifier(ir: IR): string {
|
|
|
1940
2205
|
return quartz;
|
|
1941
2206
|
}
|
|
1942
2207
|
|
|
1943
|
-
|
|
2208
|
+
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
2209
|
+
// form. The IR stays canonical (Sunday=0). The helper flattens steps.
|
|
2210
|
+
const segments = orderWeekdaysForDisplay(fieldSegments(ir, 'weekday'));
|
|
1944
2211
|
const allSingles = segments.every(function single(segment) {
|
|
1945
2212
|
return segment.kind === 'single';
|
|
1946
2213
|
});
|