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,18 +4,20 @@
4
4
  //
5
5
  // Spanish is the pilot language for the i18n architecture
6
6
  // (docs/i18n-design.md §7): it consumes only the IR, owns all of its
7
- // words, and is free to re-strategize where Spanish grammar prefers a
7
+ // words, and is free to re-plan where Spanish grammar prefers a
8
8
  // different shape than the plan hint (e.g. wildcard minutes over hour
9
9
  // lists render as per-hour windows).
10
10
 
11
11
  import {clockDigits, numeral} from '../../core/format.js';
12
12
  import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
13
+ import {isOpenStep} from '../../core/shapes.js';
13
14
  import {
14
- arithmeticStep, orderWeekdaysForDisplay, toFieldNumber
15
+ arithmeticStep, hourListStride, offsetCleanStride, orderWeekdaysForDisplay,
16
+ segmentsOf, singleValues, stepSegment, toFieldNumber
15
17
  } from '../../core/util.js';
16
18
  import type {Cronli5Options} from '../../types.js';
17
19
  import type {
18
- Field, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode,
20
+ HourTimesPlan, IR, Language, NormalizedOptions, PlanNode,
19
21
  Segment
20
22
  } from '../../core/ir.js';
21
23
  import {resolveDialect, type SpanishStyle} from './dialects.js';
@@ -62,26 +64,6 @@ type NameSegment =
62
64
  type RangeNameSegment = Extract<NameSegment, {kind: 'range'}>;
63
65
  type SingleNameSegment = Extract<NameSegment, {kind: 'single'}>;
64
66
 
65
- // The first (and only) segment of a step field. The plan only routes here
66
- // for step shapes, whose segments list is present and step-kinded; this
67
- // asserts what the analysis guarantees but the type cannot express.
68
- function stepSegment(segments: Segment[] | null): StepSegment {
69
- return (segments as Segment[])[0] as StepSegment;
70
- }
71
-
72
- // The hour field's classified segments. Callers reach here only for hour
73
- // shapes the analysis segmented, so the list is present; the type permits
74
- // null (wildcard/quartz) that these paths never carry.
75
- function hourSegments(ir: IR): Segment[] {
76
- return ir.analyses.segments.hour as Segment[];
77
- }
78
-
79
- // A field's classified segments. Callers reach a segment list only when the
80
- // field is non-wildcard and non-quartz, where the analysis always produced
81
- // one; the type's null (those two shapes) is unreachable on these paths.
82
- function fieldSegments(ir: IR, field: Field): Segment[] {
83
- return ir.analyses.segments[field] as Segment[];
84
- }
85
67
 
86
68
  // Spanish number names for the integers zero through ten.
87
69
  const numeros = [
@@ -240,9 +222,9 @@ function secondsListAtClock(
240
222
  // prepend "de " to produce the genitive form "de las 09:00 y 17:00".
241
223
  const clockList = grouped.startsWith('a ') ? grouped.slice(2) : grouped;
242
224
  const stride =
243
- strideFromSegments(fieldSegments(ir, 'second'), 'segundo', '', opts);
225
+ strideFromSegments(segmentsOf(ir, 'second'), 'segundo', '', opts);
244
226
  const secondsPhrase = stride ?? 'en los segundos ' +
245
- joinList(segmentWords(fieldSegments(ir, 'second')));
227
+ joinList(segmentWords(segmentsOf(ir, 'second')));
246
228
  const dayFrame = trailingQualifier(ir, opts);
247
229
 
248
230
  return (dayFrame ? dayFrame.trimStart() + ', ' : '') +
@@ -318,7 +300,7 @@ function renderComposeSeconds(
318
300
  const window = hourWindow(boundedWindow(restNode), opts);
319
301
  const dayFrame = weekdayQualifier(ir) + monthScope(ir);
320
302
  const cadence = 'cada ' +
321
- numero(stepSegment(ir.analyses.segments.second).interval, opts) +
303
+ numero(stepSegment(ir, 'second').interval, opts) +
322
304
  ' segundos del minuto ' + ir.pattern.minute;
323
305
 
324
306
  return dayFrame + ', ' + window + ', ' + cadence;
@@ -355,7 +337,7 @@ function isEveryOtherMinuteSeconds(
355
337
  return false;
356
338
  }
357
339
 
358
- const minuteStep = stepSegment(ir.analyses.segments.minute);
340
+ const minuteStep = stepSegment(ir, 'minute');
359
341
 
360
342
  return minuteStep.startToken === '*' && minuteStep.interval === 2;
361
343
  }
@@ -411,7 +393,7 @@ function secondsClause(ir: IR, anchor: string, opts: Opts): string {
411
393
  }
412
394
 
413
395
  if (shape === 'step') {
414
- return stepCycle60(stepSegment(ir.analyses.segments.second), 'segundo',
396
+ return stepCycle60(stepSegment(ir, 'second'), 'segundo',
415
397
  anchor, opts);
416
398
  }
417
399
 
@@ -426,9 +408,9 @@ function secondsClause(ir: IR, anchor: string, opts: Opts): string {
426
408
  return 'en el segundo ' + secondField + ' de cada ' + anchor;
427
409
  }
428
410
 
429
- return strideFromSegments(fieldSegments(ir, 'second'), 'segundo', anchor,
411
+ return strideFromSegments(segmentsOf(ir, 'second'), 'segundo', anchor,
430
412
  opts) ?? 'en los segundos ' +
431
- joinList(segmentWords(fieldSegments(ir, 'second'))) +
413
+ joinList(segmentWords(segmentsOf(ir, 'second'))) +
432
414
  ' de cada ' + anchor;
433
415
  }
434
416
 
@@ -472,9 +454,9 @@ function renderMultipleMinutes(
472
454
  // enumerated to this list reads as a stride cadence when the fires form a
473
455
  // long-enough progression.
474
456
  function minutesList(ir: IR, opts: Opts): string {
475
- return strideFromSegments(fieldSegments(ir, 'minute'), 'minuto', 'hora',
457
+ return strideFromSegments(segmentsOf(ir, 'minute'), 'minuto', 'hora',
476
458
  opts) ?? 'en los minutos ' +
477
- joinList(segmentWords(fieldSegments(ir, 'minute'))) + ' de cada hora';
459
+ joinList(segmentWords(segmentsOf(ir, 'minute'))) + ' de cada hora';
478
460
  }
479
461
 
480
462
  // "cada minuto del 0 al 30". The standalone renderer adds "de cada hora";
@@ -496,7 +478,7 @@ function singleHourStep(segments: Segment[] | null): boolean {
496
478
  // A single hour step as a confinement. A stride of two over the whole day
497
479
  // reads idiomatically as the even ("las horas pares") or odd ("impares")
498
480
  // hours; any other step names its active hours, which pins the schedule
499
- // precisely (a panel found ordinal/colloquial forms imprecise).
481
+ // precisely (ordinal/colloquial forms would be imprecise here).
500
482
  function stepHourSpan(segment: StepSegment, opts: Opts): string {
501
483
  const bounded = segment.startToken.indexOf('-') !== -1;
502
484
  const start = segment.startToken === '*' ? 0 : +segment.startToken;
@@ -580,7 +562,7 @@ function renderMinuteFrequency(
580
562
  plan: Extract<PlanNode, {kind: 'minuteFrequency'}>,
581
563
  opts: Opts
582
564
  ): string {
583
- let phrase = stepCycle60(stepSegment(ir.analyses.segments.minute), 'minuto',
565
+ let phrase = stepCycle60(stepSegment(ir, 'minute'), 'minuto',
584
566
  'hora', opts);
585
567
 
586
568
  if (plan.hours.kind === 'during') {
@@ -595,7 +577,7 @@ function renderMinuteFrequency(
595
577
  // An offset step (e.g. 1/2) arrives here; a single step reads as a
596
578
  // confinement, not the verbose window list.
597
579
  phrase += singleHourStep(ir.analyses.segments.hour) ?
598
- ', ' + stepHourSpan(stepSegment(ir.analyses.segments.hour), opts) :
580
+ ', ' + stepHourSpan(stepSegment(ir, 'hour'), opts) :
599
581
  ' ' + hourSpanFromTimes(ir, plan.hours.times, opts);
600
582
  }
601
583
  }
@@ -605,7 +587,7 @@ function renderMinuteFrequency(
605
587
  else if (plan.hours.kind === 'step') {
606
588
  // A clean stride is a confinement ("las horas pares", or the active-hour
607
589
  // list), never a juxtaposed cadence ("cada dos horas").
608
- phrase += ', ' + stepHourSpan(stepSegment(ir.analyses.segments.hour), opts);
590
+ phrase += ', ' + stepHourSpan(stepSegment(ir, 'hour'), opts);
609
591
  }
610
592
 
611
593
  return phrase + trailingQualifier(ir, opts);
@@ -632,7 +614,7 @@ function renderMinuteSpanInHour(
632
614
  trailingQualifier(ir, opts);
633
615
  }
634
616
 
635
- // A minute window under discrete hours. Spanish re-strategizes the
617
+ // A minute window under discrete hours. Spanish re-plans the
636
618
  // wildcard form: rather than "during the X hours", each hour reads as its
637
619
  // own window ("de las 9:00 a las 9:59").
638
620
  function renderMinutesAcrossHours(
@@ -651,7 +633,7 @@ function renderMinutesAcrossHours(
651
633
 
652
634
  if (singleHourStep(ir.analyses.segments.hour)) {
653
635
  return 'cada minuto, ' +
654
- stepHourSpan(stepSegment(ir.analyses.segments.hour), opts) +
636
+ stepHourSpan(stepSegment(ir, 'hour'), opts) +
655
637
  trailingQualifier(ir, opts);
656
638
  }
657
639
 
@@ -676,7 +658,7 @@ function renderMinuteSpanAcrossHourStep(
676
658
  plan: Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>,
677
659
  opts: Opts
678
660
  ): string {
679
- const segment = stepSegment(ir.analyses.segments.hour);
661
+ const segment = stepSegment(ir, 'hour');
680
662
  // A bounded or uneven hour step reads as its endpoint-pinning cadence; an
681
663
  // offset-clean step keeps its confinement / per-step phrasing.
682
664
  const cadence = unevenHourCadence(ir, opts);
@@ -752,7 +734,7 @@ function renderHourStep(
752
734
  return cadence + trailingQualifier(ir, opts);
753
735
  }
754
736
 
755
- return stepHours(stepSegment(ir.analyses.segments.hour), opts) +
737
+ return stepHours(stepSegment(ir, 'hour'), opts) +
756
738
  trailingQualifier(ir, opts);
757
739
  }
758
740
 
@@ -797,7 +779,7 @@ function unionMonthLeadFull(ir: IR): string {
797
779
  }
798
780
 
799
781
  const lead = monthPhrase(ir, monthRanged(ir) ? 'de ' : 'en ');
800
- const segments = flattenSteps(fieldSegments(ir, 'month'));
782
+ const segments = flattenSteps(segmentsOf(ir, 'month'));
801
783
  const isEnumeration = !monthRanged(ir) && segments.length >= 2;
802
784
 
803
785
  return isEnumeration ? lead + ',' : lead;
@@ -819,7 +801,7 @@ function domArm(ir: IR, opts: Opts): string {
819
801
  return stepDates(date, opts);
820
802
  }
821
803
 
822
- const segments = fieldSegments(ir, 'date');
804
+ const segments = segmentsOf(ir, 'date');
823
805
 
824
806
  if (segments.length === 1 && segments[0].kind === 'range') {
825
807
  return 'del ' + segments[0].bounds[0] + ' al ' +
@@ -848,7 +830,7 @@ function dowArm(ir: IR): string {
848
830
 
849
831
  // Weekday lists display Monday-first (Sunday last); a lone range keeps its
850
832
  // form. The IR stays canonical (Sunday=0). The helper flattens steps.
851
- const segments = orderWeekdaysForDisplay(fieldSegments(ir, 'weekday'));
833
+ const segments = orderWeekdaysForDisplay(segmentsOf(ir, 'weekday'));
852
834
  const allSingles = segments.every(function single(segment) {
853
835
  return segment.kind === 'single';
854
836
  });
@@ -1245,7 +1227,7 @@ function renderCompactClockTimes(
1245
1227
  return cadence;
1246
1228
  }
1247
1229
 
1248
- const ranged = hourSegments(ir).some(function range(segment) {
1230
+ const ranged = segmentsOf(ir, 'hour').some(function range(segment) {
1249
1231
  return segment.kind === 'range';
1250
1232
  });
1251
1233
 
@@ -1383,21 +1365,6 @@ function strideFromSegments(
1383
1365
  null;
1384
1366
  }
1385
1367
 
1386
- // The sorted numeric values a field's segments cover, or null if any segment
1387
- // is not a discrete single (a range or sub-step is not a plain fire list).
1388
- function singleValues(segments: Segment[]): number[] | null {
1389
- const values: number[] = [];
1390
-
1391
- for (const segment of segments) {
1392
- if (segment.kind !== 'single') {
1393
- return null;
1394
- }
1395
-
1396
- values.push(+segment.value);
1397
- }
1398
-
1399
- return values;
1400
- }
1401
1368
 
1402
1369
  // "cada seis horas", "a las 9:00, a las 11:00 y a la 1:00", or "cada
1403
1370
  // cinco horas a partir de las 2:00".
@@ -1452,17 +1419,6 @@ function hourStrideCadence(
1452
1419
  timePhrase(last, 0, null, opts);
1453
1420
  }
1454
1421
 
1455
- // Whether an hour stride wraps the day cleanly from within its first interval
1456
- // (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
1457
- // stride has no distinct endpoint and keeps its bare or "a partir de" cadence.
1458
- // Every other stride — a uneven interval, or one starting at or past its
1459
- // interval (a bounded `a-b/n`) — is a bounded set the cadence pins the ends of.
1460
- function offsetCleanStride(
1461
- stride: {start: number; interval: number}
1462
- ): boolean {
1463
- return stride.start < stride.interval && 24 % stride.interval === 0;
1464
- }
1465
-
1466
1422
  // The bounded cadence for an hour stride that pins both clock-time endpoints,
1467
1423
  // or null when the hour is not such a stride. The core rewrites a uneven step
1468
1424
  // to its fire list, so a minute window/list/step crossed with it lands in the
@@ -1480,41 +1436,6 @@ function unevenHourCadence(ir: IR, opts: Opts): string | null {
1480
1436
  return hourStrideCadence(stride, opts);
1481
1437
  }
1482
1438
 
1483
- // An hour list's arithmetic progression, or null when its values are not a
1484
- // step the renderer should speak as a cadence. The core rewrites a uneven hour
1485
- // step (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its
1486
- // literal fire list, indistinguishable in the IR from a hand-written list; the
1487
- // renderer recovers the cadence from the values. A progression starting at zero
1488
- // is a `*/n` step however short (0,7,14,21 is `*/7`); a non-zero progression is
1489
- // only a step when it is too long to be a deliberate clock-time list (e.g. 9,17
1490
- // is two named times, not a cadence). Interval one is a plain range, never a
1491
- // step.
1492
- function hourListStride(
1493
- values: number[]
1494
- ): {start: number; interval: number; last: number} | null {
1495
- if (values.length < 2) {
1496
- return null;
1497
- }
1498
-
1499
- const interval = values[1] - values[0];
1500
-
1501
- if (interval < 2) {
1502
- return null;
1503
- }
1504
-
1505
- for (let i = 2; i < values.length; i += 1) {
1506
- if (values[i] - values[i - 1] !== interval) {
1507
- return null;
1508
- }
1509
- }
1510
-
1511
- if (values[0] !== 0 && values.length < 5) {
1512
- return null;
1513
- }
1514
-
1515
- return {interval, last: values[values.length - 1], start: values[0]};
1516
- }
1517
-
1518
1439
  // The hour field's stride, or null when the hour is not a cadence: a step
1519
1440
  // segment yields its {start, interval, last} directly; an all-single hour
1520
1441
  // list yields one only when its values form a step progression (so an irregular
@@ -1524,7 +1445,7 @@ function hourListStride(
1524
1445
  function hourStride(
1525
1446
  ir: IR
1526
1447
  ): {start: number; interval: number; last: number} | null {
1527
- const segments = fieldSegments(ir, 'hour');
1448
+ const segments = segmentsOf(ir, 'hour');
1528
1449
 
1529
1450
  if (segments.length === 1 && segments[0].kind === 'step') {
1530
1451
  const segment = segments[0];
@@ -1638,7 +1559,7 @@ function hourCadence(ir: IR, minute: number, opts: Opts): string | null {
1638
1559
  // bounded step, an uneven stride, or an arithmetic-progression list, which
1639
1560
  // keep the bounded cadence form).
1640
1561
  function cleanStrideSegment(ir: IR): StepSegment | null {
1641
- const segments = fieldSegments(ir, 'hour');
1562
+ const segments = segmentsOf(ir, 'hour');
1642
1563
  const segment = segments.length === 1 && segments[0];
1643
1564
 
1644
1565
  if (!segment || segment.kind !== 'step' ||
@@ -1654,7 +1575,7 @@ function cleanStrideSegment(ir: IR): StepSegment | null {
1654
1575
  // A pure single-value list (9,17) has no range to span and still enumerates;
1655
1576
  // a step is handled by hourStride/hourCadence.
1656
1577
  function hasHourWindow(ir: IR): boolean {
1657
- return hourSegments(ir).some(function range(segment) {
1578
+ return segmentsOf(ir, 'hour').some(function range(segment) {
1658
1579
  return segment.kind === 'range';
1659
1580
  });
1660
1581
  }
@@ -1697,7 +1618,7 @@ function hourRangeCadence(ir: IR, minute: number, opts: Opts): string | null {
1697
1618
  // Used by the compact-clock non-fold path, where the minute is a step or list
1698
1619
  // (a single-value minute keeps its real "a las HH:MM" clock time elsewhere).
1699
1620
  function hourContextTimes(ir: IR, opts: Opts): string {
1700
- const segments = hourSegments(ir);
1621
+ const segments = segmentsOf(ir, 'hour');
1701
1622
 
1702
1623
  // Collect the point hours (singles and step fires) — a range stays a window.
1703
1624
  const points: number[] = [];
@@ -1801,7 +1722,7 @@ function hourWindowsFromTimes(
1801
1722
  }));
1802
1723
  }
1803
1724
 
1804
- return joinList(hourSegments(ir).map(function window(segment) {
1725
+ return joinList(segmentsOf(ir, 'hour').map(function window(segment) {
1805
1726
  if (segment.kind === 'range') {
1806
1727
  return timeRange({hour: +segment.bounds[0], minute: 0},
1807
1728
  {hour: +segment.bounds[1], minute: 59}, opts);
@@ -1830,7 +1751,7 @@ function hourSegmentTimes(
1830
1751
  const pieces: string[] = [];
1831
1752
  const fromRange: boolean[] = [];
1832
1753
 
1833
- hourSegments(ir).forEach(function clock(segment) {
1754
+ segmentsOf(ir, 'hour').forEach(function clock(segment) {
1834
1755
  if (segment.kind === 'step') {
1835
1756
  segment.fires.forEach(function each(hour) {
1836
1757
  pieces.push(atTime(timePhrase(hour, minute, second, opts)));
@@ -2100,7 +2021,7 @@ function dateClause(
2100
2021
  return stepDates(pattern.date, opts);
2101
2022
  }
2102
2023
 
2103
- const segments = fieldSegments(ir, 'date');
2024
+ const segments = segmentsOf(ir, 'date');
2104
2025
 
2105
2026
  if (segments.length === 1 && segments[0].kind === 'range') {
2106
2027
  return 'del ' + segments[0].bounds[0] + ' al ' +
@@ -2118,7 +2039,7 @@ function dateClause(
2118
2039
  // Whether the month field contains a range segment.
2119
2040
  function monthRanged(ir: IR): boolean {
2120
2041
  return ir.pattern.month !== '*' &&
2121
- fieldSegments(ir, 'month').some(function range(segment) {
2042
+ segmentsOf(ir, 'month').some(function range(segment) {
2122
2043
  return segment.kind === 'range';
2123
2044
  });
2124
2045
  }
@@ -2207,7 +2128,7 @@ function weekdayQualifier(ir: IR): string {
2207
2128
 
2208
2129
  // Weekday lists display Monday-first (Sunday last); a lone range keeps its
2209
2130
  // form. The IR stays canonical (Sunday=0). The helper flattens steps.
2210
- const segments = orderWeekdaysForDisplay(fieldSegments(ir, 'weekday'));
2131
+ const segments = orderWeekdaysForDisplay(segmentsOf(ir, 'weekday'));
2211
2132
  const allSingles = segments.every(function single(segment) {
2212
2133
  return segment.kind === 'single';
2213
2134
  });
@@ -2260,7 +2181,7 @@ function flattenSteps(segments: Segment[]): NameSegment[] {
2260
2181
  // ("en enero y de marzo a junio") — a bare "enero y marzo a junio" parses
2261
2182
  // as "(enero y marzo) a junio".
2262
2183
  function monthPhrase(ir: IR, lead: string): string {
2263
- const segments = flattenSteps(fieldSegments(ir, 'month'));
2184
+ const segments = flattenSteps(segmentsOf(ir, 'month'));
2264
2185
  const ranged = segments.some(function range(segment) {
2265
2186
  return segment.kind === 'range';
2266
2187
  });
@@ -2423,11 +2344,6 @@ function monthName(token: NameToken): string {
2423
2344
  return monthNames[+token] as string;
2424
2345
  }
2425
2346
 
2426
- // Whether a canonical field value is an open step (`*/n` or `a/n`).
2427
- function isOpenStep(field: string): boolean {
2428
- return field.indexOf('/') !== -1 && field.indexOf('-') === -1 &&
2429
- field.indexOf(',') === -1;
2430
- }
2431
2347
 
2432
2348
  // The Spanish language module: the IR renderer plus the language-owned
2433
2349
  // strings and option normalization.