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.
@@ -348,6 +348,14 @@ function renderSecondsWithinMinute(
348
348
  trailingQualifier(schedule, opts);
349
349
  }
350
350
 
351
+ // A second LIST or RANGE under a single minute confines that minute in the
352
+ // genitive ("nos segundos 5 e 10 do minuto 30 de cada hora"), never the comma
353
+ // juxtaposition; a STEP second is a cadence and keeps its own lead.
354
+ if (secondsConfinesMinute(schedule)) {
355
+ return secondsBareLead(schedule) + ' ' +
356
+ confinedMinutePhrase(schedule) + trailingQualifier(schedule, opts);
357
+ }
358
+
351
359
  return secondsLeadClause(schedule, opts) + ', no minuto ' + minuteField +
352
360
  ' de cada hora' + trailingQualifier(schedule, opts);
353
361
  }
@@ -413,6 +421,161 @@ function isPinnedMinuteSeconds(
413
421
  schedule.shapes.second === 'step');
414
422
  }
415
423
 
424
+ // The minute field's step stride for the confinement frame, or null when the
425
+ // minute is not a stepped cadence. A `step`-shaped field reads its segment; a
426
+ // `list`-shaped field the core enumerated from a uneven step (`2/7` → 2,9,…,58)
427
+ // recovers the progression from its values.
428
+ function minuteStride(
429
+ schedule: Schedule
430
+ ): {start: number; interval: number; last: number} | null {
431
+ if (schedule.shapes.minute === 'step') {
432
+ const segment = stepSegment(schedule, 'minute');
433
+ const start = segment.startToken === '*' ? 0 : +segment.startToken;
434
+
435
+ return {interval: segment.interval, last:
436
+ segment.fires[segment.fires.length - 1], start};
437
+ }
438
+
439
+ const values = singleValues(segmentsOf(schedule, 'minute'));
440
+
441
+ return values && arithmeticStep(values);
442
+ }
443
+
444
+ // A stepped minute under a wildcard/stepped second and wildcard hour: bind the
445
+ // second clause leads, a COMMA, then the minute's own STANDALONE cardinal
446
+ // cadence ("a cada segundo, a cada seis minutos a partir do minuto 4 de cada
447
+ // hora"; "nos segundos 5, 10 e 15, a cada seis minutos …"). The ordinal "no
448
+ // sexto minuto" read as a single minute (the 10th), not the every-sixth series;
449
+ // the standalone cardinal "a cada seis minutos" reads it correctly and handles
450
+ // every stride (offset, bounded, uneven) for free. The lead is the cadence
451
+ // clause for a wildcard/stepped second, the bare clock-point clause otherwise.
452
+ function steppedMinuteConfinement(
453
+ schedule: Schedule,
454
+ plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
455
+ lead: string,
456
+ opts: Opts
457
+ ): string {
458
+ return lead + ', ' + render(schedule, plan.rest, opts) +
459
+ trailingQualifier(schedule, opts);
460
+ }
461
+
462
+ // Whether a stepped minute fills a wildcard hour under a wildcard/stepped
463
+ // second — the shape the confinement frame above handles.
464
+ function isSteppedMinuteSeconds(
465
+ schedule: Schedule,
466
+ plan: Extract<PlanNode, {kind: 'composeSeconds'}>
467
+ ): boolean {
468
+ return (plan.rest.kind === 'minuteFrequency' ||
469
+ plan.rest.kind === 'multipleMinutes') &&
470
+ (schedule.shapes.second === 'wildcard' ||
471
+ schedule.shapes.second === 'step') &&
472
+ schedule.shapes.hour === 'wildcard' &&
473
+ schedule.pattern.minute !== '*/2' &&
474
+ minuteStride(schedule) !== null;
475
+ }
476
+
477
+ // The leading seconds words for a clock-point second, WITHOUT the trailing "de
478
+ // cada minuto" anchor: a confined second attaches to the CONFINED minute ("do
479
+ // sexto minuto…"), so the generic minute anchor would be redundant.
480
+ function secondsBareLead(schedule: Schedule): string {
481
+ const secondField = schedule.pattern.second;
482
+ const shape = schedule.shapes.second;
483
+
484
+ if (shape === 'range') {
485
+ const bounds = secondField.split('-');
486
+
487
+ return 'a cada segundo do ' + bounds[0] + ' ao ' + bounds[1];
488
+ }
489
+
490
+ if (shape === 'single') {
491
+ return 'no segundo ' + secondField;
492
+ }
493
+
494
+ return 'nos segundos ' +
495
+ joinList(segmentWords(segmentsOf(schedule, 'second')));
496
+ }
497
+
498
+ // The CONFINED-minute genitive phrase a clock-point second attaches to ("dos
499
+ // minutos 0, 15 e 30 de cada hora", "do minuto 30 de cada hora", "de cada
500
+ // minuto do 0 ao 30 de cada hora"). A stepped minute is handled by the
501
+ // standalone-cadence confinement before this point; a list, range, or single
502
+ // names the minute(s) — so the bare seconds lead never stacks a redundant "de
503
+ // cada minuto".
504
+ function confinedMinutePhrase(schedule: Schedule): string {
505
+ if (schedule.shapes.minute === 'range') {
506
+ // `minuteRangeLead` is "a cada minuto do 0 ao 30"; the genitive "de"
507
+ // absorbs its leading "a" ("de cada minuto …", not "de a cada minuto").
508
+ const range = minuteRangeLead(schedule.pattern.minute).replace(/^a /u, '');
509
+
510
+ return 'de ' + range + ' de cada hora';
511
+ }
512
+
513
+ if (schedule.shapes.minute === 'list') {
514
+ return 'dos minutos ' +
515
+ joinList(segmentWords(segmentsOf(schedule, 'minute'))) + ' de cada hora';
516
+ }
517
+
518
+ return 'do minuto ' + schedule.pattern.minute + ' de cada hora';
519
+ }
520
+
521
+ // Whether a clock-point second (list, range, or single) sits under a restricted
522
+ // minute and a wildcard hour — the shape that must CONFINE the minute in the
523
+ // genitive rather than juxtapose it behind a comma (two independent schedules).
524
+ // A second LIST the core enumerated from a step (`3/2`) is really a stride
525
+ // cadence and stays out. The single-second + single-minute pair folds into one
526
+ // coherent clock point and is excluded.
527
+ function secondsConfinesMinute(schedule: Schedule): boolean {
528
+ const {second, minute, hour} = schedule.shapes;
529
+
530
+ if (second === 'list') {
531
+ const values = singleValues(segmentsOf(schedule, 'second'));
532
+
533
+ if (values && arithmeticStep(values)) {
534
+ return false;
535
+ }
536
+ }
537
+
538
+ const clockPoint = second === 'single' || second === 'range' ||
539
+ second === 'list';
540
+
541
+ return clockPoint && minute !== 'wildcard' && hour === 'wildcard' &&
542
+ !(second === 'single' && minute === 'single');
543
+ }
544
+
545
+ // The minute-confinement rendering for a compose-seconds plan, or null when the
546
+ // plan is not one. A CADENCE second over a stepped minute uses the ordinal
547
+ // cadence form; a CLOCK-POINT second (list/range/single) over any restricted
548
+ // minute uses the genitive form anchored to the confined minute. Both bind the
549
+ // second beneath the minute instead of juxtaposing the two behind a comma.
550
+ // Folded into one helper so `renderComposeSeconds` carries a single branch.
551
+ function minuteConfinementRender(
552
+ plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
553
+ schedule: Schedule, opts: Opts
554
+ ): string | null {
555
+ if (isSteppedMinuteSeconds(schedule, plan)) {
556
+ return steppedMinuteConfinement(schedule, plan,
557
+ secondsLeadClause(schedule, opts), opts);
558
+ }
559
+
560
+ const minuteRest = plan.rest.kind === 'minuteFrequency' ||
561
+ plan.rest.kind === 'multipleMinutes' ||
562
+ plan.rest.kind === 'rangeOfMinutes';
563
+
564
+ if (minuteRest && secondsConfinesMinute(schedule)) {
565
+ // A clock-point second over a STEPPED minute reuses the standalone cardinal
566
+ // cadence the same way; only a list/range/single minute keeps the genitive.
567
+ if (minuteStride(schedule) && schedule.pattern.minute !== '*/2') {
568
+ return steppedMinuteConfinement(schedule, plan,
569
+ secondsBareLead(schedule), opts);
570
+ }
571
+
572
+ return secondsBareLead(schedule) + ' ' +
573
+ confinedMinutePhrase(schedule) + trailingQualifier(schedule, opts);
574
+ }
575
+
576
+ return null;
577
+ }
578
+
416
579
  function renderComposeSeconds(
417
580
  schedule: Schedule,
418
581
  plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
@@ -456,6 +619,17 @@ function renderComposeSeconds(
456
619
  return dayFrame + ', ' + window + ', ' + cadence;
457
620
  }
458
621
 
622
+ // A second confines the minute restriction (open hour), never the comma
623
+ // juxtaposition that reads as two independent cadences: a CADENCE second over
624
+ // a stepped minute uses the ordinal-cadence form ("a cada segundo no sexto
625
+ // minuto …"); a CLOCK-POINT second uses the genitive form anchored to the
626
+ // confined minute ("nos segundos 5, 10 e 15 do sexto minuto …").
627
+ const confined = minuteConfinementRender(plan, schedule, opts);
628
+
629
+ if (confined !== null) {
630
+ return confined;
631
+ }
632
+
459
633
  // A wildcard second under a minute */2 with a wildcard hour juxtaposes two
460
634
  // cadences that read as contradictory ("a cada segundo, a cada dois
461
635
  // minutos"). Bind them with the genitive "de" ("a cada segundo de cada dois
@@ -833,8 +833,12 @@ function secondClause(schedule: Schedule): string {
833
833
 
834
834
  const first = segs[0];
835
835
 
836
- if (segs.length === 1 && first.kind === 'step' && first.startToken === '*') {
837
- return cadence(first.interval, UNITS.second);
836
+ // A STEP-shaped second reads as its stride cadence ("每6秒"), whether written
837
+ // "*/6" or the offset-clean "0/6" — both fire 0,6,…,54 — never the enumerated
838
+ // "第0、6、…、54秒". stepClause routes a clean/offset-clean stride through the
839
+ // bare cadence and only lists a bounded `a-b/n` or a short offset.
840
+ if (segs.length === 1 && first.kind === 'step') {
841
+ return stepClause(first, '秒', '秒', '每分钟');
838
842
  }
839
843
 
840
844
  // An offset/uneven step the core enumerated to this list reads as a stride
@@ -909,10 +913,14 @@ function composeSecondsOnHour(
909
913
  return '每天' + restText + secTail;
910
914
  }
911
915
 
912
- // A stated minute (e.g. minute 0 under a sub-minute second) takes the same
913
- // "" connector the listed-minute path uses.
916
+ // A stated single minute (minute 0 under an open hour) confines the second
917
+ // beneath it with "" when the second is a cadence ("每小时0分的每一秒"), the
918
+ // same fusion the other pinned-minute paths use; the bare comma ("每小时0分,
919
+ // 每秒") reads as two independent cadences. A single/list/range second is a
920
+ // clock-point, not a cadence, so it keeps the "," connector.
914
921
  if (rest.kind === 'singleMinute') {
915
- return restText + ',' + sec;
922
+ return secondIsCadence(schedule) ?
923
+ restText + confinedSecondTail(sec) : restText + ',' + sec;
916
924
  }
917
925
 
918
926
  return restText + secTail;
@@ -1040,6 +1048,13 @@ function composeSecondsCadence(schedule: Schedule): string {
1040
1048
  }
1041
1049
  }
1042
1050
 
1051
+ // A CLOCK-POINT second (a single/list/range, not a stride cadence) under a
1052
+ // clean minute step fuses beneath the minute with "的" ("每6分钟的第30秒"),
1053
+ // never the comma that reads as two independent schedules.
1054
+ if (!secondIsCadence(schedule) && !secondIsStride(schedule)) {
1055
+ return minuteClause(schedule) + '的' + sec;
1056
+ }
1057
+
1043
1058
  return sec + ',' + minuteClause(schedule);
1044
1059
  }
1045
1060
 
@@ -1069,7 +1084,14 @@ function composeSecondsListed(schedule: Schedule): string {
1069
1084
  }
1070
1085
 
1071
1086
  if (schedule.shapes.hour === 'wildcard') {
1072
- return minutes + ',' + sec;
1087
+ // The minute(s) are stated and the hour is open, so the second — whether a
1088
+ // cadence ("每秒"/"每N秒") or a clock-point ("第5、10、15秒") — fuses beneath
1089
+ // the minute(s) with "的" ("每小时30分的每一秒", "每小时0、15、30分的第5、10、15
1090
+ // 秒"). The bare comma ("…,第5、10、15秒") reads as two independent schedules.
1091
+ // A second STRIDE the core enumerated to a list ("3/2" → "每2秒,至59秒") is a
1092
+ // bounded cadence with its own trailing ",至N秒"; it keeps the comma.
1093
+ return secondIsStride(schedule) ?
1094
+ minutes + ',' + sec : minutes + confinedSecondTail(sec);
1073
1095
  }
1074
1096
 
1075
1097
  const hourCad = unevenHourCadence(schedule);
@@ -1084,6 +1106,68 @@ function composeSecondsListed(schedule: Schedule): string {
1084
1106
  return hourFrame(schedule) + minutes + ',' + sec;
1085
1107
  }
1086
1108
 
1109
+ // Whether the minute field is a stepped cadence (a clean `*/n`, an offset
1110
+ // `m/n`, or a uneven step the core enumerated to an arithmetic fire list). The
1111
+ // shape the seconds-wildcard confinement below fuses with "的".
1112
+ function isMinuteStride(schedule: Schedule): boolean {
1113
+ if (schedule.shapes.minute === 'step') {
1114
+ return true;
1115
+ }
1116
+
1117
+ const values = singleValues(segmentsOf(schedule, 'minute'));
1118
+
1119
+ return values !== null && arithmeticStep(values) !== null;
1120
+ }
1121
+
1122
+ // Whether the second is a CADENCE (wildcard "每秒" or a clean step "每N秒") rather
1123
+ // than a clock-point (a single/list/range). A cadence second under a stated
1124
+ // minute fuses beneath it with "的"; a clock-point keeps the "," connector.
1125
+ function secondIsCadence(schedule: Schedule): boolean {
1126
+ return schedule.pattern.second === '*' || schedule.shapes.second === 'step';
1127
+ }
1128
+
1129
+ // Whether a second LIST is really a stride the core enumerated from a step
1130
+ // ("3/2" → 3,5,…,59), spoken as a bounded cadence ("每2秒,至59秒") with its own
1131
+ // trailing comma — not a clock-point list ("第5、10、15秒"). Such a stride keeps
1132
+ // its comma form rather than fusing beneath the minute with "的".
1133
+ function secondIsStride(schedule: Schedule): boolean {
1134
+ if (schedule.shapes.second !== 'list') {
1135
+ return false;
1136
+ }
1137
+
1138
+ const values = singleValues(segmentsOf(schedule, 'second'));
1139
+
1140
+ return values !== null && arithmeticStep(values) !== null;
1141
+ }
1142
+
1143
+ // The "的"-fused second tail for a clause that already states its minute(s):
1144
+ // "的每一秒" for a wildcard second, else "的" + the second's own cadence clause.
1145
+ // The fusion binds the second beneath the minute rather than leaving a bare
1146
+ // trailing "每秒" that reads as a second, independent cadence.
1147
+ function confinedSecondTail(sec: string): string {
1148
+ return sec === '每秒' ? '的每一秒' : '的' + sec;
1149
+ }
1150
+
1151
+ // Whether a compose-seconds plan is a stepped minute under a cadence second and
1152
+ // wildcard hour — the shape the "的"-fused confinement below handles, kept
1153
+ // distinct from the */2 even-minutes idiom and the composed-clock paths.
1154
+ function isSteppedMinuteSeconds(
1155
+ schedule: Schedule, composedClock: boolean
1156
+ ): boolean {
1157
+ return !composedClock && schedule.shapes.hour === 'wildcard' &&
1158
+ secondIsCadence(schedule) && schedule.pattern.minute !== '*/2' &&
1159
+ isMinuteStride(schedule);
1160
+ }
1161
+
1162
+ // A stepped minute under a cadence second and wildcard hour: fuse the minute
1163
+ // cadence and the second cadence with "的" ("每小时从4分起每6分钟的每一秒"), never
1164
+ // the comma juxtaposition ("…每6分钟,每秒") that reads as two independent
1165
+ // cadences. The minute clause carries the offset/bound ("从4分起" / ",至58分").
1166
+ function minuteStrideConfinement(schedule: Schedule): string {
1167
+ return minuteHourClause(schedule) +
1168
+ confinedSecondTail(secondClause(schedule));
1169
+ }
1170
+
1087
1171
  // Seconds composed with the minute/hour structure, dispatched on the minute.
1088
1172
  // A single minute over a composed clock-time rest (the core already joined the
1089
1173
  // lone hour and minute into "N点M分") keeps that composition, attaching the
@@ -1096,6 +1180,13 @@ function renderComposeSeconds(
1096
1180
  const composedClock =
1097
1181
  rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes';
1098
1182
 
1183
+ // A stepped minute under a cadence second and wildcard hour confines the
1184
+ // second beneath the minute cadence with "的", never the comma that reads as
1185
+ // two independent cadences. The */2 step keeps its own "每偶数分钟" idiom.
1186
+ if (isSteppedMinuteSeconds(schedule, composedClock)) {
1187
+ return minuteStrideConfinement(schedule);
1188
+ }
1189
+
1099
1190
  if (schedule.pattern.minute === '0' ||
1100
1191
  composedClock && schedule.shapes.minute === 'single') {
1101
1192
  return composeSecondsOnHour(schedule, plan, opts);