cronli5 0.2.0 → 0.2.1

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.
@@ -4,7 +4,8 @@
4
4
  import {pad} from '../../core/format.js';
5
5
  import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
6
6
  import {
7
- arithmeticStep, orderWeekdaysForDisplay, toFieldNumber
7
+ arithmeticStep, hourListStride, offsetCleanStride, orderWeekdaysForDisplay,
8
+ segmentsOf, singleValues, stepSegment, toFieldNumber
8
9
  } from '../../core/util.js';
9
10
  import type {Cronli5Options} from '../../types.js';
10
11
  import type {
@@ -59,11 +60,6 @@ function withAnchor(clause: string, anchor: string): string {
59
60
  return anchor ? clause + ' ' + anchor : clause;
60
61
  }
61
62
 
62
- // The first segment of a step field, which the plan guarantees is step-kinded.
63
- function stepSegment(segments: Segment[] | null): StepSegment {
64
- return (segments as Segment[])[0] as StepSegment;
65
- }
66
-
67
63
  // A step is "clean" when it starts at 0 and evenly divides its cycle (60 for
68
64
  // minutes/seconds, 24 for hours) — only then does "alle N" describe it; an
69
65
  // uneven step fires at discrete points that must be listed.
@@ -131,22 +127,6 @@ function stepClause(segment: StepSegment, unit: Unit, anchor: string): string {
131
127
  });
132
128
  }
133
129
 
134
- // The sorted numeric values a field's segments cover, or null if any segment
135
- // is not a discrete single (a range or sub-step is not a plain fire list).
136
- function singleValues(segments: Segment[]): number[] | null {
137
- const values: number[] = [];
138
-
139
- for (const segment of segments) {
140
- if (segment.kind !== 'single') {
141
- return null;
142
- }
143
-
144
- values.push(+segment.value);
145
- }
146
-
147
- return values;
148
- }
149
-
150
130
  // Speak a minute/second field's enumerated fires as a step cadence when they
151
131
  // form an arithmetic progression long enough to beat the list (the core
152
132
  // enumerates an offset/uneven step to this fire list; the IR is unchanged, so
@@ -177,10 +157,6 @@ const weekdayNames = [
177
157
  'freitags', 'samstags'
178
158
  ];
179
159
 
180
- function fieldSegments(ir: IR, field: Field): Segment[] {
181
- return ir.analyses.segments[field] as Segment[];
182
- }
183
-
184
160
  // Expand step segments into their fires as singles so a name list reads flat.
185
161
  function flattenSteps(segments: Segment[]): NameSegment[] {
186
162
  return segments.flatMap(function flat(segment): NameSegment[] {
@@ -219,7 +195,7 @@ function weekdayRange(bounds: [string, string]): string {
219
195
  function weekdayQualifier(ir: IR): string {
220
196
  // Weekday lists display Monday-first (Sunday last); a lone range keeps its
221
197
  // form. The IR stays canonical (Sunday=0). The helper flattens steps.
222
- const segments = orderWeekdaysForDisplay(fieldSegments(ir, 'weekday'));
198
+ const segments = orderWeekdaysForDisplay(segmentsOf(ir, 'weekday'));
223
199
 
224
200
  if (segments.length === 1 && segments[0].kind === 'range') {
225
201
  return weekdayRange(segments[0].bounds);
@@ -329,7 +305,7 @@ function monthRange(bounds: [string, string], months: Months): string {
329
305
 
330
306
  // Bare month names: "Januar", "Januar und Juli", "von Juni bis August".
331
307
  function monthNamesList(ir: IR, months: Months): string {
332
- return joinList(flattenSteps(fieldSegments(ir, 'month'))
308
+ return joinList(flattenSteps(segmentsOf(ir, 'month'))
333
309
  .map(function name(segment): string {
334
310
  return segment.kind === 'range' ?
335
311
  monthRange(segment.bounds, months) :
@@ -340,7 +316,7 @@ function monthNamesList(ir: IR, months: Months): string {
340
316
  // The month qualifier: "im Januar", "im Januar und Juli", "von Juni bis
341
317
  // August". A lone range carries its own "von … bis"; names take "im".
342
318
  function monthClause(ir: IR, months: Months): string {
343
- const segments = flattenSteps(fieldSegments(ir, 'month'));
319
+ const segments = flattenSteps(segmentsOf(ir, 'month'));
344
320
 
345
321
  if (segments.length === 1 && segments[0].kind === 'range') {
346
322
  return monthRange(segments[0].bounds, months);
@@ -367,7 +343,7 @@ function dateRange(bounds: [string, string]): string {
367
343
  // The bare date clause, without a month: "am 1.", "am 1. und 15.", "vom 1.
368
344
  // bis zum 5.", "vom 1. bis zum 5. und am 10.".
369
345
  function dateClauseBare(ir: IR): string {
370
- const segments = flattenSteps(fieldSegments(ir, 'date'));
346
+ const segments = flattenSteps(segmentsOf(ir, 'date'));
371
347
 
372
348
  if (segments.length === 1 && segments[0].kind === 'range') {
373
349
  return dateRange(segments[0].bounds);
@@ -399,7 +375,7 @@ function datePhrase(ir: IR, months: Months): string {
399
375
  return clause;
400
376
  }
401
377
 
402
- const monthRanged = flattenSteps(fieldSegments(ir, 'month'))
378
+ const monthRanged = flattenSteps(segmentsOf(ir, 'month'))
403
379
  .some((segment) => segment.kind === 'range');
404
380
 
405
381
  return monthRanged ?
@@ -450,7 +426,7 @@ function hourWindow(
450
426
 
451
427
  // A field's values as strings, a range rendered "a bis b".
452
428
  function fieldValues(ir: IR, field: Field): string[] {
453
- return flattenSteps(fieldSegments(ir, field)).map(function value(segment) {
429
+ return flattenSteps(segmentsOf(ir, field)).map(function value(segment) {
454
430
  return segment.kind === 'range' ?
455
431
  segment.bounds[0] + ' bis ' + segment.bounds[1] :
456
432
  String(segment.value);
@@ -504,7 +480,7 @@ function secondsClause(ir: IR, anchor: string): string {
504
480
  // enumerated to a list is recognized as a progression. Both fall back to the
505
481
  // counted list (a short or irregular set).
506
482
  if (ir.shapes.second === 'step') {
507
- return stepClause(stepSegment(segments), UNITS.second, anchor);
483
+ return stepClause(stepSegment(ir, 'second'), UNITS.second, anchor);
508
484
  }
509
485
 
510
486
  return strideFromSegments(segments as Segment[], UNITS.second, anchor) ??
@@ -523,7 +499,7 @@ function atHours(hours: number[]): string {
523
499
 
524
500
  // The discrete hour fires, single and step values flattened: [9, 17, 19, …].
525
501
  function hourFires(ir: IR): number[] {
526
- return flattenSteps(fieldSegments(ir, 'hour')).map(function fire(segment) {
502
+ return flattenSteps(segmentsOf(ir, 'hour')).map(function fire(segment) {
527
503
  return segment.kind === 'range' ? +segment.bounds[0] : +segment.value;
528
504
  });
529
505
  }
@@ -550,7 +526,7 @@ function hourSegmentParts(
550
526
  second: number | undefined,
551
527
  sep: string
552
528
  ): string[] {
553
- return fieldSegments(ir, 'hour').map(function part(segment): string {
529
+ return segmentsOf(ir, 'hour').map(function part(segment): string {
554
530
  if (segment.kind === 'range') {
555
531
  return 'von ' + partTime(+segment.bounds[0], minute, second, sep) +
556
532
  ' bis ' + partTime(+segment.bounds[1], minute, second, sep) + ' Uhr';
@@ -575,7 +551,7 @@ function duringWindows(ir: IR, times: HourTimesPlan, sep: string): string[] {
575
551
  });
576
552
  }
577
553
 
578
- return fieldSegments(ir, 'hour').flatMap(function part(segment): string[] {
554
+ return segmentsOf(ir, 'hour').flatMap(function part(segment): string[] {
579
555
  if (segment.kind === 'range') {
580
556
  return [hourWindow(+segment.bounds[0], +segment.bounds[1], 59, sep)];
581
557
  }
@@ -634,7 +610,7 @@ function renderSeconds(ir: IR): string {
634
610
  // enumerated to this list reads as a stride cadence when the fires form a
635
611
  // long-enough progression ("alle 2 Minuten von Minute 3 bis 59 jeder Stunde").
636
612
  function minutePastClause(ir: IR): string {
637
- return strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute,
613
+ return strideFromSegments(segmentsOf(ir, 'minute'), UNITS.minute,
638
614
  'jeder Stunde') ??
639
615
  countedPhrase(ir, 'minute', 'Minute', 'Minuten') + ' jeder Stunde';
640
616
  }
@@ -706,7 +682,7 @@ function isEveryOtherMinuteSeconds(
706
682
  return false;
707
683
  }
708
684
 
709
- const minuteStep = stepSegment(ir.analyses.segments.minute);
685
+ const minuteStep = stepSegment(ir, 'minute');
710
686
 
711
687
  return minuteStep.startToken === '*' && minuteStep.interval === 2;
712
688
  }
@@ -806,7 +782,7 @@ function renderMinutesAcrossHours(
806
782
  }
807
783
 
808
784
  const minuteLead =
809
- strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute, '') ??
785
+ strideFromSegments(segmentsOf(ir, 'minute'), UNITS.minute, '') ??
810
786
  countedPhrase(ir, 'minute', 'Minute', 'Minuten');
811
787
 
812
788
  if (cadence !== null) {
@@ -836,19 +812,19 @@ function renderMinuteSpanAcrossHourStep(
836
812
  // bounded or uneven step routes through minutesAcrossHours instead).
837
813
  if (plan.form === 'wildcard') {
838
814
  return 'jede Minute ' +
839
- everyNthHour(stepSegment(ir.analyses.segments.hour));
815
+ everyNthHour(stepSegment(ir, 'hour'));
840
816
  }
841
817
 
842
818
  // The minute (range or list) leads; the hour trails. A clean stride confines
843
819
  // to "in jeder N-ten Stunde" — the same cadence the wildcard form and the
844
820
  // minute-step compositions use, never a juxtaposed second frequency. A
845
821
  // bounded or uneven stride trails its endpoint-pinning cadence instead.
846
- const segment = stepSegment(ir.analyses.segments.hour);
822
+ const segment = stepSegment(ir, 'hour');
847
823
  const hours = cadence ?? (confinedHourStride(segment) ?
848
824
  everyNthHour(segment) :
849
825
  atHours(segment.fires));
850
826
 
851
- return (strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute, '') ??
827
+ return (strideFromSegments(segmentsOf(ir, 'minute'), UNITS.minute, '') ??
852
828
  countedPhrase(ir, 'minute', 'Minute', 'Minuten')) + ', ' + hours;
853
829
  }
854
830
 
@@ -875,7 +851,7 @@ function renderCompactClockTimes(
875
851
  return cadence;
876
852
  }
877
853
 
878
- const hourly = fieldSegments(ir, 'hour')
854
+ const hourly = segmentsOf(ir, 'hour')
879
855
  .some((segment) => segment.kind === 'range');
880
856
 
881
857
  return (hourly ? 'stündlich ' : 'täglich ') +
@@ -885,7 +861,7 @@ function renderCompactClockTimes(
885
861
  // A bounded or uneven hour stride reads as its endpoint-pinning cadence; else
886
862
  // a range among the hours reads as a window, otherwise a flat hour list.
887
863
  const hours = unevenHourCadence(ir) ??
888
- (fieldSegments(ir, 'hour').some((segment) => segment.kind === 'range') ?
864
+ (segmentsOf(ir, 'hour').some((segment) => segment.kind === 'range') ?
889
865
  joinList(hourSegmentParts(ir, 0, 0, sep)) :
890
866
  atHours(hourFires(ir)));
891
867
 
@@ -896,7 +872,7 @@ function renderCompactClockTimes(
896
872
  countedPhrase(ir, 'second', 'Sekunde', 'Sekunden') + ', ' : '';
897
873
 
898
874
  return lead +
899
- (strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute, '') ??
875
+ (strideFromSegments(segmentsOf(ir, 'minute'), UNITS.minute, '') ??
900
876
  countedPhrase(ir, 'minute', 'Minute', 'Minuten')) + ', ' + hours;
901
877
  }
902
878
 
@@ -907,7 +883,7 @@ function renderMinuteFrequency(
907
883
  plan: Extract<PlanNode, {kind: 'minuteFrequency'}>,
908
884
  opts: Opts
909
885
  ): string {
910
- const segment = stepSegment(ir.analyses.segments.minute);
886
+ const segment = stepSegment(ir, 'minute');
911
887
  const sep = opts.style.sep;
912
888
  const clean = cleanStep(segment, 60);
913
889
 
@@ -941,7 +917,7 @@ function renderMinuteFrequency(
941
917
  // The plan carries a step only for a clean step (dividing the day):
942
918
  // confine the cadence to every Nth hour ("in jeder zweiten Stunde").
943
919
  return base + ' ' +
944
- everyNthHour(stepSegment(ir.analyses.segments.hour));
920
+ everyNthHour(stepSegment(ir, 'hour'));
945
921
  }
946
922
 
947
923
  return base;
@@ -961,7 +937,7 @@ function hourStepPhrase(ir: IR): string {
961
937
  return cadence;
962
938
  }
963
939
 
964
- const segment = stepSegment(ir.analyses.segments.hour);
940
+ const segment = stepSegment(ir, 'hour');
965
941
 
966
942
  if (cleanStep(segment, 24)) {
967
943
  return everyN(segment.interval, UNITS.hour);
@@ -1020,51 +996,6 @@ function hourStrideCadence(
1020
996
  return cadence + ' von ' + start + ' bis ' + last + ' Uhr';
1021
997
  }
1022
998
 
1023
- // An hour list's arithmetic progression, or null when its values are not a step
1024
- // the renderer should speak as a cadence. The core rewrites a uneven hour step
1025
- // (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its literal
1026
- // fire list, indistinguishable in the IR from a hand-written list; the renderer
1027
- // recovers the cadence from the values. A progression starting at zero is a
1028
- // `*/n` step however short (0,7,14,21 is `*/7`); a non-zero progression is only
1029
- // a step when it is too long to be a deliberate clock-time list (9,17 is two
1030
- // named times, not a cadence). Interval one is a plain range, never a step.
1031
- function hourListStride(
1032
- values: number[]
1033
- ): {start: number; interval: number; last: number} | null {
1034
- if (values.length < 2) {
1035
- return null;
1036
- }
1037
-
1038
- const interval = values[1] - values[0];
1039
-
1040
- if (interval < 2) {
1041
- return null;
1042
- }
1043
-
1044
- for (let i = 2; i < values.length; i += 1) {
1045
- if (values[i] - values[i - 1] !== interval) {
1046
- return null;
1047
- }
1048
- }
1049
-
1050
- if (values[0] !== 0 && values.length < 5) {
1051
- return null;
1052
- }
1053
-
1054
- return {interval, last: values[values.length - 1], start: values[0]};
1055
- }
1056
-
1057
- // Whether an hour stride wraps the day cleanly from within its first interval
1058
- // (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
1059
- // stride has no distinct endpoint and keeps its bare or "ab" cadence. Every
1060
- // other stride — a uneven interval, or one starting at or past its interval (a
1061
- // bounded `a-b/n`) — is a bounded set the cadence pins both endpoints of.
1062
- function offsetCleanStride(
1063
- stride: {start: number; interval: number}
1064
- ): boolean {
1065
- return stride.start < stride.interval && 24 % stride.interval === 0;
1066
- }
1067
-
1068
999
  // The hour field's stride, or null when the hour is not a cadence: a step
1069
1000
  // segment yields its {start, interval, last} directly; an all-single hour list
1070
1001
  // yields one only when its values form a step progression (so an irregular list
@@ -1073,7 +1004,7 @@ function offsetCleanStride(
1073
1004
  function hourStride(
1074
1005
  ir: IR
1075
1006
  ): {start: number; interval: number; last: number} | null {
1076
- const segments = fieldSegments(ir, 'hour');
1007
+ const segments = segmentsOf(ir, 'hour');
1077
1008
 
1078
1009
  // A wildcard hour carries no segments (no discrete hours to stride over).
1079
1010
  if (!segments) {
@@ -1186,9 +1117,9 @@ function hourCadence(ir: IR, minute: number): string | null {
1186
1117
  // stride is a confinement, not a juxtaposed cadence: it reads "für eine
1187
1118
  // Minute in jeder zweiten Stunde", reusing the every-Nth-hour idiom so the
1188
1119
  // minute-0 window is never heard as the bare hour cadence.
1189
- const segment = fieldSegments(ir, 'hour')[0];
1120
+ const segment = segmentsOf(ir, 'hour')[0];
1190
1121
  const confined = minute === 0 && subMinuteSecond(ir) &&
1191
- fieldSegments(ir, 'hour').length === 1 && segment.kind === 'step' &&
1122
+ segmentsOf(ir, 'hour').length === 1 && segment.kind === 'step' &&
1192
1123
  confinedHourStride(segment);
1193
1124
 
1194
1125
  if (confined) {
@@ -1225,7 +1156,7 @@ function hourCadenceApplies(ir: IR): boolean {
1225
1156
  // A pure single-value list (9,17) has no range to span and still enumerates;
1226
1157
  // a step is handled by hourStride/hourCadence.
1227
1158
  function hasHourWindow(ir: IR): boolean {
1228
- const segments = fieldSegments(ir, 'hour');
1159
+ const segments = segmentsOf(ir, 'hour');
1229
1160
 
1230
1161
  return !!segments && segments.some(function range(segment) {
1231
1162
  return segment.kind === 'range';
@@ -1288,7 +1219,7 @@ function renderHourRange(
1288
1219
  // bounded cadence ("alle 2 Minuten von Minute 3 bis 59 jeder Stunde") instead
1289
1220
  // of the wall of fires; an irregular list or a single minute keeps the
1290
1221
  // counted form.
1291
- return (strideFromSegments(fieldSegments(ir, 'minute'), UNITS.minute,
1222
+ return (strideFromSegments(segmentsOf(ir, 'minute'), UNITS.minute,
1292
1223
  'jeder Stunde') ??
1293
1224
  countedPhrase(ir, 'minute', 'Minute', 'Minuten') + ' jeder Stunde') +
1294
1225
  ', ' + window;
@@ -1412,7 +1343,7 @@ function needsDailyFrame(ir: IR): boolean {
1412
1343
  // frequency, not a daily clock-time list, so it takes no "täglich" frame —
1413
1344
  // only a bounded `a-b/n` step that enumerates its hours ("um 1, 3, … Uhr")
1414
1345
  // needs the recurring frame.
1415
- const segment = stepSegment(ir.analyses.segments.hour);
1346
+ const segment = stepSegment(ir, 'hour');
1416
1347
 
1417
1348
  return !cleanStep(segment, 24) && !openOffsetCleanStride(ir, segment);
1418
1349
  }