cronli5 0.8.0 → 0.8.3
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 +49 -0
- package/README.md +6 -6
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +58 -8
- package/dist/cronli5.js +58 -8
- package/dist/lang/de.cjs +48 -9
- package/dist/lang/de.js +48 -9
- package/dist/lang/en.cjs +58 -8
- package/dist/lang/en.js +58 -8
- package/dist/lang/es.cjs +45 -3
- package/dist/lang/es.js +45 -3
- package/dist/lang/fi.cjs +50 -6
- package/dist/lang/fi.js +50 -6
- package/dist/lang/fr.cjs +45 -3
- package/dist/lang/fr.js +45 -3
- package/dist/lang/pt.cjs +45 -3
- package/dist/lang/pt.js +45 -3
- package/dist/lang/zh.cjs +52 -10
- package/dist/lang/zh.js +52 -10
- package/package.json +1 -1
- package/src/lang/de/index.ts +117 -30
- package/src/lang/en/index.ts +121 -28
- package/src/lang/es/index.ts +106 -7
- package/src/lang/fi/index.ts +121 -18
- package/src/lang/fr/index.ts +101 -7
- package/src/lang/pt/index.ts +101 -6
- package/src/lang/zh/index.ts +149 -16
package/src/lang/fr/index.ts
CHANGED
|
@@ -127,6 +127,16 @@ const weekdayNames = [
|
|
|
127
127
|
const nthWeekdayMasculine =
|
|
128
128
|
[null, 'premier', 'deuxième', 'troisième', 'quatrième', 'cinquième'];
|
|
129
129
|
|
|
130
|
+
// French ordinals (gender-neutral "-ième") for a stepped-minute cadence under a
|
|
131
|
+
// seconds lead ("à la sixième minute"). The interval-2 step keeps its own
|
|
132
|
+
// idiom and never reaches here; a lookup miss falls back to the cardinal-with-
|
|
133
|
+
// preposition form, which still confines (see `minuteStepConfinement`).
|
|
134
|
+
const stepOrdinals: Record<number, string> = {
|
|
135
|
+
3: 'troisième', 4: 'quatrième', 5: 'cinquième', 6: 'sixième',
|
|
136
|
+
7: 'septième', 8: 'huitième', 9: 'neuvième', 10: 'dixième',
|
|
137
|
+
12: 'douzième', 15: 'quinzième', 20: 'vingtième', 30: 'trentième'
|
|
138
|
+
};
|
|
139
|
+
|
|
130
140
|
// Normalize raw user options.
|
|
131
141
|
function normalizeOptions(options?: Cronli5Options): Opts {
|
|
132
142
|
options = options || {};
|
|
@@ -279,6 +289,67 @@ function isPinnedMinuteSeconds(
|
|
|
279
289
|
schedule.shapes.second === 'step');
|
|
280
290
|
}
|
|
281
291
|
|
|
292
|
+
// The minute field's step stride for the confinement frame, or null when the
|
|
293
|
+
// minute is not a stepped cadence. A `step`-shaped field reads its segment; a
|
|
294
|
+
// `list`-shaped field the core enumerated from a uneven step (`2/7` → 2,9,…,58)
|
|
295
|
+
// recovers the progression from its values.
|
|
296
|
+
function minuteStride(
|
|
297
|
+
schedule: Schedule
|
|
298
|
+
): {start: number; interval: number; last: number} | null {
|
|
299
|
+
if (schedule.shapes.minute === 'step') {
|
|
300
|
+
const segment = stepSegment(schedule, 'minute');
|
|
301
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
302
|
+
|
|
303
|
+
return {interval: segment.interval, last:
|
|
304
|
+
segment.fires[segment.fires.length - 1], start};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
308
|
+
|
|
309
|
+
return values && arithmeticStep(values);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// A stepped minute under a wildcard/stepped second and wildcard hour: bind the
|
|
313
|
+
// second cadence to the minute cadence as a CONFINEMENT ("chaque seconde à la
|
|
314
|
+
// sixième minute à partir de la minute 4 de chaque heure"), never the comma
|
|
315
|
+
// juxtaposition that reads as two independent cadences. The cadence is ORDINAL
|
|
316
|
+
// ("à la sixième minute") — the cardinal "toutes les six minutes" is what fuels
|
|
317
|
+
// the misread — and the start/bound mirror the standalone minute cadence.
|
|
318
|
+
function minuteStepConfinement(
|
|
319
|
+
schedule: Schedule,
|
|
320
|
+
stride: {start: number; interval: number; last: number},
|
|
321
|
+
opts: Opts
|
|
322
|
+
): string {
|
|
323
|
+
const ordinal = stepOrdinals[stride.interval];
|
|
324
|
+
const head = ordinal ?
|
|
325
|
+
'à la ' + ordinal + ' minute' :
|
|
326
|
+
'à la minute toutes les ' + numero(stride.interval, opts);
|
|
327
|
+
|
|
328
|
+
const tail = chooseStride({...stride, cycle: 60}, {
|
|
329
|
+
bare: () => '',
|
|
330
|
+
offset: () => ' à partir de la minute ' + stride.start,
|
|
331
|
+
bounded: () => ' de la minute ' + stride.start + ' à ' + stride.last
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return secondsLeadClause(schedule, opts) + ' ' + head + tail +
|
|
335
|
+
' de chaque heure' + trailingQualifier(schedule, opts);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Whether a stepped minute fills a wildcard hour under a wildcard/stepped
|
|
339
|
+
// second — the shape the confinement frame above handles.
|
|
340
|
+
function isSteppedMinuteSeconds(
|
|
341
|
+
schedule: Schedule,
|
|
342
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
343
|
+
): boolean {
|
|
344
|
+
return (plan.rest.kind === 'minuteFrequency' ||
|
|
345
|
+
plan.rest.kind === 'multipleMinutes') &&
|
|
346
|
+
(schedule.shapes.second === 'wildcard' ||
|
|
347
|
+
schedule.shapes.second === 'step') &&
|
|
348
|
+
schedule.shapes.hour === 'wildcard' &&
|
|
349
|
+
schedule.pattern.minute !== '*/2' &&
|
|
350
|
+
minuteStride(schedule) !== null;
|
|
351
|
+
}
|
|
352
|
+
|
|
282
353
|
function renderComposeSeconds(
|
|
283
354
|
schedule: Schedule,
|
|
284
355
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
@@ -322,6 +393,14 @@ function renderComposeSeconds(
|
|
|
322
393
|
return dayFrame + ', ' + window + ', ' + cadence;
|
|
323
394
|
}
|
|
324
395
|
|
|
396
|
+
// A stepped minute under a wildcard/stepped second + wildcard hour confines
|
|
397
|
+
// the second cadence to the ordinal minute cadence ("chaque seconde à la
|
|
398
|
+
// sixième minute à partir de la minute 4 de chaque heure"), never the comma
|
|
399
|
+
// juxtaposition that reads as two independent cadences.
|
|
400
|
+
if (isSteppedMinuteSeconds(schedule, plan)) {
|
|
401
|
+
return minuteStepConfinement(schedule, minuteStride(schedule)!, opts);
|
|
402
|
+
}
|
|
403
|
+
|
|
325
404
|
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
326
405
|
// cadences that read as contradictory ("chaque seconde, toutes les deux
|
|
327
406
|
// minutes"). Bind them with the genitive "de" ("chaque seconde de chaque
|
|
@@ -476,6 +555,16 @@ function minutesList(schedule: Schedule, opts: Opts): string {
|
|
|
476
555
|
joinList(segmentWords(segmentsOf(schedule, 'minute'))) + ' de chaque heure';
|
|
477
556
|
}
|
|
478
557
|
|
|
558
|
+
// Strip the generic "de chaque heure" anchor from a minute-cadence lead. Under
|
|
559
|
+
// an hour STEP the hour clause is the sole hour authority, so the cadence must
|
|
560
|
+
// not also assert "de chaque heure" — alongside a stepped hour it reads as a
|
|
561
|
+
// conflicting every-hour scope ("de chaque heure, toutes les quatre heures").
|
|
562
|
+
// An hour WINDOW and an unrestricted hour keep the anchor (the window already
|
|
563
|
+
// names the hours; an open hour has no other hour statement).
|
|
564
|
+
function withoutHourAnchor(lead: string): string {
|
|
565
|
+
return lead.replace(/ de chaque heure$/, '');
|
|
566
|
+
}
|
|
567
|
+
|
|
479
568
|
// "chaque minute de 0 à 30". The standalone renderer adds "de chaque heure";
|
|
480
569
|
// when an hour qualifier follows ("..., à 9 h", "..., toutes les deux heures")
|
|
481
570
|
// it would contradict, so it is not baked in here.
|
|
@@ -550,8 +639,10 @@ function renderMinuteFrequency(
|
|
|
550
639
|
}
|
|
551
640
|
else if (plan.hours.kind === 'step') {
|
|
552
641
|
// A clean stride is a confinement ("les heures paires", or the active-hour
|
|
553
|
-
// list), never a juxtaposed cadence ("toutes les deux heures").
|
|
554
|
-
|
|
642
|
+
// list), never a juxtaposed cadence ("toutes les deux heures"). The hour
|
|
643
|
+
// step scopes the hours, so an offset cadence drops "de chaque heure".
|
|
644
|
+
phrase = withoutHourAnchor(phrase) + ', ' +
|
|
645
|
+
stepHourSpan(stepSegment(schedule, 'hour'), opts);
|
|
555
646
|
}
|
|
556
647
|
|
|
557
648
|
return phrase + trailingQualifier(schedule, opts);
|
|
@@ -635,10 +726,10 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
635
726
|
|
|
636
727
|
// A minute list keeps the same cadence clause as the range; only its lead
|
|
637
728
|
// differs ("aux minutes 5 et 30 de chaque heure" vs "chaque minute de 0 à
|
|
638
|
-
// 30").
|
|
639
|
-
const lead = plan.form === 'list' ?
|
|
729
|
+
// 30"). The hour step scopes the hours, so the lead drops "de chaque heure".
|
|
730
|
+
const lead = withoutHourAnchor(plan.form === 'list' ?
|
|
640
731
|
minutesList(schedule, opts) :
|
|
641
|
-
minuteRangeLead(schedule.pattern.minute);
|
|
732
|
+
minuteRangeLead(schedule.pattern.minute));
|
|
642
733
|
|
|
643
734
|
return lead + ', ' +
|
|
644
735
|
(cadence ?? stepHours(segment, opts)) + trailingQualifier(schedule, opts);
|
|
@@ -991,10 +1082,13 @@ function renderCompactClockTimes(
|
|
|
991
1082
|
}
|
|
992
1083
|
|
|
993
1084
|
// A uneven hour stride reads as a cadence after the minute lead, not a wall
|
|
994
|
-
// of clock-time columns.
|
|
1085
|
+
// of clock-time columns. That hour step is the sole hour authority, so the
|
|
1086
|
+
// minute lead drops its generic "de chaque heure" (an every-hour scope that
|
|
1087
|
+
// would conflict with the step); the clock-time branch keeps it, naming
|
|
1088
|
+
// specific hours rather than a step.
|
|
995
1089
|
const cadence = unevenHourCadence(schedule, opts);
|
|
996
1090
|
const phrase = cadence ?
|
|
997
|
-
minutesList(schedule, opts) + ', ' + cadence +
|
|
1091
|
+
withoutHourAnchor(minutesList(schedule, opts)) + ', ' + cadence +
|
|
998
1092
|
trailingQualifier(schedule, opts) :
|
|
999
1093
|
minutesList(schedule, opts) + ', ' +
|
|
1000
1094
|
hourContextTimes(schedule, opts) + trailingQualifier(schedule, opts);
|
package/src/lang/pt/index.ts
CHANGED
|
@@ -144,6 +144,16 @@ const nthWeekdayMasculine =
|
|
|
144
144
|
const nthWeekdayFeminine =
|
|
145
145
|
[null, 'primeira', 'segunda', 'terceira', 'quarta', 'quinta'];
|
|
146
146
|
|
|
147
|
+
// Portuguese ordinals (masculine) for a stepped-minute cadence under a seconds
|
|
148
|
+
// lead ("a cada segundo no sexto minuto"). The interval-2 step keeps its own
|
|
149
|
+
// idiom and never reaches here, so the colliding "segundo" is unused; a lookup
|
|
150
|
+
// miss falls back to the cardinal-with-"no" form, which still confines.
|
|
151
|
+
const stepOrdinals: Record<number, string> = {
|
|
152
|
+
3: 'terceiro', 4: 'quarto', 5: 'quinto', 6: 'sexto', 7: 'sétimo',
|
|
153
|
+
8: 'oitavo', 9: 'nono', 10: 'décimo', 12: 'décimo segundo',
|
|
154
|
+
15: 'décimo quinto', 20: 'vigésimo', 30: 'trigésimo'
|
|
155
|
+
};
|
|
156
|
+
|
|
147
157
|
// --- Contractions (the principal es->pt divergence). ---
|
|
148
158
|
//
|
|
149
159
|
// Portuguese fuses a preposition with the following article wherever es emitted
|
|
@@ -413,6 +423,67 @@ function isPinnedMinuteSeconds(
|
|
|
413
423
|
schedule.shapes.second === 'step');
|
|
414
424
|
}
|
|
415
425
|
|
|
426
|
+
// The minute field's step stride for the confinement frame, or null when the
|
|
427
|
+
// minute is not a stepped cadence. A `step`-shaped field reads its segment; a
|
|
428
|
+
// `list`-shaped field the core enumerated from a uneven step (`2/7` → 2,9,…,58)
|
|
429
|
+
// recovers the progression from its values.
|
|
430
|
+
function minuteStride(
|
|
431
|
+
schedule: Schedule
|
|
432
|
+
): {start: number; interval: number; last: number} | null {
|
|
433
|
+
if (schedule.shapes.minute === 'step') {
|
|
434
|
+
const segment = stepSegment(schedule, 'minute');
|
|
435
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
436
|
+
|
|
437
|
+
return {interval: segment.interval, last:
|
|
438
|
+
segment.fires[segment.fires.length - 1], start};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
442
|
+
|
|
443
|
+
return values && arithmeticStep(values);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// A stepped minute under a wildcard/stepped second and wildcard hour: bind the
|
|
447
|
+
// second cadence to the minute cadence as a CONFINEMENT ("a cada segundo no
|
|
448
|
+
// sexto minuto a partir do minuto 4 de cada hora"), never the comma
|
|
449
|
+
// juxtaposition that reads as two independent cadences. The cadence is ORDINAL
|
|
450
|
+
// ("no sexto minuto") — the cardinal "a cada seis minutos" is what fuels the
|
|
451
|
+
// misread — and the start/bound mirror the standalone minute cadence.
|
|
452
|
+
function minuteStepConfinement(
|
|
453
|
+
schedule: Schedule,
|
|
454
|
+
stride: {start: number; interval: number; last: number},
|
|
455
|
+
opts: Opts
|
|
456
|
+
): string {
|
|
457
|
+
const ordinal = stepOrdinals[stride.interval];
|
|
458
|
+
const head = ordinal ?
|
|
459
|
+
'no ' + ordinal + ' minuto' :
|
|
460
|
+
'a cada ' + numero(stride.interval, opts) + ' minutos';
|
|
461
|
+
|
|
462
|
+
const tail = chooseStride({...stride, cycle: 60}, {
|
|
463
|
+
bare: () => '',
|
|
464
|
+
offset: () => ' a partir do minuto ' + stride.start,
|
|
465
|
+
bounded: () => ' do minuto ' + stride.start + ' ao ' + stride.last
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return secondsLeadClause(schedule, opts) + ' ' + head + tail +
|
|
469
|
+
' de cada hora' + trailingQualifier(schedule, opts);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Whether a stepped minute fills a wildcard hour under a wildcard/stepped
|
|
473
|
+
// second — the shape the confinement frame above handles.
|
|
474
|
+
function isSteppedMinuteSeconds(
|
|
475
|
+
schedule: Schedule,
|
|
476
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
477
|
+
): boolean {
|
|
478
|
+
return (plan.rest.kind === 'minuteFrequency' ||
|
|
479
|
+
plan.rest.kind === 'multipleMinutes') &&
|
|
480
|
+
(schedule.shapes.second === 'wildcard' ||
|
|
481
|
+
schedule.shapes.second === 'step') &&
|
|
482
|
+
schedule.shapes.hour === 'wildcard' &&
|
|
483
|
+
schedule.pattern.minute !== '*/2' &&
|
|
484
|
+
minuteStride(schedule) !== null;
|
|
485
|
+
}
|
|
486
|
+
|
|
416
487
|
function renderComposeSeconds(
|
|
417
488
|
schedule: Schedule,
|
|
418
489
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
@@ -456,6 +527,14 @@ function renderComposeSeconds(
|
|
|
456
527
|
return dayFrame + ', ' + window + ', ' + cadence;
|
|
457
528
|
}
|
|
458
529
|
|
|
530
|
+
// A stepped minute under a wildcard/stepped second + wildcard hour confines
|
|
531
|
+
// the second cadence to the ordinal minute cadence ("a cada segundo no sexto
|
|
532
|
+
// minuto a partir do minuto 4 de cada hora"), never the comma juxtaposition
|
|
533
|
+
// that reads as two independent cadences.
|
|
534
|
+
if (isSteppedMinuteSeconds(schedule, plan)) {
|
|
535
|
+
return minuteStepConfinement(schedule, minuteStride(schedule)!, opts);
|
|
536
|
+
}
|
|
537
|
+
|
|
459
538
|
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
460
539
|
// cadences that read as contradictory ("a cada segundo, a cada dois
|
|
461
540
|
// minutos"). Bind them with the genitive "de" ("a cada segundo de cada dois
|
|
@@ -616,6 +695,16 @@ function minutesList(schedule: Schedule, opts: Opts): string {
|
|
|
616
695
|
joinList(segmentWords(segmentsOf(schedule, 'minute'))) + ' de cada hora';
|
|
617
696
|
}
|
|
618
697
|
|
|
698
|
+
// Strip the generic "de cada hora" anchor from a minute-cadence lead. Under an
|
|
699
|
+
// hour STEP the hour clause is the sole hour authority, so the cadence must not
|
|
700
|
+
// also assert "de cada hora" — alongside a stepped hour it reads as a
|
|
701
|
+
// conflicting every-hour scope ("de cada hora, a cada quatro horas"). An hour
|
|
702
|
+
// WINDOW and an unrestricted hour keep the anchor (the window already names the
|
|
703
|
+
// hours; an open hour has no other hour statement).
|
|
704
|
+
function withoutHourAnchor(lead: string): string {
|
|
705
|
+
return lead.replace(/ de cada hora$/, '');
|
|
706
|
+
}
|
|
707
|
+
|
|
619
708
|
// "a cada minuto do 0 ao 30". The standalone renderer adds "de cada hora";
|
|
620
709
|
// when an hour qualifier follows ("..., às 09:00", "..., a cada duas horas")
|
|
621
710
|
// it would contradict, so it is not baked in here.
|
|
@@ -746,8 +835,10 @@ function renderMinuteFrequency(
|
|
|
746
835
|
}
|
|
747
836
|
else if (plan.hours.kind === 'step') {
|
|
748
837
|
// A clean stride is a confinement ("as horas pares", or the active-hour
|
|
749
|
-
// list), never a juxtaposed cadence ("a cada duas horas").
|
|
750
|
-
|
|
838
|
+
// list), never a juxtaposed cadence ("a cada duas horas"). The hour step
|
|
839
|
+
// scopes the hours, so an offset cadence drops "de cada hora".
|
|
840
|
+
phrase = withoutHourAnchor(phrase) + ', ' +
|
|
841
|
+
stepHourSpan(stepSegment(schedule, 'hour'), opts);
|
|
751
842
|
}
|
|
752
843
|
|
|
753
844
|
return phrase + trailingQualifier(schedule, opts);
|
|
@@ -832,9 +923,10 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
832
923
|
|
|
833
924
|
// A minute list keeps the same cadence clause as the range; only its lead
|
|
834
925
|
// differs ("nos minutos 5 e 30 de cada hora" vs "a cada minuto do 0 ao 30").
|
|
835
|
-
|
|
926
|
+
// The hour step scopes the hours, so the lead drops "de cada hora".
|
|
927
|
+
const lead = withoutHourAnchor(plan.form === 'list' ?
|
|
836
928
|
minutesList(schedule, opts) :
|
|
837
|
-
minuteRangeLead(schedule.pattern.minute);
|
|
929
|
+
minuteRangeLead(schedule.pattern.minute));
|
|
838
930
|
|
|
839
931
|
return lead + ', ' +
|
|
840
932
|
(cadence ?? stepHours(segment, opts)) + trailingQualifier(schedule, opts);
|
|
@@ -1423,10 +1515,13 @@ function renderCompactClockTimes(
|
|
|
1423
1515
|
}
|
|
1424
1516
|
|
|
1425
1517
|
// A uneven hour stride reads as a cadence after the minute lead, not a wall
|
|
1426
|
-
// of clock-time columns.
|
|
1518
|
+
// of clock-time columns. That hour step is the sole hour authority, so the
|
|
1519
|
+
// minute lead drops its generic "de cada hora" (an every-hour scope that
|
|
1520
|
+
// would conflict with the step); the clock-time branch keeps it, naming
|
|
1521
|
+
// specific hours rather than a step.
|
|
1427
1522
|
const cadence = unevenHourCadence(schedule, opts);
|
|
1428
1523
|
const phrase = cadence ?
|
|
1429
|
-
minutesList(schedule, opts) + ', ' + cadence +
|
|
1524
|
+
withoutHourAnchor(minutesList(schedule, opts)) + ', ' + cadence +
|
|
1430
1525
|
trailingQualifier(schedule, opts) :
|
|
1431
1526
|
minutesList(schedule, opts) + ', ' +
|
|
1432
1527
|
hourContextTimes(schedule, opts) + trailingQualifier(schedule, opts);
|
package/src/lang/zh/index.ts
CHANGED
|
@@ -327,6 +327,16 @@ function renderMinutePast(schedule: Schedule): string {
|
|
|
327
327
|
return minuteHourClause(schedule);
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
// Strip the generic "每小时" (every-hour) anchor that leads a minute clause.
|
|
331
|
+
// Under an hour STEP the hour cadence is the sole hour authority, so the minute
|
|
332
|
+
// clause must not also assert "每小时" — alongside a stepped hour ("每4小时…每小
|
|
333
|
+
// 时…") it reads as a conflicting every-hour scope. An hour WINDOW and an
|
|
334
|
+
// unrestricted hour keep "每小时" (the window already names the hours; an open
|
|
335
|
+
// hour has no other hour statement).
|
|
336
|
+
function withoutHourAnchor(clause: string): string {
|
|
337
|
+
return clause.replace(/^每小时/, '');
|
|
338
|
+
}
|
|
339
|
+
|
|
330
340
|
// One hour segment as clock words by its form: a range is a span ("9点至20点"),
|
|
331
341
|
// a single is one clock word ("22点"), a step keeps its fires enumerated as
|
|
332
342
|
// clock words ("9点、11点、13点"). A range stated as a list element should read
|
|
@@ -383,7 +393,14 @@ function renderMinuteFrequency(schedule: Schedule, plan: PlanNode): string {
|
|
|
383
393
|
const hourCad = unevenHourCadence(schedule);
|
|
384
394
|
|
|
385
395
|
if (hourCad !== null) {
|
|
386
|
-
|
|
396
|
+
// An hour STEP is the sole hour authority, so an offset minute cadence
|
|
397
|
+
// drops its leading "每小时" ("每4小时从5分起每10分钟"); a discrete hour
|
|
398
|
+
// list (during) keeps it. Only the step path reaches a non-null cadence
|
|
399
|
+
// here — an irregular list falls through to the enumerated frame below.
|
|
400
|
+
const minuteBase = hours.kind === 'step' ?
|
|
401
|
+
withoutHourAnchor(base) : base;
|
|
402
|
+
|
|
403
|
+
return hourCad + (hourCad.indexOf('至') === -1 ? '' : ',') + minuteBase;
|
|
387
404
|
}
|
|
388
405
|
}
|
|
389
406
|
|
|
@@ -445,15 +462,17 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
445
462
|
const {form} = plan as Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>;
|
|
446
463
|
|
|
447
464
|
// A minute list reads as the hour cadence plus the minute list ("每2小时,
|
|
448
|
-
//
|
|
449
|
-
//
|
|
465
|
+
// 0、25、50分"; offset "从1点起每2小时,5分和30分"), the same compaction the
|
|
466
|
+
// wildcard/range minute already uses, rather than the enumerated hours. The
|
|
467
|
+
// hour cadence scopes the hours, so the minute clause drops its "每小时".
|
|
450
468
|
if (form === 'list') {
|
|
451
|
-
return hourCadencePhrase(schedule) + ',' +
|
|
469
|
+
return hourCadencePhrase(schedule) + ',' +
|
|
470
|
+
withoutHourAnchor(renderMinutePast(schedule));
|
|
452
471
|
}
|
|
453
472
|
|
|
454
473
|
const minuteTail = form === 'wildcard' ?
|
|
455
474
|
'每分钟' :
|
|
456
|
-
minuteHourClause(schedule) + ',每分钟';
|
|
475
|
+
withoutHourAnchor(minuteHourClause(schedule)) + ',每分钟';
|
|
457
476
|
|
|
458
477
|
// An offset or non-tiling stride (2/6 fires at 2,8,14,20) reads as its
|
|
459
478
|
// cadence ("从2点起每6小时"). A wildcard minute hangs off it with a comma; a
|
|
@@ -518,9 +537,13 @@ function renderCompactClockTimes(schedule: Schedule, plan: PlanNode): string {
|
|
|
518
537
|
if (!compact.fold) {
|
|
519
538
|
const hourCad = unevenHourCadence(schedule);
|
|
520
539
|
|
|
540
|
+
// A bounded/uneven hour step leads as the cadence and is the sole hour
|
|
541
|
+
// authority, so the minute clause drops its generic "每小时" every-hour
|
|
542
|
+
// scope; an enumerated hour list (hourCad null) names specific hours and
|
|
543
|
+
// keeps the anchor.
|
|
521
544
|
return hourCad === null ?
|
|
522
545
|
minuteHourClause(schedule) + ',在' + hourList(schedule) + tail :
|
|
523
|
-
hourCad + ',' + minuteHourClause(schedule) + tail;
|
|
546
|
+
hourCad + ',' + withoutHourAnchor(minuteHourClause(schedule)) + tail;
|
|
524
547
|
}
|
|
525
548
|
|
|
526
549
|
// A single pinned minute past 0 leads with its clause; a pinned 0 folds into
|
|
@@ -870,6 +893,15 @@ function composeSecondsOnHour(
|
|
|
870
893
|
return composeMinuteZeroClocks(schedule, sec);
|
|
871
894
|
}
|
|
872
895
|
|
|
896
|
+
// A single fixed (non-zero) minute under enumerated clock times fuses the
|
|
897
|
+
// seconds onto the composed clock time the same way ("0点2分的每一秒").
|
|
898
|
+
const fusedSingleMinute =
|
|
899
|
+
composeSingleMinuteClocks(schedule, rest, sec, opts);
|
|
900
|
+
|
|
901
|
+
if (fusedSingleMinute !== null) {
|
|
902
|
+
return fusedSingleMinute;
|
|
903
|
+
}
|
|
904
|
+
|
|
873
905
|
const restText = render(schedule, rest, opts);
|
|
874
906
|
const secTail = clockRestCarriesSecond(rest) ? '' : sec;
|
|
875
907
|
|
|
@@ -877,15 +909,42 @@ function composeSecondsOnHour(
|
|
|
877
909
|
return '每天' + restText + secTail;
|
|
878
910
|
}
|
|
879
911
|
|
|
880
|
-
// A stated minute (
|
|
881
|
-
// "
|
|
912
|
+
// A stated single minute (minute 0 under an open hour) confines the second
|
|
913
|
+
// beneath it with "的" when the second is a cadence ("每小时0分的每一秒"), the
|
|
914
|
+
// same fusion the other pinned-minute paths use; the bare comma ("每小时0分,
|
|
915
|
+
// 每秒") reads as two independent cadences. A single/list/range second is a
|
|
916
|
+
// clock-point, not a cadence, so it keeps the "," connector.
|
|
882
917
|
if (rest.kind === 'singleMinute') {
|
|
883
|
-
return
|
|
918
|
+
return secondIsCadence(schedule) ?
|
|
919
|
+
restText + confinedSecondTail(sec) : restText + ',' + sec;
|
|
884
920
|
}
|
|
885
921
|
|
|
886
922
|
return restText + secTail;
|
|
887
923
|
}
|
|
888
924
|
|
|
925
|
+
// A single fixed (non-zero) minute under enumerated clock times: each clock
|
|
926
|
+
// point already names the minute ("0点2分", "9点5分和17点5分"), so bind the
|
|
927
|
+
// seconds to it with "的" — the same fusion the minute-0 ("0分的每一秒") and
|
|
928
|
+
// minute-step ("5、20…分的每一秒") cases use — rather than leaving a bare
|
|
929
|
+
// trailing "每秒" that floats as a second, unlinked adverbial. A single second
|
|
930
|
+
// already folded into each clock time ("9点5分30秒") is not re-appended. The
|
|
931
|
+
// compactClockTimes window form states its minute separately ("每小时5分") and
|
|
932
|
+
// keeps its own seconds clause, so it does not qualify (returns null). minute 0
|
|
933
|
+
// is handled by composeMinuteZeroClocks before this point.
|
|
934
|
+
function composeSingleMinuteClocks(
|
|
935
|
+
schedule: Schedule, rest: PlanNode, sec: string, opts: Opts
|
|
936
|
+
): string | null {
|
|
937
|
+
if (rest.kind !== 'clockTimes' || schedule.shapes.minute !== 'single' ||
|
|
938
|
+
clockRestCarriesSecond(rest)) {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const core =
|
|
943
|
+
render(schedule, rest, opts) + minuteZeroSecondTail(schedule, sec);
|
|
944
|
+
|
|
945
|
+
return isDaily(schedule) ? '每天' + core : core;
|
|
946
|
+
}
|
|
947
|
+
|
|
889
948
|
// A minute pinned to 0 under specific clock hours (not a compacted cadence): a
|
|
890
949
|
// bare clock word ("9点") would hide the :00 and leave the second dangling
|
|
891
950
|
// ("…9点每秒"), reading as the whole hour. Fuse the seconds with the explicit
|
|
@@ -907,14 +966,24 @@ function composeMinuteZeroClocks(schedule: Schedule, sec: string): string {
|
|
|
907
966
|
// midnight (凌晨0点) and other hours still need it to pin the minute.
|
|
908
967
|
return hour === 12 ? '正午' : hourWord(hour) + '0分';
|
|
909
968
|
});
|
|
910
|
-
|
|
911
|
-
|
|
969
|
+
const core = joinAnd(clocks) + minuteZeroSecondTail(schedule, sec);
|
|
970
|
+
|
|
971
|
+
return isDaily(schedule) ? '每天' + core : core;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// The "的"-fused second tail for a clock time that already names a single pinned
|
|
975
|
+
// minute ("…的每一秒" for a wildcard second, else "…的" + the second's clause).
|
|
976
|
+
// A pinned minute makes the seconds' own "每分钟" anchor misleading (it is a
|
|
977
|
+
// single minute, not every minute), so a stride here drops it.
|
|
978
|
+
function minuteZeroSecondTail(schedule: Schedule, sec: string): string {
|
|
979
|
+
if (sec === '每秒') {
|
|
980
|
+
return '的每一秒';
|
|
981
|
+
}
|
|
982
|
+
|
|
912
983
|
const nested =
|
|
913
984
|
strideFromSegments(segmentsOf(schedule, 'second'), '秒', '秒', '');
|
|
914
|
-
const tail = sec === '每秒' ? '的每一秒' : '的' + (nested ?? sec);
|
|
915
|
-
const core = joinAnd(clocks) + tail;
|
|
916
985
|
|
|
917
|
-
return
|
|
986
|
+
return '的' + (nested ?? sec);
|
|
918
987
|
}
|
|
919
988
|
|
|
920
989
|
// Whether the hour field is a range — or a list whose segments include a
|
|
@@ -1004,18 +1073,75 @@ function composeSecondsListed(schedule: Schedule): string {
|
|
|
1004
1073
|
}
|
|
1005
1074
|
|
|
1006
1075
|
if (schedule.shapes.hour === 'wildcard') {
|
|
1007
|
-
|
|
1076
|
+
// A wildcard or stepped second is a cadence; the bare comma ("每小时30分,
|
|
1077
|
+
// 每秒") reads as two independent cadences, so fuse the second beneath the
|
|
1078
|
+
// stated minute(s) with "的" ("每小时30分的每一秒"), the same confinement the
|
|
1079
|
+
// minute-stride and pinned-clock paths use. A single/list/range second is a
|
|
1080
|
+
// clock-point, not a cadence, so it keeps the comma.
|
|
1081
|
+
return secondIsCadence(schedule) ?
|
|
1082
|
+
minutes + confinedSecondTail(sec) : minutes + ',' + sec;
|
|
1008
1083
|
}
|
|
1009
1084
|
|
|
1010
1085
|
const hourCad = unevenHourCadence(schedule);
|
|
1011
1086
|
|
|
1012
1087
|
if (hourCad !== null) {
|
|
1013
|
-
|
|
1088
|
+
// An hour STEP cadence is the sole hour authority, so the minute clause
|
|
1089
|
+
// drops its "每小时" ("每2小时,0至30分,每秒"); a discrete hour list keeps it
|
|
1090
|
+
// (it falls through to the hourFrame branch below with a null cadence).
|
|
1091
|
+
return hourCad + ',' + withoutHourAnchor(minutes) + ',' + sec;
|
|
1014
1092
|
}
|
|
1015
1093
|
|
|
1016
1094
|
return hourFrame(schedule) + minutes + ',' + sec;
|
|
1017
1095
|
}
|
|
1018
1096
|
|
|
1097
|
+
// Whether the minute field is a stepped cadence (a clean `*/n`, an offset
|
|
1098
|
+
// `m/n`, or a uneven step the core enumerated to an arithmetic fire list). The
|
|
1099
|
+
// shape the seconds-wildcard confinement below fuses with "的".
|
|
1100
|
+
function isMinuteStride(schedule: Schedule): boolean {
|
|
1101
|
+
if (schedule.shapes.minute === 'step') {
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
1106
|
+
|
|
1107
|
+
return values !== null && arithmeticStep(values) !== null;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Whether the second is a CADENCE (wildcard "每秒" or a clean step "每N秒") rather
|
|
1111
|
+
// than a clock-point (a single/list/range). A cadence second under a stated
|
|
1112
|
+
// minute fuses beneath it with "的"; a clock-point keeps the "," connector.
|
|
1113
|
+
function secondIsCadence(schedule: Schedule): boolean {
|
|
1114
|
+
return schedule.pattern.second === '*' || schedule.shapes.second === 'step';
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// The "的"-fused second tail for a clause that already states its minute(s):
|
|
1118
|
+
// "的每一秒" for a wildcard second, else "的" + the second's own cadence clause.
|
|
1119
|
+
// The fusion binds the second beneath the minute rather than leaving a bare
|
|
1120
|
+
// trailing "每秒" that reads as a second, independent cadence.
|
|
1121
|
+
function confinedSecondTail(sec: string): string {
|
|
1122
|
+
return sec === '每秒' ? '的每一秒' : '的' + sec;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Whether a compose-seconds plan is a stepped minute under a cadence second and
|
|
1126
|
+
// wildcard hour — the shape the "的"-fused confinement below handles, kept
|
|
1127
|
+
// distinct from the */2 even-minutes idiom and the composed-clock paths.
|
|
1128
|
+
function isSteppedMinuteSeconds(
|
|
1129
|
+
schedule: Schedule, composedClock: boolean
|
|
1130
|
+
): boolean {
|
|
1131
|
+
return !composedClock && schedule.shapes.hour === 'wildcard' &&
|
|
1132
|
+
secondIsCadence(schedule) && schedule.pattern.minute !== '*/2' &&
|
|
1133
|
+
isMinuteStride(schedule);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// A stepped minute under a cadence second and wildcard hour: fuse the minute
|
|
1137
|
+
// cadence and the second cadence with "的" ("每小时从4分起每6分钟的每一秒"), never
|
|
1138
|
+
// the comma juxtaposition ("…每6分钟,每秒") that reads as two independent
|
|
1139
|
+
// cadences. The minute clause carries the offset/bound ("从4分起" / ",至58分").
|
|
1140
|
+
function minuteStrideConfinement(schedule: Schedule): string {
|
|
1141
|
+
return minuteHourClause(schedule) +
|
|
1142
|
+
confinedSecondTail(secondClause(schedule));
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1019
1145
|
// Seconds composed with the minute/hour structure, dispatched on the minute.
|
|
1020
1146
|
// A single minute over a composed clock-time rest (the core already joined the
|
|
1021
1147
|
// lone hour and minute into "N点M分") keeps that composition, attaching the
|
|
@@ -1028,6 +1154,13 @@ function renderComposeSeconds(
|
|
|
1028
1154
|
const composedClock =
|
|
1029
1155
|
rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes';
|
|
1030
1156
|
|
|
1157
|
+
// A stepped minute under a cadence second and wildcard hour confines the
|
|
1158
|
+
// second beneath the minute cadence with "的", never the comma that reads as
|
|
1159
|
+
// two independent cadences. The */2 step keeps its own "每偶数分钟" idiom.
|
|
1160
|
+
if (isSteppedMinuteSeconds(schedule, composedClock)) {
|
|
1161
|
+
return minuteStrideConfinement(schedule);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1031
1164
|
if (schedule.pattern.minute === '0' ||
|
|
1032
1165
|
composedClock && schedule.shapes.minute === 'single') {
|
|
1033
1166
|
return composeSecondsOnHour(schedule, plan, opts);
|