cronli5 0.8.2 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +69 -5
- package/dist/cronli5.js +69 -5
- package/dist/lang/de.cjs +86 -1
- package/dist/lang/de.js +86 -1
- package/dist/lang/en.cjs +69 -5
- package/dist/lang/en.js +69 -5
- package/dist/lang/es.cjs +105 -3
- package/dist/lang/es.js +105 -3
- package/dist/lang/fi.cjs +70 -0
- package/dist/lang/fi.js +70 -0
- package/dist/lang/fr.cjs +77 -0
- package/dist/lang/fr.js +77 -0
- package/dist/lang/pt.cjs +78 -0
- package/dist/lang/pt.js +78 -0
- package/dist/lang/zh.cjs +36 -4
- package/dist/lang/zh.js +36 -4
- package/package.json +1 -1
- package/src/lang/de/index.ts +190 -1
- package/src/lang/en/index.ts +154 -24
- package/src/lang/es/index.ts +236 -6
- package/src/lang/fi/index.ts +163 -0
- package/src/lang/fr/index.ts +178 -0
- package/src/lang/pt/index.ts +174 -0
- package/src/lang/zh/index.ts +97 -6
package/dist/lang/zh.js
CHANGED
|
@@ -547,8 +547,8 @@ function secondClause(schedule) {
|
|
|
547
547
|
return "\u6BCF\u79D2";
|
|
548
548
|
}
|
|
549
549
|
const first = segs[0];
|
|
550
|
-
if (segs.length === 1 && first.kind === "step"
|
|
551
|
-
return
|
|
550
|
+
if (segs.length === 1 && first.kind === "step") {
|
|
551
|
+
return stepClause(first, "\u79D2", "\u79D2", "\u6BCF\u5206\u949F");
|
|
552
552
|
}
|
|
553
553
|
return strideFromSegments(segs, "\u79D2", "\u79D2", "\u6BCF\u5206\u949F") ?? "\u7B2C" + valueText(segs) + "\u79D2";
|
|
554
554
|
}
|
|
@@ -587,7 +587,7 @@ function composeSecondsOnHour(schedule, plan, opts) {
|
|
|
587
587
|
return "\u6BCF\u5929" + restText + secTail;
|
|
588
588
|
}
|
|
589
589
|
if (rest.kind === "singleMinute") {
|
|
590
|
-
return restText + "\uFF0C" + sec;
|
|
590
|
+
return secondIsCadence(schedule) ? restText + confinedSecondTail(sec) : restText + "\uFF0C" + sec;
|
|
591
591
|
}
|
|
592
592
|
return restText + secTail;
|
|
593
593
|
}
|
|
@@ -644,6 +644,9 @@ function composeSecondsCadence(schedule) {
|
|
|
644
644
|
return "\u6BCF\u5076\u6570\u5206\u949F\u7684\u6BCF\u4E00\u79D2";
|
|
645
645
|
}
|
|
646
646
|
}
|
|
647
|
+
if (!secondIsCadence(schedule) && !secondIsStride(schedule)) {
|
|
648
|
+
return minuteClause(schedule) + "\u7684" + sec;
|
|
649
|
+
}
|
|
647
650
|
return sec + "\uFF0C" + minuteClause(schedule);
|
|
648
651
|
}
|
|
649
652
|
return hourFrame(schedule) + tail;
|
|
@@ -657,7 +660,7 @@ function composeSecondsListed(schedule) {
|
|
|
657
660
|
return hourWord(hourFires(schedule)[0]) + minuteCad + "\u7684\u6BCF\u4E00\u79D2";
|
|
658
661
|
}
|
|
659
662
|
if (schedule.shapes.hour === "wildcard") {
|
|
660
|
-
return minutes + "\uFF0C" + sec;
|
|
663
|
+
return secondIsStride(schedule) ? minutes + "\uFF0C" + sec : minutes + confinedSecondTail(sec);
|
|
661
664
|
}
|
|
662
665
|
const hourCad = unevenHourCadence(schedule);
|
|
663
666
|
if (hourCad !== null) {
|
|
@@ -665,9 +668,38 @@ function composeSecondsListed(schedule) {
|
|
|
665
668
|
}
|
|
666
669
|
return hourFrame(schedule) + minutes + "\uFF0C" + sec;
|
|
667
670
|
}
|
|
671
|
+
function isMinuteStride(schedule) {
|
|
672
|
+
if (schedule.shapes.minute === "step") {
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
const values = singleValues(segmentsOf(schedule, "minute"));
|
|
676
|
+
return values !== null && arithmeticStep(values) !== null;
|
|
677
|
+
}
|
|
678
|
+
function secondIsCadence(schedule) {
|
|
679
|
+
return schedule.pattern.second === "*" || schedule.shapes.second === "step";
|
|
680
|
+
}
|
|
681
|
+
function secondIsStride(schedule) {
|
|
682
|
+
if (schedule.shapes.second !== "list") {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
const values = singleValues(segmentsOf(schedule, "second"));
|
|
686
|
+
return values !== null && arithmeticStep(values) !== null;
|
|
687
|
+
}
|
|
688
|
+
function confinedSecondTail(sec) {
|
|
689
|
+
return sec === "\u6BCF\u79D2" ? "\u7684\u6BCF\u4E00\u79D2" : "\u7684" + sec;
|
|
690
|
+
}
|
|
691
|
+
function isSteppedMinuteSeconds(schedule, composedClock) {
|
|
692
|
+
return !composedClock && schedule.shapes.hour === "wildcard" && secondIsCadence(schedule) && schedule.pattern.minute !== "*/2" && isMinuteStride(schedule);
|
|
693
|
+
}
|
|
694
|
+
function minuteStrideConfinement(schedule) {
|
|
695
|
+
return minuteHourClause(schedule) + confinedSecondTail(secondClause(schedule));
|
|
696
|
+
}
|
|
668
697
|
function renderComposeSeconds(schedule, plan, opts) {
|
|
669
698
|
const { rest } = plan;
|
|
670
699
|
const composedClock = rest.kind === "clockTimes" || rest.kind === "compactClockTimes";
|
|
700
|
+
if (isSteppedMinuteSeconds(schedule, composedClock)) {
|
|
701
|
+
return minuteStrideConfinement(schedule);
|
|
702
|
+
}
|
|
671
703
|
if (schedule.pattern.minute === "0" || composedClock && schedule.shapes.minute === "single") {
|
|
672
704
|
return composeSecondsOnHour(schedule, plan, opts);
|
|
673
705
|
}
|
package/package.json
CHANGED
package/src/lang/de/index.ts
CHANGED
|
@@ -223,6 +223,16 @@ const stepOrdinals: {[interval: number]: string} = {
|
|
|
223
223
|
12: 'zwölften'
|
|
224
224
|
};
|
|
225
225
|
|
|
226
|
+
// Dative ordinals for "in jeder N-ten Minute" — the step intervals a minute
|
|
227
|
+
// cadence can take. The interval-2 step keeps its own "jeder zweiten Minute"
|
|
228
|
+
// idiom and never reaches the confinement helper; a lookup miss falls back to
|
|
229
|
+
// the cardinal "alle N Minuten" form, which still confines.
|
|
230
|
+
const minuteStepOrdinals: {[interval: number]: string} = {
|
|
231
|
+
3: 'dritten', 4: 'vierten', 5: 'fünften', 6: 'sechsten', 7: 'siebten',
|
|
232
|
+
8: 'achten', 9: 'neunten', 10: 'zehnten', 12: 'zwölften',
|
|
233
|
+
15: 'fünfzehnten', 20: 'zwanzigsten', 30: 'dreißigsten'
|
|
234
|
+
};
|
|
235
|
+
|
|
226
236
|
// Confine a cadence to a clean hour stride: "in jeder zweiten Stunde", with
|
|
227
237
|
// the start named when it is not midnight ("…ab 1 Uhr" for an odd stride).
|
|
228
238
|
function everyNthHour(segment: StepSegment): string {
|
|
@@ -658,7 +668,17 @@ function renderSecondsWithinMinute(
|
|
|
658
668
|
schedule.pattern.second + ' jeder Stunde';
|
|
659
669
|
}
|
|
660
670
|
|
|
661
|
-
|
|
671
|
+
// A second LIST or RANGE under a single minute confines that minute in the
|
|
672
|
+
// genitive ("in den Sekunden 5 und 10 der Minute 30 jeder Stunde"), never the
|
|
673
|
+
// comma juxtaposition; a STEP second is a cadence and keeps its own lead.
|
|
674
|
+
if (secondsConfinesMinute(schedule)) {
|
|
675
|
+
return secondsLead(schedule) + ' ' + confinedMinutePhrase(schedule);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// A cadence/stepped second leads straight into the locative "in Minute …"
|
|
679
|
+
// with NO comma ("alle 15 Sekunden in Minute 30 jeder Stunde"); the locative
|
|
680
|
+
// binds the two specs, matching the no-comma list/single confinement.
|
|
681
|
+
return secondsLead(schedule) + ' in Minute ' + schedule.pattern.minute +
|
|
662
682
|
' jeder Stunde';
|
|
663
683
|
}
|
|
664
684
|
|
|
@@ -717,6 +737,155 @@ function isEveryOtherMinuteSeconds(
|
|
|
717
737
|
return minuteStep.startToken === '*' && minuteStep.interval === 2;
|
|
718
738
|
}
|
|
719
739
|
|
|
740
|
+
// The minute field's step stride for the confinement frame, or null when the
|
|
741
|
+
// minute is not a stepped cadence. A `step`-shaped field reads its segment; a
|
|
742
|
+
// `list`-shaped field the core enumerated from a uneven step (`2/7` → 2,9,…,58)
|
|
743
|
+
// recovers the progression from its values.
|
|
744
|
+
function minuteStride(
|
|
745
|
+
schedule: Schedule
|
|
746
|
+
): {start: number; interval: number; last: number} | null {
|
|
747
|
+
if (schedule.shapes.minute === 'step') {
|
|
748
|
+
const segment = stepSegment(schedule, 'minute');
|
|
749
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
750
|
+
|
|
751
|
+
return {interval: segment.interval, last:
|
|
752
|
+
segment.fires[segment.fires.length - 1], start};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
756
|
+
|
|
757
|
+
return values && arithmeticStep(values);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// A stepped minute under a wildcard/stepped second and wildcard hour: bind the
|
|
761
|
+
// second cadence to the minute cadence as a CONFINEMENT ("jede Sekunde in jeder
|
|
762
|
+
// sechsten Minute ab Minute 4 jeder Stunde"), never the comma juxtaposition
|
|
763
|
+
// that reads as two independent cadences. The cadence is ORDINAL ("in jeder
|
|
764
|
+
// sechsten Minute") — the cardinal "alle 6 Minuten" is what fuels the misread —
|
|
765
|
+
// and the start/bound mirror the standalone minute cadence.
|
|
766
|
+
function minuteStepConfinement(
|
|
767
|
+
schedule: Schedule,
|
|
768
|
+
stride: {start: number; interval: number; last: number}
|
|
769
|
+
): string {
|
|
770
|
+
const ordinal = minuteStepOrdinals[stride.interval];
|
|
771
|
+
const head = ordinal ?
|
|
772
|
+
'in jeder ' + ordinal + ' Minute' :
|
|
773
|
+
'alle ' + stride.interval + ' Minuten';
|
|
774
|
+
|
|
775
|
+
const tail = chooseStride({...stride, cycle: 60}, {
|
|
776
|
+
bare: () => '',
|
|
777
|
+
offset: () => ' ab Minute ' + stride.start,
|
|
778
|
+
bounded: () => ' von Minute ' + stride.start + ' bis ' + stride.last
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
return secondsLead(schedule) + ' ' + head + tail + ' jeder Stunde';
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Whether a stepped minute fills a wildcard hour under a wildcard/stepped
|
|
785
|
+
// second — the shape the confinement frame above handles.
|
|
786
|
+
function isSteppedMinuteSeconds(
|
|
787
|
+
schedule: Schedule,
|
|
788
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
789
|
+
): boolean {
|
|
790
|
+
return (plan.rest.kind === 'minuteFrequency' ||
|
|
791
|
+
plan.rest.kind === 'multipleMinutes') &&
|
|
792
|
+
(schedule.shapes.second === 'wildcard' ||
|
|
793
|
+
schedule.shapes.second === 'step') &&
|
|
794
|
+
schedule.shapes.hour === 'wildcard' &&
|
|
795
|
+
schedule.pattern.minute !== '*/2' &&
|
|
796
|
+
minuteStride(schedule) !== null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// The CONFINED-minute phrase in the genitive that a clock-point second attaches
|
|
800
|
+
// to ("jeder sechsten Minute ab Minute 4 jeder Stunde", "der Minuten 0, 15 und
|
|
801
|
+
// 30 jeder Stunde", "der Minute 30 jeder Stunde"). A stepped minute reuses the
|
|
802
|
+
// ordinal cadence; a list, range, or single names the minute(s) in the genitive
|
|
803
|
+
// — so the seconds clause's bare lead never stacks a redundant "jeder Minute".
|
|
804
|
+
function confinedMinutePhrase(schedule: Schedule): string {
|
|
805
|
+
const stride = minuteStride(schedule);
|
|
806
|
+
|
|
807
|
+
if (stride && schedule.pattern.minute !== '*/2') {
|
|
808
|
+
const ordinal = minuteStepOrdinals[stride.interval];
|
|
809
|
+
const head = ordinal ?
|
|
810
|
+
'jeder ' + ordinal + ' Minute' :
|
|
811
|
+
'alle ' + stride.interval + ' Minuten';
|
|
812
|
+
const tail = chooseStride({...stride, cycle: 60}, {
|
|
813
|
+
bare: () => '',
|
|
814
|
+
offset: () => ' ab Minute ' + stride.start,
|
|
815
|
+
bounded: () => ' von Minute ' + stride.start + ' bis ' + stride.last
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
return head + tail + ' jeder Stunde';
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const genitive = schedule.shapes.minute === 'single' ?
|
|
822
|
+
'der Minute ' + schedule.pattern.minute :
|
|
823
|
+
'der Minuten ' + joinList(fieldValues(schedule, 'minute'));
|
|
824
|
+
|
|
825
|
+
return genitive + ' jeder Stunde';
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// The minute-confinement rendering for a compose-seconds plan, or null when the
|
|
829
|
+
// plan is not one. A CADENCE second over a stepped minute uses the ordinal
|
|
830
|
+
// cadence form; a CLOCK-POINT second (list/range/single) over any restricted
|
|
831
|
+
// minute uses the genitive form. Both bind the second beneath the minute
|
|
832
|
+
// instead of juxtaposing the two behind a comma. Folded into one helper so
|
|
833
|
+
// `renderComposeSeconds` carries a single branch.
|
|
834
|
+
function minuteConfinementRender(
|
|
835
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>, schedule: Schedule
|
|
836
|
+
): string | null {
|
|
837
|
+
if (isSteppedMinuteSeconds(schedule, plan)) {
|
|
838
|
+
return minuteStepConfinement(schedule, minuteStride(schedule)!);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const minuteRest = plan.rest.kind === 'minuteFrequency' ||
|
|
842
|
+
plan.rest.kind === 'multipleMinutes' ||
|
|
843
|
+
plan.rest.kind === 'rangeOfMinutes';
|
|
844
|
+
|
|
845
|
+
if (minuteRest && secondsConfinesMinute(schedule)) {
|
|
846
|
+
return secondsLead(schedule) + ' ' + confinedMinutePhrase(schedule);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Whether a clock-point second (list, range, or single) sits under a restricted
|
|
853
|
+
// minute and a wildcard hour — the shape that must CONFINE the minute in the
|
|
854
|
+
// genitive rather than juxtapose it behind a comma (two independent schedules).
|
|
855
|
+
// A second LIST the core enumerated from a step (`3/2`) is really a stride
|
|
856
|
+
// cadence and stays out. The single-second + single-minute pair folds into one
|
|
857
|
+
// coherent clock point ("in Minute 5 und Sekunde 30 jeder Stunde") and is
|
|
858
|
+
// excluded.
|
|
859
|
+
function secondsConfinesMinute(schedule: Schedule): boolean {
|
|
860
|
+
const {second, minute, hour} = schedule.shapes;
|
|
861
|
+
|
|
862
|
+
if (second === 'list') {
|
|
863
|
+
const values = singleValues(segmentsOf(schedule, 'second'));
|
|
864
|
+
|
|
865
|
+
if (values && arithmeticStep(values)) {
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const clockPoint = second === 'single' || second === 'range' ||
|
|
871
|
+
second === 'list';
|
|
872
|
+
|
|
873
|
+
return clockPoint && minute !== 'wildcard' && hour === 'wildcard' &&
|
|
874
|
+
!(second === 'single' && minute === 'single');
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Whether a compose-seconds plan is a cadence/stepped second under a minute
|
|
878
|
+
// LIST or SINGLE and a wildcard hour — the shape that leads into the locative
|
|
879
|
+
// "in …" minute phrase with no comma. A restricted/cadence hour keeps the
|
|
880
|
+
// comma, so it does not qualify.
|
|
881
|
+
function isLocativeMinuteConfinement(
|
|
882
|
+
schedule: Schedule,
|
|
883
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
884
|
+
): boolean {
|
|
885
|
+
return (plan.rest.kind === 'multipleMinutes' ||
|
|
886
|
+
plan.rest.kind === 'singleMinute') && schedule.shapes.hour === 'wildcard';
|
|
887
|
+
}
|
|
888
|
+
|
|
720
889
|
function renderComposeSeconds(
|
|
721
890
|
schedule: Schedule,
|
|
722
891
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
@@ -749,12 +918,32 @@ function renderComposeSeconds(
|
|
|
749
918
|
clockMinuteGenitive(plan.rest.times, opts.style.sep);
|
|
750
919
|
}
|
|
751
920
|
|
|
921
|
+
// A second confines the minute restriction (open hour), never the comma
|
|
922
|
+
// juxtaposition that reads as two independent cadences: a CADENCE second over
|
|
923
|
+
// a stepped minute uses the ordinal-cadence form ("jede Sekunde in jeder
|
|
924
|
+
// sechsten Minute …"); a CLOCK-POINT second uses the genitive form ("in den
|
|
925
|
+
// Sekunden 5, 10 und 15 jeder sechsten Minute …").
|
|
926
|
+
const confined = minuteConfinementRender(plan, schedule);
|
|
927
|
+
|
|
928
|
+
if (confined !== null) {
|
|
929
|
+
return confined;
|
|
930
|
+
}
|
|
931
|
+
|
|
752
932
|
// A wildcard second under a minute */2 with a wildcard hour binds in the
|
|
753
933
|
// genitive ("jede Sekunde jeder zweiten Minute").
|
|
754
934
|
if (isEveryOtherMinuteSeconds(schedule, plan)) {
|
|
755
935
|
return secondsLead(schedule) + ' jeder zweiten Minute';
|
|
756
936
|
}
|
|
757
937
|
|
|
938
|
+
// A cadence/stepped second under a minute LIST or SINGLE and a wildcard hour
|
|
939
|
+
// leads straight into the locative minute phrase with NO comma ("jede Sekunde
|
|
940
|
+
// in den Minuten 0, 15 und 30 jeder Stunde"). The locative "in" already binds
|
|
941
|
+
// the two specs; the comma read as two independent specifications and is
|
|
942
|
+
// inconsistent with the no-comma stepped-minute and list-tier confinements.
|
|
943
|
+
if (isLocativeMinuteConfinement(schedule, plan)) {
|
|
944
|
+
return secondsLead(schedule) + ' ' + render(schedule, plan.rest, opts);
|
|
945
|
+
}
|
|
946
|
+
|
|
758
947
|
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
759
948
|
// leading clause, so the composer must not prepend a second lead that would
|
|
760
949
|
// double it. A wildcard or stepped second is not folded there (no
|
package/src/lang/en/index.ts
CHANGED
|
@@ -677,6 +677,67 @@ const stepOrdinals: Record<number, string> = {
|
|
|
677
677
|
2: 'other', 3: 'third', 4: 'fourth', 6: 'sixth', 8: 'eighth', 12: 'twelfth'
|
|
678
678
|
};
|
|
679
679
|
|
|
680
|
+
// Spelled ordinals for "every Nth minute" — the step intervals a minute
|
|
681
|
+
// cadence can take (2 reads idiomatically as "other"). A lookup miss falls back
|
|
682
|
+
// to the suffixed numeric ordinal, so an unusually large interval still reads.
|
|
683
|
+
const spelledOrdinals: Record<number, string> = {
|
|
684
|
+
2: 'other', 3: 'third', 4: 'fourth', 5: 'fifth', 6: 'sixth', 7: 'seventh',
|
|
685
|
+
8: 'eighth', 9: 'ninth', 10: 'tenth', 11: 'eleventh', 12: 'twelfth',
|
|
686
|
+
15: 'fifteenth', 20: 'twentieth', 30: 'thirtieth'
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// The ordinal word for a cadence interval ("sixth", "seventh"), spelled where
|
|
690
|
+
// known and suffixed-numeric ("13th") otherwise.
|
|
691
|
+
function ordinalWord(interval: number): string {
|
|
692
|
+
return spelledOrdinals[interval] ?? getOrdinal(interval);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// A stepped minute under a seconds lead reads as a CONFINEMENT of that cadence,
|
|
696
|
+
// not a juxtaposed clause (a comma there reads as two independent cadences) nor
|
|
697
|
+
// a wall of enumerated minutes: "during every Nth minute" plus the step's
|
|
698
|
+
// offset/bound. The cadence is ORDINAL ("every sixth minute"); the cardinal
|
|
699
|
+
// ("every six minutes") is the form that reads as a separate cadence. The
|
|
700
|
+
// offset/bound mirrors the standalone minute cadence: a clean stride from the
|
|
701
|
+
// top names no offset, an offset-clean stride names only its start ("from four
|
|
702
|
+
// minutes past the hour"), and an uneven one pins both endpoints ("from 2
|
|
703
|
+
// through 58 minutes past the hour").
|
|
704
|
+
function minuteStrideConfinement(stride: {start: number; interval: number;
|
|
705
|
+
last: number}, opts: NormalizedOptions): string {
|
|
706
|
+
const base = ' during every ' + ordinalWord(stride.interval) + ' minute';
|
|
707
|
+
|
|
708
|
+
return chooseStride({...stride, cycle: 60}, {
|
|
709
|
+
bare: () => base,
|
|
710
|
+
offset: () => base + ' from ' + getNumber(stride.start, opts) + ' ' +
|
|
711
|
+
pluralize(stride.start, 'minute') + ' past the hour',
|
|
712
|
+
bounded: () => {
|
|
713
|
+
const num = seriesNumber();
|
|
714
|
+
|
|
715
|
+
return base + ' from ' + num(stride.start) + through(opts) +
|
|
716
|
+
num(stride.last) + ' ' + pluralize(stride.last, 'minute') +
|
|
717
|
+
' past the hour';
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// The minute field's step stride for the confinement frame, or null when the
|
|
723
|
+
// minute is not a stepped cadence. A `step`-shaped field (`*/6`) reads its
|
|
724
|
+
// segment directly; a `list`-shaped field the core enumerated from an uneven
|
|
725
|
+
// step (`2/7` → 2,9,…,58) recovers the progression from its values.
|
|
726
|
+
function minuteStride(schedule: Schedule):
|
|
727
|
+
{start: number; interval: number; last: number} | null {
|
|
728
|
+
if (schedule.shapes.minute === 'step') {
|
|
729
|
+
const segment = stepSegment(schedule, 'minute');
|
|
730
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
731
|
+
|
|
732
|
+
return {interval: segment.interval, last:
|
|
733
|
+
segment.fires[segment.fires.length - 1], start};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
737
|
+
|
|
738
|
+
return values && arithmeticStep(values);
|
|
739
|
+
}
|
|
740
|
+
|
|
680
741
|
// Confine a cadence to a clean hour stride: "during every other hour", with
|
|
681
742
|
// the start named when it is not midnight ("…from 1 a.m." for an odd stride).
|
|
682
743
|
function everyNthHour(segment: StepSegment, opts: NormalizedOptions): string {
|
|
@@ -1041,15 +1102,69 @@ function isCadenceField(token: string): boolean {
|
|
|
1041
1102
|
token.startsWith('*/') && token.indexOf('-') === -1;
|
|
1042
1103
|
}
|
|
1043
1104
|
|
|
1105
|
+
// Whether the second field leads the confinement frame as a clean cadence. A
|
|
1106
|
+
// wildcard ("every second") and a clean `*/n` step both lead via
|
|
1107
|
+
// `isCadenceField`; an OPEN OFFSET step (`m/n`) is the SAME cadence, only named
|
|
1108
|
+
// from its offset ("every six seconds from five seconds past the minute"), so
|
|
1109
|
+
// it leads the SAME confinement rather than juxtaposing the minute restriction
|
|
1110
|
+
// behind a comma — whether the offset is clean from the top (`0/n`) or not
|
|
1111
|
+
// (`5/n`). A bounded step (`a-b/n`, a windowed set) is not an open cadence and
|
|
1112
|
+
// keeps its existing form.
|
|
1113
|
+
function secondLeadsCadence(schedule: Schedule): boolean {
|
|
1114
|
+
if (isCadenceField(schedule.pattern.second)) {
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (schedule.shapes.second !== 'step') {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Reached only under a stepped second the `isCadenceField` guard did not
|
|
1123
|
+
// already admit, so its `*/n` clean-cadence forms are gone and the remaining
|
|
1124
|
+
// open form is the offset step `m/n` (`0/n` or non-zero). A bounded step
|
|
1125
|
+
// `a-b/n` is a windowed set, not a cadence, and stays out.
|
|
1126
|
+
return isOpenStep(schedule.pattern.second);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Whether the second leads the confinement frame as a CLOCK-POINT clause (a
|
|
1130
|
+
// list, range, or single second), as opposed to a cadence. A clock-point second
|
|
1131
|
+
// under a minute restriction confines that restriction exactly as the cadence
|
|
1132
|
+
// does ("at 5, 10, and 15 seconds past the minute during every sixth minute …")
|
|
1133
|
+
// rather than juxtaposing it behind a comma, which reads as two independent
|
|
1134
|
+
// schedules. The single-second + single-minute pair is excluded: it folds into
|
|
1135
|
+
// one coherent clock point ("30 minutes and 15 seconds past the hour"), not a
|
|
1136
|
+
// juxtaposition, so it keeps that fold. The confinement only applies where the
|
|
1137
|
+
// minute is the restriction and the hour is open; `confinementEligible` gates
|
|
1138
|
+
// the rest (a restricted hour folds into a clock time, left to that renderer).
|
|
1139
|
+
function secondLeadsClockPoint(schedule: Schedule): boolean {
|
|
1140
|
+
// Only a MEANINGFUL second leads a clause: the two seconds-bearing plans the
|
|
1141
|
+
// core chooses for a real second. A 5-field pattern (or an explicit `0`
|
|
1142
|
+
// second) carries no seconds clause — its plan is the minute's own — so it is
|
|
1143
|
+
// not confined here, which would otherwise prepend "at zero seconds …".
|
|
1144
|
+
if (schedule.plan.kind !== 'composeSeconds' &&
|
|
1145
|
+
schedule.plan.kind !== 'secondsWithinMinute') {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const {second, minute, hour} = schedule.shapes;
|
|
1150
|
+
const clockPoint = second === 'single' || second === 'range' ||
|
|
1151
|
+
second === 'list';
|
|
1152
|
+
const minuteRestricted = minute !== 'wildcard';
|
|
1153
|
+
|
|
1154
|
+
return clockPoint && minuteRestricted && hour === 'wildcard' &&
|
|
1155
|
+
!(second === 'single' && minute === 'single');
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1044
1158
|
// The leading cadence and whether the second is the leading field, or null when
|
|
1045
1159
|
// the pattern has no cadence lead (the finest restricted field is a clock-point
|
|
1046
|
-
// single/range/list). The seconds lead when restricted as a cadence
|
|
1047
|
-
//
|
|
1160
|
+
// single/range/list). The seconds lead when restricted as a cadence, or as a
|
|
1161
|
+
// clock-point clause that confines a minute restriction; otherwise the minute
|
|
1162
|
+
// leads when the second is a plain :00 and the minute is a cadence.
|
|
1048
1163
|
function leadingCadence(schedule: Schedule, opts: NormalizedOptions):
|
|
1049
1164
|
{text: string; secondLead: boolean} | null {
|
|
1050
1165
|
const {second, minute} = schedule.pattern;
|
|
1051
1166
|
|
|
1052
|
-
if (
|
|
1167
|
+
if (secondLeadsCadence(schedule) || secondLeadsClockPoint(schedule)) {
|
|
1053
1168
|
return {secondLead: true, text: secondsClause(schedule, 'minute', opts)};
|
|
1054
1169
|
}
|
|
1055
1170
|
|
|
@@ -1078,12 +1193,22 @@ function minuteConfinement(schedule: Schedule,
|
|
|
1078
1193
|
return '';
|
|
1079
1194
|
}
|
|
1080
1195
|
|
|
1081
|
-
if (
|
|
1082
|
-
// The
|
|
1083
|
-
// minute steps
|
|
1196
|
+
if (minute === '*/2') {
|
|
1197
|
+
// The `*/2` clean step reads idiomatically as "every other minute" with no
|
|
1198
|
+
// offset; other minute steps take the ordinal stride-cadence below.
|
|
1084
1199
|
return ' of every other minute';
|
|
1085
1200
|
}
|
|
1086
1201
|
|
|
1202
|
+
// A stepped minute (a clean `*/n`, an offset `m/n`, or a uneven step the core
|
|
1203
|
+
// enumerated to an arithmetic list) confines as "during every Nth minute"
|
|
1204
|
+
// plus the step's offset/bound — the ordinal cadence, not the cardinal that
|
|
1205
|
+
// reads as a separate cadence, nor a wall of enumerated ":NN" minutes.
|
|
1206
|
+
const stride = minuteStride(schedule);
|
|
1207
|
+
|
|
1208
|
+
if (stride) {
|
|
1209
|
+
return minuteStrideConfinement(stride, opts);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1087
1212
|
// A minute single/range/list under the seconds lead. The minute reads as a
|
|
1088
1213
|
// ":NN" clock-minute confinement, never "N minutes past the hour" (that is
|
|
1089
1214
|
// the minute-lead clock-point form).
|
|
@@ -1119,10 +1244,11 @@ function hourConfinement(schedule: Schedule, opts: NormalizedOptions): string {
|
|
|
1119
1244
|
|
|
1120
1245
|
if (hour === '*') {
|
|
1121
1246
|
// A pinned minute confinement ("during minute :00") repeats across every
|
|
1122
|
-
// hour, so the hour is named as the unit of recurrence; a
|
|
1123
|
-
// ("of every other minute"
|
|
1247
|
+
// hour, so the hour is named as the unit of recurrence; a minute cadence
|
|
1248
|
+
// ("of every other minute", "during every sixth minute …") or an absent
|
|
1249
|
+
// minute already implies all hours, so the hour is not restated.
|
|
1124
1250
|
const minutePinned = schedule.pattern.minute !== '*' &&
|
|
1125
|
-
!isCadenceField(schedule.pattern.minute);
|
|
1251
|
+
!isCadenceField(schedule.pattern.minute) && !minuteStride(schedule);
|
|
1126
1252
|
|
|
1127
1253
|
return minutePinned ? ' of every hour' : '';
|
|
1128
1254
|
}
|
|
@@ -1216,24 +1342,28 @@ function confinementEligible(schedule: Schedule,
|
|
|
1216
1342
|
}
|
|
1217
1343
|
|
|
1218
1344
|
if (lead.secondLead) {
|
|
1219
|
-
// A minute STEP
|
|
1220
|
-
//
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1223
|
-
// the
|
|
1224
|
-
//
|
|
1225
|
-
// renderer rather than this confinement frame, which closes on the top of
|
|
1226
|
-
// the next hour.
|
|
1345
|
+
// A minute STEP confines as an ordinal cadence ("during every sixth minute
|
|
1346
|
+
// from four minutes past the hour"), but only where it fills the coarser
|
|
1347
|
+
// field: under a WILDCARD hour the step repeats every hour, so the cadence
|
|
1348
|
+
// is the whole confinement. A single hour or a contiguous range closes on
|
|
1349
|
+
// the minute's real last fire, which the windowing renderer already speaks,
|
|
1350
|
+
// so those defer. The `*/2` step keeps its "of every other minute" idiom.
|
|
1227
1351
|
if (minuteStep) {
|
|
1228
|
-
return minute === '*/2'
|
|
1352
|
+
return minute === '*/2' ?
|
|
1353
|
+
schedule.shapes.hour !== 'range' :
|
|
1354
|
+
schedule.pattern.hour === '*';
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// A minute list that is really an arithmetic stride confines as that same
|
|
1358
|
+
// ordinal cadence when it fills a wildcard hour; under a restricted hour it
|
|
1359
|
+
// keeps its existing cadence form. A short explicit minute list crossed
|
|
1360
|
+
// with a discrete hour LIST is a wall of distinct clock times ("9:00 a.m.,
|
|
1361
|
+
// 9:25 a.m., …"), not a single minute confinement, so it stays enumerated.
|
|
1362
|
+
if (isMinuteStride(schedule)) {
|
|
1363
|
+
return schedule.pattern.hour === '*';
|
|
1229
1364
|
}
|
|
1230
1365
|
|
|
1231
|
-
|
|
1232
|
-
// explicit minute list crossed with a discrete hour LIST is a wall of
|
|
1233
|
-
// distinct clock times ("9:00 a.m., 9:25 a.m., …"), not a single minute
|
|
1234
|
-
// confinement. Both stay with the enumerating renderer.
|
|
1235
|
-
if (isMinuteStride(schedule) ||
|
|
1236
|
-
schedule.shapes.minute === 'list' && schedule.shapes.hour === 'list') {
|
|
1366
|
+
if (schedule.shapes.minute === 'list' && schedule.shapes.hour === 'list') {
|
|
1237
1367
|
return false;
|
|
1238
1368
|
}
|
|
1239
1369
|
|