cronli5 0.1.4 → 0.1.6
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 +53 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +286 -45
- package/dist/cronli5.js +286 -45
- package/dist/lang/de.cjs +252 -13
- package/dist/lang/de.js +252 -13
- package/dist/lang/en.cjs +281 -38
- package/dist/lang/en.js +281 -38
- package/dist/lang/es.cjs +259 -29
- package/dist/lang/es.js +259 -29
- package/dist/lang/fi.cjs +285 -49
- package/dist/lang/fi.js +285 -49
- package/dist/lang/zh.cjs +225 -42
- package/dist/lang/zh.js +225 -42
- package/package.json +3 -2
- package/src/core/analyze.ts +7 -0
- package/src/core/ir.ts +1 -1
- package/src/core/util.ts +31 -1
- package/src/lang/de/index.ts +561 -30
- package/src/lang/en/index.ts +593 -59
- package/src/lang/es/index.ts +576 -52
- package/src/lang/fi/index.ts +633 -95
- package/src/lang/zh/index.ts +484 -77
- package/types/core/ir.d.ts +1 -1
- package/types/core/util.d.ts +6 -1
package/src/lang/zh/index.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// big-endian dates, 每 for recurrence, 24-hour clock with 凌晨0点/正午 anchors,
|
|
5
5
|
// day periods under `ampm`. The style contract is src/lang/zh/notes.md.
|
|
6
6
|
|
|
7
|
-
import {toFieldNumber} from '../../core/util.js';
|
|
8
|
-
import {monthNumbers, weekdayNumbers} from '../../core/specs.js';
|
|
7
|
+
import {arithmeticStep, toFieldNumber} from '../../core/util.js';
|
|
8
|
+
import {maxClockTimes, monthNumbers, weekdayNumbers} from '../../core/specs.js';
|
|
9
9
|
import type {Cronli5Options} from '../../types.js';
|
|
10
10
|
import type {
|
|
11
11
|
Field, IR, Language, NormalizedOptions, PlanNode, Segment
|
|
@@ -43,6 +43,108 @@ function cadence(interval: number, unit: string): string {
|
|
|
43
43
|
return interval === 1 ? '每' + unit : '每' + interval + unit;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// A step cadence to phrase over a `cycle`-long field (60 for minute/second),
|
|
47
|
+
// running from `start` to `last`. `unit` is the cadence noun ("分钟"/"秒"),
|
|
48
|
+
// `mark` the bound measure word ("分"/"秒"), `anchor` the larger frame
|
|
49
|
+
// ("每小时"/"每分钟").
|
|
50
|
+
interface Stride {
|
|
51
|
+
interval: number;
|
|
52
|
+
start: number;
|
|
53
|
+
last: number;
|
|
54
|
+
cycle: number;
|
|
55
|
+
unit: string;
|
|
56
|
+
mark: string;
|
|
57
|
+
anchor: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Speak a step cadence over a `cycle`-long field. A clean stride from the top
|
|
61
|
+
// of the cycle is the bare cadence ("每2分钟"); a uniform offset (start within
|
|
62
|
+
// the first interval, the interval still dividing the cycle) names only its
|
|
63
|
+
// start, since it wraps cleanly with no distinct endpoint ("每小时从5分起每6分
|
|
64
|
+
// 钟"); a non-uniform stride (start >= interval, or an interval that does not
|
|
65
|
+
// divide the cycle) pins both endpoints so the bounded, non-wrapping set reads
|
|
66
|
+
// unambiguously ("每小时从3分起每2分钟,至59分"). This is the one phrasing for
|
|
67
|
+
// every step the renderer speaks, whether the core kept it a step shape (a
|
|
68
|
+
// clean cadence) or enumerated it to a fire list (an offset/uneven set the
|
|
69
|
+
// list path recognizes as a progression).
|
|
70
|
+
function renderStride(stride: Stride): string {
|
|
71
|
+
const {interval, start, last, cycle, unit, mark, anchor} = stride;
|
|
72
|
+
const tiles = cycle % interval === 0;
|
|
73
|
+
|
|
74
|
+
if (start === 0 && tiles) {
|
|
75
|
+
return cadence(interval, unit);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const lead = anchor + '从' + start + mark + '起' + cadence(interval, unit);
|
|
79
|
+
|
|
80
|
+
return start < interval && tiles ? lead : lead + ',至' + last + mark;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The sorted numeric values a field's segments cover, or null if any segment
|
|
84
|
+
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
85
|
+
function singleValues(segments: Segment[]): number[] | null {
|
|
86
|
+
const values: number[] = [];
|
|
87
|
+
|
|
88
|
+
for (const segment of segments) {
|
|
89
|
+
if (segment.kind !== 'single') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
values.push(+segment.value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return values;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
100
|
+
// form an arithmetic progression long enough to beat the list (the core
|
|
101
|
+
// enumerates an offset/uneven step to this fire list; the IR is unchanged, so
|
|
102
|
+
// the renderer recognizes the progression). Returns null for a non-progression
|
|
103
|
+
// or a too-short list, leaving the caller to enumerate.
|
|
104
|
+
function strideFromSegments(
|
|
105
|
+
segments: Segment[],
|
|
106
|
+
unit: string,
|
|
107
|
+
mark: string,
|
|
108
|
+
anchor: string
|
|
109
|
+
): string | null {
|
|
110
|
+
const values = singleValues(segments);
|
|
111
|
+
const step = values && arithmeticStep(values);
|
|
112
|
+
|
|
113
|
+
return step ?
|
|
114
|
+
renderStride({...step, cycle: 60, unit, mark, anchor}) :
|
|
115
|
+
null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// A step *shape* segment as its cadence ("每小时从5分起每6分钟"). A bounded
|
|
119
|
+
// sub-range step (`a-b/n`) is not a whole-cycle stride, so it lists its fires;
|
|
120
|
+
// a short offset cadence (three fires or fewer) also lists, since the list is
|
|
121
|
+
// no longer than the cadence. Everything else routes through `renderStride`.
|
|
122
|
+
// The uneven whole-cycle step never reaches here as a step shape — the core
|
|
123
|
+
// enumerates it to a fire list, which the list path recognizes instead.
|
|
124
|
+
function stepClause(
|
|
125
|
+
segment: StepSegment,
|
|
126
|
+
unit: string,
|
|
127
|
+
mark: string,
|
|
128
|
+
anchor: string
|
|
129
|
+
): string {
|
|
130
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
131
|
+
const short = start !== 0 && segment.fires.length <= 3;
|
|
132
|
+
|
|
133
|
+
if (segment.startToken.indexOf('-') !== -1 || short) {
|
|
134
|
+
return anchor + segment.fires.join('、') + mark;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return renderStride({
|
|
138
|
+
interval: segment.interval,
|
|
139
|
+
start,
|
|
140
|
+
last: segment.fires[segment.fires.length - 1],
|
|
141
|
+
cycle: 60,
|
|
142
|
+
unit,
|
|
143
|
+
mark,
|
|
144
|
+
anchor
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
46
148
|
// The day period a 12-hour clock prepends under `ampm` (notes.md boundaries).
|
|
47
149
|
function dayPeriod(hour: number): string {
|
|
48
150
|
if (hour < 6) {
|
|
@@ -193,13 +295,49 @@ function renderEveryHour(): string {
|
|
|
193
295
|
}
|
|
194
296
|
|
|
195
297
|
// A minute anchored to the hour: "每小时1分", "每小时5分和30分", "每小时0至30分".
|
|
298
|
+
// A regular step reads as a stride cadence ("每小时从3分起每2分钟,至59分"),
|
|
299
|
+
// whether the core kept it a step shape (a uniform offset divides 60) or
|
|
300
|
+
// enumerated it to a fire list (an offset/uneven set) — both route through the
|
|
301
|
+
// stride; a short or irregular set keeps the enumerated "每小时…分" list.
|
|
302
|
+
function minuteHourClause(ir: IR): string {
|
|
303
|
+
const segments = fieldSegments(ir, 'minute');
|
|
304
|
+
|
|
305
|
+
if (ir.shapes.minute === 'step') {
|
|
306
|
+
return stepClause(stepSegment(ir, 'minute'), '分钟', '分', '每小时');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return strideFromSegments(segments, '分钟', '分', '每小时') ??
|
|
310
|
+
'每小时' + valueList(segments, '分');
|
|
311
|
+
}
|
|
312
|
+
|
|
196
313
|
function renderMinutePast(ir: IR): string {
|
|
197
|
-
return
|
|
314
|
+
return minuteHourClause(ir);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// One hour segment as clock words by its form: a range is a span ("9点至20点"),
|
|
318
|
+
// a single is one clock word ("22点"), a step keeps its fires enumerated as
|
|
319
|
+
// clock words ("9点、11点、13点"). A range stated as a list element should read
|
|
320
|
+
// as the span the source wrote, not the hours it expands to — the same choice
|
|
321
|
+
// en/es/de/fi make ("from 9 a.m. through 8 p.m. and at 10 p.m.").
|
|
322
|
+
function hourSegmentWords(segment: Segment): string[] {
|
|
323
|
+
if (segment.kind === 'range') {
|
|
324
|
+
return [hourWord(+segment.bounds[0]) + '至' + hourWord(+segment.bounds[1])];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (segment.kind === 'step') {
|
|
328
|
+
return segment.fires.map(hourWord);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return [hourWord(+segment.value)];
|
|
198
332
|
}
|
|
199
333
|
|
|
200
|
-
// The hour
|
|
334
|
+
// The hour field as clock words, by segment form: "9点、11点和13点" for a list
|
|
335
|
+
// of singles, "9点至20点和22点" for a range plus a single. Each segment renders
|
|
336
|
+
// as the operator the source wrote (range → span), not its expanded fires.
|
|
201
337
|
function hourList(ir: IR): string {
|
|
202
|
-
|
|
338
|
+
const words = fieldSegments(ir, 'hour').flatMap(hourSegmentWords);
|
|
339
|
+
|
|
340
|
+
return joinAnd(words);
|
|
203
341
|
}
|
|
204
342
|
|
|
205
343
|
// A frame that confines a cadence to active hours: a range gives "在F点至T点之
|
|
@@ -218,21 +356,22 @@ function hourFrame(ir: IR): string {
|
|
|
218
356
|
// A repeating minute step, optionally confined to active hours.
|
|
219
357
|
function renderMinuteFrequency(ir: IR, plan: PlanNode): string {
|
|
220
358
|
const minuteStep = stepSegment(ir, 'minute');
|
|
221
|
-
// A "每N分钟" cadence
|
|
222
|
-
//
|
|
223
|
-
const base = minuteStep.
|
|
224
|
-
cadence(minuteStep.interval, UNITS.minute) :
|
|
225
|
-
renderMinutePast(ir);
|
|
359
|
+
// A clean stride is the bare "每N分钟" cadence; an offset step keeps its start
|
|
360
|
+
// ("每小时从5分起每6分钟"). A short offset cadence still lists its fires.
|
|
361
|
+
const base = stepClause(minuteStep, UNITS.minute, '分', '每小时');
|
|
226
362
|
const {hours} = plan as Extract<PlanNode, {kind: 'minuteFrequency'}>;
|
|
227
363
|
|
|
228
|
-
|
|
229
|
-
|
|
364
|
+
// An hour stride (a clean step, or an offset/non-tiling progression the core
|
|
365
|
+
// kept a step shape or enumerated to a list) leads the minute cadence:
|
|
366
|
+
// "每2小时每5分钟", "从2点起每6小时每15分钟". A clean cadence concatenates as
|
|
367
|
+
// before; a bounded cadence ends on "至K点", so a comma keeps that endpoint
|
|
368
|
+
// from gluing onto the minute clause ("从9点起每2小时,至17点,每2分钟").
|
|
369
|
+
if (hours.kind === 'step' || hours.kind === 'during') {
|
|
370
|
+
const hourCad = hourCadencePhrase(ir);
|
|
230
371
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
cadence(hourStep.interval, UNITS.hour) + base :
|
|
235
|
-
'在' + hourList(ir) + ',' + base;
|
|
372
|
+
if (hourCad !== null) {
|
|
373
|
+
return hourCad + (hourCad.indexOf('至') === -1 ? '' : ',') + base;
|
|
374
|
+
}
|
|
236
375
|
}
|
|
237
376
|
|
|
238
377
|
if (hours.kind === 'single' ||
|
|
@@ -268,16 +407,20 @@ function renderMinuteSpanInHour(ir: IR, plan: PlanNode): string {
|
|
|
268
407
|
}
|
|
269
408
|
|
|
270
409
|
// A minute clause across discrete hours. A wildcard minute reads "在9点、11点…,
|
|
271
|
-
// 每分钟"; a ranged/listed minute names it: "9点和17点,每小时0至30分,每分钟".
|
|
410
|
+
// 每分钟"; a ranged/listed minute names it: "9点和17点,每小时0至30分,每分钟". An
|
|
411
|
+
// hour progression reads as its cadence ("从9点起每2小时,至17点,每分钟") rather
|
|
412
|
+
// than the enumerated hours, the same idiom the minute field uses.
|
|
272
413
|
function renderMinutesAcrossHours(ir: IR, plan: PlanNode): string {
|
|
273
414
|
const {form} = plan as Extract<PlanNode, {kind: 'minutesAcrossHours'}>;
|
|
415
|
+
const hourCad = hourCadencePhrase(ir);
|
|
274
416
|
|
|
275
417
|
if (form === 'wildcard') {
|
|
276
|
-
return
|
|
418
|
+
return hourCad === null ?
|
|
419
|
+
'在' + hourList(ir) + ',每分钟' :
|
|
420
|
+
hourCad + ',每分钟';
|
|
277
421
|
}
|
|
278
422
|
|
|
279
|
-
return hourList(ir) + '
|
|
280
|
-
valueList(fieldSegments(ir, 'minute'), '分') + ',每分钟';
|
|
423
|
+
return (hourCad ?? hourList(ir)) + ',' + minuteHourClause(ir) + ',每分钟';
|
|
281
424
|
}
|
|
282
425
|
|
|
283
426
|
// A minute clause across a stepped hour field. A wildcard minute reads "每2小时
|
|
@@ -285,16 +428,23 @@ function renderMinutesAcrossHours(ir: IR, plan: PlanNode): string {
|
|
|
285
428
|
function renderMinuteSpanAcrossHourStep(ir: IR, plan: PlanNode): string {
|
|
286
429
|
const hourStep = stepSegment(ir, 'hour');
|
|
287
430
|
const {form} = plan as Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>;
|
|
431
|
+
|
|
432
|
+
// A minute list reads as the hour cadence plus the minute list ("每2小时,
|
|
433
|
+
// 每小时0、25、50分"; offset "从1点起每2小时,每小时5分和30分"), the same compaction
|
|
434
|
+
// the wildcard/range minute already uses, rather than the enumerated hours.
|
|
435
|
+
if (form === 'list') {
|
|
436
|
+
return hourCadencePhrase(ir) + ',' + renderMinutePast(ir);
|
|
437
|
+
}
|
|
438
|
+
|
|
288
439
|
const minuteTail = form === 'wildcard' ?
|
|
289
440
|
'每分钟' :
|
|
290
|
-
|
|
441
|
+
minuteHourClause(ir) + ',每分钟';
|
|
291
442
|
|
|
292
|
-
// An offset stride (2/6 fires at 2,8,14,20)
|
|
293
|
-
//
|
|
443
|
+
// An offset or non-tiling stride (2/6 fires at 2,8,14,20) reads as its
|
|
444
|
+
// cadence ("从2点起每6小时"). A wildcard minute hangs off it with a comma; a
|
|
445
|
+
// named minute follows the cadence and its own comma.
|
|
294
446
|
if (hourStep.startToken !== '*') {
|
|
295
|
-
return
|
|
296
|
-
'在' + hourList(ir) + ',' + minuteTail :
|
|
297
|
-
hourList(ir) + ',' + minuteTail;
|
|
447
|
+
return hourCadencePhrase(ir) + ',' + minuteTail;
|
|
298
448
|
}
|
|
299
449
|
|
|
300
450
|
// A step-2 hour from midnight IS exactly the even hours; name them so, rather
|
|
@@ -312,6 +462,14 @@ function renderMinuteSpanAcrossHourStep(ir: IR, plan: PlanNode): string {
|
|
|
312
462
|
|
|
313
463
|
// Discrete clock times: "9点", "9点和17点".
|
|
314
464
|
function renderClockTimes(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
465
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
466
|
+
// minute reads as a cadence rather than a cross-product of clock times.
|
|
467
|
+
const cad = hourCadenceText(ir);
|
|
468
|
+
|
|
469
|
+
if (cad !== null) {
|
|
470
|
+
return cad;
|
|
471
|
+
}
|
|
472
|
+
|
|
315
473
|
const {times} = plan as Extract<PlanNode, {kind: 'clockTimes'}>;
|
|
316
474
|
|
|
317
475
|
return joinAnd(times.map((t) => clockTime(t.hour, t.minute, t.second, opts)));
|
|
@@ -320,14 +478,38 @@ function renderClockTimes(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
320
478
|
// Compact clock times past the cap: the hour list (the minute is folded in),
|
|
321
479
|
// with a fixed second appended ("…,第30秒").
|
|
322
480
|
function renderCompactClockTimes(ir: IR, plan: PlanNode): string {
|
|
323
|
-
|
|
481
|
+
// An hour step (or arithmetic-progression hour list) under the single pinned
|
|
482
|
+
// minute reads as a cadence, not a wall of clock times. (Returns null for an
|
|
483
|
+
// irregular list or a range, which keep enumerating below.)
|
|
484
|
+
const cad = hourCadenceText(ir);
|
|
485
|
+
|
|
486
|
+
if (cad !== null) {
|
|
487
|
+
return cad;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const compact = plan as Extract<PlanNode, {kind: 'compactClockTimes'}>;
|
|
324
491
|
const secs = fieldSegments(ir, 'second');
|
|
325
492
|
const tail = secs.length && ir.pattern.second !== '0' ?
|
|
326
493
|
',第' + valueText(secs) + '秒' : '';
|
|
327
494
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
495
|
+
// A multi-valued minute (`fold` false) names its whole set, never just its
|
|
496
|
+
// first fire — a list starting at 0 ("*/25" -> :00,:25,:50) must keep the
|
|
497
|
+
// minute clause, not drop it because the leading fire is 0. The hour reads as
|
|
498
|
+
// its bounded cadence when its fires form a progression ("从0点起每5小时,至20
|
|
499
|
+
// 点"), composed after the minute set, the same idiom the stepped-hour path
|
|
500
|
+
// uses; an irregular hour list keeps enumerating with the "在…" frame.
|
|
501
|
+
if (!compact.fold) {
|
|
502
|
+
const hourCad = hourCadencePhrase(ir);
|
|
503
|
+
|
|
504
|
+
return hourCad === null ?
|
|
505
|
+
minuteHourClause(ir) + ',在' + hourList(ir) + tail :
|
|
506
|
+
hourCad + ',' + minuteHourClause(ir) + tail;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// A single pinned minute past 0 leads with its clause; a pinned 0 folds into
|
|
510
|
+
// the hour times (the :00 is implicit).
|
|
511
|
+
if (compact.minute > 0) {
|
|
512
|
+
return minuteHourClause(ir) + ',在' + hourList(ir) + tail;
|
|
331
513
|
}
|
|
332
514
|
|
|
333
515
|
return hourList(ir) + tail;
|
|
@@ -340,7 +522,7 @@ function renderHourRange(ir: IR, plan: PlanNode): string {
|
|
|
340
522
|
if (range.minuteForm === 'lead') {
|
|
341
523
|
const minuteSegs = fieldSegments(ir, 'minute');
|
|
342
524
|
const past = minuteSegs.length && ir.pattern.minute !== '0' ?
|
|
343
|
-
|
|
525
|
+
minuteHourClause(ir) : '每小时';
|
|
344
526
|
|
|
345
527
|
return '在' + hourWord(range.from) + '至' + hourWord(range.to) +
|
|
346
528
|
'之间,' + past;
|
|
@@ -356,27 +538,149 @@ function renderHourRange(ir: IR, plan: PlanNode): string {
|
|
|
356
538
|
range.last + '分之间,每分钟';
|
|
357
539
|
}
|
|
358
540
|
|
|
359
|
-
// A stepped hour field: "每2小时",
|
|
360
|
-
//
|
|
361
|
-
//
|
|
541
|
+
// A stepped hour field as its cadence: "每2小时" (clean), "从1点起每2小时"
|
|
542
|
+
// (offset), "从9点起每2小时,至17点" (bounded). A stride that fires only twice
|
|
543
|
+
// reads instead as its two clock words ("凌晨0点和正午", "8点和20点"), shorter and
|
|
544
|
+
// clearer than a cadence for a pair.
|
|
362
545
|
function renderHourStep(ir: IR): string {
|
|
363
546
|
const segment = stepSegment(ir, 'hour');
|
|
364
547
|
|
|
365
|
-
if (segment.startToken !== '*') {
|
|
366
|
-
return hourList(ir);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// A step that fires only twice reads as two clock times ("凌晨0点和正午").
|
|
370
548
|
if (segment.fires.length <= 2) {
|
|
371
549
|
return joinAnd(segment.fires.map(hourWord));
|
|
372
550
|
}
|
|
373
551
|
|
|
374
|
-
return
|
|
552
|
+
return hourCadencePhrase(ir) as string;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// --- Hour-step cadence (the 24-cycle analog of renderStride). ---
|
|
556
|
+
|
|
557
|
+
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
558
|
+
// segment yields its {interval, start, last}; an all-single hour list yields
|
|
559
|
+
// one only when its values form a long-enough arithmetic progression (so an
|
|
560
|
+
// irregular list, or a too-short one like 9,17, keeps enumerating). An offset
|
|
561
|
+
// (start > 0) or non-tiling (interval ∤ 24) stride is still a cadence — Chinese
|
|
562
|
+
// names its start and endpoint ("从M点起每N小时,至K点"), the same idiom the
|
|
563
|
+
// minute field already uses — so it is no longer rejected. The IR is unchanged.
|
|
564
|
+
function hourStride(
|
|
565
|
+
ir: IR
|
|
566
|
+
): {interval: number; start: number; last: number} | null {
|
|
567
|
+
const segments = fieldSegments(ir, 'hour');
|
|
568
|
+
|
|
569
|
+
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
570
|
+
const {fires, interval} = segments[0];
|
|
571
|
+
|
|
572
|
+
return {interval, start: fires[0], last: fires[fires.length - 1]};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const values = singleValues(segments);
|
|
576
|
+
|
|
577
|
+
return values && arithmeticStep(values);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// The hour field's cadence phrase ("每2小时", "从1点起每2小时", "从0点起每5小时,
|
|
581
|
+
// 至20点"), or null when the hour is not a single arithmetic progression (an
|
|
582
|
+
// irregular list, a range, or a too-short list keeps enumerating). The 24-cycle
|
|
583
|
+
// analog of strideFromSegments — it routes the stride through the one phrasing
|
|
584
|
+
// renderStride speaks, so a clean, offset, or non-tiling hour stride all read
|
|
585
|
+
// as the cadence the equivalent minute step does.
|
|
586
|
+
function hourCadencePhrase(ir: IR): string | null {
|
|
587
|
+
const stride = hourStride(ir);
|
|
588
|
+
|
|
589
|
+
return stride && renderStride({
|
|
590
|
+
...stride, cycle: 24, unit: UNITS.hour, mark: '点', anchor: ''
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
595
|
+
// pinned minute and a second as a cadence — the hour cadence plus the
|
|
596
|
+
// minute/second — instead of cross-multiplying the hours into a wall of clock
|
|
597
|
+
// times. Returns null when the hour is not a stride, when the cross-product is
|
|
598
|
+
// short enough that enumeration is no longer than the cadence (a meaningful
|
|
599
|
+
// second makes every clock time carry a second, so any stride is worth
|
|
600
|
+
// compacting; otherwise the stride must exceed the clock-time cap, the same
|
|
601
|
+
// point at which the core itself stops enumerating), or when the cadence is
|
|
602
|
+
// bounded ("…,至K点"): a trailing minute fused onto its endpoint ("至20点0分")
|
|
603
|
+
// would read as a clock time, so a bounded stride keeps enumerating its fused
|
|
604
|
+
// clock times here, naming the cadence only where no minute follows it (the
|
|
605
|
+
// bare hour field). Renderer-only; the IR is unchanged.
|
|
606
|
+
function hourCadence(ir: IR): string | null {
|
|
607
|
+
const stride = hourStride(ir);
|
|
608
|
+
|
|
609
|
+
if (!stride) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const fires = (stride.last - stride.start) / stride.interval + 1;
|
|
614
|
+
|
|
615
|
+
if (ir.pattern.second === '0' && fires <= maxClockTimes) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const prefix = hourCadencePhrase(ir) as string;
|
|
620
|
+
|
|
621
|
+
// A bounded cadence cannot carry a fused minute unambiguously; enumerate.
|
|
622
|
+
if (prefix.indexOf('至') !== -1) {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const minute = +ir.pattern.minute;
|
|
627
|
+
const subMinute = ir.pattern.second === '*' || ir.shapes.second === 'step';
|
|
628
|
+
|
|
629
|
+
// A wildcard or sub-minute step second confined to minute 0 of the even-hour
|
|
630
|
+
// stride is a confinement, not a juxtaposed cadence. Reuse the even-hours
|
|
631
|
+
// idiom ("在偶数小时0分的每一秒") so the form does NOT contain the bare "每2小时"
|
|
632
|
+
// and can never be misread as the absorbing hour cadence (the same reason en
|
|
633
|
+
// says "for one minute during every other hour", not "every two hours"). The
|
|
634
|
+
// idiom exists only for the even-hour stride (interval 2 from midnight);
|
|
635
|
+
// another stride keeps enumerating (return null) rather than coin a
|
|
636
|
+
// misleading "…小时…" form.
|
|
637
|
+
if (minute === 0 && subMinute) {
|
|
638
|
+
return stride.interval === 2 && stride.start === 0 ?
|
|
639
|
+
'在偶数小时0分' + secondTail(ir) : null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// A pinned minute 0 folds into the cadence with the explicit "0分" so the
|
|
643
|
+
// confinement stays visible ("每2小时0分的第30秒"); a non-zero minute is a real
|
|
644
|
+
// clock minute named after the cadence ("每2小时5分的每一秒"). A plain :00
|
|
645
|
+
// second adds nothing.
|
|
646
|
+
if (minute === 0) {
|
|
647
|
+
return prefix + '0分' + secondTail(ir);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return ir.pattern.second === '0' ?
|
|
651
|
+
prefix + minute + '分' :
|
|
652
|
+
prefix + minute + '分' + secondTail(ir);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// The cadence a clock-point core (clockTimes/compactClockTimes/composeSeconds)
|
|
656
|
+
// renders an hour stride to, or null. A bare hour stride (minute 0 on the plain
|
|
657
|
+
// :00 second) is the cadence phrase itself — "每2小时", "从0点起每5小时,至20点" —
|
|
658
|
+
// so a short non-tiling stride like */5, which hourCadence keeps enumerating
|
|
659
|
+
// (no minute to fold, nothing to disambiguate), still reads as the cadence. A
|
|
660
|
+
// pinned minute or meaningful second folds into the cadence via hourCadence.
|
|
661
|
+
function hourCadenceText(ir: IR): string | null {
|
|
662
|
+
if (ir.shapes.minute !== 'single') {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (+ir.pattern.minute === 0 && ir.pattern.second === '0') {
|
|
667
|
+
return hourCadencePhrase(ir);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return hourCadence(ir);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// The fused second tail for an hour cadence: "的每一秒" for a wildcard second,
|
|
674
|
+
// else "的" + the second's own clause ("的第30秒", "的每15秒").
|
|
675
|
+
function secondTail(ir: IR): string {
|
|
676
|
+
const sec = secondClause(ir);
|
|
677
|
+
|
|
678
|
+
return sec === '每秒' ? '的每一秒' : '的' + sec;
|
|
375
679
|
}
|
|
376
680
|
|
|
377
681
|
// A continuous minute range fires every minute within it: "每小时0至30分,每分钟".
|
|
378
682
|
function renderRangeOfMinutes(ir: IR): string {
|
|
379
|
-
return
|
|
683
|
+
return minuteHourClause(ir) + ',每分钟';
|
|
380
684
|
}
|
|
381
685
|
|
|
382
686
|
// A standalone second field: "每7秒" (step cadence) or "每分钟第4、17、42秒".
|
|
@@ -388,7 +692,8 @@ function renderStandaloneSeconds(ir: IR): string {
|
|
|
388
692
|
return cadence(first.interval, UNITS.second);
|
|
389
693
|
}
|
|
390
694
|
|
|
391
|
-
return
|
|
695
|
+
return strideFromSegments(segs, '秒', '秒', '每分钟') ??
|
|
696
|
+
'每分钟第' + valueText(segs) + '秒';
|
|
392
697
|
}
|
|
393
698
|
|
|
394
699
|
// A second anchored to the minute: "每分钟第1秒", "每分钟第4、17、42秒".
|
|
@@ -423,7 +728,10 @@ function secondClause(ir: IR): string {
|
|
|
423
728
|
return cadence(first.interval, UNITS.second);
|
|
424
729
|
}
|
|
425
730
|
|
|
426
|
-
|
|
731
|
+
// An offset/uneven step the core enumerated to this list reads as a stride
|
|
732
|
+
// cadence when the fires form a long-enough progression.
|
|
733
|
+
return strideFromSegments(segs, '秒', '秒', '每分钟') ??
|
|
734
|
+
'第' + valueText(segs) + '秒';
|
|
427
735
|
}
|
|
428
736
|
|
|
429
737
|
// The minute clause for a composed (seconds) schedule.
|
|
@@ -439,41 +747,46 @@ function minuteClause(ir: IR): string {
|
|
|
439
747
|
return valueList(fieldSegments(ir, 'minute'), '分');
|
|
440
748
|
}
|
|
441
749
|
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
750
|
+
// A single second folds into each clock time a clockTimes rest renders
|
|
751
|
+
// ("9点5分30秒"), so it is already spoken; appending the second clause again
|
|
752
|
+
// would double it. A wildcard/list/range second does not fold, so it still
|
|
753
|
+
// leads its own clause after the clock times.
|
|
754
|
+
function clockRestCarriesSecond(rest: PlanNode): boolean {
|
|
755
|
+
return rest.kind === 'clockTimes' &&
|
|
756
|
+
rest.times.some((time) => Boolean(time.second));
|
|
447
757
|
}
|
|
448
758
|
|
|
449
759
|
// minute = 0 ("on the hour"): render the rest schedule and attach the second.
|
|
450
760
|
function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
451
761
|
const sec = secondClause(ir);
|
|
452
762
|
const {rest} = plan as Extract<PlanNode, {kind: 'composeSeconds'}>;
|
|
763
|
+
const composedClock =
|
|
764
|
+
rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes';
|
|
453
765
|
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
return
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const core = joinAnd(clocks) + tail;
|
|
766
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
767
|
+
// minute reads as a cadence, not a wall of clock times ("每2小时0分的第30秒").
|
|
768
|
+
// hourCadence folds in the second itself, so it is returned whole, never the
|
|
769
|
+
// rest-plus-second fall-through that would double the second clause. The
|
|
770
|
+
// cadence is sub-daily (no 每天); a qualifier is added by describe().
|
|
771
|
+
if (composedClock) {
|
|
772
|
+
const hourCad = hourCadence(ir);
|
|
773
|
+
|
|
774
|
+
if (hourCad !== null) {
|
|
775
|
+
return hourCad;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
467
778
|
|
|
468
|
-
|
|
779
|
+
// The minute is pinned to 0 under specific hours that did not compact to a
|
|
780
|
+
// cadence: fuse the seconds with each explicit clock minute.
|
|
781
|
+
if (composedClock && ir.pattern.minute === '0') {
|
|
782
|
+
return composeMinuteZeroClocks(ir, sec);
|
|
469
783
|
}
|
|
470
784
|
|
|
471
785
|
const restText = render(ir, rest, opts);
|
|
786
|
+
const secTail = clockRestCarriesSecond(rest) ? '' : sec;
|
|
472
787
|
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
return '每天' + restText + sec;
|
|
476
|
-
}
|
|
788
|
+
if (composedClock && isDaily(ir)) {
|
|
789
|
+
return '每天' + restText + secTail;
|
|
477
790
|
}
|
|
478
791
|
|
|
479
792
|
// A stated minute (e.g. minute 0 under a sub-minute second) takes the same
|
|
@@ -482,7 +795,64 @@ function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
|
482
795
|
return restText + ',' + sec;
|
|
483
796
|
}
|
|
484
797
|
|
|
485
|
-
return restText +
|
|
798
|
+
return restText + secTail;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// A minute pinned to 0 under specific clock hours (not a compacted cadence): a
|
|
802
|
+
// bare clock word ("9点") would hide the :00 and leave the second dangling
|
|
803
|
+
// ("…9点每秒"), reading as the whole hour. Fuse the seconds with the explicit
|
|
804
|
+
// clock minute ("9点0分的每一秒"), so the one-minute confinement (60 fires in
|
|
805
|
+
// :00, not 3,600 across the hour) stays visible. The daily frame leads with
|
|
806
|
+
// 每天; a weekday or date qualifier is added by describe().
|
|
807
|
+
function composeMinuteZeroClocks(ir: IR, sec: string): string {
|
|
808
|
+
// An hour RANGE (or a list whose segments include a range) reads as the span
|
|
809
|
+
// the source wrote ("9点至17点"), not the wall of clock words it expands to —
|
|
810
|
+
// the hour-RANGE analog of the hour-step cadence. A pure single-value list
|
|
811
|
+
// (9,17) has no range to span and keeps enumerating below.
|
|
812
|
+
if (hasHourWindow(ir)) {
|
|
813
|
+
return isDaily(ir) ? '每天' + hourRangeWindow(ir, sec) :
|
|
814
|
+
hourRangeWindow(ir, sec);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const clocks = hourFires(ir).map(function clock(hour): string {
|
|
818
|
+
// Noon's word (正午) already pins 12:00, so the "0分" is redundant for it;
|
|
819
|
+
// midnight (凌晨0点) and other hours still need it to pin the minute.
|
|
820
|
+
return hour === 12 ? '正午' : hourWord(hour) + '0分';
|
|
821
|
+
});
|
|
822
|
+
// A pinned minute makes the seconds' own "每分钟" anchor misleading (it is a
|
|
823
|
+
// single minute, not every minute), so the stride here drops it.
|
|
824
|
+
const nested = strideFromSegments(fieldSegments(ir, 'second'), '秒', '秒', '');
|
|
825
|
+
const tail = sec === '每秒' ? '的每一秒' : '的' + (nested ?? sec);
|
|
826
|
+
const core = joinAnd(clocks) + tail;
|
|
827
|
+
|
|
828
|
+
return isDaily(ir) ? '每天' + core : core;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Whether the hour field is a range — or a list whose segments include a
|
|
832
|
+
// range — and so forms a window ("9点至17点") rather than a wall of clock
|
|
833
|
+
// words. A pure single-value list (9,17) has no range to span; a step is
|
|
834
|
+
// handled by hourStride/hourCadence.
|
|
835
|
+
function hasHourWindow(ir: IR): boolean {
|
|
836
|
+
return fieldSegments(ir, 'hour').some(function range(segment) {
|
|
837
|
+
return segment.kind === 'range';
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// The hour-range window under a pinned minute 0 and a meaningful or wildcard
|
|
842
|
+
// second: the hour span list ("9点至17点", "9点至20点和22点") plus the second.
|
|
843
|
+
// A wildcard or sub-minute step second pins the explicit "0分" so the
|
|
844
|
+
// one-minute confinement stays visible ("9点至17点0分的每一秒"), distinct from
|
|
845
|
+
// the bare hourly window ("在9点至17点之间,每小时"); a single/list/range second
|
|
846
|
+
// reads as a clock-point span with the second appended ("9点至17点,第30秒"),
|
|
847
|
+
// matching the folded compact form for the same shape.
|
|
848
|
+
function hourRangeWindow(ir: IR, sec: string): string {
|
|
849
|
+
const span = hourList(ir);
|
|
850
|
+
|
|
851
|
+
if (ir.pattern.second === '*' || ir.shapes.second === 'step') {
|
|
852
|
+
return span + '0分' + (sec === '每秒' ? '的每一秒' : '的' + sec);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return span + ',' + sec;
|
|
486
856
|
}
|
|
487
857
|
|
|
488
858
|
// Wildcard or stepped minute: hang the "每分钟/每N分钟每秒" tail off the hour.
|
|
@@ -490,8 +860,13 @@ function composeSecondsCadence(ir: IR): string {
|
|
|
490
860
|
const sec = secondClause(ir);
|
|
491
861
|
const tail = minuteClause(ir) + sec;
|
|
492
862
|
|
|
493
|
-
|
|
494
|
-
|
|
863
|
+
const hourCad = hourCadencePhrase(ir);
|
|
864
|
+
|
|
865
|
+
if (hourCad !== null) {
|
|
866
|
+
// The cadence absorbs the tail with "的" ("每2小时的每分钟每秒",
|
|
867
|
+
// "从1点起每2小时的每分钟每秒"); a bounded cadence ends on "至K点", so its tail
|
|
868
|
+
// takes a comma to keep that endpoint from reading as a fused clock time.
|
|
869
|
+
return hourCad + (hourCad.indexOf('至') === -1 ? '的' : ',') + tail;
|
|
495
870
|
}
|
|
496
871
|
|
|
497
872
|
if (ir.shapes.hour === 'single') {
|
|
@@ -499,6 +874,18 @@ function composeSecondsCadence(ir: IR): string {
|
|
|
499
874
|
}
|
|
500
875
|
|
|
501
876
|
if (ir.shapes.hour === 'wildcard') {
|
|
877
|
+
// "每秒,每2分钟" juxtaposes two cadences that read as contradictory. A
|
|
878
|
+
// step-2 minute from the top of the hour IS exactly the even minutes; bind
|
|
879
|
+
// the every-second cadence to them ("每偶数分钟的每一秒") rather than listing
|
|
880
|
+
// the two side by side. Other strides keep the juxtaposed form.
|
|
881
|
+
if (ir.shapes.minute === 'step' && sec === '每秒') {
|
|
882
|
+
const minuteStep = stepSegment(ir, 'minute');
|
|
883
|
+
|
|
884
|
+
if (minuteStep.startToken === '*' && minuteStep.interval === 2) {
|
|
885
|
+
return '每偶数分钟的每一秒';
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
502
889
|
return sec + ',' + minuteClause(ir);
|
|
503
890
|
}
|
|
504
891
|
|
|
@@ -511,23 +898,30 @@ function composeSecondsCadence(ir: IR): string {
|
|
|
511
898
|
// last fire onto the window end ("…17点30分") and reading as a continuous span.
|
|
512
899
|
function composeSecondsListed(ir: IR): string {
|
|
513
900
|
const sec = secondClause(ir);
|
|
514
|
-
const minutes =
|
|
901
|
+
const minutes = minuteHourClause(ir);
|
|
515
902
|
|
|
516
903
|
// A single restricted hour with an every-second cadence fuses the clock time
|
|
517
904
|
// with its minutes — "凌晨0点5、20、35、50分的每一秒" — rather than the "每小时"
|
|
518
905
|
// that falsely implies every hour. A non-wildcard second keeps the list form.
|
|
906
|
+
// A non-uniform minute step the core enumerated to a fire list reads as its
|
|
907
|
+
// stride cadence ("凌晨0点从3分起每2分钟,至59分的每一秒"); the hour fuses, so the
|
|
908
|
+
// stride drops its "每小时" anchor. A short or irregular set keeps the list.
|
|
519
909
|
if (ir.shapes.hour === 'single' && sec === '每秒') {
|
|
520
|
-
|
|
521
|
-
|
|
910
|
+
const minuteSegs = fieldSegments(ir, 'minute');
|
|
911
|
+
const minuteCad = strideFromSegments(minuteSegs, '分钟', '分', '') ??
|
|
912
|
+
valueList(minuteSegs, '分');
|
|
913
|
+
|
|
914
|
+
return hourWord(hourFires(ir)[0]) + minuteCad + '的每一秒';
|
|
522
915
|
}
|
|
523
916
|
|
|
524
917
|
if (ir.shapes.hour === 'wildcard') {
|
|
525
918
|
return minutes + ',' + sec;
|
|
526
919
|
}
|
|
527
920
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
921
|
+
const hourCad = hourCadencePhrase(ir);
|
|
922
|
+
|
|
923
|
+
if (hourCad !== null) {
|
|
924
|
+
return hourCad + ',' + minutes + ',' + sec;
|
|
531
925
|
}
|
|
532
926
|
|
|
533
927
|
return hourFrame(ir) + minutes + ',' + sec;
|
|
@@ -861,14 +1255,27 @@ function composeWindow(ir: IR, core: string): string {
|
|
|
861
1255
|
return qualifier(ir) + core;
|
|
862
1256
|
}
|
|
863
1257
|
|
|
1258
|
+
// Whether an hour cadence applies — a single pinned minute over an hour stride
|
|
1259
|
+
// (clean, offset, or non-tiling) — so the clock-point plans take the cadence
|
|
1260
|
+
// frame, not the daily one.
|
|
1261
|
+
function hourCadenceApplies(ir: IR): boolean {
|
|
1262
|
+
return hourCadenceText(ir) !== null;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
864
1265
|
function describe(ir: IR, opts: Opts): string {
|
|
865
1266
|
const {kind} = ir.plan;
|
|
866
1267
|
const core = render(ir, ir.plan, opts);
|
|
867
1268
|
let composed = core;
|
|
868
1269
|
|
|
1270
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
1271
|
+
// minute is a sub-daily cadence, not a daily clock point — it takes the
|
|
1272
|
+
// cadence frame (no 每天), like the bare "每2小时" form.
|
|
1273
|
+
if (hourCadenceApplies(ir)) {
|
|
1274
|
+
composed = composeCadence(ir, core);
|
|
1275
|
+
}
|
|
869
1276
|
// A compact clock list with a minute past the hour ("每小时5分…") reads as a
|
|
870
1277
|
// cadence, not a daily clock point — no 每天.
|
|
871
|
-
if (kind === 'clockTimes' ||
|
|
1278
|
+
else if (kind === 'clockTimes' ||
|
|
872
1279
|
kind === 'compactClockTimes' && ir.pattern.minute === '0') {
|
|
873
1280
|
composed = composePoint(ir, core);
|
|
874
1281
|
}
|