cronli5 0.1.2 → 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 +89 -0
- package/cli.js +9 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +280 -64
- package/dist/cronli5.js +280 -64
- package/dist/lang/de.cjs +227 -42
- package/dist/lang/de.js +227 -42
- package/dist/lang/en.cjs +216 -50
- package/dist/lang/en.js +216 -50
- package/dist/lang/es.cjs +259 -54
- package/dist/lang/es.js +259 -54
- package/dist/lang/fi.cjs +230 -69
- package/dist/lang/fi.js +230 -69
- package/dist/lang/zh.cjs +190 -19
- package/dist/lang/zh.js +190 -19
- package/package.json +3 -1
- package/src/core/analyze.ts +7 -0
- package/src/core/ir.ts +1 -1
- package/src/core/normalize.ts +94 -4
- package/src/core/util.ts +31 -1
- package/src/lang/de/index.ts +449 -46
- package/src/lang/en/index.ts +433 -63
- package/src/lang/es/index.ts +505 -63
- package/src/lang/fi/index.ts +455 -89
- package/src/lang/zh/index.ts +393 -30
- 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,15 +335,19 @@ 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') {
|
|
229
|
-
|
|
344
|
+
const hourStep = stepSegment(ir, 'hour');
|
|
345
|
+
|
|
346
|
+
// "每N小时" is only faithful from midnight; an offset step (2/6 fires at
|
|
347
|
+
// 2,8,14,20) enumerates its hours instead.
|
|
348
|
+
return hourStep.startToken === '*' ?
|
|
349
|
+
cadence(hourStep.interval, UNITS.hour) + base :
|
|
350
|
+
'在' + hourList(ir) + ',' + base;
|
|
230
351
|
}
|
|
231
352
|
|
|
232
353
|
if (hours.kind === 'single' ||
|
|
@@ -247,10 +368,16 @@ function renderMinuteFrequency(ir: IR, plan: PlanNode): string {
|
|
|
247
368
|
return base;
|
|
248
369
|
}
|
|
249
370
|
|
|
250
|
-
// A minute span within a single hour
|
|
371
|
+
// A minute span within a single hour. A wildcard minute reads as that hour
|
|
372
|
+
// itself — "凌晨0点的每一分钟" — not a synthesized "在H点至H点59分之间" range the
|
|
373
|
+
// source never stated; a partial minute span keeps the named range.
|
|
251
374
|
function renderMinuteSpanInHour(ir: IR, plan: PlanNode): string {
|
|
252
375
|
const span = plan as Extract<PlanNode, {kind: 'minuteSpanInHour'}>;
|
|
253
376
|
|
|
377
|
+
if (ir.pattern.minute === '*') {
|
|
378
|
+
return hourWord(span.hour) + '的每一分钟';
|
|
379
|
+
}
|
|
380
|
+
|
|
254
381
|
return '在' + hourWord(span.hour) + '至' + span.hour + '点' +
|
|
255
382
|
span.span[1] + '分之间,每分钟';
|
|
256
383
|
}
|
|
@@ -264,26 +391,58 @@ function renderMinutesAcrossHours(ir: IR, plan: PlanNode): string {
|
|
|
264
391
|
return '在' + hourList(ir) + ',每分钟';
|
|
265
392
|
}
|
|
266
393
|
|
|
267
|
-
return hourList(ir) + '
|
|
268
|
-
valueList(fieldSegments(ir, 'minute'), '分') + ',每分钟';
|
|
394
|
+
return hourList(ir) + ',' + minuteHourClause(ir) + ',每分钟';
|
|
269
395
|
}
|
|
270
396
|
|
|
271
397
|
// A minute clause across a stepped hour field. A wildcard minute reads "每2小时
|
|
272
398
|
// 内,每分钟"; a ranged minute names it: "每2小时,每小时0至30分,每分钟".
|
|
273
399
|
function renderMinuteSpanAcrossHourStep(ir: IR, plan: PlanNode): string {
|
|
274
|
-
const
|
|
400
|
+
const hourStep = stepSegment(ir, 'hour');
|
|
275
401
|
const {form} = plan as Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>;
|
|
276
402
|
|
|
277
|
-
|
|
278
|
-
|
|
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);
|
|
279
407
|
}
|
|
280
408
|
|
|
281
|
-
|
|
282
|
-
'
|
|
409
|
+
const minuteTail = form === 'wildcard' ?
|
|
410
|
+
'每分钟' :
|
|
411
|
+
minuteHourClause(ir) + ',每分钟';
|
|
412
|
+
|
|
413
|
+
// An offset stride (2/6 fires at 2,8,14,20) enumerates its hours like a
|
|
414
|
+
// discrete list; "每N小时" is faithful only from midnight.
|
|
415
|
+
if (hourStep.startToken !== '*') {
|
|
416
|
+
return form === 'wildcard' ?
|
|
417
|
+
'在' + hourList(ir) + ',' + minuteTail :
|
|
418
|
+
hourList(ir) + ',' + minuteTail;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// A step-2 hour from midnight IS exactly the even hours; name them so, rather
|
|
422
|
+
// than the vague "每2小时内" that reads as an interval. Other strides keep it.
|
|
423
|
+
if (hourStep.interval === 2 && form === 'wildcard') {
|
|
424
|
+
return '在偶数小时,' + minuteTail;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const cad = cadence(hourStep.interval, UNITS.hour);
|
|
428
|
+
|
|
429
|
+
return form === 'wildcard' ?
|
|
430
|
+
cad + '内,' + minuteTail :
|
|
431
|
+
cad + ',' + minuteTail;
|
|
283
432
|
}
|
|
284
433
|
|
|
285
434
|
// Discrete clock times: "9点", "9点和17点".
|
|
286
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
|
+
|
|
287
446
|
const {times} = plan as Extract<PlanNode, {kind: 'clockTimes'}>;
|
|
288
447
|
|
|
289
448
|
return joinAnd(times.map((t) => clockTime(t.hour, t.minute, t.second, opts)));
|
|
@@ -292,14 +451,24 @@ function renderClockTimes(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
292
451
|
// Compact clock times past the cap: the hour list (the minute is folded in),
|
|
293
452
|
// with a fixed second appended ("…,第30秒").
|
|
294
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
|
+
|
|
295
465
|
const {minute} = plan as Extract<PlanNode, {kind: 'compactClockTimes'}>;
|
|
296
466
|
const secs = fieldSegments(ir, 'second');
|
|
297
467
|
const tail = secs.length && ir.pattern.second !== '0' ?
|
|
298
468
|
',第' + valueText(secs) + '秒' : '';
|
|
299
469
|
|
|
300
470
|
if (minute > 0) {
|
|
301
|
-
return '
|
|
302
|
-
',在' + hourList(ir) + tail;
|
|
471
|
+
return minuteHourClause(ir) + ',在' + hourList(ir) + tail;
|
|
303
472
|
}
|
|
304
473
|
|
|
305
474
|
return hourList(ir) + tail;
|
|
@@ -312,7 +481,7 @@ function renderHourRange(ir: IR, plan: PlanNode): string {
|
|
|
312
481
|
if (range.minuteForm === 'lead') {
|
|
313
482
|
const minuteSegs = fieldSegments(ir, 'minute');
|
|
314
483
|
const past = minuteSegs.length && ir.pattern.minute !== '0' ?
|
|
315
|
-
|
|
484
|
+
minuteHourClause(ir) : '每小时';
|
|
316
485
|
|
|
317
486
|
return '在' + hourWord(range.from) + '至' + hourWord(range.to) +
|
|
318
487
|
'之间,' + past;
|
|
@@ -346,9 +515,101 @@ function renderHourStep(ir: IR): string {
|
|
|
346
515
|
return cadence(segment.interval, UNITS.hour);
|
|
347
516
|
}
|
|
348
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
|
+
|
|
349
610
|
// A continuous minute range fires every minute within it: "每小时0至30分,每分钟".
|
|
350
611
|
function renderRangeOfMinutes(ir: IR): string {
|
|
351
|
-
return
|
|
612
|
+
return minuteHourClause(ir) + ',每分钟';
|
|
352
613
|
}
|
|
353
614
|
|
|
354
615
|
// A standalone second field: "每7秒" (step cadence) or "每分钟第4、17、42秒".
|
|
@@ -360,7 +621,8 @@ function renderStandaloneSeconds(ir: IR): string {
|
|
|
360
621
|
return cadence(first.interval, UNITS.second);
|
|
361
622
|
}
|
|
362
623
|
|
|
363
|
-
return
|
|
624
|
+
return strideFromSegments(segs, '秒', '秒', '每分钟') ??
|
|
625
|
+
'每分钟第' + valueText(segs) + '秒';
|
|
364
626
|
}
|
|
365
627
|
|
|
366
628
|
// A second anchored to the minute: "每分钟第1秒", "每分钟第4、17、42秒".
|
|
@@ -395,7 +657,10 @@ function secondClause(ir: IR): string {
|
|
|
395
657
|
return cadence(first.interval, UNITS.second);
|
|
396
658
|
}
|
|
397
659
|
|
|
398
|
-
|
|
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) + '秒';
|
|
399
664
|
}
|
|
400
665
|
|
|
401
666
|
// The minute clause for a composed (seconds) schedule.
|
|
@@ -422,11 +687,34 @@ function isHourCadence(ir: IR): boolean {
|
|
|
422
687
|
function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
423
688
|
const sec = secondClause(ir);
|
|
424
689
|
const {rest} = plan as Extract<PlanNode, {kind: 'composeSeconds'}>;
|
|
690
|
+
const composedClock =
|
|
691
|
+
rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes';
|
|
692
|
+
|
|
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);
|
|
700
|
+
|
|
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);
|
|
710
|
+
}
|
|
711
|
+
|
|
425
712
|
const restText = render(ir, rest, opts);
|
|
426
713
|
|
|
427
|
-
if (
|
|
428
|
-
isDaily(ir)) {
|
|
429
|
-
|
|
714
|
+
if (rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes') {
|
|
715
|
+
if (isDaily(ir)) {
|
|
716
|
+
return '每天' + restText + sec;
|
|
717
|
+
}
|
|
430
718
|
}
|
|
431
719
|
|
|
432
720
|
// A stated minute (e.g. minute 0 under a sub-minute second) takes the same
|
|
@@ -438,6 +726,27 @@ function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
438
726
|
return restText + sec;
|
|
439
727
|
}
|
|
440
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
|
+
|
|
441
750
|
// Wildcard or stepped minute: hang the "每分钟/每N分钟每秒" tail off the hour.
|
|
442
751
|
function composeSecondsCadence(ir: IR): string {
|
|
443
752
|
const sec = secondClause(ir);
|
|
@@ -452,6 +761,18 @@ function composeSecondsCadence(ir: IR): string {
|
|
|
452
761
|
}
|
|
453
762
|
|
|
454
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
|
+
|
|
455
776
|
return sec + ',' + minuteClause(ir);
|
|
456
777
|
}
|
|
457
778
|
|
|
@@ -464,7 +785,21 @@ function composeSecondsCadence(ir: IR): string {
|
|
|
464
785
|
// last fire onto the window end ("…17点30分") and reading as a continuous span.
|
|
465
786
|
function composeSecondsListed(ir: IR): string {
|
|
466
787
|
const sec = secondClause(ir);
|
|
467
|
-
const minutes =
|
|
788
|
+
const minutes = minuteHourClause(ir);
|
|
789
|
+
|
|
790
|
+
// A single restricted hour with an every-second cadence fuses the clock time
|
|
791
|
+
// with its minutes — "凌晨0点5、20、35、50分的每一秒" — rather than the "每小时"
|
|
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.
|
|
796
|
+
if (ir.shapes.hour === 'single' && sec === '每秒') {
|
|
797
|
+
const minuteSegs = fieldSegments(ir, 'minute');
|
|
798
|
+
const minuteCad = strideFromSegments(minuteSegs, '分钟', '分', '') ??
|
|
799
|
+
valueList(minuteSegs, '分');
|
|
800
|
+
|
|
801
|
+
return hourWord(hourFires(ir)[0]) + minuteCad + '的每一秒';
|
|
802
|
+
}
|
|
468
803
|
|
|
469
804
|
if (ir.shapes.hour === 'wildcard') {
|
|
470
805
|
return minutes + ',' + sec;
|
|
@@ -479,12 +814,28 @@ function composeSecondsListed(ir: IR): string {
|
|
|
479
814
|
}
|
|
480
815
|
|
|
481
816
|
// Seconds composed with the minute/hour structure, dispatched on the minute.
|
|
817
|
+
// A single minute over a composed clock-time rest (the core already joined the
|
|
818
|
+
// lone hour and minute into "N点M分") keeps that composition, attaching the
|
|
819
|
+
// second to it rather than splitting the minute back out into the "每小时N分"
|
|
820
|
+
// list path; a minute list stays on that list path so each fire is named.
|
|
482
821
|
function renderComposeSeconds(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
483
|
-
|
|
822
|
+
const {rest} = plan as Extract<PlanNode, {kind: 'composeSeconds'}>;
|
|
823
|
+
const composedClock =
|
|
824
|
+
rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes';
|
|
825
|
+
|
|
826
|
+
if (ir.pattern.minute === '0' ||
|
|
827
|
+
composedClock && ir.shapes.minute === 'single') {
|
|
484
828
|
return composeSecondsOnHour(ir, plan, opts);
|
|
485
829
|
}
|
|
486
830
|
|
|
487
|
-
|
|
831
|
+
// "每N分钟" is faithful only for a wildcard or top-of-hour step; an offset
|
|
832
|
+
// step (5/15 fires at :05,:20,…) takes the enumerated list path so its start
|
|
833
|
+
// is named, never dropped.
|
|
834
|
+
const minuteCadence = ir.pattern.minute === '*' ||
|
|
835
|
+
ir.shapes.minute === 'step' &&
|
|
836
|
+
stepSegment(ir, 'minute').startToken === '*';
|
|
837
|
+
|
|
838
|
+
if (minuteCadence) {
|
|
488
839
|
return composeSecondsCadence(ir);
|
|
489
840
|
}
|
|
490
841
|
|
|
@@ -790,14 +1141,26 @@ function composeWindow(ir: IR, core: string): string {
|
|
|
790
1141
|
return qualifier(ir) + core;
|
|
791
1142
|
}
|
|
792
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
|
+
|
|
793
1150
|
function describe(ir: IR, opts: Opts): string {
|
|
794
1151
|
const {kind} = ir.plan;
|
|
795
1152
|
const core = render(ir, ir.plan, opts);
|
|
796
1153
|
let composed = core;
|
|
797
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
|
+
}
|
|
798
1161
|
// A compact clock list with a minute past the hour ("每小时5分…") reads as a
|
|
799
1162
|
// cadence, not a daily clock point — no 每天.
|
|
800
|
-
if (kind === 'clockTimes' ||
|
|
1163
|
+
else if (kind === 'clockTimes' ||
|
|
801
1164
|
kind === 'compactClockTimes' && ir.pattern.minute === '0') {
|
|
802
1165
|
composed = composePoint(ir, core);
|
|
803
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 };
|