cronli5 0.2.0 → 0.3.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.
- package/CHANGELOG.md +90 -0
- package/README.md +4 -4
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +514 -407
- package/dist/cronli5.js +514 -407
- package/dist/lang/de.cjs +296 -225
- package/dist/lang/de.js +296 -225
- package/dist/lang/en.cjs +471 -364
- package/dist/lang/en.js +471 -364
- package/dist/lang/es.cjs +318 -281
- package/dist/lang/es.js +318 -281
- package/dist/lang/fi.cjs +326 -276
- package/dist/lang/fi.js +326 -276
- package/dist/lang/zh.cjs +308 -236
- package/dist/lang/zh.js +308 -236
- package/package.json +1 -1
- package/src/core/analyze.ts +22 -21
- package/src/core/cadence.ts +164 -0
- package/src/core/index.ts +3 -1
- package/src/core/normalize.ts +3 -3
- package/src/core/parse.ts +1 -1
- package/src/core/{ir.ts → schedule.ts} +23 -24
- package/src/core/shapes.ts +8 -1
- package/src/core/specs.ts +1 -1
- package/src/core/util.ts +4 -83
- package/src/core/validate.ts +2 -2
- package/src/core/weekday.ts +54 -0
- package/src/cronli5.ts +7 -7
- package/src/lang/de/index.ts +329 -288
- package/src/lang/en/dialects.ts +1 -1
- package/src/lang/en/index.ts +640 -516
- package/src/lang/es/index.ts +342 -374
- package/src/lang/es/notes.md +1 -1
- package/src/lang/fi/dialects.ts +1 -1
- package/src/lang/fi/index.ts +367 -372
- package/src/lang/fi/notes.md +23 -8
- package/src/lang/fi/status.json +1 -1
- package/src/lang/zh/index.ts +344 -262
- package/src/types.ts +6 -6
- package/types/core/analyze.d.ts +4 -4
- package/types/core/cadence.d.ts +33 -0
- package/types/core/index.d.ts +3 -1
- package/types/core/normalize.d.ts +1 -1
- package/types/core/parse.d.ts +1 -1
- package/types/core/{ir.d.ts → schedule.d.ts} +16 -21
- package/types/core/shapes.d.ts +2 -1
- package/types/core/specs.d.ts +1 -1
- package/types/core/util.d.ts +1 -15
- package/types/core/weekday.d.ts +10 -0
- package/types/lang/de/index.d.ts +1 -1
- package/types/lang/en/dialects.d.ts +1 -1
- package/types/lang/en/index.d.ts +1 -1
- package/types/lang/es/index.d.ts +1 -1
- package/types/lang/fi/dialects.d.ts +1 -1
- package/types/lang/fi/index.d.ts +1 -1
- package/types/lang/zh/index.d.ts +1 -1
- package/types/types.d.ts +5 -5
package/src/lang/zh/index.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
// The Chinese (Mandarin) language module: renders the analyzed cron pattern
|
|
2
|
-
// (
|
|
3
|
-
// vocab + assembly: Arabic numerals with measure words (点/分/秒, 月/日),
|
|
2
|
+
// (Schedule) as Simplified Chinese. Mandarin is analytic (no inflection), so
|
|
3
|
+
// this is vocab + assembly: Arabic numerals with measure words (点/分/秒, 月/日),
|
|
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
7
|
import {
|
|
8
|
-
arithmeticStep,
|
|
9
|
-
|
|
8
|
+
arithmeticStep, hourListStride, offsetCleanStride,
|
|
9
|
+
renderStride as chooseStride, segmentsOf, singleValues, stepSegment
|
|
10
|
+
} from '../../core/cadence.js';
|
|
11
|
+
import {isOpenStep} from '../../core/shapes.js';
|
|
12
|
+
import {orderWeekdaysForDisplay} from '../../core/weekday.js';
|
|
13
|
+
import {toFieldNumber} from '../../core/util.js';
|
|
10
14
|
import {maxClockTimes, monthNumbers, weekdayNumbers} from '../../core/specs.js';
|
|
11
15
|
import type {Cronli5Options} from '../../types.js';
|
|
12
16
|
import type {
|
|
13
|
-
|
|
14
|
-
} from '../../core/
|
|
17
|
+
Schedule, Language, NormalizedOptions, PlanNode, Segment
|
|
18
|
+
} from '../../core/schedule.js';
|
|
15
19
|
import {resolveDialect, type ChineseStyle} from './dialects.js';
|
|
16
20
|
|
|
17
21
|
type Opts = NormalizedOptions<ChineseStyle>;
|
|
18
|
-
type Renderer = (
|
|
22
|
+
type Renderer = (schedule: Schedule, plan: PlanNode, opts: Opts) => string;
|
|
19
23
|
type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
20
24
|
|
|
21
25
|
const UNITS = {hour: '小时', minute: '分钟', second: '秒'};
|
|
@@ -30,16 +34,6 @@ function joinAnd(items: string[]): string {
|
|
|
30
34
|
return items.slice(0, -1).join('、') + '和' + items[items.length - 1];
|
|
31
35
|
}
|
|
32
36
|
|
|
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
37
|
// "每N分钟" / "每分钟" — a cadence over a unit (the numeral 1 is suppressed).
|
|
44
38
|
function cadence(interval: number, unit: string): string {
|
|
45
39
|
return interval === 1 ? '每' + unit : '每' + interval + unit;
|
|
@@ -71,36 +65,19 @@ interface Stride {
|
|
|
71
65
|
// list path recognizes as a progression).
|
|
72
66
|
function renderStride(stride: Stride): string {
|
|
73
67
|
const {interval, start, last, cycle, unit, mark, anchor} = stride;
|
|
74
|
-
const tiles = cycle % interval === 0;
|
|
75
|
-
|
|
76
|
-
if (start === 0 && tiles) {
|
|
77
|
-
return cadence(interval, unit);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
68
|
const lead = anchor + '从' + start + mark + '起' + cadence(interval, unit);
|
|
81
69
|
|
|
82
|
-
return start
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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;
|
|
70
|
+
return chooseStride({start, interval, cycle}, {
|
|
71
|
+
bare: () => cadence(interval, unit),
|
|
72
|
+
offset: () => lead,
|
|
73
|
+
bounded: () => lead + ',至' + last + mark
|
|
74
|
+
});
|
|
99
75
|
}
|
|
100
76
|
|
|
101
77
|
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
102
78
|
// form an arithmetic progression long enough to beat the list (the core
|
|
103
|
-
// enumerates an offset/uneven step to this fire list; the
|
|
79
|
+
// enumerates an offset/uneven step to this fire list; the Schedule is
|
|
80
|
+
// unchanged, so
|
|
104
81
|
// the renderer recognizes the progression). Returns null for a non-progression
|
|
105
82
|
// or a too-short list, leaving the caller to enumerate.
|
|
106
83
|
function strideFromSegments(
|
|
@@ -209,10 +186,10 @@ function hourWord(hour: number): string {
|
|
|
209
186
|
}
|
|
210
187
|
|
|
211
188
|
// The active hour fires of a discrete hour field, expanding every segment.
|
|
212
|
-
function hourFires(
|
|
189
|
+
function hourFires(schedule: Schedule): number[] {
|
|
213
190
|
const fires: number[] = [];
|
|
214
191
|
|
|
215
|
-
|
|
192
|
+
segmentsOf(schedule, 'hour').forEach(function expand(segment) {
|
|
216
193
|
if (segment.kind === 'step') {
|
|
217
194
|
fires.push(...segment.fires);
|
|
218
195
|
}
|
|
@@ -301,19 +278,19 @@ function renderEveryHour(): string {
|
|
|
301
278
|
// whether the core kept it a step shape (a uniform offset divides 60) or
|
|
302
279
|
// enumerated it to a fire list (an offset/uneven set) — both route through the
|
|
303
280
|
// stride; a short or irregular set keeps the enumerated "每小时…分" list.
|
|
304
|
-
function minuteHourClause(
|
|
305
|
-
const segments =
|
|
281
|
+
function minuteHourClause(schedule: Schedule): string {
|
|
282
|
+
const segments = segmentsOf(schedule, 'minute');
|
|
306
283
|
|
|
307
|
-
if (
|
|
308
|
-
return stepClause(stepSegment(
|
|
284
|
+
if (schedule.shapes.minute === 'step') {
|
|
285
|
+
return stepClause(stepSegment(schedule, 'minute'), '分钟', '分', '每小时');
|
|
309
286
|
}
|
|
310
287
|
|
|
311
288
|
return strideFromSegments(segments, '分钟', '分', '每小时') ??
|
|
312
289
|
'每小时' + valueList(segments, '分');
|
|
313
290
|
}
|
|
314
291
|
|
|
315
|
-
function renderMinutePast(
|
|
316
|
-
return minuteHourClause(
|
|
292
|
+
function renderMinutePast(schedule: Schedule): string {
|
|
293
|
+
return minuteHourClause(schedule);
|
|
317
294
|
}
|
|
318
295
|
|
|
319
296
|
// One hour segment as clock words by its form: a range is a span ("9点至20点"),
|
|
@@ -336,28 +313,28 @@ function hourSegmentWords(segment: Segment): string[] {
|
|
|
336
313
|
// The hour field as clock words, by segment form: "9点、11点和13点" for a list
|
|
337
314
|
// of singles, "9点至20点和22点" for a range plus a single. Each segment renders
|
|
338
315
|
// as the operator the source wrote (range → span), not its expanded fires.
|
|
339
|
-
function hourList(
|
|
340
|
-
const words =
|
|
316
|
+
function hourList(schedule: Schedule): string {
|
|
317
|
+
const words = segmentsOf(schedule, 'hour').flatMap(hourSegmentWords);
|
|
341
318
|
|
|
342
319
|
return joinAnd(words);
|
|
343
320
|
}
|
|
344
321
|
|
|
345
322
|
// A frame that confines a cadence to active hours: a range gives "在F点至T点之
|
|
346
323
|
// 间,", a discrete hour list gives "在H、H…,".
|
|
347
|
-
function hourFrame(
|
|
348
|
-
if (
|
|
349
|
-
const [from, to] = (
|
|
324
|
+
function hourFrame(schedule: Schedule): string {
|
|
325
|
+
if (schedule.shapes.hour === 'range') {
|
|
326
|
+
const [from, to] = (segmentsOf(schedule, 'hour')[0] as
|
|
350
327
|
Extract<Segment, {kind: 'range'}>).bounds;
|
|
351
328
|
|
|
352
329
|
return '在' + hourWord(+from) + '至' + hourWord(+to) + '之间,';
|
|
353
330
|
}
|
|
354
331
|
|
|
355
|
-
return '在' + hourList(
|
|
332
|
+
return '在' + hourList(schedule) + ',';
|
|
356
333
|
}
|
|
357
334
|
|
|
358
335
|
// A repeating minute step, optionally confined to active hours.
|
|
359
|
-
function renderMinuteFrequency(
|
|
360
|
-
const minuteStep = stepSegment(
|
|
336
|
+
function renderMinuteFrequency(schedule: Schedule, plan: PlanNode): string {
|
|
337
|
+
const minuteStep = stepSegment(schedule, 'minute');
|
|
361
338
|
// A clean stride is the bare "每N分钟" cadence; an offset step keeps its start
|
|
362
339
|
// ("每小时从5分起每6分钟"). A short offset cadence still lists its fires.
|
|
363
340
|
const base = stepClause(minuteStep, UNITS.minute, '分', '每小时');
|
|
@@ -369,15 +346,14 @@ function renderMinuteFrequency(ir: IR, plan: PlanNode): string {
|
|
|
369
346
|
// before; a bounded cadence ends on "至K点", so a comma keeps that endpoint
|
|
370
347
|
// from gluing onto the minute clause ("从9点起每2小时,至17点,每2分钟").
|
|
371
348
|
if (hours.kind === 'step' || hours.kind === 'during') {
|
|
372
|
-
const hourCad =
|
|
349
|
+
const hourCad = unevenHourCadence(schedule);
|
|
373
350
|
|
|
374
351
|
if (hourCad !== null) {
|
|
375
352
|
return hourCad + (hourCad.indexOf('至') === -1 ? '' : ',') + base;
|
|
376
353
|
}
|
|
377
354
|
}
|
|
378
355
|
|
|
379
|
-
if (hours.kind === '
|
|
380
|
-
hours.kind === 'window' && hours.from === hours.to) {
|
|
356
|
+
if (hours.kind === 'window' && hours.from === hours.to) {
|
|
381
357
|
return '在' + hourWord(hours.from) + '至' + hours.from + '点' +
|
|
382
358
|
hours.last + '分之间,' + base;
|
|
383
359
|
}
|
|
@@ -388,7 +364,7 @@ function renderMinuteFrequency(ir: IR, plan: PlanNode): string {
|
|
|
388
364
|
}
|
|
389
365
|
|
|
390
366
|
if (hours.kind === 'during') {
|
|
391
|
-
return '在' + hourList(
|
|
367
|
+
return '在' + hourList(schedule) + ',' + base;
|
|
392
368
|
}
|
|
393
369
|
|
|
394
370
|
return base;
|
|
@@ -397,10 +373,10 @@ function renderMinuteFrequency(ir: IR, plan: PlanNode): string {
|
|
|
397
373
|
// A minute span within a single hour. A wildcard minute reads as that hour
|
|
398
374
|
// itself — "凌晨0点的每一分钟" — not a synthesized "在H点至H点59分之间" range the
|
|
399
375
|
// source never stated; a partial minute span keeps the named range.
|
|
400
|
-
function renderMinuteSpanInHour(
|
|
376
|
+
function renderMinuteSpanInHour(schedule: Schedule, plan: PlanNode): string {
|
|
401
377
|
const span = plan as Extract<PlanNode, {kind: 'minuteSpanInHour'}>;
|
|
402
378
|
|
|
403
|
-
if (
|
|
379
|
+
if (schedule.pattern.minute === '*') {
|
|
404
380
|
return hourWord(span.hour) + '的每一分钟';
|
|
405
381
|
}
|
|
406
382
|
|
|
@@ -412,41 +388,44 @@ function renderMinuteSpanInHour(ir: IR, plan: PlanNode): string {
|
|
|
412
388
|
// 每分钟"; a ranged/listed minute names it: "9点和17点,每小时0至30分,每分钟". An
|
|
413
389
|
// hour progression reads as its cadence ("从9点起每2小时,至17点,每分钟") rather
|
|
414
390
|
// than the enumerated hours, the same idiom the minute field uses.
|
|
415
|
-
function renderMinutesAcrossHours(
|
|
391
|
+
function renderMinutesAcrossHours(schedule: Schedule, plan: PlanNode): string {
|
|
416
392
|
const {form} = plan as Extract<PlanNode, {kind: 'minutesAcrossHours'}>;
|
|
417
|
-
const hourCad =
|
|
393
|
+
const hourCad = unevenHourCadence(schedule);
|
|
418
394
|
|
|
419
395
|
if (form === 'wildcard') {
|
|
420
396
|
return hourCad === null ?
|
|
421
|
-
'在' + hourList(
|
|
397
|
+
'在' + hourList(schedule) + ',每分钟' :
|
|
422
398
|
hourCad + ',每分钟';
|
|
423
399
|
}
|
|
424
400
|
|
|
425
|
-
return (hourCad ?? hourList(
|
|
401
|
+
return (hourCad ?? hourList(schedule)) + ',' + minuteHourClause(schedule) +
|
|
402
|
+
',每分钟';
|
|
426
403
|
}
|
|
427
404
|
|
|
428
405
|
// A minute clause across a stepped hour field. A wildcard minute reads "每2小时
|
|
429
406
|
// 内,每分钟"; a ranged minute names it: "每2小时,每小时0至30分,每分钟".
|
|
430
|
-
function renderMinuteSpanAcrossHourStep(
|
|
431
|
-
|
|
407
|
+
function renderMinuteSpanAcrossHourStep(
|
|
408
|
+
schedule: Schedule, plan: PlanNode
|
|
409
|
+
): string {
|
|
410
|
+
const hourStep = stepSegment(schedule, 'hour');
|
|
432
411
|
const {form} = plan as Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>;
|
|
433
412
|
|
|
434
413
|
// A minute list reads as the hour cadence plus the minute list ("每2小时,
|
|
435
414
|
// 每小时0、25、50分"; offset "从1点起每2小时,每小时5分和30分"), the same compaction
|
|
436
415
|
// the wildcard/range minute already uses, rather than the enumerated hours.
|
|
437
416
|
if (form === 'list') {
|
|
438
|
-
return hourCadencePhrase(
|
|
417
|
+
return hourCadencePhrase(schedule) + ',' + renderMinutePast(schedule);
|
|
439
418
|
}
|
|
440
419
|
|
|
441
420
|
const minuteTail = form === 'wildcard' ?
|
|
442
421
|
'每分钟' :
|
|
443
|
-
minuteHourClause(
|
|
422
|
+
minuteHourClause(schedule) + ',每分钟';
|
|
444
423
|
|
|
445
424
|
// An offset or non-tiling stride (2/6 fires at 2,8,14,20) reads as its
|
|
446
425
|
// cadence ("从2点起每6小时"). A wildcard minute hangs off it with a comma; a
|
|
447
426
|
// named minute follows the cadence and its own comma.
|
|
448
427
|
if (hourStep.startToken !== '*') {
|
|
449
|
-
return hourCadencePhrase(
|
|
428
|
+
return hourCadencePhrase(schedule) + ',' + minuteTail;
|
|
450
429
|
}
|
|
451
430
|
|
|
452
431
|
// A step-2 hour from midnight IS exactly the even hours; name them so, rather
|
|
@@ -463,10 +442,12 @@ function renderMinuteSpanAcrossHourStep(ir: IR, plan: PlanNode): string {
|
|
|
463
442
|
}
|
|
464
443
|
|
|
465
444
|
// Discrete clock times: "9点", "9点和17点".
|
|
466
|
-
function renderClockTimes(
|
|
445
|
+
function renderClockTimes(
|
|
446
|
+
schedule: Schedule, plan: PlanNode, opts: Opts
|
|
447
|
+
): string {
|
|
467
448
|
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
468
449
|
// minute reads as a cadence rather than a cross-product of clock times.
|
|
469
|
-
const cad = hourCadenceText(
|
|
450
|
+
const cad = hourCadenceText(schedule);
|
|
470
451
|
|
|
471
452
|
if (cad !== null) {
|
|
472
453
|
return cad;
|
|
@@ -479,19 +460,19 @@ function renderClockTimes(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
479
460
|
|
|
480
461
|
// Compact clock times past the cap: the hour list (the minute is folded in),
|
|
481
462
|
// with a fixed second appended ("…,第30秒").
|
|
482
|
-
function renderCompactClockTimes(
|
|
463
|
+
function renderCompactClockTimes(schedule: Schedule, plan: PlanNode): string {
|
|
483
464
|
// An hour step (or arithmetic-progression hour list) under the single pinned
|
|
484
465
|
// minute reads as a cadence, not a wall of clock times. (Returns null for an
|
|
485
466
|
// irregular list or a range, which keep enumerating below.)
|
|
486
|
-
const cad = hourCadenceText(
|
|
467
|
+
const cad = hourCadenceText(schedule);
|
|
487
468
|
|
|
488
469
|
if (cad !== null) {
|
|
489
470
|
return cad;
|
|
490
471
|
}
|
|
491
472
|
|
|
492
473
|
const compact = plan as Extract<PlanNode, {kind: 'compactClockTimes'}>;
|
|
493
|
-
const secs =
|
|
494
|
-
const tail = secs.length &&
|
|
474
|
+
const secs = segmentsOf(schedule, 'second');
|
|
475
|
+
const tail = secs.length && schedule.pattern.second !== '0' ?
|
|
495
476
|
',第' + valueText(secs) + '秒' : '';
|
|
496
477
|
|
|
497
478
|
// A multi-valued minute (`fold` false) names its whole set, never just its
|
|
@@ -501,30 +482,30 @@ function renderCompactClockTimes(ir: IR, plan: PlanNode): string {
|
|
|
501
482
|
// 点"), composed after the minute set, the same idiom the stepped-hour path
|
|
502
483
|
// uses; an irregular hour list keeps enumerating with the "在…" frame.
|
|
503
484
|
if (!compact.fold) {
|
|
504
|
-
const hourCad =
|
|
485
|
+
const hourCad = unevenHourCadence(schedule);
|
|
505
486
|
|
|
506
487
|
return hourCad === null ?
|
|
507
|
-
minuteHourClause(
|
|
508
|
-
hourCad + ',' + minuteHourClause(
|
|
488
|
+
minuteHourClause(schedule) + ',在' + hourList(schedule) + tail :
|
|
489
|
+
hourCad + ',' + minuteHourClause(schedule) + tail;
|
|
509
490
|
}
|
|
510
491
|
|
|
511
492
|
// A single pinned minute past 0 leads with its clause; a pinned 0 folds into
|
|
512
493
|
// the hour times (the :00 is implicit).
|
|
513
494
|
if (compact.minute > 0) {
|
|
514
|
-
return minuteHourClause(
|
|
495
|
+
return minuteHourClause(schedule) + ',在' + hourList(schedule) + tail;
|
|
515
496
|
}
|
|
516
497
|
|
|
517
|
-
return hourList(
|
|
498
|
+
return hourList(schedule) + tail;
|
|
518
499
|
}
|
|
519
500
|
|
|
520
501
|
// An hour window: "在9点至17点之间,每小时" (lead) or "…59分之间,每分钟".
|
|
521
|
-
function renderHourRange(
|
|
502
|
+
function renderHourRange(schedule: Schedule, plan: PlanNode): string {
|
|
522
503
|
const range = plan as Extract<PlanNode, {kind: 'hourRange'}>;
|
|
523
504
|
|
|
524
505
|
if (range.minuteForm === 'lead') {
|
|
525
|
-
const minuteSegs =
|
|
526
|
-
const past = minuteSegs.length &&
|
|
527
|
-
minuteHourClause(
|
|
506
|
+
const minuteSegs = segmentsOf(schedule, 'minute');
|
|
507
|
+
const past = minuteSegs.length && schedule.pattern.minute !== '0' ?
|
|
508
|
+
minuteHourClause(schedule) : '每小时';
|
|
528
509
|
|
|
529
510
|
return '在' + hourWord(range.from) + '至' + hourWord(range.to) +
|
|
530
511
|
'之间,' + past;
|
|
@@ -533,7 +514,7 @@ function renderHourRange(ir: IR, plan: PlanNode): string {
|
|
|
533
514
|
// A minute range is named separately ("每小时0至30分"), not folded into the end.
|
|
534
515
|
if (range.minuteForm === 'range') {
|
|
535
516
|
return '在' + hourWord(range.from) + '至' + hourWord(range.to) +
|
|
536
|
-
'之间,每小时' + valueList(
|
|
517
|
+
'之间,每小时' + valueList(segmentsOf(schedule, 'minute'), '分') + ',每分钟';
|
|
537
518
|
}
|
|
538
519
|
|
|
539
520
|
return '在' + hourWord(range.from) + '至' + range.to + '点' +
|
|
@@ -544,29 +525,31 @@ function renderHourRange(ir: IR, plan: PlanNode): string {
|
|
|
544
525
|
// (offset), "从9点起每2小时,至17点" (bounded). A stride that fires only twice
|
|
545
526
|
// reads instead as its two clock words ("凌晨0点和正午", "8点和20点"), shorter and
|
|
546
527
|
// clearer than a cadence for a pair.
|
|
547
|
-
function renderHourStep(
|
|
548
|
-
const segment = stepSegment(
|
|
528
|
+
function renderHourStep(schedule: Schedule): string {
|
|
529
|
+
const segment = stepSegment(schedule, 'hour');
|
|
549
530
|
|
|
550
531
|
if (segment.fires.length <= 2) {
|
|
551
532
|
return joinAnd(segment.fires.map(hourWord));
|
|
552
533
|
}
|
|
553
534
|
|
|
554
|
-
return hourCadencePhrase(
|
|
535
|
+
return hourCadencePhrase(schedule) as string;
|
|
555
536
|
}
|
|
556
537
|
|
|
557
538
|
// --- Hour-step cadence (the 24-cycle analog of renderStride). ---
|
|
558
539
|
|
|
559
540
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
560
541
|
// segment yields its {interval, start, last}; an all-single hour list yields
|
|
561
|
-
// one only when its values form a
|
|
562
|
-
//
|
|
563
|
-
// (
|
|
564
|
-
//
|
|
565
|
-
//
|
|
542
|
+
// one only when its values form a step progression (hourListStride, the same
|
|
543
|
+
// gate the other languages use), so a uneven progression from midnight however
|
|
544
|
+
// short (0,7,14,21 = `*/7`) is recognized, while an irregular list or a too-
|
|
545
|
+
// short non-zero one (9,17) keeps enumerating. An offset (start > 0) or non-
|
|
546
|
+
// tiling (interval ∤ 24) stride is still a cadence — Chinese names its start
|
|
547
|
+
// and endpoint ("从M点起每N小时,至K点"), the same idiom the minute field
|
|
548
|
+
// already uses. The Schedule is unchanged.
|
|
566
549
|
function hourStride(
|
|
567
|
-
|
|
550
|
+
schedule: Schedule
|
|
568
551
|
): {interval: number; start: number; last: number} | null {
|
|
569
|
-
const segments =
|
|
552
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
570
553
|
|
|
571
554
|
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
572
555
|
const {fires, interval} = segments[0];
|
|
@@ -576,7 +559,7 @@ function hourStride(
|
|
|
576
559
|
|
|
577
560
|
const values = singleValues(segments);
|
|
578
561
|
|
|
579
|
-
return values &&
|
|
562
|
+
return values && hourListStride(values);
|
|
580
563
|
}
|
|
581
564
|
|
|
582
565
|
// The hour field's cadence phrase ("每2小时", "从1点起每2小时", "从0点起每5小时,
|
|
@@ -585,14 +568,53 @@ function hourStride(
|
|
|
585
568
|
// analog of strideFromSegments — it routes the stride through the one phrasing
|
|
586
569
|
// renderStride speaks, so a clean, offset, or non-tiling hour stride all read
|
|
587
570
|
// as the cadence the equivalent minute step does.
|
|
588
|
-
function hourCadencePhrase(
|
|
589
|
-
const stride = hourStride(
|
|
571
|
+
function hourCadencePhrase(schedule: Schedule): string | null {
|
|
572
|
+
const stride = hourStride(schedule);
|
|
590
573
|
|
|
591
574
|
return stride && renderStride({
|
|
592
575
|
...stride, cycle: 24, unit: UNITS.hour, mark: '点', anchor: ''
|
|
593
576
|
});
|
|
594
577
|
}
|
|
595
578
|
|
|
579
|
+
// The hour cadence phrase for the minute-across-hours paths, where the hours
|
|
580
|
+
// frame a minute clause, or null when the hours should enumerate instead. A
|
|
581
|
+
// genuine `*/N` step always reads as its cadence ("每8小时内,每分钟"). A hour
|
|
582
|
+
// LIST is a cadence only when it pins a distinct endpoint (an uneven or
|
|
583
|
+
// bounded stride, e.g. 0,7,14,21); an offset-clean list (0,8,16, whose
|
|
584
|
+
// interval tiles 24 from within its first interval) wraps the day with no
|
|
585
|
+
// endpoint, so it reads better as its enumerated hours ("在凌晨0点、8点和16点")
|
|
586
|
+
// than a bare cadence that hides which hours fire — the same split the core
|
|
587
|
+
// draws by keeping a clean step a step but rewriting a uneven one to a list.
|
|
588
|
+
// The bare hour field and the second-folding paths apply their own
|
|
589
|
+
// length/second-aware guard.
|
|
590
|
+
function unevenHourCadence(schedule: Schedule): string | null {
|
|
591
|
+
const stride = hourStride(schedule);
|
|
592
|
+
|
|
593
|
+
if (!stride) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (schedule.shapes.hour !== 'step' && offsetCleanStride(stride)) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return hourCadencePhrase(schedule);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Whether a short offset-clean hour stride should keep enumerating its hours
|
|
605
|
+
// rather than compact to a bare cadence: a clean wrap of no more than the
|
|
606
|
+
// clock-time cap of fires (0,8,16; 0,4,8,12,16,20) names its hours, the bare
|
|
607
|
+
// "每8小时" hides them and is no shorter. A longer clean wrap (0,3,…,21) does
|
|
608
|
+
// compact, and an uneven stride always compacts (it pins an endpoint). Mirrors
|
|
609
|
+
// the other languages' `fires <= maxClockTimes && offsetCleanStride` guard.
|
|
610
|
+
function shortCleanHourStride(
|
|
611
|
+
stride: {interval: number; start: number; last: number}
|
|
612
|
+
): boolean {
|
|
613
|
+
const fires = (stride.last - stride.start) / stride.interval + 1;
|
|
614
|
+
|
|
615
|
+
return fires <= maxClockTimes && offsetCleanStride(stride);
|
|
616
|
+
}
|
|
617
|
+
|
|
596
618
|
// A wildcard or sub-minute step second confined to minute 0 of an hour stride
|
|
597
619
|
// is a confinement, not a juxtaposed cadence. The even-hour stride (interval 2
|
|
598
620
|
// from midnight) reuses the even-hours idiom ("在偶数小时0分的每一秒") so the form
|
|
@@ -604,14 +626,14 @@ function hourCadencePhrase(ir: IR): string | null {
|
|
|
604
626
|
// cadence from midnight (no start named, e.g. "每3小时") keeps enumerating its
|
|
605
627
|
// hours so it is never heard as the absorbing form.
|
|
606
628
|
function minuteZeroConfinement(
|
|
607
|
-
|
|
629
|
+
schedule: Schedule, stride: {interval: number; start: number}, prefix: string
|
|
608
630
|
): string | null {
|
|
609
631
|
if (stride.interval === 2 && stride.start === 0) {
|
|
610
|
-
return '在偶数小时0分' + secondTail(
|
|
632
|
+
return '在偶数小时0分' + secondTail(schedule);
|
|
611
633
|
}
|
|
612
634
|
|
|
613
635
|
if (prefix.indexOf('从') !== -1) {
|
|
614
|
-
return prefix + '0分' + secondTail(
|
|
636
|
+
return prefix + '0分' + secondTail(schedule);
|
|
615
637
|
}
|
|
616
638
|
|
|
617
639
|
return null;
|
|
@@ -628,32 +650,38 @@ function minuteZeroConfinement(
|
|
|
628
650
|
// bounded ("…,至K点"): a trailing minute fused onto its endpoint ("至20点0分")
|
|
629
651
|
// would read as a clock time, so a bounded stride keeps enumerating its fused
|
|
630
652
|
// clock times here, naming the cadence only where no minute follows it (the
|
|
631
|
-
// bare hour field). Renderer-only; the
|
|
632
|
-
function hourCadence(
|
|
633
|
-
const stride = hourStride(
|
|
653
|
+
// bare hour field). Renderer-only; the Schedule is unchanged.
|
|
654
|
+
function hourCadence(schedule: Schedule): string | null {
|
|
655
|
+
const stride = hourStride(schedule);
|
|
634
656
|
|
|
635
657
|
if (!stride) {
|
|
636
658
|
return null;
|
|
637
659
|
}
|
|
638
660
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
661
|
+
// A short stride that spells out as few clock times keeps enumerating only
|
|
662
|
+
// when it wraps cleanly (an offset-clean stride with no endpoint): the bare
|
|
663
|
+
// "每8小时" is no shorter than the list and hides which hours fire, so the
|
|
664
|
+
// list reads fine. A bounded or uneven stride has an endpoint its cadence
|
|
665
|
+
// pins ("从0点起每7小时,至21点"), so it compacts however few its fires; a
|
|
666
|
+
// meaningful second makes every clock time carry a second, so any stride is
|
|
667
|
+
// worth compacting then too.
|
|
668
|
+
if (schedule.pattern.second === '0' && shortCleanHourStride(stride)) {
|
|
642
669
|
return null;
|
|
643
670
|
}
|
|
644
671
|
|
|
645
|
-
const prefix = hourCadencePhrase(
|
|
672
|
+
const prefix = hourCadencePhrase(schedule) as string;
|
|
646
673
|
|
|
647
674
|
// A bounded cadence cannot carry a fused minute unambiguously; enumerate.
|
|
648
675
|
if (prefix.indexOf('至') !== -1) {
|
|
649
676
|
return null;
|
|
650
677
|
}
|
|
651
678
|
|
|
652
|
-
const minute = +
|
|
653
|
-
const subMinute =
|
|
679
|
+
const minute = +schedule.pattern.minute;
|
|
680
|
+
const subMinute = schedule.pattern.second === '*' ||
|
|
681
|
+
schedule.shapes.second === 'step';
|
|
654
682
|
|
|
655
683
|
if (minute === 0 && subMinute) {
|
|
656
|
-
return minuteZeroConfinement(
|
|
684
|
+
return minuteZeroConfinement(schedule, stride, prefix);
|
|
657
685
|
}
|
|
658
686
|
|
|
659
687
|
// A pinned minute 0 folds into the cadence with the explicit "0分" so the
|
|
@@ -661,12 +689,12 @@ function hourCadence(ir: IR): string | null {
|
|
|
661
689
|
// clock minute named after the cadence ("每2小时5分的每一秒"). A plain :00
|
|
662
690
|
// second adds nothing.
|
|
663
691
|
if (minute === 0) {
|
|
664
|
-
return prefix + '0分' + secondTail(
|
|
692
|
+
return prefix + '0分' + secondTail(schedule);
|
|
665
693
|
}
|
|
666
694
|
|
|
667
|
-
return
|
|
695
|
+
return schedule.pattern.second === '0' ?
|
|
668
696
|
prefix + minute + '分' :
|
|
669
|
-
prefix + minute + '分' + secondTail(
|
|
697
|
+
prefix + minute + '分' + secondTail(schedule);
|
|
670
698
|
}
|
|
671
699
|
|
|
672
700
|
// The cadence a clock-point core (clockTimes/compactClockTimes/composeSeconds)
|
|
@@ -675,34 +703,41 @@ function hourCadence(ir: IR): string | null {
|
|
|
675
703
|
// so a short non-tiling stride like */5, which hourCadence keeps enumerating
|
|
676
704
|
// (no minute to fold, nothing to disambiguate), still reads as the cadence. A
|
|
677
705
|
// pinned minute or meaningful second folds into the cadence via hourCadence.
|
|
678
|
-
function hourCadenceText(
|
|
679
|
-
if (
|
|
706
|
+
function hourCadenceText(schedule: Schedule): string | null {
|
|
707
|
+
if (schedule.shapes.minute !== 'single') {
|
|
680
708
|
return null;
|
|
681
709
|
}
|
|
682
710
|
|
|
683
|
-
if (+
|
|
684
|
-
|
|
711
|
+
if (+schedule.pattern.minute === 0 && schedule.pattern.second === '0') {
|
|
712
|
+
const stride = hourStride(schedule);
|
|
713
|
+
|
|
714
|
+
// A short clean wrap (0,8,16) keeps enumerating its hours; an uneven or
|
|
715
|
+
// longer stride reads as its cadence, the same split hourCadence draws once
|
|
716
|
+
// a minute or second folds in.
|
|
717
|
+
return stride && !shortCleanHourStride(stride) ?
|
|
718
|
+
hourCadencePhrase(schedule) :
|
|
719
|
+
null;
|
|
685
720
|
}
|
|
686
721
|
|
|
687
|
-
return hourCadence(
|
|
722
|
+
return hourCadence(schedule);
|
|
688
723
|
}
|
|
689
724
|
|
|
690
725
|
// The fused second tail for an hour cadence: "的每一秒" for a wildcard second,
|
|
691
726
|
// else "的" + the second's own clause ("的第30秒", "的每15秒").
|
|
692
|
-
function secondTail(
|
|
693
|
-
const sec = secondClause(
|
|
727
|
+
function secondTail(schedule: Schedule): string {
|
|
728
|
+
const sec = secondClause(schedule);
|
|
694
729
|
|
|
695
730
|
return sec === '每秒' ? '的每一秒' : '的' + sec;
|
|
696
731
|
}
|
|
697
732
|
|
|
698
733
|
// A continuous minute range fires every minute within it: "每小时0至30分,每分钟".
|
|
699
|
-
function renderRangeOfMinutes(
|
|
700
|
-
return minuteHourClause(
|
|
734
|
+
function renderRangeOfMinutes(schedule: Schedule): string {
|
|
735
|
+
return minuteHourClause(schedule) + ',每分钟';
|
|
701
736
|
}
|
|
702
737
|
|
|
703
738
|
// A standalone second field: "每7秒" (step cadence) or "每分钟第4、17、42秒".
|
|
704
|
-
function renderStandaloneSeconds(
|
|
705
|
-
const segs =
|
|
739
|
+
function renderStandaloneSeconds(schedule: Schedule): string {
|
|
740
|
+
const segs = segmentsOf(schedule, 'second');
|
|
706
741
|
const first = segs[0];
|
|
707
742
|
|
|
708
743
|
if (segs.length === 1 && first.kind === 'step' && first.startToken === '*') {
|
|
@@ -714,14 +749,14 @@ function renderStandaloneSeconds(ir: IR): string {
|
|
|
714
749
|
}
|
|
715
750
|
|
|
716
751
|
// A second anchored to the minute: "每分钟第1秒", "每分钟第4、17、42秒".
|
|
717
|
-
function renderSecondPastMinute(
|
|
718
|
-
return '每分钟第' + valueText(
|
|
752
|
+
function renderSecondPastMinute(schedule: Schedule): string {
|
|
753
|
+
return '每分钟第' + valueText(segmentsOf(schedule, 'second')) + '秒';
|
|
719
754
|
}
|
|
720
755
|
|
|
721
756
|
// A second within a single specific minute: "每小时0分第1秒" / "…,每15秒".
|
|
722
|
-
function renderSecondsWithinMinute(
|
|
723
|
-
const base = '每小时' +
|
|
724
|
-
const segs =
|
|
757
|
+
function renderSecondsWithinMinute(schedule: Schedule): string {
|
|
758
|
+
const base = '每小时' + schedule.pattern.minute + '分';
|
|
759
|
+
const segs = segmentsOf(schedule, 'second');
|
|
725
760
|
const first = segs[0];
|
|
726
761
|
|
|
727
762
|
if (segs.length === 1 && first.kind === 'step' && first.startToken === '*') {
|
|
@@ -732,8 +767,8 @@ function renderSecondsWithinMinute(ir: IR): string {
|
|
|
732
767
|
}
|
|
733
768
|
|
|
734
769
|
// The second clause for a composed schedule: "每秒" / "每7秒" / "第4、17、42秒".
|
|
735
|
-
function secondClause(
|
|
736
|
-
const segs =
|
|
770
|
+
function secondClause(schedule: Schedule): string {
|
|
771
|
+
const segs = segmentsOf(schedule, 'second');
|
|
737
772
|
|
|
738
773
|
if (!segs.length) {
|
|
739
774
|
return '每秒';
|
|
@@ -752,16 +787,16 @@ function secondClause(ir: IR): string {
|
|
|
752
787
|
}
|
|
753
788
|
|
|
754
789
|
// The minute clause for a composed (seconds) schedule.
|
|
755
|
-
function minuteClause(
|
|
756
|
-
if (
|
|
790
|
+
function minuteClause(schedule: Schedule): string {
|
|
791
|
+
if (schedule.pattern.minute === '*') {
|
|
757
792
|
return '每分钟';
|
|
758
793
|
}
|
|
759
794
|
|
|
760
|
-
if (
|
|
761
|
-
return cadence(stepSegment(
|
|
795
|
+
if (schedule.shapes.minute === 'step') {
|
|
796
|
+
return cadence(stepSegment(schedule, 'minute').interval, UNITS.minute);
|
|
762
797
|
}
|
|
763
798
|
|
|
764
|
-
return valueList(
|
|
799
|
+
return valueList(segmentsOf(schedule, 'minute'), '分');
|
|
765
800
|
}
|
|
766
801
|
|
|
767
802
|
// A single second folds into each clock time a clockTimes rest renders
|
|
@@ -774,8 +809,10 @@ function clockRestCarriesSecond(rest: PlanNode): boolean {
|
|
|
774
809
|
}
|
|
775
810
|
|
|
776
811
|
// minute = 0 ("on the hour"): render the rest schedule and attach the second.
|
|
777
|
-
function composeSecondsOnHour(
|
|
778
|
-
|
|
812
|
+
function composeSecondsOnHour(
|
|
813
|
+
schedule: Schedule, plan: PlanNode, opts: Opts
|
|
814
|
+
): string {
|
|
815
|
+
const sec = secondClause(schedule);
|
|
779
816
|
const {rest} = plan as Extract<PlanNode, {kind: 'composeSeconds'}>;
|
|
780
817
|
const composedClock =
|
|
781
818
|
rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes';
|
|
@@ -786,7 +823,7 @@ function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
786
823
|
// rest-plus-second fall-through that would double the second clause. The
|
|
787
824
|
// cadence is sub-daily (no 每天); a qualifier is added by describe().
|
|
788
825
|
if (composedClock) {
|
|
789
|
-
const hourCad = hourCadence(
|
|
826
|
+
const hourCad = hourCadence(schedule);
|
|
790
827
|
|
|
791
828
|
if (hourCad !== null) {
|
|
792
829
|
return hourCad;
|
|
@@ -795,14 +832,14 @@ function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
795
832
|
|
|
796
833
|
// The minute is pinned to 0 under specific hours that did not compact to a
|
|
797
834
|
// cadence: fuse the seconds with each explicit clock minute.
|
|
798
|
-
if (composedClock &&
|
|
799
|
-
return composeMinuteZeroClocks(
|
|
835
|
+
if (composedClock && schedule.pattern.minute === '0') {
|
|
836
|
+
return composeMinuteZeroClocks(schedule, sec);
|
|
800
837
|
}
|
|
801
838
|
|
|
802
|
-
const restText = render(
|
|
839
|
+
const restText = render(schedule, rest, opts);
|
|
803
840
|
const secTail = clockRestCarriesSecond(rest) ? '' : sec;
|
|
804
841
|
|
|
805
|
-
if (composedClock && isDaily(
|
|
842
|
+
if (composedClock && isDaily(schedule)) {
|
|
806
843
|
return '每天' + restText + secTail;
|
|
807
844
|
}
|
|
808
845
|
|
|
@@ -821,36 +858,37 @@ function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
821
858
|
// clock minute ("9点0分的每一秒"), so the one-minute confinement (60 fires in
|
|
822
859
|
// :00, not 3,600 across the hour) stays visible. The daily frame leads with
|
|
823
860
|
// 每天; a weekday or date qualifier is added by describe().
|
|
824
|
-
function composeMinuteZeroClocks(
|
|
861
|
+
function composeMinuteZeroClocks(schedule: Schedule, sec: string): string {
|
|
825
862
|
// An hour RANGE (or a list whose segments include a range) reads as the span
|
|
826
863
|
// the source wrote ("9点至17点"), not the wall of clock words it expands to —
|
|
827
864
|
// the hour-RANGE analog of the hour-step cadence. A pure single-value list
|
|
828
865
|
// (9,17) has no range to span and keeps enumerating below.
|
|
829
|
-
if (hasHourWindow(
|
|
830
|
-
return isDaily(
|
|
831
|
-
hourRangeWindow(
|
|
866
|
+
if (hasHourWindow(schedule)) {
|
|
867
|
+
return isDaily(schedule) ? '每天' + hourRangeWindow(schedule, sec) :
|
|
868
|
+
hourRangeWindow(schedule, sec);
|
|
832
869
|
}
|
|
833
870
|
|
|
834
|
-
const clocks = hourFires(
|
|
871
|
+
const clocks = hourFires(schedule).map(function clock(hour): string {
|
|
835
872
|
// Noon's word (正午) already pins 12:00, so the "0分" is redundant for it;
|
|
836
873
|
// midnight (凌晨0点) and other hours still need it to pin the minute.
|
|
837
874
|
return hour === 12 ? '正午' : hourWord(hour) + '0分';
|
|
838
875
|
});
|
|
839
876
|
// A pinned minute makes the seconds' own "每分钟" anchor misleading (it is a
|
|
840
877
|
// single minute, not every minute), so the stride here drops it.
|
|
841
|
-
const nested =
|
|
878
|
+
const nested =
|
|
879
|
+
strideFromSegments(segmentsOf(schedule, 'second'), '秒', '秒', '');
|
|
842
880
|
const tail = sec === '每秒' ? '的每一秒' : '的' + (nested ?? sec);
|
|
843
881
|
const core = joinAnd(clocks) + tail;
|
|
844
882
|
|
|
845
|
-
return isDaily(
|
|
883
|
+
return isDaily(schedule) ? '每天' + core : core;
|
|
846
884
|
}
|
|
847
885
|
|
|
848
886
|
// Whether the hour field is a range — or a list whose segments include a
|
|
849
887
|
// range — and so forms a window ("9点至17点") rather than a wall of clock
|
|
850
888
|
// words. A pure single-value list (9,17) has no range to span; a step is
|
|
851
889
|
// handled by hourStride/hourCadence.
|
|
852
|
-
function hasHourWindow(
|
|
853
|
-
return
|
|
890
|
+
function hasHourWindow(schedule: Schedule): boolean {
|
|
891
|
+
return segmentsOf(schedule, 'hour').some(function range(segment) {
|
|
854
892
|
return segment.kind === 'range';
|
|
855
893
|
});
|
|
856
894
|
}
|
|
@@ -862,10 +900,10 @@ function hasHourWindow(ir: IR): boolean {
|
|
|
862
900
|
// the bare hourly window ("在9点至17点之间,每小时"); a single/list/range second
|
|
863
901
|
// reads as a clock-point span with the second appended ("9点至17点,第30秒"),
|
|
864
902
|
// matching the folded compact form for the same shape.
|
|
865
|
-
function hourRangeWindow(
|
|
866
|
-
const span = hourList(
|
|
903
|
+
function hourRangeWindow(schedule: Schedule, sec: string): string {
|
|
904
|
+
const span = hourList(schedule);
|
|
867
905
|
|
|
868
|
-
if (
|
|
906
|
+
if (schedule.pattern.second === '*' || schedule.shapes.second === 'step') {
|
|
869
907
|
return span + '0分' + (sec === '每秒' ? '的每一秒' : '的' + sec);
|
|
870
908
|
}
|
|
871
909
|
|
|
@@ -873,11 +911,11 @@ function hourRangeWindow(ir: IR, sec: string): string {
|
|
|
873
911
|
}
|
|
874
912
|
|
|
875
913
|
// Wildcard or stepped minute: hang the "每分钟/每N分钟每秒" tail off the hour.
|
|
876
|
-
function composeSecondsCadence(
|
|
877
|
-
const sec = secondClause(
|
|
878
|
-
const tail = minuteClause(
|
|
914
|
+
function composeSecondsCadence(schedule: Schedule): string {
|
|
915
|
+
const sec = secondClause(schedule);
|
|
916
|
+
const tail = minuteClause(schedule) + sec;
|
|
879
917
|
|
|
880
|
-
const hourCad =
|
|
918
|
+
const hourCad = unevenHourCadence(schedule);
|
|
881
919
|
|
|
882
920
|
if (hourCad !== null) {
|
|
883
921
|
// The cadence absorbs the tail with "的" ("每2小时的每分钟每秒",
|
|
@@ -886,36 +924,36 @@ function composeSecondsCadence(ir: IR): string {
|
|
|
886
924
|
return hourCad + (hourCad.indexOf('至') === -1 ? '的' : ',') + tail;
|
|
887
925
|
}
|
|
888
926
|
|
|
889
|
-
if (
|
|
890
|
-
return hourWord(hourFires(
|
|
927
|
+
if (schedule.shapes.hour === 'single') {
|
|
928
|
+
return hourWord(hourFires(schedule)[0]) + '的' + tail;
|
|
891
929
|
}
|
|
892
930
|
|
|
893
|
-
if (
|
|
931
|
+
if (schedule.shapes.hour === 'wildcard') {
|
|
894
932
|
// "每秒,每2分钟" juxtaposes two cadences that read as contradictory. A
|
|
895
933
|
// step-2 minute from the top of the hour IS exactly the even minutes; bind
|
|
896
934
|
// the every-second cadence to them ("每偶数分钟的每一秒") rather than listing
|
|
897
935
|
// the two side by side. Other strides keep the juxtaposed form.
|
|
898
|
-
if (
|
|
899
|
-
const minuteStep = stepSegment(
|
|
936
|
+
if (schedule.shapes.minute === 'step' && sec === '每秒') {
|
|
937
|
+
const minuteStep = stepSegment(schedule, 'minute');
|
|
900
938
|
|
|
901
939
|
if (minuteStep.startToken === '*' && minuteStep.interval === 2) {
|
|
902
940
|
return '每偶数分钟的每一秒';
|
|
903
941
|
}
|
|
904
942
|
}
|
|
905
943
|
|
|
906
|
-
return sec + ',' + minuteClause(
|
|
944
|
+
return sec + ',' + minuteClause(schedule);
|
|
907
945
|
}
|
|
908
946
|
|
|
909
|
-
return hourFrame(
|
|
947
|
+
return hourFrame(schedule) + tail;
|
|
910
948
|
}
|
|
911
949
|
|
|
912
950
|
// Listed/ranged minute: "每小时<minutes>,每秒", confined by any hour frame.
|
|
913
951
|
// A minute list or range under an hour range closes on the bare hour frame
|
|
914
952
|
// ("在9点至17点之间"), stating its minutes separately, rather than gluing its
|
|
915
953
|
// last fire onto the window end ("…17点30分") and reading as a continuous span.
|
|
916
|
-
function composeSecondsListed(
|
|
917
|
-
const sec = secondClause(
|
|
918
|
-
const minutes = minuteHourClause(
|
|
954
|
+
function composeSecondsListed(schedule: Schedule): string {
|
|
955
|
+
const sec = secondClause(schedule);
|
|
956
|
+
const minutes = minuteHourClause(schedule);
|
|
919
957
|
|
|
920
958
|
// A single restricted hour with an every-second cadence fuses the clock time
|
|
921
959
|
// with its minutes — "凌晨0点5、20、35、50分的每一秒" — rather than the "每小时"
|
|
@@ -923,25 +961,25 @@ function composeSecondsListed(ir: IR): string {
|
|
|
923
961
|
// A non-uniform minute step the core enumerated to a fire list reads as its
|
|
924
962
|
// stride cadence ("凌晨0点从3分起每2分钟,至59分的每一秒"); the hour fuses, so the
|
|
925
963
|
// stride drops its "每小时" anchor. A short or irregular set keeps the list.
|
|
926
|
-
if (
|
|
927
|
-
const minuteSegs =
|
|
964
|
+
if (schedule.shapes.hour === 'single' && sec === '每秒') {
|
|
965
|
+
const minuteSegs = segmentsOf(schedule, 'minute');
|
|
928
966
|
const minuteCad = strideFromSegments(minuteSegs, '分钟', '分', '') ??
|
|
929
967
|
valueList(minuteSegs, '分');
|
|
930
968
|
|
|
931
|
-
return hourWord(hourFires(
|
|
969
|
+
return hourWord(hourFires(schedule)[0]) + minuteCad + '的每一秒';
|
|
932
970
|
}
|
|
933
971
|
|
|
934
|
-
if (
|
|
972
|
+
if (schedule.shapes.hour === 'wildcard') {
|
|
935
973
|
return minutes + ',' + sec;
|
|
936
974
|
}
|
|
937
975
|
|
|
938
|
-
const hourCad =
|
|
976
|
+
const hourCad = unevenHourCadence(schedule);
|
|
939
977
|
|
|
940
978
|
if (hourCad !== null) {
|
|
941
979
|
return hourCad + ',' + minutes + ',' + sec;
|
|
942
980
|
}
|
|
943
981
|
|
|
944
|
-
return hourFrame(
|
|
982
|
+
return hourFrame(schedule) + minutes + ',' + sec;
|
|
945
983
|
}
|
|
946
984
|
|
|
947
985
|
// Seconds composed with the minute/hour structure, dispatched on the minute.
|
|
@@ -949,28 +987,30 @@ function composeSecondsListed(ir: IR): string {
|
|
|
949
987
|
// lone hour and minute into "N点M分") keeps that composition, attaching the
|
|
950
988
|
// second to it rather than splitting the minute back out into the "每小时N分"
|
|
951
989
|
// list path; a minute list stays on that list path so each fire is named.
|
|
952
|
-
function renderComposeSeconds(
|
|
990
|
+
function renderComposeSeconds(
|
|
991
|
+
schedule: Schedule, plan: PlanNode, opts: Opts
|
|
992
|
+
): string {
|
|
953
993
|
const {rest} = plan as Extract<PlanNode, {kind: 'composeSeconds'}>;
|
|
954
994
|
const composedClock =
|
|
955
995
|
rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes';
|
|
956
996
|
|
|
957
|
-
if (
|
|
958
|
-
composedClock &&
|
|
959
|
-
return composeSecondsOnHour(
|
|
997
|
+
if (schedule.pattern.minute === '0' ||
|
|
998
|
+
composedClock && schedule.shapes.minute === 'single') {
|
|
999
|
+
return composeSecondsOnHour(schedule, plan, opts);
|
|
960
1000
|
}
|
|
961
1001
|
|
|
962
1002
|
// "每N分钟" is faithful only for a wildcard or top-of-hour step; an offset
|
|
963
1003
|
// step (5/15 fires at :05,:20,…) takes the enumerated list path so its start
|
|
964
1004
|
// is named, never dropped.
|
|
965
|
-
const minuteCadence =
|
|
966
|
-
|
|
967
|
-
stepSegment(
|
|
1005
|
+
const minuteCadence = schedule.pattern.minute === '*' ||
|
|
1006
|
+
schedule.shapes.minute === 'step' &&
|
|
1007
|
+
stepSegment(schedule, 'minute').startToken === '*';
|
|
968
1008
|
|
|
969
1009
|
if (minuteCadence) {
|
|
970
|
-
return composeSecondsCadence(
|
|
1010
|
+
return composeSecondsCadence(schedule);
|
|
971
1011
|
}
|
|
972
1012
|
|
|
973
|
-
return composeSecondsListed(
|
|
1013
|
+
return composeSecondsListed(schedule);
|
|
974
1014
|
}
|
|
975
1015
|
|
|
976
1016
|
const renderers = {
|
|
@@ -994,21 +1034,21 @@ const renderers = {
|
|
|
994
1034
|
standaloneSeconds: renderStandaloneSeconds
|
|
995
1035
|
};
|
|
996
1036
|
|
|
997
|
-
function render(
|
|
1037
|
+
function render(schedule: Schedule, plan: PlanNode, opts: Opts): string {
|
|
998
1038
|
return (renderers[plan.kind as keyof typeof renderers] as Renderer)(
|
|
999
|
-
|
|
1039
|
+
schedule, plan, opts);
|
|
1000
1040
|
}
|
|
1001
1041
|
|
|
1002
1042
|
// --- Day-level qualifier (date / month / weekday / year). ---
|
|
1003
1043
|
|
|
1004
1044
|
// The month phrase: "" (wildcard), "每个奇数月"/"每个偶数月" (step ×2),
|
|
1005
1045
|
// "1月至3月" (range), else the enumerated numbers sharing one 月 ("1、4、7、10月").
|
|
1006
|
-
function monthPhrase(
|
|
1007
|
-
if (
|
|
1046
|
+
function monthPhrase(schedule: Schedule): string {
|
|
1047
|
+
if (schedule.pattern.month === '*') {
|
|
1008
1048
|
return '';
|
|
1009
1049
|
}
|
|
1010
1050
|
|
|
1011
|
-
const segs =
|
|
1051
|
+
const segs = segmentsOf(schedule, 'month');
|
|
1012
1052
|
const first = segs[0];
|
|
1013
1053
|
|
|
1014
1054
|
if (segs.length === 1 && first.kind === 'step' && first.interval === 2) {
|
|
@@ -1036,8 +1076,8 @@ function monthPhrase(ir: IR): string {
|
|
|
1036
1076
|
|
|
1037
1077
|
// The day-of-month list. A pure list of singles shares one trailing 日
|
|
1038
1078
|
// ("1、3、8日"); any range gives each segment its own 日 ("1至5日、10日").
|
|
1039
|
-
function dayList(
|
|
1040
|
-
const segs =
|
|
1079
|
+
function dayList(schedule: Schedule): string {
|
|
1080
|
+
const segs = segmentsOf(schedule, 'date');
|
|
1041
1081
|
|
|
1042
1082
|
if (segs.every((seg) => seg.kind === 'single')) {
|
|
1043
1083
|
return segs.map((seg) => (seg as {value: string}).value).join('、') + '日';
|
|
@@ -1070,50 +1110,79 @@ function quartzDate(token: string, monthPrefix: string): string {
|
|
|
1070
1110
|
return monthPrefix + '最接近' + token.slice(0, -1) + '日的工作日';
|
|
1071
1111
|
}
|
|
1072
1112
|
|
|
1113
|
+
// An open interval-2 day-of-month step covers a parity set, so in an OR union
|
|
1114
|
+
// it reads as the parity class — "单数日" (odd days, resetting each month) for
|
|
1115
|
+
// `*/2`/`1/2`, "双数日" (even days) for `2/2` — rather than the continuous
|
|
1116
|
+
// "每2天" cadence, which buries the union beside the 或 and mis-implies a fixed
|
|
1117
|
+
// 48-hour cycle. The standalone date-only case keeps "每2天" (a parity-neutral
|
|
1118
|
+
// cadence). With a wildcard month the predicate leads with 每月 ("每月单数日");
|
|
1119
|
+
// a fronted month already scopes it, so the bare predicate is used ("单数日").
|
|
1120
|
+
// Mirrors en's odd/even-numbered-day idiom and de/fi's split. Null otherwise.
|
|
1121
|
+
function oddEvenDay(dateField: string, monthLead: boolean): string | null {
|
|
1122
|
+
if (!isOpenStep(dateField)) {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const [start, step] = dateField.split('/');
|
|
1127
|
+
|
|
1128
|
+
if (+step !== 2) {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const lead = monthLead ? '每月' : '';
|
|
1133
|
+
|
|
1134
|
+
if (start === '*' || start === '1') {
|
|
1135
|
+
return lead + '单数日';
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return start === '2' ? lead + '双数日' : null;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1073
1141
|
// The date side of a qualifier (month folded in): "每月1日", "1月1日",
|
|
1074
1142
|
// "每2天", "1月每2天", "本月最后一天", "每个奇数月1日至15日".
|
|
1075
|
-
function datePhrase(
|
|
1076
|
-
const month = monthPhrase(
|
|
1077
|
-
const date =
|
|
1143
|
+
function datePhrase(schedule: Schedule): string {
|
|
1144
|
+
const month = monthPhrase(schedule);
|
|
1145
|
+
const date = schedule.pattern.date;
|
|
1078
1146
|
|
|
1079
1147
|
if (date === '*' || date === '?') {
|
|
1080
1148
|
return month;
|
|
1081
1149
|
}
|
|
1082
1150
|
|
|
1083
|
-
if (
|
|
1151
|
+
if (schedule.shapes.date === 'quartz') {
|
|
1084
1152
|
return quartzDate(date, month || '本月');
|
|
1085
1153
|
}
|
|
1086
1154
|
|
|
1087
|
-
if (
|
|
1088
|
-
return month + cadence(stepSegment(
|
|
1155
|
+
if (schedule.shapes.date === 'step') {
|
|
1156
|
+
return month + cadence(stepSegment(schedule, 'date').interval, '天');
|
|
1089
1157
|
}
|
|
1090
1158
|
|
|
1091
1159
|
if (!month) {
|
|
1092
|
-
return '每月' + dayList(
|
|
1160
|
+
return '每月' + dayList(schedule);
|
|
1093
1161
|
}
|
|
1094
1162
|
|
|
1095
1163
|
// A multi-month scope (range/list) ends in 月 and would run straight into the
|
|
1096
1164
|
// day — "6月至8月1日" reads "8月1日" as August 1st. The comma keeps the month
|
|
1097
1165
|
// scope distinct from the day ("6月至8月,1日"). A single month stays glued
|
|
1098
1166
|
// ("6月1日"), which is unambiguous.
|
|
1099
|
-
const monthMulti =
|
|
1167
|
+
const monthMulti = schedule.shapes.month === 'range' ||
|
|
1168
|
+
schedule.shapes.month === 'list';
|
|
1100
1169
|
|
|
1101
|
-
return month + (monthMulti ? ',' : '') + dayList(
|
|
1170
|
+
return month + (monthMulti ? ',' : '') + dayList(schedule);
|
|
1102
1171
|
}
|
|
1103
1172
|
|
|
1104
1173
|
// The date side WITHOUT its month or 每月 lead — just the day part: "1日",
|
|
1105
1174
|
// "每2天", "1日至15日", or quartz ("最后一天"). Used when a leading month scopes
|
|
1106
1175
|
// an OR union over both the date and weekday sides.
|
|
1107
|
-
function dateCore(
|
|
1108
|
-
if (
|
|
1109
|
-
return quartzDate(
|
|
1176
|
+
function dateCore(schedule: Schedule, quartzPrefix: string): string {
|
|
1177
|
+
if (schedule.shapes.date === 'quartz') {
|
|
1178
|
+
return quartzDate(schedule.pattern.date, quartzPrefix);
|
|
1110
1179
|
}
|
|
1111
1180
|
|
|
1112
|
-
if (
|
|
1113
|
-
return cadence(stepSegment(
|
|
1181
|
+
if (schedule.shapes.date === 'step') {
|
|
1182
|
+
return cadence(stepSegment(schedule, 'date').interval, '天');
|
|
1114
1183
|
}
|
|
1115
1184
|
|
|
1116
|
-
return dayList(
|
|
1185
|
+
return dayList(schedule);
|
|
1117
1186
|
}
|
|
1118
1187
|
|
|
1119
1188
|
// A weekday name, resolving a token (MON → 周一); cron treats 7 as Sunday.
|
|
@@ -1153,13 +1222,13 @@ function quartzWeekday(token: string, monthPrefix: string): string {
|
|
|
1153
1222
|
// The weekday phrase: "每周一", "每周一至周五", "每周日、二、四、六", quartz
|
|
1154
1223
|
// ("本月最后一个周五"). In an OR a multi-day list drops the recurrence 每.
|
|
1155
1224
|
function weekdayPhrase(
|
|
1156
|
-
|
|
1225
|
+
schedule: Schedule, orContext: boolean, monthPrefix: string
|
|
1157
1226
|
): string {
|
|
1158
|
-
if (
|
|
1159
|
-
return quartzWeekday(
|
|
1227
|
+
if (schedule.shapes.weekday === 'quartz') {
|
|
1228
|
+
return quartzWeekday(schedule.pattern.weekday, monthPrefix);
|
|
1160
1229
|
}
|
|
1161
1230
|
|
|
1162
|
-
const segs =
|
|
1231
|
+
const segs = segmentsOf(schedule, 'weekday');
|
|
1163
1232
|
|
|
1164
1233
|
if (segs.length === 1 && segs[0].kind === 'range') {
|
|
1165
1234
|
const [from, to] = (segs[0] as Extract<Segment, {kind: 'range'}>).bounds;
|
|
@@ -1167,8 +1236,9 @@ function weekdayPhrase(
|
|
|
1167
1236
|
return '每' + weekdayName(from) + '至' + weekdayName(to);
|
|
1168
1237
|
}
|
|
1169
1238
|
|
|
1170
|
-
// Weekday lists display Monday-first (Sunday last); the
|
|
1171
|
-
// (Sunday=0). The helper flattens steps into singles and orders the
|
|
1239
|
+
// Weekday lists display Monday-first (Sunday last); the Schedule stays
|
|
1240
|
+
// canonical (Sunday=0). The helper flattens steps into singles and orders the
|
|
1241
|
+
// list.
|
|
1172
1242
|
const days: number[] = [];
|
|
1173
1243
|
|
|
1174
1244
|
orderWeekdaysForDisplay(segs).forEach(function expand(seg) {
|
|
@@ -1189,60 +1259,66 @@ function isSet(token: string): boolean {
|
|
|
1189
1259
|
|
|
1190
1260
|
// The leading day qualifier, or "" when none. cron's OR (both day fields set)
|
|
1191
1261
|
// joins with 或; a quartz weekday in an OR anchors to 本月.
|
|
1192
|
-
function qualifier(
|
|
1193
|
-
const dateSet = isSet(
|
|
1194
|
-
const weekdaySet = isSet(
|
|
1262
|
+
function qualifier(schedule: Schedule): string {
|
|
1263
|
+
const dateSet = isSet(schedule.pattern.date);
|
|
1264
|
+
const weekdaySet = isSet(schedule.pattern.weekday);
|
|
1195
1265
|
|
|
1196
1266
|
// cron's OR: a restricted month scopes BOTH sides of the union (Fridays are
|
|
1197
1267
|
// in June too), so lead with it — "6月,1日或每周五", not "6月1日或每周五",
|
|
1198
1268
|
// which would read as Fridays year-round. With a wildcard month there is
|
|
1199
1269
|
// nothing to scope, so the date side carries its own (每月/本月) lead.
|
|
1200
1270
|
if (dateSet && weekdaySet) {
|
|
1201
|
-
const month = monthPhrase(
|
|
1271
|
+
const month = monthPhrase(schedule);
|
|
1202
1272
|
|
|
1203
1273
|
if (month) {
|
|
1204
|
-
|
|
1274
|
+
const date = oddEvenDay(schedule.pattern.date, false) ||
|
|
1275
|
+
dateCore(schedule, '');
|
|
1276
|
+
|
|
1277
|
+
return month + ',' + date + '或' + weekdayPhrase(schedule, true, '');
|
|
1205
1278
|
}
|
|
1206
1279
|
|
|
1207
|
-
|
|
1280
|
+
const date = oddEvenDay(schedule.pattern.date, true) ||
|
|
1281
|
+
datePhrase(schedule);
|
|
1282
|
+
|
|
1283
|
+
return date + '或' + weekdayPhrase(schedule, true, '本月');
|
|
1208
1284
|
}
|
|
1209
1285
|
|
|
1210
1286
|
if (dateSet) {
|
|
1211
|
-
return datePhrase(
|
|
1287
|
+
return datePhrase(schedule);
|
|
1212
1288
|
}
|
|
1213
1289
|
|
|
1214
1290
|
if (weekdaySet) {
|
|
1215
|
-
const month = monthPhrase(
|
|
1291
|
+
const month = monthPhrase(schedule);
|
|
1216
1292
|
|
|
1217
|
-
if (
|
|
1218
|
-
return quartzWeekday(
|
|
1293
|
+
if (schedule.shapes.weekday === 'quartz') {
|
|
1294
|
+
return quartzWeekday(schedule.pattern.weekday, month || '本月');
|
|
1219
1295
|
}
|
|
1220
1296
|
|
|
1221
|
-
return month + weekdayPhrase(
|
|
1297
|
+
return month + weekdayPhrase(schedule, false, '本月');
|
|
1222
1298
|
}
|
|
1223
1299
|
|
|
1224
|
-
return monthPhrase(
|
|
1300
|
+
return monthPhrase(schedule);
|
|
1225
1301
|
}
|
|
1226
1302
|
|
|
1227
1303
|
// --- Composition: join the qualifier and the time core per plan kind. ---
|
|
1228
1304
|
|
|
1229
1305
|
// Whether the day fields name a clock-point core's recurrence as daily.
|
|
1230
|
-
function isDaily(
|
|
1231
|
-
return !isSet(
|
|
1306
|
+
function isDaily(schedule: Schedule): boolean {
|
|
1307
|
+
return !isSet(schedule.pattern.date) && !isSet(schedule.pattern.weekday);
|
|
1232
1308
|
}
|
|
1233
1309
|
|
|
1234
1310
|
// A clock-point core (clockTimes/compactClockTimes): the qualifier leads, with
|
|
1235
1311
|
// 每天 inserted when daily and a comma before the core for OR/date-cadence.
|
|
1236
|
-
function composePoint(
|
|
1237
|
-
const qual = qualifier(
|
|
1312
|
+
function composePoint(schedule: Schedule, core: string): string {
|
|
1313
|
+
const qual = qualifier(schedule);
|
|
1238
1314
|
|
|
1239
|
-
if (isDaily(
|
|
1315
|
+
if (isDaily(schedule)) {
|
|
1240
1316
|
return qual + '每天' + core;
|
|
1241
1317
|
}
|
|
1242
1318
|
|
|
1243
|
-
const dateSet = isSet(
|
|
1244
|
-
const weekdaySet = isSet(
|
|
1245
|
-
const comma = dateSet && weekdaySet ||
|
|
1319
|
+
const dateSet = isSet(schedule.pattern.date);
|
|
1320
|
+
const weekdaySet = isSet(schedule.pattern.weekday);
|
|
1321
|
+
const comma = dateSet && weekdaySet || schedule.shapes.date === 'step';
|
|
1246
1322
|
|
|
1247
1323
|
return qual + (comma ? ',' : '') + core;
|
|
1248
1324
|
}
|
|
@@ -1250,80 +1326,86 @@ function composePoint(ir: IR, core: string): string {
|
|
|
1250
1326
|
// A cadence core. A bare minute frequency trails the qualifier ("每5分钟,每周
|
|
1251
1327
|
// 一"); one confined to hours leads it. An hour step leads a weekday/month/
|
|
1252
1328
|
// date-cadence qualifier and trails an explicit-day/quartz date or OR.
|
|
1253
|
-
function composeCadence(
|
|
1254
|
-
const qual = qualifier(
|
|
1329
|
+
function composeCadence(schedule: Schedule, core: string): string {
|
|
1330
|
+
const qual = qualifier(schedule);
|
|
1255
1331
|
|
|
1256
1332
|
if (!qual) {
|
|
1257
1333
|
return core;
|
|
1258
1334
|
}
|
|
1259
1335
|
|
|
1260
|
-
if (
|
|
1261
|
-
const lead = (
|
|
1336
|
+
if (schedule.plan.kind === 'minuteFrequency') {
|
|
1337
|
+
const lead = (schedule.plan as Extract<PlanNode, {kind: 'minuteFrequency'}>)
|
|
1262
1338
|
.hours.kind !== 'none';
|
|
1263
1339
|
|
|
1264
1340
|
return lead ? qual + ',' + core : core + ',' + qual;
|
|
1265
1341
|
}
|
|
1266
1342
|
|
|
1267
1343
|
// A compact clock list with a minute past the hour leads its qualifier.
|
|
1268
|
-
if (
|
|
1344
|
+
if (schedule.plan.kind === 'compactClockTimes') {
|
|
1269
1345
|
return qual + ',' + core;
|
|
1270
1346
|
}
|
|
1271
1347
|
|
|
1272
|
-
const dateSet = isSet(
|
|
1273
|
-
const weekdaySet = isSet(
|
|
1274
|
-
const trail = dateSet && (
|
|
1348
|
+
const dateSet = isSet(schedule.pattern.date);
|
|
1349
|
+
const weekdaySet = isSet(schedule.pattern.weekday);
|
|
1350
|
+
const trail = dateSet && (schedule.shapes.date !== 'step' || weekdaySet);
|
|
1275
1351
|
|
|
1276
1352
|
return trail ? core + ',' + qual : qual + ',' + core;
|
|
1277
1353
|
}
|
|
1278
1354
|
|
|
1279
|
-
// A window core (hourRange) whose 在…之间 frame the qualifier leads
|
|
1280
|
-
|
|
1281
|
-
|
|
1355
|
+
// A window core (hourRange) whose 在…之间 frame the qualifier leads. A single-arm
|
|
1356
|
+
// day qualifier glues to the window ("每月1日在9点至17点之间…"); a day-union
|
|
1357
|
+
// (both date and weekday set) takes a comma before the window so the time frame
|
|
1358
|
+
// reads as binding the whole union, not just the trailing weekday arm
|
|
1359
|
+
// ("…1日或每周五,在9点至17点之间…").
|
|
1360
|
+
function composeWindow(schedule: Schedule, core: string): string {
|
|
1361
|
+
const union = isSet(schedule.pattern.date) && isSet(schedule.pattern.weekday);
|
|
1362
|
+
|
|
1363
|
+
return qualifier(schedule) + (union ? ',' : '') + core;
|
|
1282
1364
|
}
|
|
1283
1365
|
|
|
1284
1366
|
// Whether an hour cadence applies — a single pinned minute over an hour stride
|
|
1285
1367
|
// (clean, offset, or non-tiling) — so the clock-point plans take the cadence
|
|
1286
1368
|
// frame, not the daily one.
|
|
1287
|
-
function hourCadenceApplies(
|
|
1288
|
-
return hourCadenceText(
|
|
1369
|
+
function hourCadenceApplies(schedule: Schedule): boolean {
|
|
1370
|
+
return hourCadenceText(schedule) !== null;
|
|
1289
1371
|
}
|
|
1290
1372
|
|
|
1291
|
-
function describe(
|
|
1292
|
-
const {kind} =
|
|
1293
|
-
const core = render(
|
|
1373
|
+
function describe(schedule: Schedule, opts: Opts): string {
|
|
1374
|
+
const {kind} = schedule.plan;
|
|
1375
|
+
const core = render(schedule, schedule.plan, opts);
|
|
1294
1376
|
let composed = core;
|
|
1295
1377
|
|
|
1296
1378
|
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
1297
1379
|
// minute is a sub-daily cadence, not a daily clock point — it takes the
|
|
1298
1380
|
// cadence frame (no 每天), like the bare "每2小时" form.
|
|
1299
|
-
if (hourCadenceApplies(
|
|
1300
|
-
composed = composeCadence(
|
|
1381
|
+
if (hourCadenceApplies(schedule)) {
|
|
1382
|
+
composed = composeCadence(schedule, core);
|
|
1301
1383
|
}
|
|
1302
1384
|
// A compact clock list with a minute past the hour ("每小时5分…") reads as a
|
|
1303
1385
|
// cadence, not a daily clock point — no 每天.
|
|
1304
1386
|
else if (kind === 'clockTimes' ||
|
|
1305
|
-
kind === 'compactClockTimes' &&
|
|
1306
|
-
composed = composePoint(
|
|
1387
|
+
kind === 'compactClockTimes' && schedule.pattern.minute === '0') {
|
|
1388
|
+
composed = composePoint(schedule, core);
|
|
1307
1389
|
}
|
|
1308
1390
|
else if (kind === 'hourStep' || kind === 'minuteFrequency' ||
|
|
1309
1391
|
kind === 'minuteSpanAcrossHourStep' || kind === 'compactClockTimes') {
|
|
1310
|
-
composed = composeCadence(
|
|
1392
|
+
composed = composeCadence(schedule, core);
|
|
1311
1393
|
}
|
|
1312
1394
|
else if (kind === 'hourRange') {
|
|
1313
|
-
composed = composeWindow(
|
|
1395
|
+
composed = composeWindow(schedule, core);
|
|
1314
1396
|
}
|
|
1315
1397
|
else {
|
|
1316
|
-
const qual = qualifier(
|
|
1398
|
+
const qual = qualifier(schedule);
|
|
1317
1399
|
|
|
1318
|
-
composed = qual ? composeCadence(
|
|
1400
|
+
composed = qual ? composeCadence(schedule, core) : core;
|
|
1319
1401
|
}
|
|
1320
1402
|
|
|
1321
|
-
if (
|
|
1403
|
+
if (schedule.pattern.year === '*') {
|
|
1322
1404
|
return composed;
|
|
1323
1405
|
}
|
|
1324
1406
|
|
|
1325
1407
|
// The year leads as "2030年", a range as "2030年至2032年", a list joined with 、.
|
|
1326
|
-
const year =
|
|
1408
|
+
const year = segmentsOf(schedule, 'year').map(function part(seg) {
|
|
1327
1409
|
if (seg.kind === 'range') {
|
|
1328
1410
|
return seg.bounds[0] + '年至' + seg.bounds[1] + '年';
|
|
1329
1411
|
}
|