cronli5 0.1.2 → 0.1.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 +64 -0
- package/cli.js +9 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +100 -28
- package/dist/cronli5.js +100 -28
- package/dist/lang/de.cjs +55 -34
- package/dist/lang/de.js +55 -34
- package/dist/lang/en.cjs +41 -21
- package/dist/lang/en.js +41 -21
- package/dist/lang/es.cjs +79 -29
- package/dist/lang/es.js +79 -29
- package/dist/lang/fi.cjs +42 -29
- package/dist/lang/fi.js +42 -29
- package/dist/lang/zh.cjs +34 -9
- package/dist/lang/zh.js +34 -9
- package/package.json +2 -1
- package/src/core/normalize.ts +94 -4
- package/src/lang/de/index.ts +92 -33
- package/src/lang/en/index.ts +100 -30
- package/src/lang/es/index.ts +134 -25
- package/src/lang/fi/index.ts +54 -20
- package/src/lang/zh/index.ts +83 -12
package/src/lang/es/index.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// lists render as per-hour windows).
|
|
10
10
|
|
|
11
11
|
import {clockDigits, numeral} from '../../core/format.js';
|
|
12
|
+
import {weekdayNumbers} from '../../core/specs.js';
|
|
13
|
+
import {toFieldNumber} from '../../core/util.js';
|
|
12
14
|
import type {Cronli5Options} from '../../types.js';
|
|
13
15
|
import type {
|
|
14
16
|
Field, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode,
|
|
@@ -109,16 +111,6 @@ const weekdayNames = [
|
|
|
109
111
|
'sábado'
|
|
110
112
|
];
|
|
111
113
|
|
|
112
|
-
// Cron token vocabulary (JAN..DEC, SUN..SAT) is part of cron syntax; map
|
|
113
|
-
// it to Spanish names.
|
|
114
|
-
const monthTokens: {[token: string]: number} = {
|
|
115
|
-
JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6,
|
|
116
|
-
JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12
|
|
117
|
-
};
|
|
118
|
-
const weekdayTokens: {[token: string]: number} = {
|
|
119
|
-
SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6
|
|
120
|
-
};
|
|
121
|
-
|
|
122
114
|
// Ordinals for Quartz `#` weekday occurrences (1-5).
|
|
123
115
|
const nthWeekdayNames =
|
|
124
116
|
[null, 'primer', 'segundo', 'tercer', 'cuarto', 'quinto'];
|
|
@@ -219,6 +211,13 @@ function renderComposeSeconds(
|
|
|
219
211
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
220
212
|
opts: Opts
|
|
221
213
|
): string {
|
|
214
|
+
// A wildcard or stepped second with the minute pinned to a single value
|
|
215
|
+
// across one or more specific hours: the seconds confine to the clock time.
|
|
216
|
+
if (plan.rest.kind === 'clockTimes' &&
|
|
217
|
+
(ir.shapes.second === 'wildcard' || ir.shapes.second === 'step')) {
|
|
218
|
+
return pinnedMinuteSeconds(ir, plan.rest, opts);
|
|
219
|
+
}
|
|
220
|
+
|
|
222
221
|
// Seconds list + fixed clock time: nest the seconds into the clock time(s)
|
|
223
222
|
// with genitive "de las HH:MM" instead of "de cada minuto"; the minute is
|
|
224
223
|
// fixed so "de cada minuto" is misleading. Single seconds already fold into
|
|
@@ -256,6 +255,33 @@ function renderComposeSeconds(
|
|
|
256
255
|
return secondsLeadClause(ir, opts) + ', ' + render(ir, plan.rest, opts);
|
|
257
256
|
}
|
|
258
257
|
|
|
258
|
+
// A wildcard or stepped second under a single pinned minute and specific
|
|
259
|
+
// hour(s). The clock-time rest folds the minute into the hour, and on the
|
|
260
|
+
// 12-hour clock a pinned minute-0 drops the :00 entirely ("a las 9 de la
|
|
261
|
+
// mañana") — and even "a las 9" reads aloud as the whole hour, hiding the
|
|
262
|
+
// one-minute confinement (60 fires in :00, not 3,600 across the hour). Minute
|
|
263
|
+
// 0 is the one-minute window at the top of each named hour: a duration frame
|
|
264
|
+
// ("durante un minuto a las 9") states the confinement outright, with the hour
|
|
265
|
+
// as a bare hour so it cannot be heard as the whole hour. A non-zero pinned
|
|
266
|
+
// minute is an unambiguous clock time, so the genitive "de las 09:05" form
|
|
267
|
+
// reads it as the minute, never the hour.
|
|
268
|
+
function pinnedMinuteSeconds(
|
|
269
|
+
ir: IR,
|
|
270
|
+
rest: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
271
|
+
opts: Opts
|
|
272
|
+
): string {
|
|
273
|
+
const dayTrail = leadingQualifier(ir, opts).trimEnd();
|
|
274
|
+
const trail = dayTrail ? ', ' + dayTrail : '';
|
|
275
|
+
|
|
276
|
+
if (+rest.times[0].minute === 0) {
|
|
277
|
+
return secondsLeadClause(ir, opts) + ' durante un minuto ' +
|
|
278
|
+
durationHourList(rest.times, opts) + trail;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return secondsLeadClause(ir, opts) + ' de ' +
|
|
282
|
+
explicitClockList(rest.times, opts) + trail;
|
|
283
|
+
}
|
|
284
|
+
|
|
259
285
|
// The leading clause describing a second field relative to the minute.
|
|
260
286
|
function secondsLeadClause(ir: IR, opts: Opts): string {
|
|
261
287
|
const secondField = ir.pattern.second;
|
|
@@ -453,12 +479,21 @@ function renderMinuteFrequency(
|
|
|
453
479
|
return phrase + trailingQualifier(ir, opts);
|
|
454
480
|
}
|
|
455
481
|
|
|
456
|
-
// "cada minuto de las 9:00 a las 9:29 de la mañana".
|
|
482
|
+
// "cada minuto de las 9:00 a las 9:29 de la mañana". A wildcard minute is the
|
|
483
|
+
// whole hour, so it reads as that hour itself ("cada minuto de la hora de las
|
|
484
|
+
// 09:00") rather than a synthesized "de las HH:00 a las HH:59" range the
|
|
485
|
+
// source never stated; a plain range is a real window and keeps "de … a …".
|
|
457
486
|
function renderMinuteSpanInHour(
|
|
458
487
|
ir: IR,
|
|
459
488
|
plan: Extract<PlanNode, {kind: 'minuteSpanInHour'}>,
|
|
460
489
|
opts: Opts
|
|
461
490
|
): string {
|
|
491
|
+
if (ir.pattern.minute === '*') {
|
|
492
|
+
return 'cada minuto de la hora ' +
|
|
493
|
+
fromTime(timePhrase(plan.hour, 0, null, opts)) +
|
|
494
|
+
trailingQualifier(ir, opts);
|
|
495
|
+
}
|
|
496
|
+
|
|
462
497
|
return 'cada minuto ' +
|
|
463
498
|
timeRange({hour: plan.hour, minute: plan.span[0]},
|
|
464
499
|
{hour: plan.hour, minute: plan.span[1]}, opts) +
|
|
@@ -689,6 +724,82 @@ function renderClockTimes(
|
|
|
689
724
|
return leadingQualifier(ir, opts) + groupClockTimes(phrases);
|
|
690
725
|
}
|
|
691
726
|
|
|
727
|
+
// The genitive clock-time list for a minute-0 compose-seconds confinement:
|
|
728
|
+
// each time with its minute forced visible ("las 09:00"), grouped as usual,
|
|
729
|
+
// then reframed from "a …" to the genitive "de …" the caller prepends. So a
|
|
730
|
+
// pinned minute-0 reads "de las 09:00", never the bare hour.
|
|
731
|
+
function explicitClockList(
|
|
732
|
+
times: {hour: number; minute: number; second?: number | null}[],
|
|
733
|
+
opts: Opts
|
|
734
|
+
): string {
|
|
735
|
+
const phrases = times.map(function clock(time) {
|
|
736
|
+
return atTime(explicitTimePhrase(time.hour, time.minute, opts));
|
|
737
|
+
});
|
|
738
|
+
const grouped = groupClockTimes(phrases);
|
|
739
|
+
|
|
740
|
+
// Strip the leading "a " so the caller's "de " produces the genitive form.
|
|
741
|
+
return grouped.startsWith('a ') ? grouped.slice(2) : grouped;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// The bare-hour list for a minute-0 duration confinement, keeping the "a …"
|
|
745
|
+
// frame the caller embeds after "durante un minuto": "a las 9",
|
|
746
|
+
// "a medianoche", "a las 9, 10, 11 y 12". The hour reads as a bare hour
|
|
747
|
+
// (no minutes), since the "durante un minuto" frame already carries the
|
|
748
|
+
// one-minute window — never "las 09:00", which would read as the whole hour.
|
|
749
|
+
function durationHourList(
|
|
750
|
+
times: {hour: number; minute: number; second?: number | null}[],
|
|
751
|
+
opts: Opts
|
|
752
|
+
): string {
|
|
753
|
+
const phrases = times.map(function clock(time) {
|
|
754
|
+
return atTime(bareHourPhrase(time.hour, opts));
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
return groupClockTimes(phrases);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// A bare hour with its article, no minutes: "las 9" / "la 1" / "mediodía" /
|
|
761
|
+
// "medianoche" on the 24-hour clock, or the 12-hour day-period form
|
|
762
|
+
// ("las 9 de la mañana"). Used by the minute-0 duration frame, where the
|
|
763
|
+
// minute is already stated and the clock minute would only mislead.
|
|
764
|
+
function bareHourPhrase(hour: number, opts: Opts): string {
|
|
765
|
+
if (opts.ampm) {
|
|
766
|
+
return timePhrase(hour, 0, null, opts);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (+hour === 0) {
|
|
770
|
+
return 'medianoche';
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (+hour === 12) {
|
|
774
|
+
return 'mediodía';
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return (+hour === 1 ? 'la ' : 'las ') + hour;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// A clock time with its minute forced visible and the noon/midnight words
|
|
781
|
+
// suppressed: "las 09:00", "las 9:00 de la mañana", "las 12:00 de la tarde".
|
|
782
|
+
// So a pinned minute-0 confinement always shows its ":00".
|
|
783
|
+
function explicitTimePhrase(hour: number, minute: number, opts: Opts): string {
|
|
784
|
+
if (!opts.ampm) {
|
|
785
|
+
const article = +hour === 1 ? 'la ' : 'las ';
|
|
786
|
+
const suffix = opts.style.hSuffix ? ' h' : '';
|
|
787
|
+
|
|
788
|
+
return article +
|
|
789
|
+
clockDigits({hour, minute, second: 0},
|
|
790
|
+
{pad: true, sep: opts.style.sep}) + suffix;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const display = hour % 12 || 12;
|
|
794
|
+
const time = (display === 1 ? 'la ' : 'las ') +
|
|
795
|
+
clockDigits({hour: display, minute, second: 0}, {sep: opts.style.sep});
|
|
796
|
+
const period = opts.style.meridiem === 'english' ?
|
|
797
|
+
meridiemMark(hour) :
|
|
798
|
+
dayPeriod(hour, opts);
|
|
799
|
+
|
|
800
|
+
return time + ' ' + period;
|
|
801
|
+
}
|
|
802
|
+
|
|
692
803
|
// Group a chronological run of "a la(s) …" clock phrases. The 12-hour clock
|
|
693
804
|
// carries day periods ("de la <period>"), which group chronologically by
|
|
694
805
|
// period; the 24-hour clock has none, so it falls through to article-grouping.
|
|
@@ -1689,16 +1800,13 @@ function numero(n: number, opts: Opts): string | number {
|
|
|
1689
1800
|
return numeral(n, numeros, opts);
|
|
1690
1801
|
}
|
|
1691
1802
|
|
|
1692
|
-
// A weekday name from a number or a
|
|
1803
|
+
// A weekday name from a canonical number, or from a Quartz stem (`5L`,
|
|
1804
|
+
// `MON#2`), which the core does not number-canonicalize: resolve any name
|
|
1805
|
+
// via the core's index and fold the Sunday alias 7 to 0.
|
|
1693
1806
|
function weekdayName(token: NameToken): string {
|
|
1694
|
-
|
|
1695
|
-
return weekdayNames[0];
|
|
1696
|
-
}
|
|
1807
|
+
const number = toFieldNumber('' + token, weekdayNumbers);
|
|
1697
1808
|
|
|
1698
|
-
|
|
1699
|
-
// the numeric path indexes the name array, the name path the token map.
|
|
1700
|
-
return weekdayNames[token as number] ||
|
|
1701
|
-
weekdayNames[weekdayTokens[token as string]];
|
|
1809
|
+
return weekdayNames[number === 7 ? 0 : number];
|
|
1702
1810
|
}
|
|
1703
1811
|
|
|
1704
1812
|
// The plural weekday form: días ending in -s are invariant ("los lunes");
|
|
@@ -1709,12 +1817,10 @@ function pluralWeekday(token: NameToken): string {
|
|
|
1709
1817
|
return name.endsWith('s') ? name : name + 's';
|
|
1710
1818
|
}
|
|
1711
1819
|
|
|
1712
|
-
// A month name from a number
|
|
1820
|
+
// A month name from a canonical month number. The name array has a leading
|
|
1821
|
+
// null hole for the 1-based index.
|
|
1713
1822
|
function monthName(token: NameToken): string {
|
|
1714
|
-
|
|
1715
|
-
// token map. The name array has a leading null hole for the 1-based index.
|
|
1716
|
-
return (monthNames[token as number] ||
|
|
1717
|
-
monthNames[monthTokens[token as string]]) as string;
|
|
1823
|
+
return monthNames[+token] as string;
|
|
1718
1824
|
}
|
|
1719
1825
|
|
|
1720
1826
|
// Whether a canonical field value is an open step (`*/n` or `a/n`).
|
|
@@ -1730,7 +1836,10 @@ const es: Language<SpanishStyle> = {
|
|
|
1730
1836
|
fallback: 'un patrón cron irreconocible',
|
|
1731
1837
|
options: normalizeOptions,
|
|
1732
1838
|
reboot: 'al arrancar el sistema',
|
|
1733
|
-
|
|
1839
|
+
// A description ending in a period already carries it, so closing the
|
|
1840
|
+
// sentence must not double it.
|
|
1841
|
+
sentence: (description) =>
|
|
1842
|
+
'Se ejecuta ' + description + (description.endsWith('.') ? '' : '.')
|
|
1734
1843
|
};
|
|
1735
1844
|
|
|
1736
1845
|
export default es;
|
package/src/lang/fi/index.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// case-pair construction wherever digits appear.
|
|
11
11
|
|
|
12
12
|
import {clockDigits, numeral} from '../../core/format.js';
|
|
13
|
+
import {weekdayNumbers} from '../../core/specs.js';
|
|
14
|
+
import {toFieldNumber} from '../../core/util.js';
|
|
13
15
|
import {resolveDialect} from './dialects.js';
|
|
14
16
|
import type {
|
|
15
17
|
ClockTime, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode,
|
|
@@ -159,16 +161,6 @@ const monthStems: (string | null)[] = [
|
|
|
159
161
|
'joulu'
|
|
160
162
|
];
|
|
161
163
|
|
|
162
|
-
// Cron token vocabulary (JAN..DEC, SUN..SAT) is part of cron syntax; map
|
|
163
|
-
// it to field numbers.
|
|
164
|
-
const monthTokens: {[token: string]: number} = {
|
|
165
|
-
JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6,
|
|
166
|
-
JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12
|
|
167
|
-
};
|
|
168
|
-
const weekdayTokens: {[token: string]: number} = {
|
|
169
|
-
SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6
|
|
170
|
-
};
|
|
171
|
-
|
|
172
164
|
// Unit form tables for the anchored-minute/second constructions.
|
|
173
165
|
// `mark` is the frequency for the "N minuutin kohdalla" ("at the
|
|
174
166
|
// N-minute mark") form; `anchor` is the possessive for the elative
|
|
@@ -349,9 +341,42 @@ function renderComposeSeconds(
|
|
|
349
341
|
hourClause + trailingQualifier(ir, opts);
|
|
350
342
|
}
|
|
351
343
|
|
|
344
|
+
// A sub-minute second with the minute pinned to 0 and a specific hour: the
|
|
345
|
+
// clock-time rest would read "klo 9", dropping the pinned :00 and so the
|
|
346
|
+
// one-minute confinement (60 fires in :00, not 3,600 across the hour). Bind
|
|
347
|
+
// the seconds to the explicit clock minute with the "minuutin HH.00 aikana"
|
|
348
|
+
// frame (an "of"/during form, never a range) and trail the day qualifier
|
|
349
|
+
// ("joka sekunti minuutin 9.00 aikana, joka päivä").
|
|
350
|
+
if (plan.rest.kind === 'clockTimes' &&
|
|
351
|
+
plan.rest.times.every((time) => +time.minute === 0)) {
|
|
352
|
+
return composeMinuteZero(ir, plan.rest, opts);
|
|
353
|
+
}
|
|
354
|
+
|
|
352
355
|
return secondsLeadClause(ir, opts) + ', ' + render(ir, plan.rest, opts);
|
|
353
356
|
}
|
|
354
357
|
|
|
358
|
+
// The minute-0 confinement: bind the seconds to the explicit clock minute(s)
|
|
359
|
+
// in the "minuutin/minuuttien HH.00 aikana" frame (an "of"/during form, never
|
|
360
|
+
// a range — a range would round-trip back to the whole hour) and trail the day
|
|
361
|
+
// qualifier ("joka sekunti minuutin 9.00 aikana, joka päivä").
|
|
362
|
+
function composeMinuteZero(
|
|
363
|
+
ir: IR,
|
|
364
|
+
rest: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
365
|
+
opts: NormalizedOptions
|
|
366
|
+
): string {
|
|
367
|
+
const clocks = rest.times.map(function clock(time): string {
|
|
368
|
+
return clockDigits({hour: time.hour, minute: time.minute},
|
|
369
|
+
{sep: opts.style.sep});
|
|
370
|
+
});
|
|
371
|
+
const frame = clocks.length === 1 ?
|
|
372
|
+
'minuutin ' + clocks[0] :
|
|
373
|
+
'minuuttien ' + joinList(clocks);
|
|
374
|
+
const dayTrail = leadingQualifier(ir, opts).trimEnd();
|
|
375
|
+
|
|
376
|
+
return secondsLeadClause(ir, opts) + ' ' + frame + ' aikana' +
|
|
377
|
+
(dayTrail ? ', ' + dayTrail : '');
|
|
378
|
+
}
|
|
379
|
+
|
|
355
380
|
// The leading clause describing a second field relative to the minute.
|
|
356
381
|
function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
357
382
|
const secondField = ir.pattern.second;
|
|
@@ -542,12 +567,20 @@ function renderMinuteFrequency(
|
|
|
542
567
|
return phrase + trailingQualifier(ir, opts);
|
|
543
568
|
}
|
|
544
569
|
|
|
545
|
-
// "joka minuutti klo 9.00–9.59".
|
|
570
|
+
// "joka minuutti klo 9.00–9.59". A wildcard minute is the whole hour, so it
|
|
571
|
+
// reads as that hour itself ("joka minuutti kello 9 aikana") rather than a
|
|
572
|
+
// synthesized "klo 9.00–9.59" range the source never stated; a plain range is
|
|
573
|
+
// a real window and keeps the dash form.
|
|
546
574
|
function renderMinuteSpanInHour(
|
|
547
575
|
ir: IR,
|
|
548
576
|
plan: Extract<PlanNode, {kind: 'minuteSpanInHour'}>,
|
|
549
577
|
opts: NormalizedOptions
|
|
550
578
|
): string {
|
|
579
|
+
if (ir.pattern.minute === '*') {
|
|
580
|
+
return 'joka minuutti kello ' + plan.hour + ' aikana' +
|
|
581
|
+
trailingQualifier(ir, opts);
|
|
582
|
+
}
|
|
583
|
+
|
|
551
584
|
return 'joka minuutti ' +
|
|
552
585
|
kloRange({hour: plan.hour, minute: plan.span[0]},
|
|
553
586
|
{hour: plan.hour, minute: plan.span[1]}, opts) +
|
|
@@ -1339,18 +1372,16 @@ function quartzWeekdayPhrase(weekdayField: string): string | undefined {
|
|
|
1339
1372
|
}
|
|
1340
1373
|
}
|
|
1341
1374
|
|
|
1342
|
-
// Resolve a weekday
|
|
1375
|
+
// Resolve a weekday to its table index. Weekday-field segments are already
|
|
1376
|
+
// canonical numbers; a Quartz stem (`5L`, `MON#2`) is not, so resolve any
|
|
1377
|
+
// name via the core's index (with the Sunday alias 7 folding to 0).
|
|
1343
1378
|
function weekdayNumber(token: string | number): number {
|
|
1344
|
-
|
|
1345
|
-
return weekdayTokens[token];
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
return +token % 7;
|
|
1379
|
+
return toFieldNumber('' + token, weekdayNumbers) % 7;
|
|
1349
1380
|
}
|
|
1350
1381
|
|
|
1351
|
-
// Resolve a month
|
|
1382
|
+
// Resolve a canonical month number to its table index.
|
|
1352
1383
|
function monthNumber(token: string | number): number {
|
|
1353
|
-
return
|
|
1384
|
+
return +token;
|
|
1354
1385
|
}
|
|
1355
1386
|
|
|
1356
1387
|
// --- Years. ---
|
|
@@ -1492,7 +1523,10 @@ const fi: Language = {
|
|
|
1492
1523
|
fallback: 'tunnistamaton cron-lauseke',
|
|
1493
1524
|
options: normalizeOptions,
|
|
1494
1525
|
reboot: 'järjestelmän käynnistyessä',
|
|
1495
|
-
|
|
1526
|
+
// A description ending in a period already carries it, so closing the
|
|
1527
|
+
// sentence must not double it.
|
|
1528
|
+
sentence: (description) =>
|
|
1529
|
+
'Suoritetaan ' + description + (description.endsWith('.') ? '' : '.')
|
|
1496
1530
|
};
|
|
1497
1531
|
|
|
1498
1532
|
export default fi;
|
package/src/lang/zh/index.ts
CHANGED
|
@@ -226,7 +226,13 @@ function renderMinuteFrequency(ir: IR, plan: PlanNode): string {
|
|
|
226
226
|
const {hours} = plan as Extract<PlanNode, {kind: 'minuteFrequency'}>;
|
|
227
227
|
|
|
228
228
|
if (hours.kind === 'step') {
|
|
229
|
-
|
|
229
|
+
const hourStep = stepSegment(ir, 'hour');
|
|
230
|
+
|
|
231
|
+
// "每N小时" is only faithful from midnight; an offset step (2/6 fires at
|
|
232
|
+
// 2,8,14,20) enumerates its hours instead.
|
|
233
|
+
return hourStep.startToken === '*' ?
|
|
234
|
+
cadence(hourStep.interval, UNITS.hour) + base :
|
|
235
|
+
'在' + hourList(ir) + ',' + base;
|
|
230
236
|
}
|
|
231
237
|
|
|
232
238
|
if (hours.kind === 'single' ||
|
|
@@ -247,10 +253,16 @@ function renderMinuteFrequency(ir: IR, plan: PlanNode): string {
|
|
|
247
253
|
return base;
|
|
248
254
|
}
|
|
249
255
|
|
|
250
|
-
// A minute span within a single hour
|
|
256
|
+
// A minute span within a single hour. A wildcard minute reads as that hour
|
|
257
|
+
// itself — "凌晨0点的每一分钟" — not a synthesized "在H点至H点59分之间" range the
|
|
258
|
+
// source never stated; a partial minute span keeps the named range.
|
|
251
259
|
function renderMinuteSpanInHour(ir: IR, plan: PlanNode): string {
|
|
252
260
|
const span = plan as Extract<PlanNode, {kind: 'minuteSpanInHour'}>;
|
|
253
261
|
|
|
262
|
+
if (ir.pattern.minute === '*') {
|
|
263
|
+
return hourWord(span.hour) + '的每一分钟';
|
|
264
|
+
}
|
|
265
|
+
|
|
254
266
|
return '在' + hourWord(span.hour) + '至' + span.hour + '点' +
|
|
255
267
|
span.span[1] + '分之间,每分钟';
|
|
256
268
|
}
|
|
@@ -271,15 +283,31 @@ function renderMinutesAcrossHours(ir: IR, plan: PlanNode): string {
|
|
|
271
283
|
// A minute clause across a stepped hour field. A wildcard minute reads "每2小时
|
|
272
284
|
// 内,每分钟"; a ranged minute names it: "每2小时,每小时0至30分,每分钟".
|
|
273
285
|
function renderMinuteSpanAcrossHourStep(ir: IR, plan: PlanNode): string {
|
|
274
|
-
const
|
|
286
|
+
const hourStep = stepSegment(ir, 'hour');
|
|
275
287
|
const {form} = plan as Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>;
|
|
288
|
+
const minuteTail = form === 'wildcard' ?
|
|
289
|
+
'每分钟' :
|
|
290
|
+
'每小时' + valueList(fieldSegments(ir, 'minute'), '分') + ',每分钟';
|
|
276
291
|
|
|
277
|
-
|
|
278
|
-
|
|
292
|
+
// An offset stride (2/6 fires at 2,8,14,20) enumerates its hours like a
|
|
293
|
+
// discrete list; "每N小时" is faithful only from midnight.
|
|
294
|
+
if (hourStep.startToken !== '*') {
|
|
295
|
+
return form === 'wildcard' ?
|
|
296
|
+
'在' + hourList(ir) + ',' + minuteTail :
|
|
297
|
+
hourList(ir) + ',' + minuteTail;
|
|
279
298
|
}
|
|
280
299
|
|
|
281
|
-
|
|
282
|
-
|
|
300
|
+
// A step-2 hour from midnight IS exactly the even hours; name them so, rather
|
|
301
|
+
// than the vague "每2小时内" that reads as an interval. Other strides keep it.
|
|
302
|
+
if (hourStep.interval === 2 && form === 'wildcard') {
|
|
303
|
+
return '在偶数小时,' + minuteTail;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const cad = cadence(hourStep.interval, UNITS.hour);
|
|
307
|
+
|
|
308
|
+
return form === 'wildcard' ?
|
|
309
|
+
cad + '内,' + minuteTail :
|
|
310
|
+
cad + ',' + minuteTail;
|
|
283
311
|
}
|
|
284
312
|
|
|
285
313
|
// Discrete clock times: "9点", "9点和17点".
|
|
@@ -422,11 +450,30 @@ function isHourCadence(ir: IR): boolean {
|
|
|
422
450
|
function composeSecondsOnHour(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
423
451
|
const sec = secondClause(ir);
|
|
424
452
|
const {rest} = plan as Extract<PlanNode, {kind: 'composeSeconds'}>;
|
|
425
|
-
const restText = render(ir, rest, opts);
|
|
426
453
|
|
|
454
|
+
// The minute is pinned to 0 under a specific hour: a bare clock word ("9点")
|
|
455
|
+
// would hide the :00 and leave the second dangling ("…9点每秒"), reading as
|
|
456
|
+
// the whole hour. Fuse the seconds with the explicit clock minute ("9点0分
|
|
457
|
+
// 的每一秒"), so the one-minute confinement (60 fires in :00, not 3,600
|
|
458
|
+
// across the hour) stays visible. The daily frame leads with 每天; a weekday
|
|
459
|
+
// or date qualifier is added by describe().
|
|
427
460
|
if ((rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes') &&
|
|
428
|
-
|
|
429
|
-
|
|
461
|
+
ir.pattern.minute === '0') {
|
|
462
|
+
const clocks = hourFires(ir).map(function clock(hour): string {
|
|
463
|
+
return hourWord(hour) + '0分';
|
|
464
|
+
});
|
|
465
|
+
const tail = sec === '每秒' ? '的每一秒' : '的' + sec;
|
|
466
|
+
const core = joinAnd(clocks) + tail;
|
|
467
|
+
|
|
468
|
+
return isDaily(ir) ? '每天' + core : core;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const restText = render(ir, rest, opts);
|
|
472
|
+
|
|
473
|
+
if (rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes') {
|
|
474
|
+
if (isDaily(ir)) {
|
|
475
|
+
return '每天' + restText + sec;
|
|
476
|
+
}
|
|
430
477
|
}
|
|
431
478
|
|
|
432
479
|
// A stated minute (e.g. minute 0 under a sub-minute second) takes the same
|
|
@@ -466,6 +513,14 @@ function composeSecondsListed(ir: IR): string {
|
|
|
466
513
|
const sec = secondClause(ir);
|
|
467
514
|
const minutes = '每小时' + valueList(fieldSegments(ir, 'minute'), '分');
|
|
468
515
|
|
|
516
|
+
// A single restricted hour with an every-second cadence fuses the clock time
|
|
517
|
+
// with its minutes — "凌晨0点5、20、35、50分的每一秒" — rather than the "每小时"
|
|
518
|
+
// that falsely implies every hour. A non-wildcard second keeps the list form.
|
|
519
|
+
if (ir.shapes.hour === 'single' && sec === '每秒') {
|
|
520
|
+
return hourWord(hourFires(ir)[0]) +
|
|
521
|
+
valueList(fieldSegments(ir, 'minute'), '分') + '的每一秒';
|
|
522
|
+
}
|
|
523
|
+
|
|
469
524
|
if (ir.shapes.hour === 'wildcard') {
|
|
470
525
|
return minutes + ',' + sec;
|
|
471
526
|
}
|
|
@@ -479,12 +534,28 @@ function composeSecondsListed(ir: IR): string {
|
|
|
479
534
|
}
|
|
480
535
|
|
|
481
536
|
// Seconds composed with the minute/hour structure, dispatched on the minute.
|
|
537
|
+
// A single minute over a composed clock-time rest (the core already joined the
|
|
538
|
+
// lone hour and minute into "N点M分") keeps that composition, attaching the
|
|
539
|
+
// second to it rather than splitting the minute back out into the "每小时N分"
|
|
540
|
+
// list path; a minute list stays on that list path so each fire is named.
|
|
482
541
|
function renderComposeSeconds(ir: IR, plan: PlanNode, opts: Opts): string {
|
|
483
|
-
|
|
542
|
+
const {rest} = plan as Extract<PlanNode, {kind: 'composeSeconds'}>;
|
|
543
|
+
const composedClock =
|
|
544
|
+
rest.kind === 'clockTimes' || rest.kind === 'compactClockTimes';
|
|
545
|
+
|
|
546
|
+
if (ir.pattern.minute === '0' ||
|
|
547
|
+
composedClock && ir.shapes.minute === 'single') {
|
|
484
548
|
return composeSecondsOnHour(ir, plan, opts);
|
|
485
549
|
}
|
|
486
550
|
|
|
487
|
-
|
|
551
|
+
// "每N分钟" is faithful only for a wildcard or top-of-hour step; an offset
|
|
552
|
+
// step (5/15 fires at :05,:20,…) takes the enumerated list path so its start
|
|
553
|
+
// is named, never dropped.
|
|
554
|
+
const minuteCadence = ir.pattern.minute === '*' ||
|
|
555
|
+
ir.shapes.minute === 'step' &&
|
|
556
|
+
stepSegment(ir, 'minute').startToken === '*';
|
|
557
|
+
|
|
558
|
+
if (minuteCadence) {
|
|
488
559
|
return composeSecondsCadence(ir);
|
|
489
560
|
}
|
|
490
561
|
|