cronli5 0.1.6 → 0.1.7

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.
@@ -10,7 +10,9 @@
10
10
 
11
11
  import {clockDigits, numeral} from '../../core/format.js';
12
12
  import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
13
- import {arithmeticStep, toFieldNumber} from '../../core/util.js';
13
+ import {
14
+ arithmeticStep, orderWeekdaysForDisplay, toFieldNumber
15
+ } from '../../core/util.js';
14
16
  import type {Cronli5Options} from '../../types.js';
15
17
  import type {
16
18
  Field, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode,
@@ -313,7 +315,7 @@ function renderComposeSeconds(
313
315
  if (plan.rest.kind === 'hourRange' && ir.shapes.second === 'step' &&
314
316
  ir.pattern.weekday !== '*') {
315
317
  const restNode = plan.rest;
316
- const window = hourWindow(restNode, opts);
318
+ const window = hourWindow(boundedWindow(restNode), opts);
317
319
  const dayFrame = weekdayQualifier(ir) + monthScope(ir);
318
320
  const cadence = 'cada ' +
319
321
  numero(stepSegment(ir.analyses.segments.second).interval, opts) +
@@ -754,13 +756,18 @@ function renderHourStep(
754
756
  trailingQualifier(ir, opts);
755
757
  }
756
758
 
757
- // The hour-range plan as a window whose closing minute honors `boundMinute`:
758
- // a bare close (`null`) lands on the top of the final hour (minute 0),
759
- // matching the minute-0 baseline, with the minutes stated separately.
759
+ // The hour-range plan as a window. The close lands on the top of the final
760
+ // hour (minute 0) unless the minute genuinely runs to the end of that hour
761
+ // i.e. a wildcard minute, which fills every minute and states no separate
762
+ // clause. A pinned/listed/ranged minute is named in its own lead clause, so
763
+ // folding it into the close too would read as a span ("a las 17:05") that
764
+ // contradicts the minute clause; the window stays bare ("a las 17:00").
760
765
  function boundedWindow(
761
766
  plan: Extract<PlanNode, {kind: 'hourRange'}>
762
767
  ): {from: number; to: number; last: number} {
763
- return {from: plan.from, last: plan.boundMinute ?? 0, to: plan.to};
768
+ const last = plan.minuteForm === 'wildcard' ? plan.boundMinute ?? 0 : 0;
769
+
770
+ return {from: plan.from, last, to: plan.to};
764
771
  }
765
772
 
766
773
  // "de las 9:00 a las 17:45": a window from the top of the first hour to
@@ -839,7 +846,9 @@ function dowArm(ir: IR): string {
839
846
  return quartz;
840
847
  }
841
848
 
842
- const segments = flattenSteps(fieldSegments(ir, 'weekday'));
849
+ // Weekday lists display Monday-first (Sunday last); a lone range keeps its
850
+ // form. The IR stays canonical (Sunday=0). The helper flattens steps.
851
+ const segments = orderWeekdaysForDisplay(fieldSegments(ir, 'weekday'));
843
852
  const allSingles = segments.every(function single(segment) {
844
853
  return segment.kind === 'single';
845
854
  });
@@ -1258,7 +1267,7 @@ function renderCompactClockTimes(
1258
1267
  const phrase = cadence ?
1259
1268
  minutesList(ir, opts) + ', ' + cadence + trailingQualifier(ir, opts) :
1260
1269
  minutesList(ir, opts) + ', ' +
1261
- hourSegmentTimes(ir, 0, null, opts) + trailingQualifier(ir, opts);
1270
+ hourContextTimes(ir, opts) + trailingQualifier(ir, opts);
1262
1271
 
1263
1272
  return ir.analyses.clockSecond ?
1264
1273
  secondsLeadClause(ir, opts) + ', ' + phrase :
@@ -1681,6 +1690,71 @@ function hourRangeCadence(ir: IR, minute: number, opts: Opts): string | null {
1681
1690
 
1682
1691
  // --- Hour-time phrasing. ---
1683
1692
 
1693
+ // The fixed hour(s) of a stepped/listed minute, named as the HOUR rather than a
1694
+ // "a las HH:00" clock instant the minute never fires at: noon and midnight read
1695
+ // as the hour word ("al mediodía"/"a medianoche"), any other hour as the whole
1696
+ // hour "de la hora de las HH:00" (the idiom a wildcard minute already uses).
1697
+ // Used by the compact-clock non-fold path, where the minute is a step or list
1698
+ // (a single-value minute keeps its real "a las HH:MM" clock time elsewhere).
1699
+ function hourContextTimes(ir: IR, opts: Opts): string {
1700
+ const segments = hourSegments(ir);
1701
+
1702
+ // Collect the point hours (singles and step fires) — a range stays a window.
1703
+ const points: number[] = [];
1704
+ const hasRange = segments.some(function range(segment) {
1705
+ return segment.kind === 'range';
1706
+ });
1707
+
1708
+ segments.forEach(function collect(segment) {
1709
+ if (segment.kind === 'step') {
1710
+ points.push(...segment.fires);
1711
+ }
1712
+ else if (segment.kind === 'single') {
1713
+ points.push(+segment.value);
1714
+ }
1715
+ });
1716
+
1717
+ // All point hours, all noon/midnight: stand alone as their own words ("a
1718
+ // medianoche y al mediodía").
1719
+ function isWord(hour: number): boolean {
1720
+ return !opts.ampm && (hour === 0 || hour === 12);
1721
+ }
1722
+
1723
+ if (!hasRange && points.every(isWord)) {
1724
+ return joinList(points.map(function each(hour) {
1725
+ return atTime(bareHourPhrase(hour, opts));
1726
+ }));
1727
+ }
1728
+
1729
+ // A point hour as the whole hour: "de la hora de las HH:00".
1730
+ function wholeHour(hour: number): string {
1731
+ return 'de la hora ' + fromTime(explicitTimePhrase(hour, 0, opts));
1732
+ }
1733
+
1734
+ // Otherwise each whole hour reads as a window ("de las HH:00 a las HH:00" for
1735
+ // a range, "de la hora de las HH:00" for a point), never a false "a las
1736
+ // HH:00" clock instant the stepped minute never fires at.
1737
+ const pieces: string[] = [];
1738
+
1739
+ segments.forEach(function place(segment) {
1740
+ if (segment.kind === 'range') {
1741
+ pieces.push(timeRange(
1742
+ {hour: +segment.bounds[0], minute: 0},
1743
+ {hour: +segment.bounds[1], minute: 0}, opts));
1744
+ }
1745
+ else if (segment.kind === 'step') {
1746
+ segment.fires.forEach(function each(hour) {
1747
+ pieces.push(wholeHour(hour));
1748
+ });
1749
+ }
1750
+ else {
1751
+ pieces.push(wholeHour(+segment.value));
1752
+ }
1753
+ });
1754
+
1755
+ return joinList(pieces);
1756
+ }
1757
+
1684
1758
  // "a las 9:00" / "a la 1:00" / "al mediodía" for each fire hour.
1685
1759
  function atTimes(hours: number[], opts: Opts): string[] {
1686
1760
  return hours.map(function each(hour) {
@@ -2131,7 +2205,9 @@ function weekdayQualifier(ir: IR): string {
2131
2205
  return quartz;
2132
2206
  }
2133
2207
 
2134
- const segments = flattenSteps(fieldSegments(ir, 'weekday'));
2208
+ // Weekday lists display Monday-first (Sunday last); a lone range keeps its
2209
+ // form. The IR stays canonical (Sunday=0). The helper flattens steps.
2210
+ const segments = orderWeekdaysForDisplay(fieldSegments(ir, 'weekday'));
2135
2211
  const allSingles = segments.every(function single(segment) {
2136
2212
  return segment.kind === 'single';
2137
2213
  });
@@ -11,7 +11,9 @@
11
11
 
12
12
  import {clockDigits, numeral} from '../../core/format.js';
13
13
  import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
14
- import {arithmeticStep, toFieldNumber} from '../../core/util.js';
14
+ import {
15
+ arithmeticStep, orderWeekdaysForDisplay, toFieldNumber
16
+ } from '../../core/util.js';
15
17
  import {resolveDialect} from './dialects.js';
16
18
  import type {
17
19
  ClockTime, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode,
@@ -1703,7 +1705,9 @@ function weekdayQualifier(ir: IR): string {
1703
1705
  return quartz;
1704
1706
  }
1705
1707
 
1706
- const segments = flattenSteps(ir.analyses.segments.weekday!);
1708
+ // Weekday lists display Monday-first (Sunday last); a lone range keeps its
1709
+ // form. The IR stays canonical (Sunday=0). The helper flattens steps.
1710
+ const segments = orderWeekdaysForDisplay(ir.analyses.segments.weekday!);
1707
1711
 
1708
1712
  return joinList(segments.map(function piece(segment: FlatSegment) {
1709
1713
  if (segment.kind === 'range') {
@@ -4,7 +4,9 @@
4
4
  // big-endian dates, 每 for recurrence, 24-hour clock with 凌晨0点/正午 anchors,
5
5
  // day periods under `ampm`. The style contract is src/lang/zh/notes.md.
6
6
 
7
- import {arithmeticStep, toFieldNumber} from '../../core/util.js';
7
+ import {
8
+ arithmeticStep, orderWeekdaysForDisplay, toFieldNumber
9
+ } from '../../core/util.js';
8
10
  import {maxClockTimes, monthNumbers, weekdayNumbers} from '../../core/specs.js';
9
11
  import type {Cronli5Options} from '../../types.js';
10
12
  import type {
@@ -591,6 +593,30 @@ function hourCadencePhrase(ir: IR): string | null {
591
593
  });
592
594
  }
593
595
 
596
+ // A wildcard or sub-minute step second confined to minute 0 of an hour stride
597
+ // is a confinement, not a juxtaposed cadence. The even-hour stride (interval 2
598
+ // from midnight) reuses the even-hours idiom ("在偶数小时0分的每一秒") so the form
599
+ // does NOT contain the bare "每2小时" and can never be misread as the absorbing
600
+ // hour cadence (the same reason en says "for one minute during every other
601
+ // hour", not "every two hours"). An OFFSET stride names its start ("从1点起每2小时"),
602
+ // already unambiguous — it cannot be heard as the bare cadence — so it folds
603
+ // "0分" and the second onto that named cadence ("从1点起每2小时0分的每一秒"). A bare
604
+ // cadence from midnight (no start named, e.g. "每3小时") keeps enumerating its
605
+ // hours so it is never heard as the absorbing form.
606
+ function minuteZeroConfinement(
607
+ ir: IR, stride: {interval: number; start: number}, prefix: string
608
+ ): string | null {
609
+ if (stride.interval === 2 && stride.start === 0) {
610
+ return '在偶数小时0分' + secondTail(ir);
611
+ }
612
+
613
+ if (prefix.indexOf('从') !== -1) {
614
+ return prefix + '0分' + secondTail(ir);
615
+ }
616
+
617
+ return null;
618
+ }
619
+
594
620
  // Render an hour step (or arithmetic-progression hour list) under a single
595
621
  // pinned minute and a second as a cadence — the hour cadence plus the
596
622
  // minute/second — instead of cross-multiplying the hours into a wall of clock
@@ -626,17 +652,8 @@ function hourCadence(ir: IR): string | null {
626
652
  const minute = +ir.pattern.minute;
627
653
  const subMinute = ir.pattern.second === '*' || ir.shapes.second === 'step';
628
654
 
629
- // A wildcard or sub-minute step second confined to minute 0 of the even-hour
630
- // stride is a confinement, not a juxtaposed cadence. Reuse the even-hours
631
- // idiom ("在偶数小时0分的每一秒") so the form does NOT contain the bare "每2小时"
632
- // and can never be misread as the absorbing hour cadence (the same reason en
633
- // says "for one minute during every other hour", not "every two hours"). The
634
- // idiom exists only for the even-hour stride (interval 2 from midnight);
635
- // another stride keeps enumerating (return null) rather than coin a
636
- // misleading "…小时…" form.
637
655
  if (minute === 0 && subMinute) {
638
- return stride.interval === 2 && stride.start === 0 ?
639
- '在偶数小时0分' + secondTail(ir) : null;
656
+ return minuteZeroConfinement(ir, stride, prefix);
640
657
  }
641
658
 
642
659
  // A pinned minute 0 folds into the cadence with the explicit "0分" so the
@@ -1050,7 +1067,7 @@ function quartzDate(token: string, monthPrefix: string): string {
1050
1067
  return monthPrefix + '最后第' + token.slice(2) + '天';
1051
1068
  }
1052
1069
 
1053
- return '最接近' + token.slice(0, -1) + '日的工作日';
1070
+ return monthPrefix + '最接近' + token.slice(0, -1) + '日的工作日';
1054
1071
  }
1055
1072
 
1056
1073
  // The date side of a qualifier (month folded in): "每月1日", "1月1日",
@@ -1071,7 +1088,17 @@ function datePhrase(ir: IR): string {
1071
1088
  return month + cadence(stepSegment(ir, 'date').interval, '天');
1072
1089
  }
1073
1090
 
1074
- return month ? month + dayList(ir) : '每月' + dayList(ir);
1091
+ if (!month) {
1092
+ return '每月' + dayList(ir);
1093
+ }
1094
+
1095
+ // A multi-month scope (range/list) ends in 月 and would run straight into the
1096
+ // day — "6月至8月1日" reads "8月1日" as August 1st. The comma keeps the month
1097
+ // scope distinct from the day ("6月至8月,1日"). A single month stays glued
1098
+ // ("6月1日"), which is unambiguous.
1099
+ const monthMulti = ir.shapes.month === 'range' || ir.shapes.month === 'list';
1100
+
1101
+ return month + (monthMulti ? ',' : '') + dayList(ir);
1075
1102
  }
1076
1103
 
1077
1104
  // The date side WITHOUT its month or 每月 lead — just the day part: "1日",
@@ -1140,13 +1167,12 @@ function weekdayPhrase(
1140
1167
  return '每' + weekdayName(from) + '至' + weekdayName(to);
1141
1168
  }
1142
1169
 
1170
+ // Weekday lists display Monday-first (Sunday last); the IR stays canonical
1171
+ // (Sunday=0). The helper flattens steps into singles and orders the list.
1143
1172
  const days: number[] = [];
1144
1173
 
1145
- segs.forEach(function expand(seg) {
1146
- if (seg.kind === 'step') {
1147
- days.push(...seg.fires);
1148
- }
1149
- else if (seg.kind === 'single') {
1174
+ orderWeekdaysForDisplay(segs).forEach(function expand(seg) {
1175
+ if (seg.kind === 'single') {
1150
1176
  days.push(toFieldNumber(seg.value, weekdayNumbers));
1151
1177
  }
1152
1178
  });
@@ -1,3 +1,4 @@
1
+ import type { Segment } from './ir.js';
1
2
  declare function includes(str: string | number, sub: string): boolean;
2
3
  declare function unique<T>(items: T[]): T[];
3
4
  declare function isNonNegativeInteger(value: string): boolean;
@@ -6,7 +7,15 @@ declare function arithmeticStep(values: number[]): {
6
7
  interval: number;
7
8
  last: number;
8
9
  } | null;
10
+ type WeekdaySegment = {
11
+ kind: 'single';
12
+ value: string;
13
+ } | {
14
+ kind: 'range';
15
+ bounds: [string, string];
16
+ };
17
+ declare function orderWeekdaysForDisplay(segments: Segment[]): WeekdaySegment[];
9
18
  declare function toFieldNumber(token: string, numberMap?: {
10
19
  [name: string]: number;
11
20
  }): number;
12
- export { arithmeticStep, includes, isNonNegativeInteger, toFieldNumber, unique };
21
+ export { arithmeticStep, includes, isNonNegativeInteger, orderWeekdaysForDisplay, toFieldNumber, unique };