cronli5 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +4 -4
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +123 -104
- package/dist/cronli5.js +123 -104
- package/dist/lang/de.cjs +65 -65
- package/dist/lang/de.js +65 -65
- package/dist/lang/en.cjs +122 -101
- package/dist/lang/en.js +122 -101
- package/dist/lang/es.cjs +71 -72
- package/dist/lang/es.js +71 -72
- package/dist/lang/fi.cjs +71 -66
- package/dist/lang/fi.js +71 -66
- package/dist/lang/zh.cjs +36 -36
- package/dist/lang/zh.js +36 -36
- package/package.json +1 -1
- package/src/core/analyze.ts +14 -13
- package/src/core/ir.ts +8 -8
- package/src/core/shapes.ts +8 -1
- package/src/core/util.ts +86 -3
- package/src/core/validate.ts +1 -1
- package/src/cronli5.ts +3 -3
- package/src/lang/de/index.ts +30 -99
- package/src/lang/en/index.ts +163 -188
- package/src/lang/es/index.ts +36 -120
- package/src/lang/fi/index.ts +33 -104
- package/src/lang/zh/index.ts +23 -48
- package/src/types.ts +2 -2
- package/types/core/analyze.d.ts +2 -2
- package/types/core/ir.d.ts +7 -7
- package/types/core/shapes.d.ts +2 -1
- package/types/core/util.d.ts +17 -2
- package/types/types.d.ts +1 -1
package/src/lang/fi/index.ts
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import {clockDigits, numeral} from '../../core/format.js';
|
|
13
13
|
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
14
|
+
import {isOpenStep} from '../../core/shapes.js';
|
|
14
15
|
import {
|
|
15
|
-
arithmeticStep, orderWeekdaysForDisplay,
|
|
16
|
+
arithmeticStep, hourListStride, offsetCleanStride, orderWeekdaysForDisplay,
|
|
17
|
+
segmentsOf, singleValues, stepSegment, toFieldNumber
|
|
16
18
|
} from '../../core/util.js';
|
|
17
19
|
import {resolveDialect} from './dialects.js';
|
|
18
20
|
import type {
|
|
@@ -39,12 +41,6 @@ interface HourWindow {
|
|
|
39
41
|
last: number;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
// The first segment of a step field, narrowed to its step variant. Step
|
|
43
|
-
// shapes always classify their (single) segment as a step.
|
|
44
|
-
function stepSegment(segments: Segment[]): StepSegment {
|
|
45
|
-
return segments[0] as StepSegment;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
44
|
// A `{hour, minute, second?}` time end for the digit/range helpers.
|
|
49
45
|
interface TimeParts {
|
|
50
46
|
hour: number;
|
|
@@ -313,7 +309,7 @@ function composeSecondsOverMinuteStep(
|
|
|
313
309
|
freq: Extract<PlanNode, {kind: 'minuteFrequency'}>,
|
|
314
310
|
opts: NormalizedOptions
|
|
315
311
|
): string {
|
|
316
|
-
const seg = stepSegment(ir
|
|
312
|
+
const seg = stepSegment(ir, 'minute');
|
|
317
313
|
const stepPhrase = stepCycle60(seg, units.minute, opts);
|
|
318
314
|
|
|
319
315
|
if (freq.hours.kind === 'during' && minuteStepIsAnchored(seg)) {
|
|
@@ -336,7 +332,7 @@ function composeSecondsOverMinuteStep(
|
|
|
336
332
|
}
|
|
337
333
|
else if (freq.hours.kind === 'step') {
|
|
338
334
|
hourClause = ' ' +
|
|
339
|
-
everyNthHour(stepSegment(ir
|
|
335
|
+
everyNthHour(stepSegment(ir, 'hour'), opts);
|
|
340
336
|
}
|
|
341
337
|
|
|
342
338
|
return stepPhrase + ', ' + secondsLeadClause(ir, opts) +
|
|
@@ -428,7 +424,7 @@ function isEveryOtherMinuteSeconds(
|
|
|
428
424
|
return false;
|
|
429
425
|
}
|
|
430
426
|
|
|
431
|
-
const seg = stepSegment(ir
|
|
427
|
+
const seg = stepSegment(ir, 'minute');
|
|
432
428
|
|
|
433
429
|
return seg.startToken === '*' && seg.interval === 2;
|
|
434
430
|
}
|
|
@@ -466,7 +462,7 @@ function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
|
466
462
|
|
|
467
463
|
if (shape === 'step') {
|
|
468
464
|
// A step shape always has segments whose first is a step segment.
|
|
469
|
-
return stepCycle60(stepSegment(ir
|
|
465
|
+
return stepCycle60(stepSegment(ir, 'second'),
|
|
470
466
|
units.second, opts);
|
|
471
467
|
}
|
|
472
468
|
|
|
@@ -480,8 +476,8 @@ function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
|
480
476
|
|
|
481
477
|
// An offset/uneven step the core enumerated to this list reads as a stride
|
|
482
478
|
// cadence when the fires form a long-enough progression.
|
|
483
|
-
return strideFromSegments(ir
|
|
484
|
-
atMarks(joinList(segmentWords(ir
|
|
479
|
+
return strideFromSegments(segmentsOf(ir, 'second'), units.second, opts) ??
|
|
480
|
+
atMarks(joinList(segmentWords(segmentsOf(ir, 'second'))),
|
|
485
481
|
units.second, marked);
|
|
486
482
|
}
|
|
487
483
|
|
|
@@ -525,8 +521,8 @@ function renderMultipleMinutes(
|
|
|
525
521
|
// the fires form a long-enough progression ("kahden minuutin välein
|
|
526
522
|
// minuutista 3 minuuttiin 59").
|
|
527
523
|
function minutesList(ir: IR, opts: NormalizedOptions): string {
|
|
528
|
-
return strideFromSegments(ir
|
|
529
|
-
atMarks(joinList(segmentWords(ir
|
|
524
|
+
return strideFromSegments(segmentsOf(ir, 'minute'), units.minute, opts) ??
|
|
525
|
+
atMarks(joinList(segmentWords(segmentsOf(ir, 'minute'))),
|
|
530
526
|
units.minute, true);
|
|
531
527
|
}
|
|
532
528
|
|
|
@@ -535,8 +531,8 @@ function minutesList(ir: IR, opts: NormalizedOptions): string {
|
|
|
535
531
|
// kohdalla". A progression reads as its bounded cadence (which carries no
|
|
536
532
|
// per-hour frequency to drop).
|
|
537
533
|
function bareMinutes(ir: IR, opts: NormalizedOptions): string {
|
|
538
|
-
return strideFromSegments(ir
|
|
539
|
-
atMarks(joinList(segmentWords(ir
|
|
534
|
+
return strideFromSegments(segmentsOf(ir, 'minute'), units.minute, opts) ??
|
|
535
|
+
atMarks(joinList(segmentWords(segmentsOf(ir, 'minute'))),
|
|
540
536
|
units.minute, false);
|
|
541
537
|
}
|
|
542
538
|
|
|
@@ -596,14 +592,14 @@ function hoursFirstMinutes(
|
|
|
596
592
|
// cadence ("aina kahden minuutin välein minuutista 3 minuuttiin 59") when
|
|
597
593
|
// the fires form a long-enough progression, rather than the kohdalla list.
|
|
598
594
|
const stride =
|
|
599
|
-
strideFromSegments(ir
|
|
595
|
+
strideFromSegments(segmentsOf(ir, 'minute'), units.minute, opts);
|
|
600
596
|
|
|
601
597
|
if (stride) {
|
|
602
598
|
return hoursStr + ' aina ' + stride;
|
|
603
599
|
}
|
|
604
600
|
|
|
605
601
|
return hoursStr + ' aina minuuttien ' +
|
|
606
|
-
joinList(segmentWords(ir
|
|
602
|
+
joinList(segmentWords(segmentsOf(ir, 'minute'))) + ' kohdalla';
|
|
607
603
|
}
|
|
608
604
|
|
|
609
605
|
// Hour segment times for a range+isolated pattern: joins the isolated hour
|
|
@@ -617,7 +613,7 @@ function hourSegmentTimesWithSeka(
|
|
|
617
613
|
): string {
|
|
618
614
|
const pieces: string[] = [];
|
|
619
615
|
|
|
620
|
-
ir.
|
|
616
|
+
segmentsOf(ir, 'hour').forEach(function clock(segment: Segment) {
|
|
621
617
|
if (segment.kind === 'range') {
|
|
622
618
|
pieces.push(rangeDigits(
|
|
623
619
|
{hour: +segment.bounds[0], minute, second},
|
|
@@ -638,7 +634,7 @@ function renderMinuteFrequency(
|
|
|
638
634
|
plan: Extract<PlanNode, {kind: 'minuteFrequency'}>,
|
|
639
635
|
opts: NormalizedOptions
|
|
640
636
|
): string {
|
|
641
|
-
const seg = stepSegment(ir
|
|
637
|
+
const seg = stepSegment(ir, 'minute');
|
|
642
638
|
|
|
643
639
|
if (plan.hours.kind === 'during') {
|
|
644
640
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence
|
|
@@ -673,7 +669,7 @@ function renderMinuteFrequency(
|
|
|
673
669
|
}
|
|
674
670
|
else if (plan.hours.kind === 'step') {
|
|
675
671
|
phrase += ' ' +
|
|
676
|
-
everyNthHour(stepSegment(ir
|
|
672
|
+
everyNthHour(stepSegment(ir, 'hour'), opts);
|
|
677
673
|
}
|
|
678
674
|
|
|
679
675
|
return phrase + trailingQualifier(ir, opts);
|
|
@@ -700,7 +696,7 @@ function renderMinuteSpanInHour(
|
|
|
700
696
|
}
|
|
701
697
|
|
|
702
698
|
// A minute window under discrete hours. Like Spanish, the wildcard form
|
|
703
|
-
// re-
|
|
699
|
+
// re-plans to per-hour windows; restricted minutes drop the
|
|
704
700
|
// "jokaisen tunnin" anchor, which the specific hours would contradict.
|
|
705
701
|
// A range or multi-point list over enumerated hours renders hours-first
|
|
706
702
|
// ("klo <hours> aina minuuttien <spec> kohdalla"); a range+isolated hour
|
|
@@ -728,7 +724,7 @@ function renderMinutesAcrossHours(
|
|
|
728
724
|
}
|
|
729
725
|
|
|
730
726
|
// Range+isolated hours: minute-first, bare minutes, sekä klo.
|
|
731
|
-
if (hoursAreRangeIsolated(ir
|
|
727
|
+
if (hoursAreRangeIsolated(segmentsOf(ir, 'hour'))) {
|
|
732
728
|
return bareMinutes(ir, opts) + ' ' +
|
|
733
729
|
hourSegmentTimesWithSeka(ir, 0, null, opts) +
|
|
734
730
|
trailingQualifier(ir, opts);
|
|
@@ -748,7 +744,7 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
748
744
|
opts: NormalizedOptions
|
|
749
745
|
): string {
|
|
750
746
|
// An hour-step plan's first hour segment is always a step segment.
|
|
751
|
-
const segment = stepSegment(ir
|
|
747
|
+
const segment = stepSegment(ir, 'hour');
|
|
752
748
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence; an
|
|
753
749
|
// offset-clean stride keeps its confinement / per-step phrasing.
|
|
754
750
|
const cadence = unevenHourCadence(ir, opts);
|
|
@@ -884,7 +880,7 @@ function renderHourStep(
|
|
|
884
880
|
return cadence + trailingQualifier(ir, opts);
|
|
885
881
|
}
|
|
886
882
|
|
|
887
|
-
return stepHours(stepSegment(ir
|
|
883
|
+
return stepHours(stepSegment(ir, 'hour'), opts) +
|
|
888
884
|
trailingQualifier(ir, opts);
|
|
889
885
|
}
|
|
890
886
|
|
|
@@ -959,7 +955,7 @@ function renderCompactClockTimes(
|
|
|
959
955
|
}
|
|
960
956
|
}
|
|
961
957
|
|
|
962
|
-
const hourSegs = ir
|
|
958
|
+
const hourSegs = segmentsOf(ir, 'hour');
|
|
963
959
|
|
|
964
960
|
// Range+isolated hours: join the isolated hour with "sekä klo" to stop it
|
|
965
961
|
// reading as a range extension. For the folded path (single minute folded
|
|
@@ -1082,21 +1078,6 @@ function strideFromSegments(
|
|
|
1082
1078
|
null;
|
|
1083
1079
|
}
|
|
1084
1080
|
|
|
1085
|
-
// The sorted numeric values a field's segments cover, or null if any segment
|
|
1086
|
-
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
1087
|
-
function singleValues(segments: Segment[]): number[] | null {
|
|
1088
|
-
const values: number[] = [];
|
|
1089
|
-
|
|
1090
|
-
for (const segment of segments) {
|
|
1091
|
-
if (segment.kind !== 'single') {
|
|
1092
|
-
return null;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
values.push(+segment.value);
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
return values;
|
|
1099
|
-
}
|
|
1100
1081
|
|
|
1101
1082
|
// "viiden minuutin välein", "joka tunti 0 ja 31 minuutin kohdalla", or
|
|
1102
1083
|
// "kolmen minuutin välein jokaisen tunnin minuutista 1 alkaen". A step shape
|
|
@@ -1181,51 +1162,6 @@ function hourStrideCadence(
|
|
|
1181
1162
|
kloRange({hour: start, minute: 0}, {hour: last, minute: 0}, opts);
|
|
1182
1163
|
}
|
|
1183
1164
|
|
|
1184
|
-
// An hour list's arithmetic progression, or null when its values are not a step
|
|
1185
|
-
// the renderer should speak as a cadence. The core rewrites a uneven hour step
|
|
1186
|
-
// (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its literal
|
|
1187
|
-
// fire list, indistinguishable in the IR from a hand-written list; the renderer
|
|
1188
|
-
// recovers the cadence from the values. A progression starting at zero is a
|
|
1189
|
-
// `*/n` step however short (0,7,14,21 is `*/7`); a non-zero progression is only
|
|
1190
|
-
// a step when it is too long to be a deliberate clock-time list (9,17 is two
|
|
1191
|
-
// named times, not a cadence). Interval one is a plain range, never a step.
|
|
1192
|
-
function hourListStride(
|
|
1193
|
-
values: number[]
|
|
1194
|
-
): {start: number; interval: number; last: number} | null {
|
|
1195
|
-
if (values.length < 2) {
|
|
1196
|
-
return null;
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
const interval = values[1] - values[0];
|
|
1200
|
-
|
|
1201
|
-
if (interval < 2) {
|
|
1202
|
-
return null;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
for (let i = 2; i < values.length; i += 1) {
|
|
1206
|
-
if (values[i] - values[i - 1] !== interval) {
|
|
1207
|
-
return null;
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
if (values[0] !== 0 && values.length < 5) {
|
|
1212
|
-
return null;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
return {interval, last: values[values.length - 1], start: values[0]};
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
// Whether an hour stride wraps the day cleanly from within its first interval
|
|
1219
|
-
// (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
|
|
1220
|
-
// stride has no distinct endpoint and keeps its bare or "alkaen" cadence. Every
|
|
1221
|
-
// other stride — a uneven interval, or one starting at or past its interval (a
|
|
1222
|
-
// bounded `a-b/n`) — is a bounded set the cadence pins both endpoints of.
|
|
1223
|
-
function offsetCleanStride(
|
|
1224
|
-
stride: {start: number; interval: number}
|
|
1225
|
-
): boolean {
|
|
1226
|
-
return stride.start < stride.interval && 24 % stride.interval === 0;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
1165
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1230
1166
|
// segment yields its {start, interval, last} directly; an all-single hour list
|
|
1231
1167
|
// yields one only when its values form a step progression (so an irregular list
|
|
@@ -1348,9 +1284,9 @@ function hourCadence(ir: IR, minute: number,
|
|
|
1348
1284
|
// stride is a confinement, not a juxtaposed cadence: it reads "minuutin ajan
|
|
1349
1285
|
// joka toisen tunnin aikana", reusing the every-Nth-hour idiom so the
|
|
1350
1286
|
// minute-0 window is never heard as the bare hour cadence.
|
|
1351
|
-
const segment = ir
|
|
1287
|
+
const segment = segmentsOf(ir, 'hour')[0];
|
|
1352
1288
|
const confined = minute === 0 && subMinuteSecond(ir) &&
|
|
1353
|
-
ir.
|
|
1289
|
+
segmentsOf(ir, 'hour').length === 1 && segment.kind === 'step' &&
|
|
1354
1290
|
cleanHourStride(segment);
|
|
1355
1291
|
|
|
1356
1292
|
if (confined) {
|
|
@@ -1400,7 +1336,7 @@ function hasHourWindow(ir: IR): boolean {
|
|
|
1400
1336
|
// window uses. The minute has folded into the lead, so the window closes on
|
|
1401
1337
|
// the top of its final hour.
|
|
1402
1338
|
function hourRangeWindowTail(ir: IR, opts: NormalizedOptions): string {
|
|
1403
|
-
return ir.
|
|
1339
|
+
return segmentsOf(ir, 'hour').length === 1 ?
|
|
1404
1340
|
hourSegmentTimes(ir, 0, null, opts) :
|
|
1405
1341
|
hourSegmentTimesWithSeka(ir, 0, null, opts);
|
|
1406
1342
|
}
|
|
@@ -1473,7 +1409,7 @@ function hourWindowsFromTimes(
|
|
|
1473
1409
|
return kloList(times.fires, opts);
|
|
1474
1410
|
}
|
|
1475
1411
|
|
|
1476
|
-
const segments = ir
|
|
1412
|
+
const segments = segmentsOf(ir, 'hour');
|
|
1477
1413
|
|
|
1478
1414
|
if (!segments.some(function ranged(segment: Segment) {
|
|
1479
1415
|
return segment.kind === 'range';
|
|
@@ -1534,7 +1470,7 @@ function hourSegmentTimes(
|
|
|
1534
1470
|
): string {
|
|
1535
1471
|
const pieces: string[] = [];
|
|
1536
1472
|
|
|
1537
|
-
ir.
|
|
1473
|
+
segmentsOf(ir, 'hour').forEach(function clock(segment: Segment) {
|
|
1538
1474
|
if (segment.kind === 'step') {
|
|
1539
1475
|
pieces.push(...segment.fires.map(function each(hour: number) {
|
|
1540
1476
|
return timeDigits(hour, minute, second, opts);
|
|
@@ -1707,7 +1643,7 @@ function weekdayQualifier(ir: IR): string {
|
|
|
1707
1643
|
|
|
1708
1644
|
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
1709
1645
|
// form. The IR stays canonical (Sunday=0). The helper flattens steps.
|
|
1710
|
-
const segments = orderWeekdaysForDisplay(ir
|
|
1646
|
+
const segments = orderWeekdaysForDisplay(segmentsOf(ir, 'weekday'));
|
|
1711
1647
|
|
|
1712
1648
|
return joinList(segments.map(function piece(segment: FlatSegment) {
|
|
1713
1649
|
if (segment.kind === 'range') {
|
|
@@ -1723,7 +1659,7 @@ function weekdayQualifier(ir: IR): string {
|
|
|
1723
1659
|
// elative–illative ranges ("kesäkuusta syyskuuhun"). The case endings
|
|
1724
1660
|
// keep mixed lists unambiguous with no preposition bookkeeping.
|
|
1725
1661
|
function monthPhrase(ir: IR): string {
|
|
1726
|
-
const segments = flattenSteps(ir
|
|
1662
|
+
const segments = flattenSteps(segmentsOf(ir, 'month'));
|
|
1727
1663
|
|
|
1728
1664
|
return joinList(segments.map(function piece(segment: FlatSegment) {
|
|
1729
1665
|
if (segment.kind === 'range') {
|
|
@@ -1808,7 +1744,7 @@ function monthAnchor(ir: IR, opts: NormalizedOptions): string {
|
|
|
1808
1744
|
return stepMonths(monthField, opts);
|
|
1809
1745
|
}
|
|
1810
1746
|
|
|
1811
|
-
const segments = flattenSteps(ir
|
|
1747
|
+
const segments = flattenSteps(segmentsOf(ir, 'month'));
|
|
1812
1748
|
|
|
1813
1749
|
return joinList(segments.map(function genitiveOf(segment: FlatSegment) {
|
|
1814
1750
|
// The anchor branch is only reached for non-ranged months, so every
|
|
@@ -1827,7 +1763,7 @@ function rangedMonthScope(ir: IR): string {
|
|
|
1827
1763
|
// Whether the month field contains a range segment.
|
|
1828
1764
|
function monthRanged(ir: IR): boolean {
|
|
1829
1765
|
return ir.pattern.month !== '*' &&
|
|
1830
|
-
ir.
|
|
1766
|
+
segmentsOf(ir, 'month').some(function range(segment: Segment) {
|
|
1831
1767
|
return segment.kind === 'range';
|
|
1832
1768
|
});
|
|
1833
1769
|
}
|
|
@@ -1835,7 +1771,7 @@ function monthRanged(ir: IR): boolean {
|
|
|
1835
1771
|
// The day-of-month words: "13.", "1. ja 15.", "1.–15.", with step
|
|
1836
1772
|
// segments expanded into their fires.
|
|
1837
1773
|
function dateWords(ir: IR): string {
|
|
1838
|
-
return joinList(ir.
|
|
1774
|
+
return joinList(segmentsOf(ir, 'date').flatMap(
|
|
1839
1775
|
function word(segment: Segment): string[] {
|
|
1840
1776
|
if (segment.kind === 'range') {
|
|
1841
1777
|
return [segment.bounds[0] + '.–' + segment.bounds[1] + '.'];
|
|
@@ -2008,13 +1944,6 @@ function segmentWords(segments: Segment[]): string[] {
|
|
|
2008
1944
|
});
|
|
2009
1945
|
}
|
|
2010
1946
|
|
|
2011
|
-
// Whether a canonical field value is an "open" step (`*/n` or `a/n`, not
|
|
2012
|
-
// a bounded range or a list). Open steps read as a frequency rather than
|
|
2013
|
-
// an enumeration.
|
|
2014
|
-
function isOpenStep(field: string): boolean {
|
|
2015
|
-
return field.indexOf('/') !== -1 && field.indexOf('-') === -1 &&
|
|
2016
|
-
field.indexOf(',') === -1;
|
|
2017
|
-
}
|
|
2018
1947
|
|
|
2019
1948
|
// Numeric fire values as digits.
|
|
2020
1949
|
function wordList(fires: number[]): string[] {
|
package/src/lang/zh/index.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
// day periods under `ampm`. The style contract is src/lang/zh/notes.md.
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
arithmeticStep, orderWeekdaysForDisplay,
|
|
8
|
+
arithmeticStep, orderWeekdaysForDisplay, segmentsOf, singleValues,
|
|
9
|
+
stepSegment, toFieldNumber
|
|
9
10
|
} from '../../core/util.js';
|
|
10
11
|
import {maxClockTimes, monthNumbers, weekdayNumbers} from '../../core/specs.js';
|
|
11
12
|
import type {Cronli5Options} from '../../types.js';
|
|
12
13
|
import type {
|
|
13
|
-
|
|
14
|
+
IR, Language, NormalizedOptions, PlanNode, Segment
|
|
14
15
|
} from '../../core/ir.js';
|
|
15
16
|
import {resolveDialect, type ChineseStyle} from './dialects.js';
|
|
16
17
|
|
|
@@ -30,16 +31,6 @@ function joinAnd(items: string[]): string {
|
|
|
30
31
|
return items.slice(0, -1).join('、') + '和' + items[items.length - 1];
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
// A field's classified segments (empty when the field is a wildcard).
|
|
34
|
-
function fieldSegments(ir: IR, field: Field): Segment[] {
|
|
35
|
-
return ir.analyses.segments[field] || [];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// The first segment of a step field, which the plan guarantees is step-kinded.
|
|
39
|
-
function stepSegment(ir: IR, field: Field): StepSegment {
|
|
40
|
-
return fieldSegments(ir, field)[0] as StepSegment;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
34
|
// "每N分钟" / "每分钟" — a cadence over a unit (the numeral 1 is suppressed).
|
|
44
35
|
function cadence(interval: number, unit: string): string {
|
|
45
36
|
return interval === 1 ? '每' + unit : '每' + interval + unit;
|
|
@@ -82,22 +73,6 @@ function renderStride(stride: Stride): string {
|
|
|
82
73
|
return start < interval && tiles ? lead : lead + ',至' + last + mark;
|
|
83
74
|
}
|
|
84
75
|
|
|
85
|
-
// The sorted numeric values a field's segments cover, or null if any segment
|
|
86
|
-
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
87
|
-
function singleValues(segments: Segment[]): number[] | null {
|
|
88
|
-
const values: number[] = [];
|
|
89
|
-
|
|
90
|
-
for (const segment of segments) {
|
|
91
|
-
if (segment.kind !== 'single') {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
values.push(+segment.value);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return values;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
76
|
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
102
77
|
// form an arithmetic progression long enough to beat the list (the core
|
|
103
78
|
// enumerates an offset/uneven step to this fire list; the IR is unchanged, so
|
|
@@ -212,7 +187,7 @@ function hourWord(hour: number): string {
|
|
|
212
187
|
function hourFires(ir: IR): number[] {
|
|
213
188
|
const fires: number[] = [];
|
|
214
189
|
|
|
215
|
-
|
|
190
|
+
segmentsOf(ir, 'hour').forEach(function expand(segment) {
|
|
216
191
|
if (segment.kind === 'step') {
|
|
217
192
|
fires.push(...segment.fires);
|
|
218
193
|
}
|
|
@@ -302,7 +277,7 @@ function renderEveryHour(): string {
|
|
|
302
277
|
// enumerated it to a fire list (an offset/uneven set) — both route through the
|
|
303
278
|
// stride; a short or irregular set keeps the enumerated "每小时…分" list.
|
|
304
279
|
function minuteHourClause(ir: IR): string {
|
|
305
|
-
const segments =
|
|
280
|
+
const segments = segmentsOf(ir, 'minute');
|
|
306
281
|
|
|
307
282
|
if (ir.shapes.minute === 'step') {
|
|
308
283
|
return stepClause(stepSegment(ir, 'minute'), '分钟', '分', '每小时');
|
|
@@ -337,7 +312,7 @@ function hourSegmentWords(segment: Segment): string[] {
|
|
|
337
312
|
// of singles, "9点至20点和22点" for a range plus a single. Each segment renders
|
|
338
313
|
// as the operator the source wrote (range → span), not its expanded fires.
|
|
339
314
|
function hourList(ir: IR): string {
|
|
340
|
-
const words =
|
|
315
|
+
const words = segmentsOf(ir, 'hour').flatMap(hourSegmentWords);
|
|
341
316
|
|
|
342
317
|
return joinAnd(words);
|
|
343
318
|
}
|
|
@@ -346,7 +321,7 @@ function hourList(ir: IR): string {
|
|
|
346
321
|
// 间,", a discrete hour list gives "在H、H…,".
|
|
347
322
|
function hourFrame(ir: IR): string {
|
|
348
323
|
if (ir.shapes.hour === 'range') {
|
|
349
|
-
const [from, to] = (
|
|
324
|
+
const [from, to] = (segmentsOf(ir, 'hour')[0] as
|
|
350
325
|
Extract<Segment, {kind: 'range'}>).bounds;
|
|
351
326
|
|
|
352
327
|
return '在' + hourWord(+from) + '至' + hourWord(+to) + '之间,';
|
|
@@ -490,7 +465,7 @@ function renderCompactClockTimes(ir: IR, plan: PlanNode): string {
|
|
|
490
465
|
}
|
|
491
466
|
|
|
492
467
|
const compact = plan as Extract<PlanNode, {kind: 'compactClockTimes'}>;
|
|
493
|
-
const secs =
|
|
468
|
+
const secs = segmentsOf(ir, 'second');
|
|
494
469
|
const tail = secs.length && ir.pattern.second !== '0' ?
|
|
495
470
|
',第' + valueText(secs) + '秒' : '';
|
|
496
471
|
|
|
@@ -522,7 +497,7 @@ function renderHourRange(ir: IR, plan: PlanNode): string {
|
|
|
522
497
|
const range = plan as Extract<PlanNode, {kind: 'hourRange'}>;
|
|
523
498
|
|
|
524
499
|
if (range.minuteForm === 'lead') {
|
|
525
|
-
const minuteSegs =
|
|
500
|
+
const minuteSegs = segmentsOf(ir, 'minute');
|
|
526
501
|
const past = minuteSegs.length && ir.pattern.minute !== '0' ?
|
|
527
502
|
minuteHourClause(ir) : '每小时';
|
|
528
503
|
|
|
@@ -533,7 +508,7 @@ function renderHourRange(ir: IR, plan: PlanNode): string {
|
|
|
533
508
|
// A minute range is named separately ("每小时0至30分"), not folded into the end.
|
|
534
509
|
if (range.minuteForm === 'range') {
|
|
535
510
|
return '在' + hourWord(range.from) + '至' + hourWord(range.to) +
|
|
536
|
-
'之间,每小时' + valueList(
|
|
511
|
+
'之间,每小时' + valueList(segmentsOf(ir, 'minute'), '分') + ',每分钟';
|
|
537
512
|
}
|
|
538
513
|
|
|
539
514
|
return '在' + hourWord(range.from) + '至' + range.to + '点' +
|
|
@@ -566,7 +541,7 @@ function renderHourStep(ir: IR): string {
|
|
|
566
541
|
function hourStride(
|
|
567
542
|
ir: IR
|
|
568
543
|
): {interval: number; start: number; last: number} | null {
|
|
569
|
-
const segments =
|
|
544
|
+
const segments = segmentsOf(ir, 'hour');
|
|
570
545
|
|
|
571
546
|
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
572
547
|
const {fires, interval} = segments[0];
|
|
@@ -702,7 +677,7 @@ function renderRangeOfMinutes(ir: IR): string {
|
|
|
702
677
|
|
|
703
678
|
// A standalone second field: "每7秒" (step cadence) or "每分钟第4、17、42秒".
|
|
704
679
|
function renderStandaloneSeconds(ir: IR): string {
|
|
705
|
-
const segs =
|
|
680
|
+
const segs = segmentsOf(ir, 'second');
|
|
706
681
|
const first = segs[0];
|
|
707
682
|
|
|
708
683
|
if (segs.length === 1 && first.kind === 'step' && first.startToken === '*') {
|
|
@@ -715,13 +690,13 @@ function renderStandaloneSeconds(ir: IR): string {
|
|
|
715
690
|
|
|
716
691
|
// A second anchored to the minute: "每分钟第1秒", "每分钟第4、17、42秒".
|
|
717
692
|
function renderSecondPastMinute(ir: IR): string {
|
|
718
|
-
return '每分钟第' + valueText(
|
|
693
|
+
return '每分钟第' + valueText(segmentsOf(ir, 'second')) + '秒';
|
|
719
694
|
}
|
|
720
695
|
|
|
721
696
|
// A second within a single specific minute: "每小时0分第1秒" / "…,每15秒".
|
|
722
697
|
function renderSecondsWithinMinute(ir: IR): string {
|
|
723
698
|
const base = '每小时' + ir.pattern.minute + '分';
|
|
724
|
-
const segs =
|
|
699
|
+
const segs = segmentsOf(ir, 'second');
|
|
725
700
|
const first = segs[0];
|
|
726
701
|
|
|
727
702
|
if (segs.length === 1 && first.kind === 'step' && first.startToken === '*') {
|
|
@@ -733,7 +708,7 @@ function renderSecondsWithinMinute(ir: IR): string {
|
|
|
733
708
|
|
|
734
709
|
// The second clause for a composed schedule: "每秒" / "每7秒" / "第4、17、42秒".
|
|
735
710
|
function secondClause(ir: IR): string {
|
|
736
|
-
const segs =
|
|
711
|
+
const segs = segmentsOf(ir, 'second');
|
|
737
712
|
|
|
738
713
|
if (!segs.length) {
|
|
739
714
|
return '每秒';
|
|
@@ -761,7 +736,7 @@ function minuteClause(ir: IR): string {
|
|
|
761
736
|
return cadence(stepSegment(ir, 'minute').interval, UNITS.minute);
|
|
762
737
|
}
|
|
763
738
|
|
|
764
|
-
return valueList(
|
|
739
|
+
return valueList(segmentsOf(ir, 'minute'), '分');
|
|
765
740
|
}
|
|
766
741
|
|
|
767
742
|
// A single second folds into each clock time a clockTimes rest renders
|
|
@@ -838,7 +813,7 @@ function composeMinuteZeroClocks(ir: IR, sec: string): string {
|
|
|
838
813
|
});
|
|
839
814
|
// A pinned minute makes the seconds' own "每分钟" anchor misleading (it is a
|
|
840
815
|
// single minute, not every minute), so the stride here drops it.
|
|
841
|
-
const nested = strideFromSegments(
|
|
816
|
+
const nested = strideFromSegments(segmentsOf(ir, 'second'), '秒', '秒', '');
|
|
842
817
|
const tail = sec === '每秒' ? '的每一秒' : '的' + (nested ?? sec);
|
|
843
818
|
const core = joinAnd(clocks) + tail;
|
|
844
819
|
|
|
@@ -850,7 +825,7 @@ function composeMinuteZeroClocks(ir: IR, sec: string): string {
|
|
|
850
825
|
// words. A pure single-value list (9,17) has no range to span; a step is
|
|
851
826
|
// handled by hourStride/hourCadence.
|
|
852
827
|
function hasHourWindow(ir: IR): boolean {
|
|
853
|
-
return
|
|
828
|
+
return segmentsOf(ir, 'hour').some(function range(segment) {
|
|
854
829
|
return segment.kind === 'range';
|
|
855
830
|
});
|
|
856
831
|
}
|
|
@@ -924,7 +899,7 @@ function composeSecondsListed(ir: IR): string {
|
|
|
924
899
|
// stride cadence ("凌晨0点从3分起每2分钟,至59分的每一秒"); the hour fuses, so the
|
|
925
900
|
// stride drops its "每小时" anchor. A short or irregular set keeps the list.
|
|
926
901
|
if (ir.shapes.hour === 'single' && sec === '每秒') {
|
|
927
|
-
const minuteSegs =
|
|
902
|
+
const minuteSegs = segmentsOf(ir, 'minute');
|
|
928
903
|
const minuteCad = strideFromSegments(minuteSegs, '分钟', '分', '') ??
|
|
929
904
|
valueList(minuteSegs, '分');
|
|
930
905
|
|
|
@@ -1008,7 +983,7 @@ function monthPhrase(ir: IR): string {
|
|
|
1008
983
|
return '';
|
|
1009
984
|
}
|
|
1010
985
|
|
|
1011
|
-
const segs =
|
|
986
|
+
const segs = segmentsOf(ir, 'month');
|
|
1012
987
|
const first = segs[0];
|
|
1013
988
|
|
|
1014
989
|
if (segs.length === 1 && first.kind === 'step' && first.interval === 2) {
|
|
@@ -1037,7 +1012,7 @@ function monthPhrase(ir: IR): string {
|
|
|
1037
1012
|
// The day-of-month list. A pure list of singles shares one trailing 日
|
|
1038
1013
|
// ("1、3、8日"); any range gives each segment its own 日 ("1至5日、10日").
|
|
1039
1014
|
function dayList(ir: IR): string {
|
|
1040
|
-
const segs =
|
|
1015
|
+
const segs = segmentsOf(ir, 'date');
|
|
1041
1016
|
|
|
1042
1017
|
if (segs.every((seg) => seg.kind === 'single')) {
|
|
1043
1018
|
return segs.map((seg) => (seg as {value: string}).value).join('、') + '日';
|
|
@@ -1159,7 +1134,7 @@ function weekdayPhrase(
|
|
|
1159
1134
|
return quartzWeekday(ir.pattern.weekday, monthPrefix);
|
|
1160
1135
|
}
|
|
1161
1136
|
|
|
1162
|
-
const segs =
|
|
1137
|
+
const segs = segmentsOf(ir, 'weekday');
|
|
1163
1138
|
|
|
1164
1139
|
if (segs.length === 1 && segs[0].kind === 'range') {
|
|
1165
1140
|
const [from, to] = (segs[0] as Extract<Segment, {kind: 'range'}>).bounds;
|
|
@@ -1323,7 +1298,7 @@ function describe(ir: IR, opts: Opts): string {
|
|
|
1323
1298
|
}
|
|
1324
1299
|
|
|
1325
1300
|
// The year leads as "2030年", a range as "2030年至2032年", a list joined with 、.
|
|
1326
|
-
const year =
|
|
1301
|
+
const year = segmentsOf(ir, 'year').map(function part(seg) {
|
|
1327
1302
|
if (seg.kind === 'range') {
|
|
1328
1303
|
return seg.bounds[0] + '年至' + seg.bounds[1] + '年';
|
|
1329
1304
|
}
|
package/src/types.ts
CHANGED
|
@@ -59,9 +59,9 @@ export interface Cronli5Language {
|
|
|
59
59
|
options(options?: Cronli5Options): any;
|
|
60
60
|
reboot: string;
|
|
61
61
|
sentence(description: string): string;
|
|
62
|
-
// Optional
|
|
62
|
+
// Optional plan override (see `core/ir.ts` `Language.plan`). Opaque
|
|
63
63
|
// at this public boundary, like `describe`/`options`.
|
|
64
|
-
|
|
64
|
+
plan?(content: any, base: any): any;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
package/types/core/analyze.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ declare function minuteSpan(minuteField: string): [number, number] | null;
|
|
|
9
9
|
declare function lastMinuteFire(minuteField: string): number;
|
|
10
10
|
declare function clockSecond(secondField: string): number | undefined;
|
|
11
11
|
declare function analyze(pattern: Pattern): IR;
|
|
12
|
-
declare function
|
|
13
|
-
export { analyze, clockSecond, enumerateFires, enumerateStep, enumerateValues, getOccurrences, lastMinuteFire, minuteSpan,
|
|
12
|
+
declare function selectPlan(content: Content): PlanNode;
|
|
13
|
+
export { analyze, clockSecond, enumerateFires, enumerateStep, enumerateValues, getOccurrences, lastMinuteFire, minuteSpan, selectPlan };
|
package/types/core/ir.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ export type HourTimesPlan = {
|
|
|
57
57
|
kind: 'segments';
|
|
58
58
|
};
|
|
59
59
|
/**
|
|
60
|
-
* The rendering
|
|
60
|
+
* The rendering plan the core selects for a pattern. The `kind`
|
|
61
61
|
* discriminant tells a renderer which fields are present.
|
|
62
62
|
*/
|
|
63
63
|
export type PlanNode = {
|
|
@@ -122,8 +122,8 @@ export interface Analyses {
|
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
124
|
* The neutral content plan: the language-independent facts about a pattern,
|
|
125
|
-
* carrying no phrasing decision. `analyze` produces this; `
|
|
126
|
-
* reads it to suggest a `plan`. The phrasing
|
|
125
|
+
* carrying no phrasing decision. `analyze` produces this; `selectPlan`
|
|
126
|
+
* reads it to suggest a `plan`. The phrasing plan is deliberately *not*
|
|
127
127
|
* part of the neutral content (docs/i18n-design.md §2.2).
|
|
128
128
|
*/
|
|
129
129
|
export interface Content {
|
|
@@ -134,7 +134,7 @@ export interface Content {
|
|
|
134
134
|
/**
|
|
135
135
|
* The semantic intermediate representation a language renders: the neutral
|
|
136
136
|
* `Content` plus the selected `plan`. A language may widen `plan` with its
|
|
137
|
-
* own `Extra`
|
|
137
|
+
* own `Extra` plan kinds via `Language.plan`; by default there are
|
|
138
138
|
* none, so `IR` is the neutral content with a core `PlanNode`.
|
|
139
139
|
*/
|
|
140
140
|
export interface IR<Extra extends {
|
|
@@ -172,8 +172,8 @@ export interface NormalizedOptions<Style = DialectStyle> {
|
|
|
172
172
|
}
|
|
173
173
|
/**
|
|
174
174
|
* The interface every language module's default export implements. `Extra`
|
|
175
|
-
* lets a language add its own
|
|
176
|
-
* `
|
|
175
|
+
* lets a language add its own plan kinds (default: none), which its
|
|
176
|
+
* `plan` override emits and its `describe` renders.
|
|
177
177
|
*/
|
|
178
178
|
export interface Language<Style = DialectStyle, Extra extends {
|
|
179
179
|
kind: string;
|
|
@@ -183,5 +183,5 @@ export interface Language<Style = DialectStyle, Extra extends {
|
|
|
183
183
|
options(options?: Cronli5Options): NormalizedOptions<Style>;
|
|
184
184
|
reboot: string;
|
|
185
185
|
sentence(description: string): string;
|
|
186
|
-
|
|
186
|
+
plan?(content: Content, base: PlanNode): PlanNode | Extra;
|
|
187
187
|
}
|
package/types/core/shapes.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ declare function isPlainRange(field: string): boolean;
|
|
|
3
3
|
declare function isPlainStep(field: string): boolean;
|
|
4
4
|
declare function isDiscreteList(field: string): boolean;
|
|
5
5
|
declare function isDiscreteHours(hourField: string): boolean;
|
|
6
|
-
|
|
6
|
+
declare function isOpenStep(field: string): boolean;
|
|
7
|
+
export { isDiscreteHours, isDiscreteList, isOpenStep, isPlainRange, isPlainStep, isSingleValue };
|