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/dist/lang/zh.cjs CHANGED
@@ -346,6 +346,9 @@ function minuteHourClause(schedule) {
346
346
  function renderMinutePast(schedule) {
347
347
  return minuteHourClause(schedule);
348
348
  }
349
+ function withoutHourAnchor(clause) {
350
+ return clause.replace(/^每小时/, "");
351
+ }
349
352
  function hourSegmentWords(segment) {
350
353
  if (segment.kind === "range") {
351
354
  return [hourWord(+segment.bounds[0]) + "\u81F3" + hourWord(+segment.bounds[1])];
@@ -373,7 +376,8 @@ function renderMinuteFrequency(schedule, plan) {
373
376
  if (hours.kind === "step" || hours.kind === "during") {
374
377
  const hourCad = unevenHourCadence(schedule);
375
378
  if (hourCad !== null) {
376
- return hourCad + (hourCad.indexOf("\u81F3") === -1 ? "" : "\uFF0C") + base;
379
+ const minuteBase = hours.kind === "step" ? withoutHourAnchor(base) : base;
380
+ return hourCad + (hourCad.indexOf("\u81F3") === -1 ? "" : "\uFF0C") + minuteBase;
377
381
  }
378
382
  }
379
383
  if (hours.kind === "window" && hours.from === hours.to) {
@@ -406,9 +410,9 @@ function renderMinuteSpanAcrossHourStep(schedule, plan) {
406
410
  const hourStep = stepSegment(schedule, "hour");
407
411
  const { form } = plan;
408
412
  if (form === "list") {
409
- return hourCadencePhrase(schedule) + "\uFF0C" + renderMinutePast(schedule);
413
+ return hourCadencePhrase(schedule) + "\uFF0C" + withoutHourAnchor(renderMinutePast(schedule));
410
414
  }
411
- const minuteTail = form === "wildcard" ? "\u6BCF\u5206\u949F" : minuteHourClause(schedule) + "\uFF0C\u6BCF\u5206\u949F";
415
+ const minuteTail = form === "wildcard" ? "\u6BCF\u5206\u949F" : withoutHourAnchor(minuteHourClause(schedule)) + "\uFF0C\u6BCF\u5206\u949F";
412
416
  if (hourStep.startToken !== "*") {
413
417
  return hourCadencePhrase(schedule) + "\uFF0C" + minuteTail;
414
418
  }
@@ -436,7 +440,7 @@ function renderCompactClockTimes(schedule, plan) {
436
440
  const tail = secs.length && schedule.pattern.second !== "0" ? "\uFF0C\u7B2C" + valueText(secs) + "\u79D2" : "";
437
441
  if (!compact.fold) {
438
442
  const hourCad = unevenHourCadence(schedule);
439
- return hourCad === null ? minuteHourClause(schedule) + "\uFF0C\u5728" + hourList(schedule) + tail : hourCad + "\uFF0C" + minuteHourClause(schedule) + tail;
443
+ return hourCad === null ? minuteHourClause(schedule) + "\uFF0C\u5728" + hourList(schedule) + tail : hourCad + "\uFF0C" + withoutHourAnchor(minuteHourClause(schedule)) + tail;
440
444
  }
441
445
  if (compact.minute > 0) {
442
446
  return minuteHourClause(schedule) + "\uFF0C\u5728" + hourList(schedule) + tail;
@@ -599,16 +603,27 @@ function composeSecondsOnHour(schedule, plan, opts) {
599
603
  if (composedClock && schedule.pattern.minute === "0") {
600
604
  return composeMinuteZeroClocks(schedule, sec);
601
605
  }
606
+ const fusedSingleMinute = composeSingleMinuteClocks(schedule, rest, sec, opts);
607
+ if (fusedSingleMinute !== null) {
608
+ return fusedSingleMinute;
609
+ }
602
610
  const restText = render(schedule, rest, opts);
603
611
  const secTail = clockRestCarriesSecond(rest) ? "" : sec;
604
612
  if (composedClock && isDaily(schedule)) {
605
613
  return "\u6BCF\u5929" + restText + secTail;
606
614
  }
607
615
  if (rest.kind === "singleMinute") {
608
- return restText + "\uFF0C" + sec;
616
+ return secondIsCadence(schedule) ? restText + confinedSecondTail(sec) : restText + "\uFF0C" + sec;
609
617
  }
610
618
  return restText + secTail;
611
619
  }
620
+ function composeSingleMinuteClocks(schedule, rest, sec, opts) {
621
+ if (rest.kind !== "clockTimes" || schedule.shapes.minute !== "single" || clockRestCarriesSecond(rest)) {
622
+ return null;
623
+ }
624
+ const core = render(schedule, rest, opts) + minuteZeroSecondTail(schedule, sec);
625
+ return isDaily(schedule) ? "\u6BCF\u5929" + core : core;
626
+ }
612
627
  function composeMinuteZeroClocks(schedule, sec) {
613
628
  if (hasHourWindow(schedule)) {
614
629
  return isDaily(schedule) ? "\u6BCF\u5929" + hourRangeWindow(schedule, sec) : hourRangeWindow(schedule, sec);
@@ -616,11 +631,16 @@ function composeMinuteZeroClocks(schedule, sec) {
616
631
  const clocks = hourFires(schedule).map(function clock(hour) {
617
632
  return hour === 12 ? "\u6B63\u5348" : hourWord(hour) + "0\u5206";
618
633
  });
619
- const nested = strideFromSegments(segmentsOf(schedule, "second"), "\u79D2", "\u79D2", "");
620
- const tail = sec === "\u6BCF\u79D2" ? "\u7684\u6BCF\u4E00\u79D2" : "\u7684" + (nested ?? sec);
621
- const core = joinAnd(clocks) + tail;
634
+ const core = joinAnd(clocks) + minuteZeroSecondTail(schedule, sec);
622
635
  return isDaily(schedule) ? "\u6BCF\u5929" + core : core;
623
636
  }
637
+ function minuteZeroSecondTail(schedule, sec) {
638
+ if (sec === "\u6BCF\u79D2") {
639
+ return "\u7684\u6BCF\u4E00\u79D2";
640
+ }
641
+ const nested = strideFromSegments(segmentsOf(schedule, "second"), "\u79D2", "\u79D2", "");
642
+ return "\u7684" + (nested ?? sec);
643
+ }
624
644
  function hasHourWindow(schedule) {
625
645
  return segmentsOf(schedule, "hour").some(function range(segment) {
626
646
  return segment.kind === "range";
@@ -663,17 +683,39 @@ function composeSecondsListed(schedule) {
663
683
  return hourWord(hourFires(schedule)[0]) + minuteCad + "\u7684\u6BCF\u4E00\u79D2";
664
684
  }
665
685
  if (schedule.shapes.hour === "wildcard") {
666
- return minutes + "\uFF0C" + sec;
686
+ return secondIsCadence(schedule) ? minutes + confinedSecondTail(sec) : minutes + "\uFF0C" + sec;
667
687
  }
668
688
  const hourCad = unevenHourCadence(schedule);
669
689
  if (hourCad !== null) {
670
- return hourCad + "\uFF0C" + minutes + "\uFF0C" + sec;
690
+ return hourCad + "\uFF0C" + withoutHourAnchor(minutes) + "\uFF0C" + sec;
671
691
  }
672
692
  return hourFrame(schedule) + minutes + "\uFF0C" + sec;
673
693
  }
694
+ function isMinuteStride(schedule) {
695
+ if (schedule.shapes.minute === "step") {
696
+ return true;
697
+ }
698
+ const values = singleValues(segmentsOf(schedule, "minute"));
699
+ return values !== null && arithmeticStep(values) !== null;
700
+ }
701
+ function secondIsCadence(schedule) {
702
+ return schedule.pattern.second === "*" || schedule.shapes.second === "step";
703
+ }
704
+ function confinedSecondTail(sec) {
705
+ return sec === "\u6BCF\u79D2" ? "\u7684\u6BCF\u4E00\u79D2" : "\u7684" + sec;
706
+ }
707
+ function isSteppedMinuteSeconds(schedule, composedClock) {
708
+ return !composedClock && schedule.shapes.hour === "wildcard" && secondIsCadence(schedule) && schedule.pattern.minute !== "*/2" && isMinuteStride(schedule);
709
+ }
710
+ function minuteStrideConfinement(schedule) {
711
+ return minuteHourClause(schedule) + confinedSecondTail(secondClause(schedule));
712
+ }
674
713
  function renderComposeSeconds(schedule, plan, opts) {
675
714
  const { rest } = plan;
676
715
  const composedClock = rest.kind === "clockTimes" || rest.kind === "compactClockTimes";
716
+ if (isSteppedMinuteSeconds(schedule, composedClock)) {
717
+ return minuteStrideConfinement(schedule);
718
+ }
677
719
  if (schedule.pattern.minute === "0" || composedClock && schedule.shapes.minute === "single") {
678
720
  return composeSecondsOnHour(schedule, plan, opts);
679
721
  }
package/dist/lang/zh.js CHANGED
@@ -320,6 +320,9 @@ function minuteHourClause(schedule) {
320
320
  function renderMinutePast(schedule) {
321
321
  return minuteHourClause(schedule);
322
322
  }
323
+ function withoutHourAnchor(clause) {
324
+ return clause.replace(/^每小时/, "");
325
+ }
323
326
  function hourSegmentWords(segment) {
324
327
  if (segment.kind === "range") {
325
328
  return [hourWord(+segment.bounds[0]) + "\u81F3" + hourWord(+segment.bounds[1])];
@@ -347,7 +350,8 @@ function renderMinuteFrequency(schedule, plan) {
347
350
  if (hours.kind === "step" || hours.kind === "during") {
348
351
  const hourCad = unevenHourCadence(schedule);
349
352
  if (hourCad !== null) {
350
- return hourCad + (hourCad.indexOf("\u81F3") === -1 ? "" : "\uFF0C") + base;
353
+ const minuteBase = hours.kind === "step" ? withoutHourAnchor(base) : base;
354
+ return hourCad + (hourCad.indexOf("\u81F3") === -1 ? "" : "\uFF0C") + minuteBase;
351
355
  }
352
356
  }
353
357
  if (hours.kind === "window" && hours.from === hours.to) {
@@ -380,9 +384,9 @@ function renderMinuteSpanAcrossHourStep(schedule, plan) {
380
384
  const hourStep = stepSegment(schedule, "hour");
381
385
  const { form } = plan;
382
386
  if (form === "list") {
383
- return hourCadencePhrase(schedule) + "\uFF0C" + renderMinutePast(schedule);
387
+ return hourCadencePhrase(schedule) + "\uFF0C" + withoutHourAnchor(renderMinutePast(schedule));
384
388
  }
385
- const minuteTail = form === "wildcard" ? "\u6BCF\u5206\u949F" : minuteHourClause(schedule) + "\uFF0C\u6BCF\u5206\u949F";
389
+ const minuteTail = form === "wildcard" ? "\u6BCF\u5206\u949F" : withoutHourAnchor(minuteHourClause(schedule)) + "\uFF0C\u6BCF\u5206\u949F";
386
390
  if (hourStep.startToken !== "*") {
387
391
  return hourCadencePhrase(schedule) + "\uFF0C" + minuteTail;
388
392
  }
@@ -410,7 +414,7 @@ function renderCompactClockTimes(schedule, plan) {
410
414
  const tail = secs.length && schedule.pattern.second !== "0" ? "\uFF0C\u7B2C" + valueText(secs) + "\u79D2" : "";
411
415
  if (!compact.fold) {
412
416
  const hourCad = unevenHourCadence(schedule);
413
- return hourCad === null ? minuteHourClause(schedule) + "\uFF0C\u5728" + hourList(schedule) + tail : hourCad + "\uFF0C" + minuteHourClause(schedule) + tail;
417
+ return hourCad === null ? minuteHourClause(schedule) + "\uFF0C\u5728" + hourList(schedule) + tail : hourCad + "\uFF0C" + withoutHourAnchor(minuteHourClause(schedule)) + tail;
414
418
  }
415
419
  if (compact.minute > 0) {
416
420
  return minuteHourClause(schedule) + "\uFF0C\u5728" + hourList(schedule) + tail;
@@ -573,16 +577,27 @@ function composeSecondsOnHour(schedule, plan, opts) {
573
577
  if (composedClock && schedule.pattern.minute === "0") {
574
578
  return composeMinuteZeroClocks(schedule, sec);
575
579
  }
580
+ const fusedSingleMinute = composeSingleMinuteClocks(schedule, rest, sec, opts);
581
+ if (fusedSingleMinute !== null) {
582
+ return fusedSingleMinute;
583
+ }
576
584
  const restText = render(schedule, rest, opts);
577
585
  const secTail = clockRestCarriesSecond(rest) ? "" : sec;
578
586
  if (composedClock && isDaily(schedule)) {
579
587
  return "\u6BCF\u5929" + restText + secTail;
580
588
  }
581
589
  if (rest.kind === "singleMinute") {
582
- return restText + "\uFF0C" + sec;
590
+ return secondIsCadence(schedule) ? restText + confinedSecondTail(sec) : restText + "\uFF0C" + sec;
583
591
  }
584
592
  return restText + secTail;
585
593
  }
594
+ function composeSingleMinuteClocks(schedule, rest, sec, opts) {
595
+ if (rest.kind !== "clockTimes" || schedule.shapes.minute !== "single" || clockRestCarriesSecond(rest)) {
596
+ return null;
597
+ }
598
+ const core = render(schedule, rest, opts) + minuteZeroSecondTail(schedule, sec);
599
+ return isDaily(schedule) ? "\u6BCF\u5929" + core : core;
600
+ }
586
601
  function composeMinuteZeroClocks(schedule, sec) {
587
602
  if (hasHourWindow(schedule)) {
588
603
  return isDaily(schedule) ? "\u6BCF\u5929" + hourRangeWindow(schedule, sec) : hourRangeWindow(schedule, sec);
@@ -590,11 +605,16 @@ function composeMinuteZeroClocks(schedule, sec) {
590
605
  const clocks = hourFires(schedule).map(function clock(hour) {
591
606
  return hour === 12 ? "\u6B63\u5348" : hourWord(hour) + "0\u5206";
592
607
  });
593
- const nested = strideFromSegments(segmentsOf(schedule, "second"), "\u79D2", "\u79D2", "");
594
- const tail = sec === "\u6BCF\u79D2" ? "\u7684\u6BCF\u4E00\u79D2" : "\u7684" + (nested ?? sec);
595
- const core = joinAnd(clocks) + tail;
608
+ const core = joinAnd(clocks) + minuteZeroSecondTail(schedule, sec);
596
609
  return isDaily(schedule) ? "\u6BCF\u5929" + core : core;
597
610
  }
611
+ function minuteZeroSecondTail(schedule, sec) {
612
+ if (sec === "\u6BCF\u79D2") {
613
+ return "\u7684\u6BCF\u4E00\u79D2";
614
+ }
615
+ const nested = strideFromSegments(segmentsOf(schedule, "second"), "\u79D2", "\u79D2", "");
616
+ return "\u7684" + (nested ?? sec);
617
+ }
598
618
  function hasHourWindow(schedule) {
599
619
  return segmentsOf(schedule, "hour").some(function range(segment) {
600
620
  return segment.kind === "range";
@@ -637,17 +657,39 @@ function composeSecondsListed(schedule) {
637
657
  return hourWord(hourFires(schedule)[0]) + minuteCad + "\u7684\u6BCF\u4E00\u79D2";
638
658
  }
639
659
  if (schedule.shapes.hour === "wildcard") {
640
- return minutes + "\uFF0C" + sec;
660
+ return secondIsCadence(schedule) ? minutes + confinedSecondTail(sec) : minutes + "\uFF0C" + sec;
641
661
  }
642
662
  const hourCad = unevenHourCadence(schedule);
643
663
  if (hourCad !== null) {
644
- return hourCad + "\uFF0C" + minutes + "\uFF0C" + sec;
664
+ return hourCad + "\uFF0C" + withoutHourAnchor(minutes) + "\uFF0C" + sec;
645
665
  }
646
666
  return hourFrame(schedule) + minutes + "\uFF0C" + sec;
647
667
  }
668
+ function isMinuteStride(schedule) {
669
+ if (schedule.shapes.minute === "step") {
670
+ return true;
671
+ }
672
+ const values = singleValues(segmentsOf(schedule, "minute"));
673
+ return values !== null && arithmeticStep(values) !== null;
674
+ }
675
+ function secondIsCadence(schedule) {
676
+ return schedule.pattern.second === "*" || schedule.shapes.second === "step";
677
+ }
678
+ function confinedSecondTail(sec) {
679
+ return sec === "\u6BCF\u79D2" ? "\u7684\u6BCF\u4E00\u79D2" : "\u7684" + sec;
680
+ }
681
+ function isSteppedMinuteSeconds(schedule, composedClock) {
682
+ return !composedClock && schedule.shapes.hour === "wildcard" && secondIsCadence(schedule) && schedule.pattern.minute !== "*/2" && isMinuteStride(schedule);
683
+ }
684
+ function minuteStrideConfinement(schedule) {
685
+ return minuteHourClause(schedule) + confinedSecondTail(secondClause(schedule));
686
+ }
648
687
  function renderComposeSeconds(schedule, plan, opts) {
649
688
  const { rest } = plan;
650
689
  const composedClock = rest.kind === "clockTimes" || rest.kind === "compactClockTimes";
690
+ if (isSteppedMinuteSeconds(schedule, composedClock)) {
691
+ return minuteStrideConfinement(schedule);
692
+ }
651
693
  if (schedule.pattern.minute === "0" || composedClock && schedule.shapes.minute === "single") {
652
694
  return composeSecondsOnHour(schedule, plan, opts);
653
695
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cronli5",
3
- "version": "0.8.0",
3
+ "version": "0.8.3",
4
4
  "description": "Cron Like I'm Five: A Cron to English Utility",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 {
@@ -717,6 +727,65 @@ function isEveryOtherMinuteSeconds(
717
727
  return minuteStep.startToken === '*' && minuteStep.interval === 2;
718
728
  }
719
729
 
730
+ // The minute field's step stride for the confinement frame, or null when the
731
+ // minute is not a stepped cadence. A `step`-shaped field reads its segment; a
732
+ // `list`-shaped field the core enumerated from a uneven step (`2/7` → 2,9,…,58)
733
+ // recovers the progression from its values.
734
+ function minuteStride(
735
+ schedule: Schedule
736
+ ): {start: number; interval: number; last: number} | null {
737
+ if (schedule.shapes.minute === 'step') {
738
+ const segment = stepSegment(schedule, 'minute');
739
+ const start = segment.startToken === '*' ? 0 : +segment.startToken;
740
+
741
+ return {interval: segment.interval, last:
742
+ segment.fires[segment.fires.length - 1], start};
743
+ }
744
+
745
+ const values = singleValues(segmentsOf(schedule, 'minute'));
746
+
747
+ return values && arithmeticStep(values);
748
+ }
749
+
750
+ // A stepped minute under a wildcard/stepped second and wildcard hour: bind the
751
+ // second cadence to the minute cadence as a CONFINEMENT ("jede Sekunde in jeder
752
+ // sechsten Minute ab Minute 4 jeder Stunde"), never the comma juxtaposition
753
+ // that reads as two independent cadences. The cadence is ORDINAL ("in jeder
754
+ // sechsten Minute") — the cardinal "alle 6 Minuten" is what fuels the misread —
755
+ // and the start/bound mirror the standalone minute cadence.
756
+ function minuteStepConfinement(
757
+ schedule: Schedule,
758
+ stride: {start: number; interval: number; last: number}
759
+ ): string {
760
+ const ordinal = minuteStepOrdinals[stride.interval];
761
+ const head = ordinal ?
762
+ 'in jeder ' + ordinal + ' Minute' :
763
+ 'alle ' + stride.interval + ' Minuten';
764
+
765
+ const tail = chooseStride({...stride, cycle: 60}, {
766
+ bare: () => '',
767
+ offset: () => ' ab Minute ' + stride.start,
768
+ bounded: () => ' von Minute ' + stride.start + ' bis ' + stride.last
769
+ });
770
+
771
+ return secondsLead(schedule) + ' ' + head + tail + ' jeder Stunde';
772
+ }
773
+
774
+ // Whether a stepped minute fills a wildcard hour under a wildcard/stepped
775
+ // second — the shape the confinement frame above handles.
776
+ function isSteppedMinuteSeconds(
777
+ schedule: Schedule,
778
+ plan: Extract<PlanNode, {kind: 'composeSeconds'}>
779
+ ): boolean {
780
+ return (plan.rest.kind === 'minuteFrequency' ||
781
+ plan.rest.kind === 'multipleMinutes') &&
782
+ (schedule.shapes.second === 'wildcard' ||
783
+ schedule.shapes.second === 'step') &&
784
+ schedule.shapes.hour === 'wildcard' &&
785
+ schedule.pattern.minute !== '*/2' &&
786
+ minuteStride(schedule) !== null;
787
+ }
788
+
720
789
  function renderComposeSeconds(
721
790
  schedule: Schedule,
722
791
  plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
@@ -738,16 +807,25 @@ function renderComposeSeconds(
738
807
  }
739
808
  }
740
809
 
741
- // A sub-minute second with the minute pinned to 0 and a specific hour: the
742
- // clock-time rest would read "um 9 Uhr", which hides the pinned :00 (and so
743
- // the one-minute confinement 60 fires in :00, not 3,600 across the hour).
744
- // Bind the seconds into the explicit clock minute in the genitive ("der
745
- // Minute 9:00"); the recurring "täglich"/day frame is added in `describe`.
746
- if (composeMinuteZero(schedule, plan)) {
810
+ // A second over a single fixed minute and a specific hour is a single fixed
811
+ // timestamp: the clock-time rest would float the seconds as a separate
812
+ // apposition ("jede Sekunde, um 9:02 Uhr"), hiding that they belong to that
813
+ // one minute. Bind the seconds into the explicit clock minute in the genitive
814
+ // ("der Minute 9:02"), the same fusion the minute-0 case ("der Minute 9:00")
815
+ // uses; the recurring "täglich"/day frame is added in `describe`.
816
+ if (composeSingleMinute(schedule, plan)) {
747
817
  return secondsLead(schedule) + ' ' +
748
818
  clockMinuteGenitive(plan.rest.times, opts.style.sep);
749
819
  }
750
820
 
821
+ // A stepped minute under a wildcard/stepped second + wildcard hour confines
822
+ // the second cadence to the ordinal minute cadence ("jede Sekunde in jeder
823
+ // sechsten Minute ab Minute 4 jeder Stunde"), never the comma juxtaposition
824
+ // that reads as two independent cadences.
825
+ if (isSteppedMinuteSeconds(schedule, plan)) {
826
+ return minuteStepConfinement(schedule, minuteStride(schedule)!);
827
+ }
828
+
751
829
  // A wildcard second under a minute */2 with a wildcard hour binds in the
752
830
  // genitive ("jede Sekunde jeder zweiten Minute").
753
831
  if (isEveryOtherMinuteSeconds(schedule, plan)) {
@@ -765,21 +843,23 @@ function renderComposeSeconds(
765
843
  return lead + render(schedule, plan.rest, opts);
766
844
  }
767
845
 
768
- // True when a compose-seconds plan is a sub-minute second over a minute-0
769
- // clock-time rest — the case that reads as the bare hour and so must surface
770
- // the pinned clock minute.
771
- function composeMinuteZero(
846
+ // True when a compose-seconds plan is a sub-minute second over a single fixed
847
+ // minute's clock-time rest — the single fixed timestamp whose seconds must fuse
848
+ // to the explicit clock minute rather than float as a separate apposition.
849
+ // Minute 0 ("der Minute 0:00") is just this with the minute being 0; any single
850
+ // fixed minute fuses the same way.
851
+ function composeSingleMinute(
772
852
  schedule: Schedule,
773
853
  plan: Extract<PlanNode, {kind: 'composeSeconds'}>
774
854
  ): plan is Extract<PlanNode, {kind: 'composeSeconds'}> &
775
855
  {rest: Extract<PlanNode, {kind: 'clockTimes'}>} {
776
856
  return plan.rest.kind === 'clockTimes' &&
777
- plan.rest.times.every((time) => +time.minute === 0);
857
+ schedule.shapes.minute === 'single';
778
858
  }
779
859
 
780
- // The pinned clock minute in the genitive: "der Minute 9:00" for one hour,
781
- // "der Minuten 9:00, 10:00 und 17:00" for several — the explicit ":00" so the
782
- // minute-0 confinement stays visible.
860
+ // The pinned clock minute in the genitive: "der Minute 9:02" for one hour,
861
+ // "der Minuten 9:00, 10:00 und 17:00" for several — the explicit minute so the
862
+ // single-fixed-minute confinement stays visible.
783
863
  function clockMinuteGenitive(
784
864
  times: {hour: number; minute: number}[],
785
865
  sep: string
@@ -940,18 +1020,22 @@ function renderMinuteFrequency(
940
1020
  if (plan.hours.kind === 'during') {
941
1021
  // A bounded or uneven hour stride confines the minute cadence to its own
942
1022
  // endpoint-pinning hour cadence ("alle 15 Minuten, alle 5 Stunden von 0 bis
943
- // 20 Uhr").
1023
+ // 20 Uhr"). That hour step is the sole hour authority, so an offset minute
1024
+ // cadence drops its generic "jeder Stunde" (an every-hour scope conflicting
1025
+ // with the step); a list of specific hours keeps it.
944
1026
  const cadence = unevenHourCadence(schedule);
945
1027
 
946
1028
  return cadence ?
947
- base + ', ' + cadence :
1029
+ stepClause(segment, UNITS.minute, '') + ', ' + cadence :
948
1030
  base + ' ' + duringHours(schedule, plan.hours.times, sep);
949
1031
  }
950
1032
 
951
1033
  if (plan.hours.kind === 'step') {
952
1034
  // The plan carries a step only for a clean step (dividing the day):
953
- // confine the cadence to every Nth hour ("in jeder zweiten Stunde").
954
- return base + ' ' +
1035
+ // confine the cadence to every Nth hour ("in jeder zweiten Stunde"). The
1036
+ // hour step is the sole hour authority, so the minute cadence drops its
1037
+ // generic "jeder Stunde".
1038
+ return stepClause(segment, UNITS.minute, '') + ' ' +
955
1039
  everyNthHour(stepSegment(schedule, 'hour'));
956
1040
  }
957
1041
 
@@ -1405,28 +1489,31 @@ function qualifier(schedule: Schedule, months: Months): string {
1405
1489
 
1406
1490
  // Plan kinds whose clause is a clock time: the qualifier leads them ("montags
1407
1491
  // um 9 Uhr"); a frequency clause trails it ("jede Minute montags"). The
1408
- // minute-0 compose-seconds clause is anchored on a clock minute too, so the
1409
- // qualifier leads it ("montags jede Sekunde der Minute 9:00").
1492
+ // single-fixed-minute compose-seconds clause is anchored on a clock minute too,
1493
+ // so the qualifier leads it ("montags jede Sekunde der Minute 9:00").
1410
1494
  const LEADING_PLANS = new Set(['clockTimes']);
1411
1495
 
1412
1496
  // True when the leading qualifier should precede the clause: a clock-time
1413
- // plan, or the minute-0 compose-seconds clause that surfaces a clock minute.
1497
+ // plan, or the single-fixed-minute compose-seconds clause that surfaces a clock
1498
+ // minute.
1414
1499
  function leadsQualifier(schedule: Schedule): boolean {
1415
- return LEADING_PLANS.has(schedule.plan.kind) || isComposeMinuteZero(schedule);
1500
+ return LEADING_PLANS.has(schedule.plan.kind) ||
1501
+ isComposeSingleMinute(schedule);
1416
1502
  }
1417
1503
 
1418
- // Whether the planned clause is the minute-0 compose-seconds confinement
1419
- // (a sub-minute second over a minute-0 clock-time rest).
1420
- function isComposeMinuteZero(schedule: Schedule): boolean {
1504
+ // Whether the planned clause is the single-fixed-minute compose-seconds
1505
+ // confinement (a sub-minute second over a single fixed minute's clock-time
1506
+ // rest).
1507
+ function isComposeSingleMinute(schedule: Schedule): boolean {
1421
1508
  return schedule.plan.kind === 'composeSeconds' &&
1422
- composeMinuteZero(schedule, schedule.plan);
1509
+ composeSingleMinute(schedule, schedule.plan);
1423
1510
  }
1424
1511
 
1425
1512
  // True when the clause is a bare daily clock-time list and so needs the
1426
1513
  // "täglich" frame to read as recurring, not a one-off: clockTimes always, the
1427
- // minute-0 compose-seconds clause (a recurring clock minute), and an uneven
1428
- // hour step (rendered as its fire list "um 0, 5, … Uhr", not the cadence "alle
1429
- // N Stunden"). A frequency clause already implies recurrence.
1514
+ // single-fixed-minute compose-seconds clause (a recurring clock minute), and an
1515
+ // uneven hour step (rendered as its fire list "um 0, 5, … Uhr", not the cadence
1516
+ // "alle N Stunden"). A frequency clause already implies recurrence.
1430
1517
  function needsDailyFrame(schedule: Schedule): boolean {
1431
1518
  // An hour cadence is a sub-daily frequency, not a daily clock-time list, so
1432
1519
  // it must not take the "täglich" frame ("alle 2 Stunden", not "täglich alle
@@ -1435,7 +1522,7 @@ function needsDailyFrame(schedule: Schedule): boolean {
1435
1522
  return false;
1436
1523
  }
1437
1524
 
1438
- if (schedule.plan.kind === 'clockTimes' || isComposeMinuteZero(schedule)) {
1525
+ if (schedule.plan.kind === 'clockTimes' || isComposeSingleMinute(schedule)) {
1439
1526
  return true;
1440
1527
  }
1441
1528