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.
@@ -11,8 +11,10 @@
11
11
 
12
12
  import {clockDigits, numeral} from '../../core/format.js';
13
13
  import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
14
+ import {isOpenStep} from '../../core/shapes.js';
14
15
  import {
15
- arithmeticStep, orderWeekdaysForDisplay, toFieldNumber
16
+ arithmeticStep, hourListStride, offsetCleanStride, orderWeekdaysForDisplay,
17
+ segmentsOf, singleValues, stepSegment, toFieldNumber
16
18
  } from '../../core/util.js';
17
19
  import {resolveDialect} from './dialects.js';
18
20
  import type {
@@ -39,12 +41,6 @@ interface HourWindow {
39
41
  last: number;
40
42
  }
41
43
 
42
- // The first segment of a step field, narrowed to its step variant. Step
43
- // shapes always classify their (single) segment as a step.
44
- function stepSegment(segments: Segment[]): StepSegment {
45
- return segments[0] as StepSegment;
46
- }
47
-
48
44
  // A `{hour, minute, second?}` time end for the digit/range helpers.
49
45
  interface TimeParts {
50
46
  hour: number;
@@ -313,7 +309,7 @@ function composeSecondsOverMinuteStep(
313
309
  freq: Extract<PlanNode, {kind: 'minuteFrequency'}>,
314
310
  opts: NormalizedOptions
315
311
  ): string {
316
- const seg = stepSegment(ir.analyses.segments.minute!);
312
+ const seg = stepSegment(ir, 'minute');
317
313
  const stepPhrase = stepCycle60(seg, units.minute, opts);
318
314
 
319
315
  if (freq.hours.kind === 'during' && minuteStepIsAnchored(seg)) {
@@ -336,7 +332,7 @@ function composeSecondsOverMinuteStep(
336
332
  }
337
333
  else if (freq.hours.kind === 'step') {
338
334
  hourClause = ' ' +
339
- everyNthHour(stepSegment(ir.analyses.segments.hour!), opts);
335
+ everyNthHour(stepSegment(ir, 'hour'), opts);
340
336
  }
341
337
 
342
338
  return stepPhrase + ', ' + secondsLeadClause(ir, opts) +
@@ -428,7 +424,7 @@ function isEveryOtherMinuteSeconds(
428
424
  return false;
429
425
  }
430
426
 
431
- const seg = stepSegment(ir.analyses.segments.minute!);
427
+ const seg = stepSegment(ir, 'minute');
432
428
 
433
429
  return seg.startToken === '*' && seg.interval === 2;
434
430
  }
@@ -466,7 +462,7 @@ function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
466
462
 
467
463
  if (shape === 'step') {
468
464
  // A step shape always has segments whose first is a step segment.
469
- return stepCycle60(stepSegment(ir.analyses.segments.second!),
465
+ return stepCycle60(stepSegment(ir, 'second'),
470
466
  units.second, opts);
471
467
  }
472
468
 
@@ -480,8 +476,8 @@ function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
480
476
 
481
477
  // An offset/uneven step the core enumerated to this list reads as a stride
482
478
  // cadence when the fires form a long-enough progression.
483
- return strideFromSegments(ir.analyses.segments.second!, units.second, opts) ??
484
- atMarks(joinList(segmentWords(ir.analyses.segments.second!)),
479
+ return strideFromSegments(segmentsOf(ir, 'second'), units.second, opts) ??
480
+ atMarks(joinList(segmentWords(segmentsOf(ir, 'second'))),
485
481
  units.second, marked);
486
482
  }
487
483
 
@@ -525,8 +521,8 @@ function renderMultipleMinutes(
525
521
  // the fires form a long-enough progression ("kahden minuutin välein
526
522
  // minuutista 3 minuuttiin 59").
527
523
  function minutesList(ir: IR, opts: NormalizedOptions): string {
528
- return strideFromSegments(ir.analyses.segments.minute!, units.minute, opts) ??
529
- atMarks(joinList(segmentWords(ir.analyses.segments.minute!)),
524
+ return strideFromSegments(segmentsOf(ir, 'minute'), units.minute, opts) ??
525
+ atMarks(joinList(segmentWords(segmentsOf(ir, 'minute'))),
530
526
  units.minute, true);
531
527
  }
532
528
 
@@ -535,8 +531,8 @@ function minutesList(ir: IR, opts: NormalizedOptions): string {
535
531
  // kohdalla". A progression reads as its bounded cadence (which carries no
536
532
  // per-hour frequency to drop).
537
533
  function bareMinutes(ir: IR, opts: NormalizedOptions): string {
538
- return strideFromSegments(ir.analyses.segments.minute!, units.minute, opts) ??
539
- atMarks(joinList(segmentWords(ir.analyses.segments.minute!)),
534
+ return strideFromSegments(segmentsOf(ir, 'minute'), units.minute, opts) ??
535
+ atMarks(joinList(segmentWords(segmentsOf(ir, 'minute'))),
540
536
  units.minute, false);
541
537
  }
542
538
 
@@ -596,14 +592,14 @@ function hoursFirstMinutes(
596
592
  // cadence ("aina kahden minuutin välein minuutista 3 minuuttiin 59") when
597
593
  // the fires form a long-enough progression, rather than the kohdalla list.
598
594
  const stride =
599
- strideFromSegments(ir.analyses.segments.minute!, units.minute, opts);
595
+ strideFromSegments(segmentsOf(ir, 'minute'), units.minute, opts);
600
596
 
601
597
  if (stride) {
602
598
  return hoursStr + ' aina ' + stride;
603
599
  }
604
600
 
605
601
  return hoursStr + ' aina minuuttien ' +
606
- joinList(segmentWords(ir.analyses.segments.minute!)) + ' kohdalla';
602
+ joinList(segmentWords(segmentsOf(ir, 'minute'))) + ' kohdalla';
607
603
  }
608
604
 
609
605
  // Hour segment times for a range+isolated pattern: joins the isolated hour
@@ -617,7 +613,7 @@ function hourSegmentTimesWithSeka(
617
613
  ): string {
618
614
  const pieces: string[] = [];
619
615
 
620
- ir.analyses.segments.hour!.forEach(function clock(segment: Segment) {
616
+ segmentsOf(ir, 'hour').forEach(function clock(segment: Segment) {
621
617
  if (segment.kind === 'range') {
622
618
  pieces.push(rangeDigits(
623
619
  {hour: +segment.bounds[0], minute, second},
@@ -638,7 +634,7 @@ function renderMinuteFrequency(
638
634
  plan: Extract<PlanNode, {kind: 'minuteFrequency'}>,
639
635
  opts: NormalizedOptions
640
636
  ): string {
641
- const seg = stepSegment(ir.analyses.segments.minute!);
637
+ const seg = stepSegment(ir, 'minute');
642
638
 
643
639
  if (plan.hours.kind === 'during') {
644
640
  // A bounded or uneven hour stride reads as its endpoint-pinning cadence
@@ -673,7 +669,7 @@ function renderMinuteFrequency(
673
669
  }
674
670
  else if (plan.hours.kind === 'step') {
675
671
  phrase += ' ' +
676
- everyNthHour(stepSegment(ir.analyses.segments.hour!), opts);
672
+ everyNthHour(stepSegment(ir, 'hour'), opts);
677
673
  }
678
674
 
679
675
  return phrase + trailingQualifier(ir, opts);
@@ -700,7 +696,7 @@ function renderMinuteSpanInHour(
700
696
  }
701
697
 
702
698
  // A minute window under discrete hours. Like Spanish, the wildcard form
703
- // re-strategizes to per-hour windows; restricted minutes drop the
699
+ // re-plans to per-hour windows; restricted minutes drop the
704
700
  // "jokaisen tunnin" anchor, which the specific hours would contradict.
705
701
  // A range or multi-point list over enumerated hours renders hours-first
706
702
  // ("klo <hours> aina minuuttien <spec> kohdalla"); a range+isolated hour
@@ -728,7 +724,7 @@ function renderMinutesAcrossHours(
728
724
  }
729
725
 
730
726
  // Range+isolated hours: minute-first, bare minutes, sekä klo.
731
- if (hoursAreRangeIsolated(ir.analyses.segments.hour!)) {
727
+ if (hoursAreRangeIsolated(segmentsOf(ir, 'hour'))) {
732
728
  return bareMinutes(ir, opts) + ' ' +
733
729
  hourSegmentTimesWithSeka(ir, 0, null, opts) +
734
730
  trailingQualifier(ir, opts);
@@ -748,7 +744,7 @@ function renderMinuteSpanAcrossHourStep(
748
744
  opts: NormalizedOptions
749
745
  ): string {
750
746
  // An hour-step plan's first hour segment is always a step segment.
751
- const segment = stepSegment(ir.analyses.segments.hour!);
747
+ const segment = stepSegment(ir, 'hour');
752
748
  // A bounded or uneven hour stride reads as its endpoint-pinning cadence; an
753
749
  // offset-clean stride keeps its confinement / per-step phrasing.
754
750
  const cadence = unevenHourCadence(ir, opts);
@@ -884,7 +880,7 @@ function renderHourStep(
884
880
  return cadence + trailingQualifier(ir, opts);
885
881
  }
886
882
 
887
- return stepHours(stepSegment(ir.analyses.segments.hour!), opts) +
883
+ return stepHours(stepSegment(ir, 'hour'), opts) +
888
884
  trailingQualifier(ir, opts);
889
885
  }
890
886
 
@@ -959,7 +955,7 @@ function renderCompactClockTimes(
959
955
  }
960
956
  }
961
957
 
962
- const hourSegs = ir.analyses.segments.hour!;
958
+ const hourSegs = segmentsOf(ir, 'hour');
963
959
 
964
960
  // Range+isolated hours: join the isolated hour with "sekä klo" to stop it
965
961
  // reading as a range extension. For the folded path (single minute folded
@@ -1082,21 +1078,6 @@ function strideFromSegments(
1082
1078
  null;
1083
1079
  }
1084
1080
 
1085
- // The sorted numeric values a field's segments cover, or null if any segment
1086
- // is not a discrete single (a range or sub-step is not a plain fire list).
1087
- function singleValues(segments: Segment[]): number[] | null {
1088
- const values: number[] = [];
1089
-
1090
- for (const segment of segments) {
1091
- if (segment.kind !== 'single') {
1092
- return null;
1093
- }
1094
-
1095
- values.push(+segment.value);
1096
- }
1097
-
1098
- return values;
1099
- }
1100
1081
 
1101
1082
  // "viiden minuutin välein", "joka tunti 0 ja 31 minuutin kohdalla", or
1102
1083
  // "kolmen minuutin välein jokaisen tunnin minuutista 1 alkaen". A step shape
@@ -1181,51 +1162,6 @@ function hourStrideCadence(
1181
1162
  kloRange({hour: start, minute: 0}, {hour: last, minute: 0}, opts);
1182
1163
  }
1183
1164
 
1184
- // An hour list's arithmetic progression, or null when its values are not a step
1185
- // the renderer should speak as a cadence. The core rewrites a uneven hour step
1186
- // (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its literal
1187
- // fire list, indistinguishable in the IR from a hand-written list; the renderer
1188
- // recovers the cadence from the values. A progression starting at zero is a
1189
- // `*/n` step however short (0,7,14,21 is `*/7`); a non-zero progression is only
1190
- // a step when it is too long to be a deliberate clock-time list (9,17 is two
1191
- // named times, not a cadence). Interval one is a plain range, never a step.
1192
- function hourListStride(
1193
- values: number[]
1194
- ): {start: number; interval: number; last: number} | null {
1195
- if (values.length < 2) {
1196
- return null;
1197
- }
1198
-
1199
- const interval = values[1] - values[0];
1200
-
1201
- if (interval < 2) {
1202
- return null;
1203
- }
1204
-
1205
- for (let i = 2; i < values.length; i += 1) {
1206
- if (values[i] - values[i - 1] !== interval) {
1207
- return null;
1208
- }
1209
- }
1210
-
1211
- if (values[0] !== 0 && values.length < 5) {
1212
- return null;
1213
- }
1214
-
1215
- return {interval, last: values[values.length - 1], start: values[0]};
1216
- }
1217
-
1218
- // Whether an hour stride wraps the day cleanly from within its first interval
1219
- // (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
1220
- // stride has no distinct endpoint and keeps its bare or "alkaen" cadence. Every
1221
- // other stride — a uneven interval, or one starting at or past its interval (a
1222
- // bounded `a-b/n`) — is a bounded set the cadence pins both endpoints of.
1223
- function offsetCleanStride(
1224
- stride: {start: number; interval: number}
1225
- ): boolean {
1226
- return stride.start < stride.interval && 24 % stride.interval === 0;
1227
- }
1228
-
1229
1165
  // The hour field's stride, or null when the hour is not a cadence: a step
1230
1166
  // segment yields its {start, interval, last} directly; an all-single hour list
1231
1167
  // yields one only when its values form a step progression (so an irregular list
@@ -1348,9 +1284,9 @@ function hourCadence(ir: IR, minute: number,
1348
1284
  // stride is a confinement, not a juxtaposed cadence: it reads "minuutin ajan
1349
1285
  // joka toisen tunnin aikana", reusing the every-Nth-hour idiom so the
1350
1286
  // minute-0 window is never heard as the bare hour cadence.
1351
- const segment = ir.analyses.segments.hour![0];
1287
+ const segment = segmentsOf(ir, 'hour')[0];
1352
1288
  const confined = minute === 0 && subMinuteSecond(ir) &&
1353
- ir.analyses.segments.hour!.length === 1 && segment.kind === 'step' &&
1289
+ segmentsOf(ir, 'hour').length === 1 && segment.kind === 'step' &&
1354
1290
  cleanHourStride(segment);
1355
1291
 
1356
1292
  if (confined) {
@@ -1400,7 +1336,7 @@ function hasHourWindow(ir: IR): boolean {
1400
1336
  // window uses. The minute has folded into the lead, so the window closes on
1401
1337
  // the top of its final hour.
1402
1338
  function hourRangeWindowTail(ir: IR, opts: NormalizedOptions): string {
1403
- return ir.analyses.segments.hour!.length === 1 ?
1339
+ return segmentsOf(ir, 'hour').length === 1 ?
1404
1340
  hourSegmentTimes(ir, 0, null, opts) :
1405
1341
  hourSegmentTimesWithSeka(ir, 0, null, opts);
1406
1342
  }
@@ -1473,7 +1409,7 @@ function hourWindowsFromTimes(
1473
1409
  return kloList(times.fires, opts);
1474
1410
  }
1475
1411
 
1476
- const segments = ir.analyses.segments.hour!;
1412
+ const segments = segmentsOf(ir, 'hour');
1477
1413
 
1478
1414
  if (!segments.some(function ranged(segment: Segment) {
1479
1415
  return segment.kind === 'range';
@@ -1534,7 +1470,7 @@ function hourSegmentTimes(
1534
1470
  ): string {
1535
1471
  const pieces: string[] = [];
1536
1472
 
1537
- ir.analyses.segments.hour!.forEach(function clock(segment: Segment) {
1473
+ segmentsOf(ir, 'hour').forEach(function clock(segment: Segment) {
1538
1474
  if (segment.kind === 'step') {
1539
1475
  pieces.push(...segment.fires.map(function each(hour: number) {
1540
1476
  return timeDigits(hour, minute, second, opts);
@@ -1707,7 +1643,7 @@ function weekdayQualifier(ir: IR): string {
1707
1643
 
1708
1644
  // Weekday lists display Monday-first (Sunday last); a lone range keeps its
1709
1645
  // form. The IR stays canonical (Sunday=0). The helper flattens steps.
1710
- const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday!);
1646
+ const segments = orderWeekdaysForDisplay(segmentsOf(ir, 'weekday'));
1711
1647
 
1712
1648
  return joinList(segments.map(function piece(segment: FlatSegment) {
1713
1649
  if (segment.kind === 'range') {
@@ -1723,7 +1659,7 @@ function weekdayQualifier(ir: IR): string {
1723
1659
  // elative–illative ranges ("kesäkuusta syyskuuhun"). The case endings
1724
1660
  // keep mixed lists unambiguous with no preposition bookkeeping.
1725
1661
  function monthPhrase(ir: IR): string {
1726
- const segments = flattenSteps(ir.analyses.segments.month!);
1662
+ const segments = flattenSteps(segmentsOf(ir, 'month'));
1727
1663
 
1728
1664
  return joinList(segments.map(function piece(segment: FlatSegment) {
1729
1665
  if (segment.kind === 'range') {
@@ -1808,7 +1744,7 @@ function monthAnchor(ir: IR, opts: NormalizedOptions): string {
1808
1744
  return stepMonths(monthField, opts);
1809
1745
  }
1810
1746
 
1811
- const segments = flattenSteps(ir.analyses.segments.month!);
1747
+ const segments = flattenSteps(segmentsOf(ir, 'month'));
1812
1748
 
1813
1749
  return joinList(segments.map(function genitiveOf(segment: FlatSegment) {
1814
1750
  // The anchor branch is only reached for non-ranged months, so every
@@ -1827,7 +1763,7 @@ function rangedMonthScope(ir: IR): string {
1827
1763
  // Whether the month field contains a range segment.
1828
1764
  function monthRanged(ir: IR): boolean {
1829
1765
  return ir.pattern.month !== '*' &&
1830
- ir.analyses.segments.month!.some(function range(segment: Segment) {
1766
+ segmentsOf(ir, 'month').some(function range(segment: Segment) {
1831
1767
  return segment.kind === 'range';
1832
1768
  });
1833
1769
  }
@@ -1835,7 +1771,7 @@ function monthRanged(ir: IR): boolean {
1835
1771
  // The day-of-month words: "13.", "1. ja 15.", "1.–15.", with step
1836
1772
  // segments expanded into their fires.
1837
1773
  function dateWords(ir: IR): string {
1838
- return joinList(ir.analyses.segments.date!.flatMap(
1774
+ return joinList(segmentsOf(ir, 'date').flatMap(
1839
1775
  function word(segment: Segment): string[] {
1840
1776
  if (segment.kind === 'range') {
1841
1777
  return [segment.bounds[0] + '.–' + segment.bounds[1] + '.'];
@@ -2008,13 +1944,6 @@ function segmentWords(segments: Segment[]): string[] {
2008
1944
  });
2009
1945
  }
2010
1946
 
2011
- // Whether a canonical field value is an "open" step (`*/n` or `a/n`, not
2012
- // a bounded range or a list). Open steps read as a frequency rather than
2013
- // an enumeration.
2014
- function isOpenStep(field: string): boolean {
2015
- return field.indexOf('/') !== -1 && field.indexOf('-') === -1 &&
2016
- field.indexOf(',') === -1;
2017
- }
2018
1947
 
2019
1948
  // Numeric fire values as digits.
2020
1949
  function wordList(fires: number[]): string[] {
@@ -5,12 +5,13 @@
5
5
  // day periods under `ampm`. The style contract is src/lang/zh/notes.md.
6
6
 
7
7
  import {
8
- arithmeticStep, orderWeekdaysForDisplay, toFieldNumber
8
+ arithmeticStep, orderWeekdaysForDisplay, segmentsOf, singleValues,
9
+ stepSegment, toFieldNumber
9
10
  } from '../../core/util.js';
10
11
  import {maxClockTimes, monthNumbers, weekdayNumbers} from '../../core/specs.js';
11
12
  import type {Cronli5Options} from '../../types.js';
12
13
  import type {
13
- Field, IR, Language, NormalizedOptions, PlanNode, Segment
14
+ IR, Language, NormalizedOptions, PlanNode, Segment
14
15
  } from '../../core/ir.js';
15
16
  import {resolveDialect, type ChineseStyle} from './dialects.js';
16
17
 
@@ -30,16 +31,6 @@ function joinAnd(items: string[]): string {
30
31
  return items.slice(0, -1).join('、') + '和' + items[items.length - 1];
31
32
  }
32
33
 
33
- // A field's classified segments (empty when the field is a wildcard).
34
- function fieldSegments(ir: IR, field: Field): Segment[] {
35
- return ir.analyses.segments[field] || [];
36
- }
37
-
38
- // The first segment of a step field, which the plan guarantees is step-kinded.
39
- function stepSegment(ir: IR, field: Field): StepSegment {
40
- return fieldSegments(ir, field)[0] as StepSegment;
41
- }
42
-
43
34
  // "每N分钟" / "每分钟" — a cadence over a unit (the numeral 1 is suppressed).
44
35
  function cadence(interval: number, unit: string): string {
45
36
  return interval === 1 ? '每' + unit : '每' + interval + unit;
@@ -82,22 +73,6 @@ function renderStride(stride: Stride): string {
82
73
  return start < interval && tiles ? lead : lead + ',至' + last + mark;
83
74
  }
84
75
 
85
- // The sorted numeric values a field's segments cover, or null if any segment
86
- // is not a discrete single (a range or sub-step is not a plain fire list).
87
- function singleValues(segments: Segment[]): number[] | null {
88
- const values: number[] = [];
89
-
90
- for (const segment of segments) {
91
- if (segment.kind !== 'single') {
92
- return null;
93
- }
94
-
95
- values.push(+segment.value);
96
- }
97
-
98
- return values;
99
- }
100
-
101
76
  // Speak a minute/second field's enumerated fires as a step cadence when they
102
77
  // form an arithmetic progression long enough to beat the list (the core
103
78
  // enumerates an offset/uneven step to this fire list; the IR is unchanged, so
@@ -212,7 +187,7 @@ function hourWord(hour: number): string {
212
187
  function hourFires(ir: IR): number[] {
213
188
  const fires: number[] = [];
214
189
 
215
- fieldSegments(ir, 'hour').forEach(function expand(segment) {
190
+ segmentsOf(ir, 'hour').forEach(function expand(segment) {
216
191
  if (segment.kind === 'step') {
217
192
  fires.push(...segment.fires);
218
193
  }
@@ -302,7 +277,7 @@ function renderEveryHour(): string {
302
277
  // enumerated it to a fire list (an offset/uneven set) — both route through the
303
278
  // stride; a short or irregular set keeps the enumerated "每小时…分" list.
304
279
  function minuteHourClause(ir: IR): string {
305
- const segments = fieldSegments(ir, 'minute');
280
+ const segments = segmentsOf(ir, 'minute');
306
281
 
307
282
  if (ir.shapes.minute === 'step') {
308
283
  return stepClause(stepSegment(ir, 'minute'), '分钟', '分', '每小时');
@@ -337,7 +312,7 @@ function hourSegmentWords(segment: Segment): string[] {
337
312
  // of singles, "9点至20点和22点" for a range plus a single. Each segment renders
338
313
  // as the operator the source wrote (range → span), not its expanded fires.
339
314
  function hourList(ir: IR): string {
340
- const words = fieldSegments(ir, 'hour').flatMap(hourSegmentWords);
315
+ const words = segmentsOf(ir, 'hour').flatMap(hourSegmentWords);
341
316
 
342
317
  return joinAnd(words);
343
318
  }
@@ -346,7 +321,7 @@ function hourList(ir: IR): string {
346
321
  // 间,", a discrete hour list gives "在H、H…,".
347
322
  function hourFrame(ir: IR): string {
348
323
  if (ir.shapes.hour === 'range') {
349
- const [from, to] = (fieldSegments(ir, 'hour')[0] as
324
+ const [from, to] = (segmentsOf(ir, 'hour')[0] as
350
325
  Extract<Segment, {kind: 'range'}>).bounds;
351
326
 
352
327
  return '在' + hourWord(+from) + '至' + hourWord(+to) + '之间,';
@@ -490,7 +465,7 @@ function renderCompactClockTimes(ir: IR, plan: PlanNode): string {
490
465
  }
491
466
 
492
467
  const compact = plan as Extract<PlanNode, {kind: 'compactClockTimes'}>;
493
- const secs = fieldSegments(ir, 'second');
468
+ const secs = segmentsOf(ir, 'second');
494
469
  const tail = secs.length && ir.pattern.second !== '0' ?
495
470
  ',第' + valueText(secs) + '秒' : '';
496
471
 
@@ -522,7 +497,7 @@ function renderHourRange(ir: IR, plan: PlanNode): string {
522
497
  const range = plan as Extract<PlanNode, {kind: 'hourRange'}>;
523
498
 
524
499
  if (range.minuteForm === 'lead') {
525
- const minuteSegs = fieldSegments(ir, 'minute');
500
+ const minuteSegs = segmentsOf(ir, 'minute');
526
501
  const past = minuteSegs.length && ir.pattern.minute !== '0' ?
527
502
  minuteHourClause(ir) : '每小时';
528
503
 
@@ -533,7 +508,7 @@ function renderHourRange(ir: IR, plan: PlanNode): string {
533
508
  // A minute range is named separately ("每小时0至30分"), not folded into the end.
534
509
  if (range.minuteForm === 'range') {
535
510
  return '在' + hourWord(range.from) + '至' + hourWord(range.to) +
536
- '之间,每小时' + valueList(fieldSegments(ir, 'minute'), '分') + ',每分钟';
511
+ '之间,每小时' + valueList(segmentsOf(ir, 'minute'), '分') + ',每分钟';
537
512
  }
538
513
 
539
514
  return '在' + hourWord(range.from) + '至' + range.to + '点' +
@@ -566,7 +541,7 @@ function renderHourStep(ir: IR): string {
566
541
  function hourStride(
567
542
  ir: IR
568
543
  ): {interval: number; start: number; last: number} | null {
569
- const segments = fieldSegments(ir, 'hour');
544
+ const segments = segmentsOf(ir, 'hour');
570
545
 
571
546
  if (segments.length === 1 && segments[0].kind === 'step') {
572
547
  const {fires, interval} = segments[0];
@@ -702,7 +677,7 @@ function renderRangeOfMinutes(ir: IR): string {
702
677
 
703
678
  // A standalone second field: "每7秒" (step cadence) or "每分钟第4、17、42秒".
704
679
  function renderStandaloneSeconds(ir: IR): string {
705
- const segs = fieldSegments(ir, 'second');
680
+ const segs = segmentsOf(ir, 'second');
706
681
  const first = segs[0];
707
682
 
708
683
  if (segs.length === 1 && first.kind === 'step' && first.startToken === '*') {
@@ -715,13 +690,13 @@ function renderStandaloneSeconds(ir: IR): string {
715
690
 
716
691
  // A second anchored to the minute: "每分钟第1秒", "每分钟第4、17、42秒".
717
692
  function renderSecondPastMinute(ir: IR): string {
718
- return '每分钟第' + valueText(fieldSegments(ir, 'second')) + '秒';
693
+ return '每分钟第' + valueText(segmentsOf(ir, 'second')) + '秒';
719
694
  }
720
695
 
721
696
  // A second within a single specific minute: "每小时0分第1秒" / "…,每15秒".
722
697
  function renderSecondsWithinMinute(ir: IR): string {
723
698
  const base = '每小时' + ir.pattern.minute + '分';
724
- const segs = fieldSegments(ir, 'second');
699
+ const segs = segmentsOf(ir, 'second');
725
700
  const first = segs[0];
726
701
 
727
702
  if (segs.length === 1 && first.kind === 'step' && first.startToken === '*') {
@@ -733,7 +708,7 @@ function renderSecondsWithinMinute(ir: IR): string {
733
708
 
734
709
  // The second clause for a composed schedule: "每秒" / "每7秒" / "第4、17、42秒".
735
710
  function secondClause(ir: IR): string {
736
- const segs = fieldSegments(ir, 'second');
711
+ const segs = segmentsOf(ir, 'second');
737
712
 
738
713
  if (!segs.length) {
739
714
  return '每秒';
@@ -761,7 +736,7 @@ function minuteClause(ir: IR): string {
761
736
  return cadence(stepSegment(ir, 'minute').interval, UNITS.minute);
762
737
  }
763
738
 
764
- return valueList(fieldSegments(ir, 'minute'), '分');
739
+ return valueList(segmentsOf(ir, 'minute'), '分');
765
740
  }
766
741
 
767
742
  // A single second folds into each clock time a clockTimes rest renders
@@ -838,7 +813,7 @@ function composeMinuteZeroClocks(ir: IR, sec: string): string {
838
813
  });
839
814
  // A pinned minute makes the seconds' own "每分钟" anchor misleading (it is a
840
815
  // single minute, not every minute), so the stride here drops it.
841
- const nested = strideFromSegments(fieldSegments(ir, 'second'), '秒', '秒', '');
816
+ const nested = strideFromSegments(segmentsOf(ir, 'second'), '秒', '秒', '');
842
817
  const tail = sec === '每秒' ? '的每一秒' : '的' + (nested ?? sec);
843
818
  const core = joinAnd(clocks) + tail;
844
819
 
@@ -850,7 +825,7 @@ function composeMinuteZeroClocks(ir: IR, sec: string): string {
850
825
  // words. A pure single-value list (9,17) has no range to span; a step is
851
826
  // handled by hourStride/hourCadence.
852
827
  function hasHourWindow(ir: IR): boolean {
853
- return fieldSegments(ir, 'hour').some(function range(segment) {
828
+ return segmentsOf(ir, 'hour').some(function range(segment) {
854
829
  return segment.kind === 'range';
855
830
  });
856
831
  }
@@ -924,7 +899,7 @@ function composeSecondsListed(ir: IR): string {
924
899
  // stride cadence ("凌晨0点从3分起每2分钟,至59分的每一秒"); the hour fuses, so the
925
900
  // stride drops its "每小时" anchor. A short or irregular set keeps the list.
926
901
  if (ir.shapes.hour === 'single' && sec === '每秒') {
927
- const minuteSegs = fieldSegments(ir, 'minute');
902
+ const minuteSegs = segmentsOf(ir, 'minute');
928
903
  const minuteCad = strideFromSegments(minuteSegs, '分钟', '分', '') ??
929
904
  valueList(minuteSegs, '分');
930
905
 
@@ -1008,7 +983,7 @@ function monthPhrase(ir: IR): string {
1008
983
  return '';
1009
984
  }
1010
985
 
1011
- const segs = fieldSegments(ir, 'month');
986
+ const segs = segmentsOf(ir, 'month');
1012
987
  const first = segs[0];
1013
988
 
1014
989
  if (segs.length === 1 && first.kind === 'step' && first.interval === 2) {
@@ -1037,7 +1012,7 @@ function monthPhrase(ir: IR): string {
1037
1012
  // The day-of-month list. A pure list of singles shares one trailing 日
1038
1013
  // ("1、3、8日"); any range gives each segment its own 日 ("1至5日、10日").
1039
1014
  function dayList(ir: IR): string {
1040
- const segs = fieldSegments(ir, 'date');
1015
+ const segs = segmentsOf(ir, 'date');
1041
1016
 
1042
1017
  if (segs.every((seg) => seg.kind === 'single')) {
1043
1018
  return segs.map((seg) => (seg as {value: string}).value).join('、') + '日';
@@ -1159,7 +1134,7 @@ function weekdayPhrase(
1159
1134
  return quartzWeekday(ir.pattern.weekday, monthPrefix);
1160
1135
  }
1161
1136
 
1162
- const segs = fieldSegments(ir, 'weekday');
1137
+ const segs = segmentsOf(ir, 'weekday');
1163
1138
 
1164
1139
  if (segs.length === 1 && segs[0].kind === 'range') {
1165
1140
  const [from, to] = (segs[0] as Extract<Segment, {kind: 'range'}>).bounds;
@@ -1323,7 +1298,7 @@ function describe(ir: IR, opts: Opts): string {
1323
1298
  }
1324
1299
 
1325
1300
  // The year leads as "2030年", a range as "2030年至2032年", a list joined with 、.
1326
- const year = fieldSegments(ir, 'year').map(function part(seg) {
1301
+ const year = segmentsOf(ir, 'year').map(function part(seg) {
1327
1302
  if (seg.kind === 'range') {
1328
1303
  return seg.bounds[0] + '年至' + seg.bounds[1] + '年';
1329
1304
  }
package/src/types.ts CHANGED
@@ -59,9 +59,9 @@ export interface Cronli5Language {
59
59
  options(options?: Cronli5Options): any;
60
60
  reboot: string;
61
61
  sentence(description: string): string;
62
- // Optional strategy override (see `core/ir.ts` `Language.strategy`). Opaque
62
+ // Optional plan override (see `core/ir.ts` `Language.plan`). Opaque
63
63
  // at this public boundary, like `describe`/`options`.
64
- strategy?(content: any, base: any): any;
64
+ plan?(content: any, base: any): any;
65
65
  }
66
66
 
67
67
  /**
@@ -9,5 +9,5 @@ declare function minuteSpan(minuteField: string): [number, number] | null;
9
9
  declare function lastMinuteFire(minuteField: string): number;
10
10
  declare function clockSecond(secondField: string): number | undefined;
11
11
  declare function analyze(pattern: Pattern): IR;
12
- declare function selectStrategy(content: Content): PlanNode;
13
- export { analyze, clockSecond, enumerateFires, enumerateStep, enumerateValues, getOccurrences, lastMinuteFire, minuteSpan, selectStrategy };
12
+ declare function selectPlan(content: Content): PlanNode;
13
+ export { analyze, clockSecond, enumerateFires, enumerateStep, enumerateValues, getOccurrences, lastMinuteFire, minuteSpan, selectPlan };
@@ -57,7 +57,7 @@ export type HourTimesPlan = {
57
57
  kind: 'segments';
58
58
  };
59
59
  /**
60
- * The rendering strategy the core selects for a pattern. The `kind`
60
+ * The rendering plan the core selects for a pattern. The `kind`
61
61
  * discriminant tells a renderer which fields are present.
62
62
  */
63
63
  export type PlanNode = {
@@ -122,8 +122,8 @@ export interface Analyses {
122
122
  }
123
123
  /**
124
124
  * The neutral content plan: the language-independent facts about a pattern,
125
- * carrying no phrasing decision. `analyze` produces this; `selectStrategy`
126
- * reads it to suggest a `plan`. The phrasing strategy is deliberately *not*
125
+ * carrying no phrasing decision. `analyze` produces this; `selectPlan`
126
+ * reads it to suggest a `plan`. The phrasing plan is deliberately *not*
127
127
  * part of the neutral content (docs/i18n-design.md §2.2).
128
128
  */
129
129
  export interface Content {
@@ -134,7 +134,7 @@ export interface Content {
134
134
  /**
135
135
  * The semantic intermediate representation a language renders: the neutral
136
136
  * `Content` plus the selected `plan`. A language may widen `plan` with its
137
- * own `Extra` strategy kinds via `Language.strategy`; by default there are
137
+ * own `Extra` plan kinds via `Language.plan`; by default there are
138
138
  * none, so `IR` is the neutral content with a core `PlanNode`.
139
139
  */
140
140
  export interface IR<Extra extends {
@@ -172,8 +172,8 @@ export interface NormalizedOptions<Style = DialectStyle> {
172
172
  }
173
173
  /**
174
174
  * The interface every language module's default export implements. `Extra`
175
- * lets a language add its own strategy kinds (default: none), which its
176
- * `strategy` override emits and its `describe` renders.
175
+ * lets a language add its own plan kinds (default: none), which its
176
+ * `plan` override emits and its `describe` renders.
177
177
  */
178
178
  export interface Language<Style = DialectStyle, Extra extends {
179
179
  kind: string;
@@ -183,5 +183,5 @@ export interface Language<Style = DialectStyle, Extra extends {
183
183
  options(options?: Cronli5Options): NormalizedOptions<Style>;
184
184
  reboot: string;
185
185
  sentence(description: string): string;
186
- strategy?(content: Content, base: PlanNode): PlanNode | Extra;
186
+ plan?(content: Content, base: PlanNode): PlanNode | Extra;
187
187
  }
@@ -3,4 +3,5 @@ declare function isPlainRange(field: string): boolean;
3
3
  declare function isPlainStep(field: string): boolean;
4
4
  declare function isDiscreteList(field: string): boolean;
5
5
  declare function isDiscreteHours(hourField: string): boolean;
6
- export { isDiscreteHours, isDiscreteList, isPlainRange, isPlainStep, isSingleValue };
6
+ declare function isOpenStep(field: string): boolean;
7
+ export { isDiscreteHours, isDiscreteList, isOpenStep, isPlainRange, isPlainStep, isSingleValue };