cronli5 0.8.3 → 0.8.6

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.
@@ -144,16 +144,6 @@ 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
-
157
147
  // --- Contractions (the principal es->pt divergence). ---
158
148
  //
159
149
  // Portuguese fuses a preposition with the following article wherever es emitted
@@ -358,6 +348,14 @@ function renderSecondsWithinMinute(
358
348
  trailingQualifier(schedule, opts);
359
349
  }
360
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
+
361
359
  return secondsLeadClause(schedule, opts) + ', no minuto ' + minuteField +
362
360
  ' de cada hora' + trailingQualifier(schedule, opts);
363
361
  }
@@ -444,29 +442,21 @@ function minuteStride(
444
442
  }
445
443
 
446
444
  // 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(
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
453
  schedule: Schedule,
454
- stride: {start: number; interval: number; last: number},
454
+ plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
455
+ lead: string,
455
456
  opts: Opts
456
457
  ): 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);
458
+ return lead + ', ' + render(schedule, plan.rest, opts) +
459
+ trailingQualifier(schedule, opts);
470
460
  }
471
461
 
472
462
  // Whether a stepped minute fills a wildcard hour under a wildcard/stepped
@@ -484,6 +474,108 @@ function isSteppedMinuteSeconds(
484
474
  minuteStride(schedule) !== null;
485
475
  }
486
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
+
487
579
  function renderComposeSeconds(
488
580
  schedule: Schedule,
489
581
  plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
@@ -527,12 +619,15 @@ function renderComposeSeconds(
527
619
  return dayFrame + ', ' + window + ', ' + cadence;
528
620
  }
529
621
 
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);
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;
536
631
  }
537
632
 
538
633
  // A wildcard second under a minute */2 with a wildcard hour juxtaposes two
@@ -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
@@ -1044,6 +1048,13 @@ function composeSecondsCadence(schedule: Schedule): string {
1044
1048
  }
1045
1049
  }
1046
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
+
1047
1058
  return sec + ',' + minuteClause(schedule);
1048
1059
  }
1049
1060
 
@@ -1073,13 +1084,14 @@ function composeSecondsListed(schedule: Schedule): string {
1073
1084
  }
1074
1085
 
1075
1086
  if (schedule.shapes.hour === 'wildcard') {
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;
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);
1083
1095
  }
1084
1096
 
1085
1097
  const hourCad = unevenHourCadence(schedule);
@@ -1114,6 +1126,20 @@ function secondIsCadence(schedule: Schedule): boolean {
1114
1126
  return schedule.pattern.second === '*' || schedule.shapes.second === 'step';
1115
1127
  }
1116
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
+
1117
1143
  // The "的"-fused second tail for a clause that already states its minute(s):
1118
1144
  // "的每一秒" for a wildcard second, else "的" + the second's own cadence clause.
1119
1145
  // The fusion binds the second beneath the minute rather than leaving a bare