cronli5 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/README.md +2 -2
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +156 -27
- package/dist/cronli5.js +156 -27
- package/dist/lang/de.cjs +157 -36
- package/dist/lang/de.js +157 -36
- package/dist/lang/en.cjs +156 -27
- package/dist/lang/en.js +156 -27
- package/dist/lang/es.cjs +156 -18
- package/dist/lang/es.js +156 -18
- package/dist/lang/fi.cjs +128 -20
- package/dist/lang/fi.js +128 -20
- package/dist/lang/zh.cjs +126 -58
- package/dist/lang/zh.js +126 -58
- package/package.json +2 -2
- package/src/core/util.ts +52 -1
- package/src/lang/de/index.ts +331 -74
- package/src/lang/en/index.ts +327 -62
- package/src/lang/es/index.ts +306 -39
- package/src/lang/fi/index.ts +251 -41
- package/src/lang/zh/index.ts +246 -105
- package/types/core/util.d.ts +10 -1
package/src/lang/zh/index.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// big-endian dates, 每 for recurrence, 24-hour clock with 凌晨0点/正午 anchors,
|
|
5
5
|
// day periods under `ampm`. The style contract is src/lang/zh/notes.md.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
arithmeticStep, orderWeekdaysForDisplay, toFieldNumber
|
|
9
|
+
} from '../../core/util.js';
|
|
8
10
|
import {maxClockTimes, monthNumbers, weekdayNumbers} from '../../core/specs.js';
|
|
9
11
|
import type {Cronli5Options} from '../../types.js';
|
|
10
12
|
import type {
|
|
@@ -314,9 +316,30 @@ function renderMinutePast(ir: IR): string {
|
|
|
314
316
|
return minuteHourClause(ir);
|
|
315
317
|
}
|
|
316
318
|
|
|
317
|
-
//
|
|
319
|
+
// One hour segment as clock words by its form: a range is a span ("9点至20点"),
|
|
320
|
+
// a single is one clock word ("22点"), a step keeps its fires enumerated as
|
|
321
|
+
// clock words ("9点、11点、13点"). A range stated as a list element should read
|
|
322
|
+
// as the span the source wrote, not the hours it expands to — the same choice
|
|
323
|
+
// en/es/de/fi make ("from 9 a.m. through 8 p.m. and at 10 p.m.").
|
|
324
|
+
function hourSegmentWords(segment: Segment): string[] {
|
|
325
|
+
if (segment.kind === 'range') {
|
|
326
|
+
return [hourWord(+segment.bounds[0]) + '至' + hourWord(+segment.bounds[1])];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (segment.kind === 'step') {
|
|
330
|
+
return segment.fires.map(hourWord);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return [hourWord(+segment.value)];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// The hour field as clock words, by segment form: "9点、11点和13点" for a list
|
|
337
|
+
// of singles, "9点至20点和22点" for a range plus a single. Each segment renders
|
|
338
|
+
// as the operator the source wrote (range → span), not its expanded fires.
|
|
318
339
|
function hourList(ir: IR): string {
|
|
319
|
-
|
|
340
|
+
const words = fieldSegments(ir, 'hour').flatMap(hourSegmentWords);
|
|
341
|
+
|
|
342
|
+
return joinAnd(words);
|
|
320
343
|
}
|
|
321
344
|
|
|
322
345
|
// A frame that confines a cadence to active hours: a range gives "在F点至T点之
|
|
@@ -340,14 +363,17 @@ function renderMinuteFrequency(ir: IR, plan: PlanNode): string {
|
|
|
340
363
|
const base = stepClause(minuteStep, UNITS.minute, '分', '每小时');
|
|
341
364
|
const {hours} = plan as Extract<PlanNode, {kind: 'minuteFrequency'}>;
|
|
342
365
|
|
|
343
|
-
|
|
344
|
-
|
|
366
|
+
// An hour stride (a clean step, or an offset/non-tiling progression the core
|
|
367
|
+
// kept a step shape or enumerated to a list) leads the minute cadence:
|
|
368
|
+
// "每2小时每5分钟", "从2点起每6小时每15分钟". A clean cadence concatenates as
|
|
369
|
+
// before; a bounded cadence ends on "至K点", so a comma keeps that endpoint
|
|
370
|
+
// from gluing onto the minute clause ("从9点起每2小时,至17点,每2分钟").
|
|
371
|
+
if (hours.kind === 'step' || hours.kind === 'during') {
|
|
372
|
+
const hourCad = hourCadencePhrase(ir);
|
|
345
373
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
cadence(hourStep.interval, UNITS.hour) + base :
|
|
350
|
-
'在' + hourList(ir) + ',' + base;
|
|
374
|
+
if (hourCad !== null) {
|
|
375
|
+
return hourCad + (hourCad.indexOf('至') === -1 ? '' : ',') + base;
|
|
376
|
+
}
|
|
351
377
|
}
|
|
352
378
|
|
|
353
379
|
if (hours.kind === 'single' ||
|
|
@@ -383,15 +409,20 @@ function renderMinuteSpanInHour(ir: IR, plan: PlanNode): string {
|
|
|
383
409
|
}
|
|
384
410
|
|
|
385
411
|
// A minute clause across discrete hours. A wildcard minute reads "在9点、11点…,
|
|
386
|
-
// 每分钟"; a ranged/listed minute names it: "9点和17点,每小时0至30分,每分钟".
|
|
412
|
+
// 每分钟"; a ranged/listed minute names it: "9点和17点,每小时0至30分,每分钟". An
|
|
413
|
+
// hour progression reads as its cadence ("从9点起每2小时,至17点,每分钟") rather
|
|
414
|
+
// than the enumerated hours, the same idiom the minute field uses.
|
|
387
415
|
function renderMinutesAcrossHours(ir: IR, plan: PlanNode): string {
|
|
388
416
|
const {form} = plan as Extract<PlanNode, {kind: 'minutesAcrossHours'}>;
|
|
417
|
+
const hourCad = hourCadencePhrase(ir);
|
|
389
418
|
|
|
390
419
|
if (form === 'wildcard') {
|
|
391
|
-
return
|
|
420
|
+
return hourCad === null ?
|
|
421
|
+
'在' + hourList(ir) + ',每分钟' :
|
|
422
|
+
hourCad + ',每分钟';
|
|
392
423
|
}
|
|
393
424
|
|
|
394
|
-
return hourList(ir) + ',' + minuteHourClause(ir) + ',每分钟';
|
|
425
|
+
return (hourCad ?? hourList(ir)) + ',' + minuteHourClause(ir) + ',每分钟';
|
|
395
426
|
}
|
|
396
427
|
|
|
397
428
|
// A minute clause across a stepped hour field. A wildcard minute reads "每2小时
|
|
@@ -400,22 +431,22 @@ function renderMinuteSpanAcrossHourStep(ir: IR, plan: PlanNode): string {
|
|
|
400
431
|
const hourStep = stepSegment(ir, 'hour');
|
|
401
432
|
const {form} = plan as Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>;
|
|
402
433
|
|
|
403
|
-
// A minute list
|
|
404
|
-
// "
|
|
434
|
+
// A minute list reads as the hour cadence plus the minute list ("每2小时,
|
|
435
|
+
// 每小时0、25、50分"; offset "从1点起每2小时,每小时5分和30分"), the same compaction
|
|
436
|
+
// the wildcard/range minute already uses, rather than the enumerated hours.
|
|
405
437
|
if (form === 'list') {
|
|
406
|
-
return
|
|
438
|
+
return hourCadencePhrase(ir) + ',' + renderMinutePast(ir);
|
|
407
439
|
}
|
|
408
440
|
|
|
409
441
|
const minuteTail = form === 'wildcard' ?
|
|
410
442
|
'每分钟' :
|
|
411
443
|
minuteHourClause(ir) + ',每分钟';
|
|
412
444
|
|
|
413
|
-
// An offset stride (2/6 fires at 2,8,14,20)
|
|
414
|
-
//
|
|
445
|
+
// An offset or non-tiling stride (2/6 fires at 2,8,14,20) reads as its
|
|
446
|
+
// cadence ("从2点起每6小时"). A wildcard minute hangs off it with a comma; a
|
|
447
|
+
// named minute follows the cadence and its own comma.
|
|
415
448
|
if (hourStep.startToken !== '*') {
|
|
416
|
-
return
|
|
417
|
-
'在' + hourList(ir) + ',' + minuteTail :
|
|
418
|
-
hourList(ir) + ',' + minuteTail;
|
|
449
|
+
return hourCadencePhrase(ir) + ',' + minuteTail;
|
|
419
450
|
}
|
|
420
451
|
|
|
421
452
|
// A step-2 hour from midnight IS exactly the even hours; name them so, rather
|
|
@@ -435,12 +466,10 @@ function renderMinuteSpanAcrossHourStep(ir: IR, plan: PlanNode): string {
|
|
|
435
466
|
function renderClockTimes(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
436
467
|
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
437
468
|
// minute reads as a cadence rather than a cross-product of clock times.
|
|
438
|
-
|
|
439
|
-
const cad = hourCadence(ir);
|
|
469
|
+
const cad = hourCadenceText(ir);
|
|
440
470
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
471
|
+
if (cad !== null) {
|
|
472
|
+
return cad;
|
|
444
473
|
}
|
|
445
474
|
|
|
446
475
|
const {times} = plan as Extract<PlanNode, {kind: 'clockTimes'}>;
|
|
@@ -454,20 +483,34 @@ function renderCompactClockTimes(ir: IR, plan: PlanNode): string {
|
|
|
454
483
|
// An hour step (or arithmetic-progression hour list) under the single pinned
|
|
455
484
|
// minute reads as a cadence, not a wall of clock times. (Returns null for an
|
|
456
485
|
// irregular list or a range, which keep enumerating below.)
|
|
457
|
-
|
|
458
|
-
const cad = hourCadence(ir);
|
|
486
|
+
const cad = hourCadenceText(ir);
|
|
459
487
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
488
|
+
if (cad !== null) {
|
|
489
|
+
return cad;
|
|
463
490
|
}
|
|
464
491
|
|
|
465
|
-
const
|
|
492
|
+
const compact = plan as Extract<PlanNode, {kind: 'compactClockTimes'}>;
|
|
466
493
|
const secs = fieldSegments(ir, 'second');
|
|
467
494
|
const tail = secs.length && ir.pattern.second !== '0' ?
|
|
468
495
|
',第' + valueText(secs) + '秒' : '';
|
|
469
496
|
|
|
470
|
-
|
|
497
|
+
// A multi-valued minute (`fold` false) names its whole set, never just its
|
|
498
|
+
// first fire — a list starting at 0 ("*/25" -> :00,:25,:50) must keep the
|
|
499
|
+
// minute clause, not drop it because the leading fire is 0. The hour reads as
|
|
500
|
+
// its bounded cadence when its fires form a progression ("从0点起每5小时,至20
|
|
501
|
+
// 点"), composed after the minute set, the same idiom the stepped-hour path
|
|
502
|
+
// uses; an irregular hour list keeps enumerating with the "在…" frame.
|
|
503
|
+
if (!compact.fold) {
|
|
504
|
+
const hourCad = hourCadencePhrase(ir);
|
|
505
|
+
|
|
506
|
+
return hourCad === null ?
|
|
507
|
+
minuteHourClause(ir) + ',在' + hourList(ir) + tail :
|
|
508
|
+
hourCad + ',' + minuteHourClause(ir) + tail;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// A single pinned minute past 0 leads with its clause; a pinned 0 folds into
|
|
512
|
+
// the hour times (the :00 is implicit).
|
|
513
|
+
if (compact.minute > 0) {
|
|
471
514
|
return minuteHourClause(ir) + ',在' + hourList(ir) + tail;
|
|
472
515
|
}
|
|
473
516
|
|
|
@@ -497,67 +540,95 @@ function renderHourRange(ir: IR, plan: PlanNode): string {
|
|
|
497
540
|
range.last + '分之间,每分钟';
|
|
498
541
|
}
|
|
499
542
|
|
|
500
|
-
// A stepped hour field: "每2小时",
|
|
501
|
-
//
|
|
502
|
-
//
|
|
543
|
+
// A stepped hour field as its cadence: "每2小时" (clean), "从1点起每2小时"
|
|
544
|
+
// (offset), "从9点起每2小时,至17点" (bounded). A stride that fires only twice
|
|
545
|
+
// reads instead as its two clock words ("凌晨0点和正午", "8点和20点"), shorter and
|
|
546
|
+
// clearer than a cadence for a pair.
|
|
503
547
|
function renderHourStep(ir: IR): string {
|
|
504
548
|
const segment = stepSegment(ir, 'hour');
|
|
505
549
|
|
|
506
|
-
if (segment.startToken !== '*') {
|
|
507
|
-
return hourList(ir);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// A step that fires only twice reads as two clock times ("凌晨0点和正午").
|
|
511
550
|
if (segment.fires.length <= 2) {
|
|
512
551
|
return joinAnd(segment.fires.map(hourWord));
|
|
513
552
|
}
|
|
514
553
|
|
|
515
|
-
return
|
|
554
|
+
return hourCadencePhrase(ir) as string;
|
|
516
555
|
}
|
|
517
556
|
|
|
518
557
|
// --- Hour-step cadence (the 24-cycle analog of renderStride). ---
|
|
519
558
|
|
|
520
|
-
// The hour field's stride, or null when the hour is not a cadence: a
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
function hourStride(
|
|
559
|
+
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
560
|
+
// segment yields its {interval, start, last}; an all-single hour list yields
|
|
561
|
+
// one only when its values form a long-enough arithmetic progression (so an
|
|
562
|
+
// irregular list, or a too-short one like 9,17, keeps enumerating). An offset
|
|
563
|
+
// (start > 0) or non-tiling (interval ∤ 24) stride is still a cadence — Chinese
|
|
564
|
+
// names its start and endpoint ("从M点起每N小时,至K点"), the same idiom the
|
|
565
|
+
// minute field already uses — so it is no longer rejected. The IR is unchanged.
|
|
566
|
+
function hourStride(
|
|
567
|
+
ir: IR
|
|
568
|
+
): {interval: number; start: number; last: number} | null {
|
|
528
569
|
const segments = fieldSegments(ir, 'hour');
|
|
529
570
|
|
|
530
571
|
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
531
|
-
const
|
|
532
|
-
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
572
|
+
const {fires, interval} = segments[0];
|
|
533
573
|
|
|
534
|
-
|
|
535
|
-
return null;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
return {interval: segment.interval,
|
|
539
|
-
last: segment.fires[segment.fires.length - 1]};
|
|
574
|
+
return {interval, start: fires[0], last: fires[fires.length - 1]};
|
|
540
575
|
}
|
|
541
576
|
|
|
542
577
|
const values = singleValues(segments);
|
|
543
|
-
const step = values && arithmeticStep(values);
|
|
544
578
|
|
|
545
|
-
|
|
546
|
-
|
|
579
|
+
return values && arithmeticStep(values);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// The hour field's cadence phrase ("每2小时", "从1点起每2小时", "从0点起每5小时,
|
|
583
|
+
// 至20点"), or null when the hour is not a single arithmetic progression (an
|
|
584
|
+
// irregular list, a range, or a too-short list keeps enumerating). The 24-cycle
|
|
585
|
+
// analog of strideFromSegments — it routes the stride through the one phrasing
|
|
586
|
+
// renderStride speaks, so a clean, offset, or non-tiling hour stride all read
|
|
587
|
+
// as the cadence the equivalent minute step does.
|
|
588
|
+
function hourCadencePhrase(ir: IR): string | null {
|
|
589
|
+
const stride = hourStride(ir);
|
|
590
|
+
|
|
591
|
+
return stride && renderStride({
|
|
592
|
+
...stride, cycle: 24, unit: UNITS.hour, mark: '点', anchor: ''
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// A wildcard or sub-minute step second confined to minute 0 of an hour stride
|
|
597
|
+
// is a confinement, not a juxtaposed cadence. The even-hour stride (interval 2
|
|
598
|
+
// from midnight) reuses the even-hours idiom ("在偶数小时0分的每一秒") so the form
|
|
599
|
+
// does NOT contain the bare "每2小时" and can never be misread as the absorbing
|
|
600
|
+
// hour cadence (the same reason en says "for one minute during every other
|
|
601
|
+
// hour", not "every two hours"). An OFFSET stride names its start ("从1点起每2小时"),
|
|
602
|
+
// already unambiguous — it cannot be heard as the bare cadence — so it folds
|
|
603
|
+
// "0分" and the second onto that named cadence ("从1点起每2小时0分的每一秒"). A bare
|
|
604
|
+
// cadence from midnight (no start named, e.g. "每3小时") keeps enumerating its
|
|
605
|
+
// hours so it is never heard as the absorbing form.
|
|
606
|
+
function minuteZeroConfinement(
|
|
607
|
+
ir: IR, stride: {interval: number; start: number}, prefix: string
|
|
608
|
+
): string | null {
|
|
609
|
+
if (stride.interval === 2 && stride.start === 0) {
|
|
610
|
+
return '在偶数小时0分' + secondTail(ir);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (prefix.indexOf('从') !== -1) {
|
|
614
|
+
return prefix + '0分' + secondTail(ir);
|
|
547
615
|
}
|
|
548
616
|
|
|
549
|
-
return
|
|
617
|
+
return null;
|
|
550
618
|
}
|
|
551
619
|
|
|
552
620
|
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
553
|
-
// pinned minute and a second as a cadence —
|
|
554
|
-
// instead of cross-multiplying the hours into a wall of clock
|
|
555
|
-
// null when the hour is not a
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
621
|
+
// pinned minute and a second as a cadence — the hour cadence plus the
|
|
622
|
+
// minute/second — instead of cross-multiplying the hours into a wall of clock
|
|
623
|
+
// times. Returns null when the hour is not a stride, when the cross-product is
|
|
624
|
+
// short enough that enumeration is no longer than the cadence (a meaningful
|
|
625
|
+
// second makes every clock time carry a second, so any stride is worth
|
|
626
|
+
// compacting; otherwise the stride must exceed the clock-time cap, the same
|
|
627
|
+
// point at which the core itself stops enumerating), or when the cadence is
|
|
628
|
+
// bounded ("…,至K点"): a trailing minute fused onto its endpoint ("至20点0分")
|
|
629
|
+
// would read as a clock time, so a bounded stride keeps enumerating its fused
|
|
630
|
+
// clock times here, naming the cadence only where no minute follows it (the
|
|
631
|
+
// bare hour field). Renderer-only; the IR is unchanged.
|
|
561
632
|
function hourCadence(ir: IR): string | null {
|
|
562
633
|
const stride = hourStride(ir);
|
|
563
634
|
|
|
@@ -565,25 +636,24 @@ function hourCadence(ir: IR): string | null {
|
|
|
565
636
|
return null;
|
|
566
637
|
}
|
|
567
638
|
|
|
568
|
-
const fires = stride.last / stride.interval + 1;
|
|
639
|
+
const fires = (stride.last - stride.start) / stride.interval + 1;
|
|
569
640
|
|
|
570
641
|
if (ir.pattern.second === '0' && fires <= maxClockTimes) {
|
|
571
642
|
return null;
|
|
572
643
|
}
|
|
573
644
|
|
|
574
|
-
const prefix =
|
|
645
|
+
const prefix = hourCadencePhrase(ir) as string;
|
|
646
|
+
|
|
647
|
+
// A bounded cadence cannot carry a fused minute unambiguously; enumerate.
|
|
648
|
+
if (prefix.indexOf('至') !== -1) {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
|
|
575
652
|
const minute = +ir.pattern.minute;
|
|
576
653
|
const subMinute = ir.pattern.second === '*' || ir.shapes.second === 'step';
|
|
577
654
|
|
|
578
|
-
// A wildcard or sub-minute step second confined to minute 0 of a clean
|
|
579
|
-
// stride is a confinement, not a juxtaposed cadence. Reuse the even-hours
|
|
580
|
-
// idiom ("在偶数小时0分的每一秒") so the form does NOT contain the bare "每2小时"
|
|
581
|
-
// and can never be misread as the absorbing hour cadence (the same reason en
|
|
582
|
-
// says "for one minute during every other hour", not "every two hours"). The
|
|
583
|
-
// idiom exists only for the even-hour stride; another clean stride keeps
|
|
584
|
-
// enumerating (return null) rather than coin a misleading "每N小时…" form.
|
|
585
655
|
if (minute === 0 && subMinute) {
|
|
586
|
-
return
|
|
656
|
+
return minuteZeroConfinement(ir, stride, prefix);
|
|
587
657
|
}
|
|
588
658
|
|
|
589
659
|
// A pinned minute 0 folds into the cadence with the explicit "0分" so the
|
|
@@ -599,6 +669,24 @@ function hourCadence(ir: IR): string | null {
|
|
|
599
669
|
prefix + minute + '分' + secondTail(ir);
|
|
600
670
|
}
|
|
601
671
|
|
|
672
|
+
// The cadence a clock-point core (clockTimes/compactClockTimes/composeSeconds)
|
|
673
|
+
// renders an hour stride to, or null. A bare hour stride (minute 0 on the plain
|
|
674
|
+
// :00 second) is the cadence phrase itself — "每2小时", "从0点起每5小时,至20点" —
|
|
675
|
+
// so a short non-tiling stride like */5, which hourCadence keeps enumerating
|
|
676
|
+
// (no minute to fold, nothing to disambiguate), still reads as the cadence. A
|
|
677
|
+
// pinned minute or meaningful second folds into the cadence via hourCadence.
|
|
678
|
+
function hourCadenceText(ir: IR): string | null {
|
|
679
|
+
if (ir.shapes.minute !== 'single') {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (+ir.pattern.minute === 0 && ir.pattern.second === '0') {
|
|
684
|
+
return hourCadencePhrase(ir);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return hourCadence(ir);
|
|
688
|
+
}
|
|
689
|
+
|
|
602
690
|
// The fused second tail for an hour cadence: "的每一秒" for a wildcard second,
|
|
603
691
|
// else "的" + the second's own clause ("的第30秒", "的每15秒").
|
|
604
692
|
function secondTail(ir: IR): string {
|
|
@@ -676,11 +764,13 @@ function minuteClause(ir: IR): string {
|
|
|
676
764
|
return valueList(fieldSegments(ir, 'minute'), '分');
|
|
677
765
|
}
|
|
678
766
|
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
767
|
+
// A single second folds into each clock time a clockTimes rest renders
|
|
768
|
+
// ("9点5分30秒"), so it is already spoken; appending the second clause again
|
|
769
|
+
// would double it. A wildcard/list/range second does not fold, so it still
|
|
770
|
+
// leads its own clause after the clock times.
|
|
771
|
+
function clockRestCarriesSecond(rest: PlanNode): boolean {
|
|
772
|
+
return rest.kind === 'clockTimes' &&
|
|
773
|
+
rest.times.some((time) => Boolean(time.second));
|
|
684
774
|
}
|
|
685
775
|
|
|
686
776
|
// minute = 0 ("on the hour"): render the rest schedule and attach the second.
|
|
@@ -710,11 +800,10 @@ function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
710
800
|
}
|
|
711
801
|
|
|
712
802
|
const restText = render(ir, rest, opts);
|
|
803
|
+
const secTail = clockRestCarriesSecond(rest) ? '' : sec;
|
|
713
804
|
|
|
714
|
-
if (
|
|
715
|
-
|
|
716
|
-
return '每天' + restText + sec;
|
|
717
|
-
}
|
|
805
|
+
if (composedClock && isDaily(ir)) {
|
|
806
|
+
return '每天' + restText + secTail;
|
|
718
807
|
}
|
|
719
808
|
|
|
720
809
|
// A stated minute (e.g. minute 0 under a sub-minute second) takes the same
|
|
@@ -723,7 +812,7 @@ function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
723
812
|
return restText + ',' + sec;
|
|
724
813
|
}
|
|
725
814
|
|
|
726
|
-
return restText +
|
|
815
|
+
return restText + secTail;
|
|
727
816
|
}
|
|
728
817
|
|
|
729
818
|
// A minute pinned to 0 under specific clock hours (not a compacted cadence): a
|
|
@@ -733,6 +822,15 @@ function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
733
822
|
// :00, not 3,600 across the hour) stays visible. The daily frame leads with
|
|
734
823
|
// 每天; a weekday or date qualifier is added by describe().
|
|
735
824
|
function composeMinuteZeroClocks(ir: IR, sec: string): string {
|
|
825
|
+
// An hour RANGE (or a list whose segments include a range) reads as the span
|
|
826
|
+
// the source wrote ("9点至17点"), not the wall of clock words it expands to —
|
|
827
|
+
// the hour-RANGE analog of the hour-step cadence. A pure single-value list
|
|
828
|
+
// (9,17) has no range to span and keeps enumerating below.
|
|
829
|
+
if (hasHourWindow(ir)) {
|
|
830
|
+
return isDaily(ir) ? '每天' + hourRangeWindow(ir, sec) :
|
|
831
|
+
hourRangeWindow(ir, sec);
|
|
832
|
+
}
|
|
833
|
+
|
|
736
834
|
const clocks = hourFires(ir).map(function clock(hour): string {
|
|
737
835
|
// Noon's word (正午) already pins 12:00, so the "0分" is redundant for it;
|
|
738
836
|
// midnight (凌晨0点) and other hours still need it to pin the minute.
|
|
@@ -747,13 +845,45 @@ function composeMinuteZeroClocks(ir: IR, sec: string): string {
|
|
|
747
845
|
return isDaily(ir) ? '每天' + core : core;
|
|
748
846
|
}
|
|
749
847
|
|
|
848
|
+
// Whether the hour field is a range — or a list whose segments include a
|
|
849
|
+
// range — and so forms a window ("9点至17点") rather than a wall of clock
|
|
850
|
+
// words. A pure single-value list (9,17) has no range to span; a step is
|
|
851
|
+
// handled by hourStride/hourCadence.
|
|
852
|
+
function hasHourWindow(ir: IR): boolean {
|
|
853
|
+
return fieldSegments(ir, 'hour').some(function range(segment) {
|
|
854
|
+
return segment.kind === 'range';
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// The hour-range window under a pinned minute 0 and a meaningful or wildcard
|
|
859
|
+
// second: the hour span list ("9点至17点", "9点至20点和22点") plus the second.
|
|
860
|
+
// A wildcard or sub-minute step second pins the explicit "0分" so the
|
|
861
|
+
// one-minute confinement stays visible ("9点至17点0分的每一秒"), distinct from
|
|
862
|
+
// the bare hourly window ("在9点至17点之间,每小时"); a single/list/range second
|
|
863
|
+
// reads as a clock-point span with the second appended ("9点至17点,第30秒"),
|
|
864
|
+
// matching the folded compact form for the same shape.
|
|
865
|
+
function hourRangeWindow(ir: IR, sec: string): string {
|
|
866
|
+
const span = hourList(ir);
|
|
867
|
+
|
|
868
|
+
if (ir.pattern.second === '*' || ir.shapes.second === 'step') {
|
|
869
|
+
return span + '0分' + (sec === '每秒' ? '的每一秒' : '的' + sec);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return span + ',' + sec;
|
|
873
|
+
}
|
|
874
|
+
|
|
750
875
|
// Wildcard or stepped minute: hang the "每分钟/每N分钟每秒" tail off the hour.
|
|
751
876
|
function composeSecondsCadence(ir: IR): string {
|
|
752
877
|
const sec = secondClause(ir);
|
|
753
878
|
const tail = minuteClause(ir) + sec;
|
|
754
879
|
|
|
755
|
-
|
|
756
|
-
|
|
880
|
+
const hourCad = hourCadencePhrase(ir);
|
|
881
|
+
|
|
882
|
+
if (hourCad !== null) {
|
|
883
|
+
// The cadence absorbs the tail with "的" ("每2小时的每分钟每秒",
|
|
884
|
+
// "从1点起每2小时的每分钟每秒"); a bounded cadence ends on "至K点", so its tail
|
|
885
|
+
// takes a comma to keep that endpoint from reading as a fused clock time.
|
|
886
|
+
return hourCad + (hourCad.indexOf('至') === -1 ? '的' : ',') + tail;
|
|
757
887
|
}
|
|
758
888
|
|
|
759
889
|
if (ir.shapes.hour === 'single') {
|
|
@@ -805,9 +935,10 @@ function composeSecondsListed(ir: IR): string {
|
|
|
805
935
|
return minutes + ',' + sec;
|
|
806
936
|
}
|
|
807
937
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
938
|
+
const hourCad = hourCadencePhrase(ir);
|
|
939
|
+
|
|
940
|
+
if (hourCad !== null) {
|
|
941
|
+
return hourCad + ',' + minutes + ',' + sec;
|
|
811
942
|
}
|
|
812
943
|
|
|
813
944
|
return hourFrame(ir) + minutes + ',' + sec;
|
|
@@ -936,7 +1067,7 @@ function quartzDate(token: string, monthPrefix: string): string {
|
|
|
936
1067
|
return monthPrefix + '最后第' + token.slice(2) + '天';
|
|
937
1068
|
}
|
|
938
1069
|
|
|
939
|
-
return '最接近' + token.slice(0, -1) + '日的工作日';
|
|
1070
|
+
return monthPrefix + '最接近' + token.slice(0, -1) + '日的工作日';
|
|
940
1071
|
}
|
|
941
1072
|
|
|
942
1073
|
// The date side of a qualifier (month folded in): "每月1日", "1月1日",
|
|
@@ -957,7 +1088,17 @@ function datePhrase(ir: IR): string {
|
|
|
957
1088
|
return month + cadence(stepSegment(ir, 'date').interval, '天');
|
|
958
1089
|
}
|
|
959
1090
|
|
|
960
|
-
|
|
1091
|
+
if (!month) {
|
|
1092
|
+
return '每月' + dayList(ir);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// A multi-month scope (range/list) ends in 月 and would run straight into the
|
|
1096
|
+
// day — "6月至8月1日" reads "8月1日" as August 1st. The comma keeps the month
|
|
1097
|
+
// scope distinct from the day ("6月至8月,1日"). A single month stays glued
|
|
1098
|
+
// ("6月1日"), which is unambiguous.
|
|
1099
|
+
const monthMulti = ir.shapes.month === 'range' || ir.shapes.month === 'list';
|
|
1100
|
+
|
|
1101
|
+
return month + (monthMulti ? ',' : '') + dayList(ir);
|
|
961
1102
|
}
|
|
962
1103
|
|
|
963
1104
|
// The date side WITHOUT its month or 每月 lead — just the day part: "1日",
|
|
@@ -1026,13 +1167,12 @@ function weekdayPhrase(
|
|
|
1026
1167
|
return '每' + weekdayName(from) + '至' + weekdayName(to);
|
|
1027
1168
|
}
|
|
1028
1169
|
|
|
1170
|
+
// Weekday lists display Monday-first (Sunday last); the IR stays canonical
|
|
1171
|
+
// (Sunday=0). The helper flattens steps into singles and orders the list.
|
|
1029
1172
|
const days: number[] = [];
|
|
1030
1173
|
|
|
1031
|
-
segs.forEach(function expand(seg) {
|
|
1032
|
-
if (seg.kind === '
|
|
1033
|
-
days.push(...seg.fires);
|
|
1034
|
-
}
|
|
1035
|
-
else if (seg.kind === 'single') {
|
|
1174
|
+
orderWeekdaysForDisplay(segs).forEach(function expand(seg) {
|
|
1175
|
+
if (seg.kind === 'single') {
|
|
1036
1176
|
days.push(toFieldNumber(seg.value, weekdayNumbers));
|
|
1037
1177
|
}
|
|
1038
1178
|
});
|
|
@@ -1141,10 +1281,11 @@ function composeWindow(ir: IR, core: string): string {
|
|
|
1141
1281
|
return qualifier(ir) + core;
|
|
1142
1282
|
}
|
|
1143
1283
|
|
|
1144
|
-
// Whether an hour cadence applies — a single pinned minute over
|
|
1145
|
-
//
|
|
1284
|
+
// Whether an hour cadence applies — a single pinned minute over an hour stride
|
|
1285
|
+
// (clean, offset, or non-tiling) — so the clock-point plans take the cadence
|
|
1286
|
+
// frame, not the daily one.
|
|
1146
1287
|
function hourCadenceApplies(ir: IR): boolean {
|
|
1147
|
-
return
|
|
1288
|
+
return hourCadenceText(ir) !== null;
|
|
1148
1289
|
}
|
|
1149
1290
|
|
|
1150
1291
|
function describe(ir: IR, opts: Opts): string {
|
package/types/core/util.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Segment } from './ir.js';
|
|
1
2
|
declare function includes(str: string | number, sub: string): boolean;
|
|
2
3
|
declare function unique<T>(items: T[]): T[];
|
|
3
4
|
declare function isNonNegativeInteger(value: string): boolean;
|
|
@@ -6,7 +7,15 @@ declare function arithmeticStep(values: number[]): {
|
|
|
6
7
|
interval: number;
|
|
7
8
|
last: number;
|
|
8
9
|
} | null;
|
|
10
|
+
type WeekdaySegment = {
|
|
11
|
+
kind: 'single';
|
|
12
|
+
value: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'range';
|
|
15
|
+
bounds: [string, string];
|
|
16
|
+
};
|
|
17
|
+
declare function orderWeekdaysForDisplay(segments: Segment[]): WeekdaySegment[];
|
|
9
18
|
declare function toFieldNumber(token: string, numberMap?: {
|
|
10
19
|
[name: string]: number;
|
|
11
20
|
}): number;
|
|
12
|
-
export { arithmeticStep, includes, isNonNegativeInteger, toFieldNumber, unique };
|
|
21
|
+
export { arithmeticStep, includes, isNonNegativeInteger, orderWeekdaysForDisplay, toFieldNumber, unique };
|