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