cronli5 0.1.4 → 0.1.5
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.
- package/CHANGELOG.md +25 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +180 -36
- package/dist/cronli5.js +180 -36
- package/dist/lang/de.cjs +172 -8
- package/dist/lang/de.js +172 -8
- package/dist/lang/en.cjs +175 -29
- package/dist/lang/en.js +175 -29
- package/dist/lang/es.cjs +180 -25
- package/dist/lang/es.js +180 -25
- package/dist/lang/fi.cjs +188 -40
- package/dist/lang/fi.js +188 -40
- package/dist/lang/zh.cjs +165 -19
- package/dist/lang/zh.js +165 -19
- package/package.json +2 -1
- package/src/core/analyze.ts +7 -0
- package/src/core/ir.ts +1 -1
- package/src/core/util.ts +31 -1
- package/src/lang/de/index.ts +360 -16
- package/src/lang/en/index.ts +333 -33
- package/src/lang/es/index.ts +373 -40
- package/src/lang/fi/index.ts +404 -72
- package/src/lang/zh/index.ts +327 -35
- package/types/core/ir.d.ts +1 -1
- package/types/core/util.d.ts +6 -1
package/src/lang/zh/index.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
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 {toFieldNumber} from '../../core/util.js';
|
|
8
|
-
import {monthNumbers, weekdayNumbers} from '../../core/specs.js';
|
|
7
|
+
import {arithmeticStep, toFieldNumber} from '../../core/util.js';
|
|
8
|
+
import {maxClockTimes, monthNumbers, weekdayNumbers} from '../../core/specs.js';
|
|
9
9
|
import type {Cronli5Options} from '../../types.js';
|
|
10
10
|
import type {
|
|
11
11
|
Field, IR, Language, NormalizedOptions, PlanNode, Segment
|
|
@@ -43,6 +43,108 @@ function cadence(interval: number, unit: string): string {
|
|
|
43
43
|
return interval === 1 ? '每' + unit : '每' + interval + unit;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// A step cadence to phrase over a `cycle`-long field (60 for minute/second),
|
|
47
|
+
// running from `start` to `last`. `unit` is the cadence noun ("分钟"/"秒"),
|
|
48
|
+
// `mark` the bound measure word ("分"/"秒"), `anchor` the larger frame
|
|
49
|
+
// ("每小时"/"每分钟").
|
|
50
|
+
interface Stride {
|
|
51
|
+
interval: number;
|
|
52
|
+
start: number;
|
|
53
|
+
last: number;
|
|
54
|
+
cycle: number;
|
|
55
|
+
unit: string;
|
|
56
|
+
mark: string;
|
|
57
|
+
anchor: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Speak a step cadence over a `cycle`-long field. A clean stride from the top
|
|
61
|
+
// of the cycle is the bare cadence ("每2分钟"); a uniform offset (start within
|
|
62
|
+
// the first interval, the interval still dividing the cycle) names only its
|
|
63
|
+
// start, since it wraps cleanly with no distinct endpoint ("每小时从5分起每6分
|
|
64
|
+
// 钟"); a non-uniform stride (start >= interval, or an interval that does not
|
|
65
|
+
// divide the cycle) pins both endpoints so the bounded, non-wrapping set reads
|
|
66
|
+
// unambiguously ("每小时从3分起每2分钟,至59分"). This is the one phrasing for
|
|
67
|
+
// every step the renderer speaks, whether the core kept it a step shape (a
|
|
68
|
+
// clean cadence) or enumerated it to a fire list (an offset/uneven set the
|
|
69
|
+
// list path recognizes as a progression).
|
|
70
|
+
function renderStride(stride: Stride): string {
|
|
71
|
+
const {interval, start, last, cycle, unit, mark, anchor} = stride;
|
|
72
|
+
const tiles = cycle % interval === 0;
|
|
73
|
+
|
|
74
|
+
if (start === 0 && tiles) {
|
|
75
|
+
return cadence(interval, unit);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const lead = anchor + '从' + start + mark + '起' + cadence(interval, unit);
|
|
79
|
+
|
|
80
|
+
return start < interval && tiles ? lead : lead + ',至' + last + mark;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The sorted numeric values a field's segments cover, or null if any segment
|
|
84
|
+
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
85
|
+
function singleValues(segments: Segment[]): number[] | null {
|
|
86
|
+
const values: number[] = [];
|
|
87
|
+
|
|
88
|
+
for (const segment of segments) {
|
|
89
|
+
if (segment.kind !== 'single') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
values.push(+segment.value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return values;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
100
|
+
// form an arithmetic progression long enough to beat the list (the core
|
|
101
|
+
// enumerates an offset/uneven step to this fire list; the IR is unchanged, so
|
|
102
|
+
// the renderer recognizes the progression). Returns null for a non-progression
|
|
103
|
+
// or a too-short list, leaving the caller to enumerate.
|
|
104
|
+
function strideFromSegments(
|
|
105
|
+
segments: Segment[],
|
|
106
|
+
unit: string,
|
|
107
|
+
mark: string,
|
|
108
|
+
anchor: string
|
|
109
|
+
): string | null {
|
|
110
|
+
const values = singleValues(segments);
|
|
111
|
+
const step = values && arithmeticStep(values);
|
|
112
|
+
|
|
113
|
+
return step ?
|
|
114
|
+
renderStride({...step, cycle: 60, unit, mark, anchor}) :
|
|
115
|
+
null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// A step *shape* segment as its cadence ("每小时从5分起每6分钟"). A bounded
|
|
119
|
+
// sub-range step (`a-b/n`) is not a whole-cycle stride, so it lists its fires;
|
|
120
|
+
// a short offset cadence (three fires or fewer) also lists, since the list is
|
|
121
|
+
// no longer than the cadence. Everything else routes through `renderStride`.
|
|
122
|
+
// The uneven whole-cycle step never reaches here as a step shape — the core
|
|
123
|
+
// enumerates it to a fire list, which the list path recognizes instead.
|
|
124
|
+
function stepClause(
|
|
125
|
+
segment: StepSegment,
|
|
126
|
+
unit: string,
|
|
127
|
+
mark: string,
|
|
128
|
+
anchor: string
|
|
129
|
+
): string {
|
|
130
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
131
|
+
const short = start !== 0 && segment.fires.length <= 3;
|
|
132
|
+
|
|
133
|
+
if (segment.startToken.indexOf('-') !== -1 || short) {
|
|
134
|
+
return anchor + segment.fires.join('、') + mark;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return renderStride({
|
|
138
|
+
interval: segment.interval,
|
|
139
|
+
start,
|
|
140
|
+
last: segment.fires[segment.fires.length - 1],
|
|
141
|
+
cycle: 60,
|
|
142
|
+
unit,
|
|
143
|
+
mark,
|
|
144
|
+
anchor
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
46
148
|
// The day period a 12-hour clock prepends under `ampm` (notes.md boundaries).
|
|
47
149
|
function dayPeriod(hour: number): string {
|
|
48
150
|
if (hour < 6) {
|
|
@@ -193,8 +295,23 @@ function renderEveryHour(): string {
|
|
|
193
295
|
}
|
|
194
296
|
|
|
195
297
|
// A minute anchored to the hour: "每小时1分", "每小时5分和30分", "每小时0至30分".
|
|
298
|
+
// A regular step reads as a stride cadence ("每小时从3分起每2分钟,至59分"),
|
|
299
|
+
// whether the core kept it a step shape (a uniform offset divides 60) or
|
|
300
|
+
// enumerated it to a fire list (an offset/uneven set) — both route through the
|
|
301
|
+
// stride; a short or irregular set keeps the enumerated "每小时…分" list.
|
|
302
|
+
function minuteHourClause(ir: IR): string {
|
|
303
|
+
const segments = fieldSegments(ir, 'minute');
|
|
304
|
+
|
|
305
|
+
if (ir.shapes.minute === 'step') {
|
|
306
|
+
return stepClause(stepSegment(ir, 'minute'), '分钟', '分', '每小时');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return strideFromSegments(segments, '分钟', '分', '每小时') ??
|
|
310
|
+
'每小时' + valueList(segments, '分');
|
|
311
|
+
}
|
|
312
|
+
|
|
196
313
|
function renderMinutePast(ir: IR): string {
|
|
197
|
-
return
|
|
314
|
+
return minuteHourClause(ir);
|
|
198
315
|
}
|
|
199
316
|
|
|
200
317
|
// The hour list as clock words: "9点、11点和13点".
|
|
@@ -218,11 +335,9 @@ function hourFrame(ir: IR): string {
|
|
|
218
335
|
// A repeating minute step, optionally confined to active hours.
|
|
219
336
|
function renderMinuteFrequency(ir: IR, plan: PlanNode): string {
|
|
220
337
|
const minuteStep = stepSegment(ir, 'minute');
|
|
221
|
-
// A "每N分钟" cadence
|
|
222
|
-
//
|
|
223
|
-
const base = minuteStep.
|
|
224
|
-
cadence(minuteStep.interval, UNITS.minute) :
|
|
225
|
-
renderMinutePast(ir);
|
|
338
|
+
// A clean stride is the bare "每N分钟" cadence; an offset step keeps its start
|
|
339
|
+
// ("每小时从5分起每6分钟"). A short offset cadence still lists its fires.
|
|
340
|
+
const base = stepClause(minuteStep, UNITS.minute, '分', '每小时');
|
|
226
341
|
const {hours} = plan as Extract<PlanNode, {kind: 'minuteFrequency'}>;
|
|
227
342
|
|
|
228
343
|
if (hours.kind === 'step') {
|
|
@@ -276,8 +391,7 @@ function renderMinutesAcrossHours(ir: IR, plan: PlanNode): string {
|
|
|
276
391
|
return '在' + hourList(ir) + ',每分钟';
|
|
277
392
|
}
|
|
278
393
|
|
|
279
|
-
return hourList(ir) + '
|
|
280
|
-
valueList(fieldSegments(ir, 'minute'), '分') + ',每分钟';
|
|
394
|
+
return hourList(ir) + ',' + minuteHourClause(ir) + ',每分钟';
|
|
281
395
|
}
|
|
282
396
|
|
|
283
397
|
// A minute clause across a stepped hour field. A wildcard minute reads "每2小时
|
|
@@ -285,9 +399,16 @@ function renderMinutesAcrossHours(ir: IR, plan: PlanNode): string {
|
|
|
285
399
|
function renderMinuteSpanAcrossHourStep(ir: IR, plan: PlanNode): string {
|
|
286
400
|
const hourStep = stepSegment(ir, 'hour');
|
|
287
401
|
const {form} = plan as Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>;
|
|
402
|
+
|
|
403
|
+
// A minute list enumerates its hours, with no "from M" idiom to lean on:
|
|
404
|
+
// "每小时5分和30分,在1点、3点…".
|
|
405
|
+
if (form === 'list') {
|
|
406
|
+
return renderMinutePast(ir) + ',在' + hourList(ir);
|
|
407
|
+
}
|
|
408
|
+
|
|
288
409
|
const minuteTail = form === 'wildcard' ?
|
|
289
410
|
'每分钟' :
|
|
290
|
-
|
|
411
|
+
minuteHourClause(ir) + ',每分钟';
|
|
291
412
|
|
|
292
413
|
// An offset stride (2/6 fires at 2,8,14,20) enumerates its hours like a
|
|
293
414
|
// discrete list; "每N小时" is faithful only from midnight.
|
|
@@ -312,6 +433,16 @@ function renderMinuteSpanAcrossHourStep(ir: IR, plan: PlanNode): string {
|
|
|
312
433
|
|
|
313
434
|
// Discrete clock times: "9点", "9点和17点".
|
|
314
435
|
function renderClockTimes(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
436
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
437
|
+
// minute reads as a cadence rather than a cross-product of clock times.
|
|
438
|
+
if (ir.shapes.minute === 'single') {
|
|
439
|
+
const cad = hourCadence(ir);
|
|
440
|
+
|
|
441
|
+
if (cad !== null) {
|
|
442
|
+
return cad;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
315
446
|
const {times} = plan as Extract<PlanNode, {kind: 'clockTimes'}>;
|
|
316
447
|
|
|
317
448
|
return joinAnd(times.map((t) => clockTime(t.hour, t.minute, t.second, opts)));
|
|
@@ -320,14 +451,24 @@ function renderClockTimes(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
320
451
|
// Compact clock times past the cap: the hour list (the minute is folded in),
|
|
321
452
|
// with a fixed second appended ("…,第30秒").
|
|
322
453
|
function renderCompactClockTimes(ir: IR, plan: PlanNode): string {
|
|
454
|
+
// An hour step (or arithmetic-progression hour list) under the single pinned
|
|
455
|
+
// minute reads as a cadence, not a wall of clock times. (Returns null for an
|
|
456
|
+
// irregular list or a range, which keep enumerating below.)
|
|
457
|
+
if (ir.shapes.minute === 'single') {
|
|
458
|
+
const cad = hourCadence(ir);
|
|
459
|
+
|
|
460
|
+
if (cad !== null) {
|
|
461
|
+
return cad;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
323
465
|
const {minute} = plan as Extract<PlanNode, {kind: 'compactClockTimes'}>;
|
|
324
466
|
const secs = fieldSegments(ir, 'second');
|
|
325
467
|
const tail = secs.length && ir.pattern.second !== '0' ?
|
|
326
468
|
',第' + valueText(secs) + '秒' : '';
|
|
327
469
|
|
|
328
470
|
if (minute > 0) {
|
|
329
|
-
return '
|
|
330
|
-
',在' + hourList(ir) + tail;
|
|
471
|
+
return minuteHourClause(ir) + ',在' + hourList(ir) + tail;
|
|
331
472
|
}
|
|
332
473
|
|
|
333
474
|
return hourList(ir) + tail;
|
|
@@ -340,7 +481,7 @@ function renderHourRange(ir: IR, plan: PlanNode): string {
|
|
|
340
481
|
if (range.minuteForm === 'lead') {
|
|
341
482
|
const minuteSegs = fieldSegments(ir, 'minute');
|
|
342
483
|
const past = minuteSegs.length && ir.pattern.minute !== '0' ?
|
|
343
|
-
|
|
484
|
+
minuteHourClause(ir) : '每小时';
|
|
344
485
|
|
|
345
486
|
return '在' + hourWord(range.from) + '至' + hourWord(range.to) +
|
|
346
487
|
'之间,' + past;
|
|
@@ -374,9 +515,101 @@ function renderHourStep(ir: IR): string {
|
|
|
374
515
|
return cadence(segment.interval, UNITS.hour);
|
|
375
516
|
}
|
|
376
517
|
|
|
518
|
+
// --- Hour-step cadence (the 24-cycle analog of renderStride). ---
|
|
519
|
+
|
|
520
|
+
// The hour field's stride, or null when the hour is not a cadence: a clean
|
|
521
|
+
// step segment from midnight yields its {interval, last}; an all-single hour
|
|
522
|
+
// list yields one only when its values form a long-enough arithmetic
|
|
523
|
+
// progression from midnight (so an irregular list like 9,17 keeps
|
|
524
|
+
// enumerating). Chinese has no "每N小时从X起" offset-hour idiom, so an offset or
|
|
525
|
+
// bounded stride (start > 0, or an interval not dividing 24) returns null and
|
|
526
|
+
// the caller enumerates its clock words, as before. The IR is unchanged.
|
|
527
|
+
function hourStride(ir: IR): {interval: number; last: number} | null {
|
|
528
|
+
const segments = fieldSegments(ir, 'hour');
|
|
529
|
+
|
|
530
|
+
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
531
|
+
const segment = segments[0];
|
|
532
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
533
|
+
|
|
534
|
+
if (start !== 0 || 24 % segment.interval !== 0) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return {interval: segment.interval,
|
|
539
|
+
last: segment.fires[segment.fires.length - 1]};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const values = singleValues(segments);
|
|
543
|
+
const step = values && arithmeticStep(values);
|
|
544
|
+
|
|
545
|
+
if (!step || step.start !== 0 || 24 % step.interval !== 0) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {interval: step.interval, last: step.last};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
553
|
+
// pinned minute and a second as a cadence — "每N小时" plus the minute/second —
|
|
554
|
+
// instead of cross-multiplying the hours into a wall of clock times. Returns
|
|
555
|
+
// null when the hour is not a clean stride from midnight, or when the
|
|
556
|
+
// cross-product is short enough that enumeration is no longer than the cadence:
|
|
557
|
+
// a meaningful second makes every clock time carry a second, so any stride is
|
|
558
|
+
// worth compacting; otherwise the stride must exceed the clock-time cap, the
|
|
559
|
+
// same point at which the core itself stops enumerating. Renderer-only; the IR
|
|
560
|
+
// is unchanged.
|
|
561
|
+
function hourCadence(ir: IR): string | null {
|
|
562
|
+
const stride = hourStride(ir);
|
|
563
|
+
|
|
564
|
+
if (!stride) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const fires = stride.last / stride.interval + 1;
|
|
569
|
+
|
|
570
|
+
if (ir.pattern.second === '0' && fires <= maxClockTimes) {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const prefix = cadence(stride.interval, UNITS.hour);
|
|
575
|
+
const minute = +ir.pattern.minute;
|
|
576
|
+
const subMinute = ir.pattern.second === '*' || ir.shapes.second === 'step';
|
|
577
|
+
|
|
578
|
+
// A wildcard or sub-minute step second confined to minute 0 of a clean
|
|
579
|
+
// stride is a confinement, not a juxtaposed cadence. Reuse the even-hours
|
|
580
|
+
// idiom ("在偶数小时0分的每一秒") so the form does NOT contain the bare "每2小时"
|
|
581
|
+
// and can never be misread as the absorbing hour cadence (the same reason en
|
|
582
|
+
// says "for one minute during every other hour", not "every two hours"). The
|
|
583
|
+
// idiom exists only for the even-hour stride; another clean stride keeps
|
|
584
|
+
// enumerating (return null) rather than coin a misleading "每N小时…" form.
|
|
585
|
+
if (minute === 0 && subMinute) {
|
|
586
|
+
return stride.interval === 2 ? '在偶数小时0分' + secondTail(ir) : null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// A pinned minute 0 folds into the cadence with the explicit "0分" so the
|
|
590
|
+
// confinement stays visible ("每2小时0分的第30秒"); a non-zero minute is a real
|
|
591
|
+
// clock minute named after the cadence ("每2小时5分的每一秒"). A plain :00
|
|
592
|
+
// second adds nothing.
|
|
593
|
+
if (minute === 0) {
|
|
594
|
+
return prefix + '0分' + secondTail(ir);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return ir.pattern.second === '0' ?
|
|
598
|
+
prefix + minute + '分' :
|
|
599
|
+
prefix + minute + '分' + secondTail(ir);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// The fused second tail for an hour cadence: "的每一秒" for a wildcard second,
|
|
603
|
+
// else "的" + the second's own clause ("的第30秒", "的每15秒").
|
|
604
|
+
function secondTail(ir: IR): string {
|
|
605
|
+
const sec = secondClause(ir);
|
|
606
|
+
|
|
607
|
+
return sec === '每秒' ? '的每一秒' : '的' + sec;
|
|
608
|
+
}
|
|
609
|
+
|
|
377
610
|
// A continuous minute range fires every minute within it: "每小时0至30分,每分钟".
|
|
378
611
|
function renderRangeOfMinutes(ir: IR): string {
|
|
379
|
-
return
|
|
612
|
+
return minuteHourClause(ir) + ',每分钟';
|
|
380
613
|
}
|
|
381
614
|
|
|
382
615
|
// A standalone second field: "每7秒" (step cadence) or "每分钟第4、17、42秒".
|
|
@@ -388,7 +621,8 @@ function renderStandaloneSeconds(ir: IR): string {
|
|
|
388
621
|
return cadence(first.interval, UNITS.second);
|
|
389
622
|
}
|
|
390
623
|
|
|
391
|
-
return
|
|
624
|
+
return strideFromSegments(segs, '秒', '秒', '每分钟') ??
|
|
625
|
+
'每分钟第' + valueText(segs) + '秒';
|
|
392
626
|
}
|
|
393
627
|
|
|
394
628
|
// A second anchored to the minute: "每分钟第1秒", "每分钟第4、17、42秒".
|
|
@@ -423,7 +657,10 @@ function secondClause(ir: IR): string {
|
|
|
423
657
|
return cadence(first.interval, UNITS.second);
|
|
424
658
|
}
|
|
425
659
|
|
|
426
|
-
|
|
660
|
+
// An offset/uneven step the core enumerated to this list reads as a stride
|
|
661
|
+
// cadence when the fires form a long-enough progression.
|
|
662
|
+
return strideFromSegments(segs, '秒', '秒', '每分钟') ??
|
|
663
|
+
'第' + valueText(segs) + '秒';
|
|
427
664
|
}
|
|
428
665
|
|
|
429
666
|
// The minute clause for a composed (seconds) schedule.
|
|
@@ -450,22 +687,26 @@ function isHourCadence(ir: IR): boolean {
|
|
|
450
687
|
function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
451
688
|
const sec = secondClause(ir);
|
|
452
689
|
const {rest} = plan as Extract<PlanNode, {kind: 'composeSeconds'}>;
|
|
690
|
+
const composedClock =
|
|
691
|
+
rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes';
|
|
453
692
|
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
ir.pattern.minute === '0') {
|
|
462
|
-
const clocks = hourFires(ir).map(function clock(hour): string {
|
|
463
|
-
return hourWord(hour) + '0分';
|
|
464
|
-
});
|
|
465
|
-
const tail = sec === '每秒' ? '的每一秒' : '的' + sec;
|
|
466
|
-
const core = joinAnd(clocks) + tail;
|
|
693
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
694
|
+
// minute reads as a cadence, not a wall of clock times ("每2小时0分的第30秒").
|
|
695
|
+
// hourCadence folds in the second itself, so it is returned whole, never the
|
|
696
|
+
// rest-plus-second fall-through that would double the second clause. The
|
|
697
|
+
// cadence is sub-daily (no 每天); a qualifier is added by describe().
|
|
698
|
+
if (composedClock) {
|
|
699
|
+
const hourCad = hourCadence(ir);
|
|
467
700
|
|
|
468
|
-
|
|
701
|
+
if (hourCad !== null) {
|
|
702
|
+
return hourCad;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// The minute is pinned to 0 under specific hours that did not compact to a
|
|
707
|
+
// cadence: fuse the seconds with each explicit clock minute.
|
|
708
|
+
if (composedClock && ir.pattern.minute === '0') {
|
|
709
|
+
return composeMinuteZeroClocks(ir, sec);
|
|
469
710
|
}
|
|
470
711
|
|
|
471
712
|
const restText = render(ir, rest, opts);
|
|
@@ -485,6 +726,27 @@ function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
485
726
|
return restText + sec;
|
|
486
727
|
}
|
|
487
728
|
|
|
729
|
+
// A minute pinned to 0 under specific clock hours (not a compacted cadence): a
|
|
730
|
+
// bare clock word ("9点") would hide the :00 and leave the second dangling
|
|
731
|
+
// ("…9点每秒"), reading as the whole hour. Fuse the seconds with the explicit
|
|
732
|
+
// clock minute ("9点0分的每一秒"), so the one-minute confinement (60 fires in
|
|
733
|
+
// :00, not 3,600 across the hour) stays visible. The daily frame leads with
|
|
734
|
+
// 每天; a weekday or date qualifier is added by describe().
|
|
735
|
+
function composeMinuteZeroClocks(ir: IR, sec: string): string {
|
|
736
|
+
const clocks = hourFires(ir).map(function clock(hour): string {
|
|
737
|
+
// Noon's word (正午) already pins 12:00, so the "0分" is redundant for it;
|
|
738
|
+
// midnight (凌晨0点) and other hours still need it to pin the minute.
|
|
739
|
+
return hour === 12 ? '正午' : hourWord(hour) + '0分';
|
|
740
|
+
});
|
|
741
|
+
// A pinned minute makes the seconds' own "每分钟" anchor misleading (it is a
|
|
742
|
+
// single minute, not every minute), so the stride here drops it.
|
|
743
|
+
const nested = strideFromSegments(fieldSegments(ir, 'second'), '秒', '秒', '');
|
|
744
|
+
const tail = sec === '每秒' ? '的每一秒' : '的' + (nested ?? sec);
|
|
745
|
+
const core = joinAnd(clocks) + tail;
|
|
746
|
+
|
|
747
|
+
return isDaily(ir) ? '每天' + core : core;
|
|
748
|
+
}
|
|
749
|
+
|
|
488
750
|
// Wildcard or stepped minute: hang the "每分钟/每N分钟每秒" tail off the hour.
|
|
489
751
|
function composeSecondsCadence(ir: IR): string {
|
|
490
752
|
const sec = secondClause(ir);
|
|
@@ -499,6 +761,18 @@ function composeSecondsCadence(ir: IR): string {
|
|
|
499
761
|
}
|
|
500
762
|
|
|
501
763
|
if (ir.shapes.hour === 'wildcard') {
|
|
764
|
+
// "每秒,每2分钟" juxtaposes two cadences that read as contradictory. A
|
|
765
|
+
// step-2 minute from the top of the hour IS exactly the even minutes; bind
|
|
766
|
+
// the every-second cadence to them ("每偶数分钟的每一秒") rather than listing
|
|
767
|
+
// the two side by side. Other strides keep the juxtaposed form.
|
|
768
|
+
if (ir.shapes.minute === 'step' && sec === '每秒') {
|
|
769
|
+
const minuteStep = stepSegment(ir, 'minute');
|
|
770
|
+
|
|
771
|
+
if (minuteStep.startToken === '*' && minuteStep.interval === 2) {
|
|
772
|
+
return '每偶数分钟的每一秒';
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
502
776
|
return sec + ',' + minuteClause(ir);
|
|
503
777
|
}
|
|
504
778
|
|
|
@@ -511,14 +785,20 @@ function composeSecondsCadence(ir: IR): string {
|
|
|
511
785
|
// last fire onto the window end ("…17点30分") and reading as a continuous span.
|
|
512
786
|
function composeSecondsListed(ir: IR): string {
|
|
513
787
|
const sec = secondClause(ir);
|
|
514
|
-
const minutes =
|
|
788
|
+
const minutes = minuteHourClause(ir);
|
|
515
789
|
|
|
516
790
|
// A single restricted hour with an every-second cadence fuses the clock time
|
|
517
791
|
// with its minutes — "凌晨0点5、20、35、50分的每一秒" — rather than the "每小时"
|
|
518
792
|
// that falsely implies every hour. A non-wildcard second keeps the list form.
|
|
793
|
+
// A non-uniform minute step the core enumerated to a fire list reads as its
|
|
794
|
+
// stride cadence ("凌晨0点从3分起每2分钟,至59分的每一秒"); the hour fuses, so the
|
|
795
|
+
// stride drops its "每小时" anchor. A short or irregular set keeps the list.
|
|
519
796
|
if (ir.shapes.hour === 'single' && sec === '每秒') {
|
|
520
|
-
|
|
521
|
-
|
|
797
|
+
const minuteSegs = fieldSegments(ir, 'minute');
|
|
798
|
+
const minuteCad = strideFromSegments(minuteSegs, '分钟', '分', '') ??
|
|
799
|
+
valueList(minuteSegs, '分');
|
|
800
|
+
|
|
801
|
+
return hourWord(hourFires(ir)[0]) + minuteCad + '的每一秒';
|
|
522
802
|
}
|
|
523
803
|
|
|
524
804
|
if (ir.shapes.hour === 'wildcard') {
|
|
@@ -861,14 +1141,26 @@ function composeWindow(ir: IR, core: string): string {
|
|
|
861
1141
|
return qualifier(ir) + core;
|
|
862
1142
|
}
|
|
863
1143
|
|
|
1144
|
+
// Whether an hour cadence applies — a single pinned minute over a clean hour
|
|
1145
|
+
// stride — so the clock-point plans take the cadence frame, not the daily one.
|
|
1146
|
+
function hourCadenceApplies(ir: IR): boolean {
|
|
1147
|
+
return ir.shapes.minute === 'single' && hourCadence(ir) !== null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
864
1150
|
function describe(ir: IR, opts: Opts): string {
|
|
865
1151
|
const {kind} = ir.plan;
|
|
866
1152
|
const core = render(ir, ir.plan, opts);
|
|
867
1153
|
let composed = core;
|
|
868
1154
|
|
|
1155
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
1156
|
+
// minute is a sub-daily cadence, not a daily clock point — it takes the
|
|
1157
|
+
// cadence frame (no 每天), like the bare "每2小时" form.
|
|
1158
|
+
if (hourCadenceApplies(ir)) {
|
|
1159
|
+
composed = composeCadence(ir, core);
|
|
1160
|
+
}
|
|
869
1161
|
// A compact clock list with a minute past the hour ("每小时5分…") reads as a
|
|
870
1162
|
// cadence, not a daily clock point — no 每天.
|
|
871
|
-
if (kind === 'clockTimes' ||
|
|
1163
|
+
else if (kind === 'clockTimes' ||
|
|
872
1164
|
kind === 'compactClockTimes' && ir.pattern.minute === '0') {
|
|
873
1165
|
composed = composePoint(ir, core);
|
|
874
1166
|
}
|
package/types/core/ir.d.ts
CHANGED
package/types/core/util.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
declare function includes(str: string | number, sub: string): boolean;
|
|
2
2
|
declare function unique<T>(items: T[]): T[];
|
|
3
3
|
declare function isNonNegativeInteger(value: string): boolean;
|
|
4
|
+
declare function arithmeticStep(values: number[]): {
|
|
5
|
+
start: number;
|
|
6
|
+
interval: number;
|
|
7
|
+
last: number;
|
|
8
|
+
} | null;
|
|
4
9
|
declare function toFieldNumber(token: string, numberMap?: {
|
|
5
10
|
[name: string]: number;
|
|
6
11
|
}): number;
|
|
7
|
-
export { includes, isNonNegativeInteger, toFieldNumber, unique };
|
|
12
|
+
export { arithmeticStep, includes, isNonNegativeInteger, toFieldNumber, unique };
|