cronli5 0.1.1 → 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 +91 -0
- package/cli.js +9 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +127 -38
- package/dist/cronli5.js +127 -38
- package/dist/lang/de.cjs +59 -36
- package/dist/lang/de.js +59 -36
- package/dist/lang/en.cjs +43 -32
- package/dist/lang/en.js +43 -32
- package/dist/lang/es.cjs +81 -40
- package/dist/lang/es.js +81 -40
- package/dist/lang/fi.cjs +46 -44
- package/dist/lang/fi.js +46 -44
- package/dist/lang/zh.cjs +36 -13
- package/dist/lang/zh.js +36 -13
- package/package.json +2 -1
- package/src/core/normalize.ts +144 -6
- package/src/lang/de/index.ts +99 -35
- package/src/lang/en/index.ts +113 -48
- package/src/lang/es/index.ts +140 -41
- package/src/lang/fi/index.ts +62 -39
- package/src/lang/zh/index.ts +93 -21
package/src/lang/en/index.ts
CHANGED
|
@@ -21,12 +21,15 @@ type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
|
21
21
|
|
|
22
22
|
// A clock-time entry assembled for rendering. Hour/minute/second arrive as
|
|
23
23
|
// numbers or as raw field tokens (a range bound or single value is a
|
|
24
|
-
// string); `plain` suppresses the noon/midnight words.
|
|
24
|
+
// string); `plain` suppresses the noon/midnight words. `explicit` forces the
|
|
25
|
+
// minute to show even when zero ("9:00 a.m.", not "9 a.m.") and suppresses
|
|
26
|
+
// the noon/midnight words, so a pinned minute-0 stays visible.
|
|
25
27
|
interface TimeEntry {
|
|
26
28
|
hour: number | string;
|
|
27
29
|
minute: number | string;
|
|
28
30
|
second?: number | string | null;
|
|
29
31
|
plain?: boolean;
|
|
32
|
+
explicit?: boolean;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
// English number names for the integers zero through ten.
|
|
@@ -80,22 +83,6 @@ const weekdayNames: [string, string][] = [
|
|
|
80
83
|
['Saturday', 'Sat']
|
|
81
84
|
];
|
|
82
85
|
|
|
83
|
-
// Month names by abbreviation.
|
|
84
|
-
const monthAbbreviations: Record<string, [string, string] | null> = {
|
|
85
|
-
JAN: monthNames[1],
|
|
86
|
-
FEB: monthNames[2],
|
|
87
|
-
MAR: monthNames[3],
|
|
88
|
-
APR: monthNames[4],
|
|
89
|
-
MAY: monthNames[5],
|
|
90
|
-
JUN: monthNames[6],
|
|
91
|
-
JUL: monthNames[7],
|
|
92
|
-
AUG: monthNames[8],
|
|
93
|
-
SEP: monthNames[9],
|
|
94
|
-
OCT: monthNames[10],
|
|
95
|
-
NOV: monthNames[11],
|
|
96
|
-
DEC: monthNames[12]
|
|
97
|
-
};
|
|
98
|
-
|
|
99
86
|
// Weekday name by abbreviation.
|
|
100
87
|
const weekdayAbbreviations: Record<string, [string, string]> = {
|
|
101
88
|
SUN: weekdayNames[0],
|
|
@@ -192,9 +179,78 @@ function renderSecondsWithinMinute(ir: IR, plan: PlanOf<'secondsWithinMinute'>,
|
|
|
192
179
|
// pattern follows.
|
|
193
180
|
function renderComposeSeconds(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
194
181
|
opts: NormalizedOptions): string {
|
|
182
|
+
// A wildcard or stepped second under a minute pinned to a single value
|
|
183
|
+
// across one or more specific hours. The clock-time rest collapses the
|
|
184
|
+
// pinned minute into the hour, and on the clock a pinned minute-0 reads as
|
|
185
|
+
// the whole hour ("9 a.m." spoken == "9:00 a.m."), losing the one-minute
|
|
186
|
+
// confinement. (A second list/range/single leads with a "past the minute"
|
|
187
|
+
// clause that an "of"/duration frame cannot follow, so it stays generic.)
|
|
188
|
+
if (plan.rest.kind === 'clockTimes' &&
|
|
189
|
+
(ir.shapes.second === 'wildcard' || ir.shapes.second === 'step')) {
|
|
190
|
+
const minute = plan.rest.times[0].minute;
|
|
191
|
+
|
|
192
|
+
// Minute 0 is the one-minute window at the top of each named hour: a
|
|
193
|
+
// duration frame ("for one minute at 9 a.m.") states the confinement
|
|
194
|
+
// outright, with the hour as its word so it cannot be heard as the hour
|
|
195
|
+
// itself. A non-zero pinned minute is an unambiguous clock time, so the
|
|
196
|
+
// compact "of 9:05 a.m." form reads it as the minute, never the hour.
|
|
197
|
+
if (+minute === 0) {
|
|
198
|
+
return secondsLeadClause(ir, opts) + ' for one minute at ' +
|
|
199
|
+
durationHours(ir, plan.rest, opts);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return secondsLeadClause(ir, opts) + ' of ' +
|
|
203
|
+
clockTimesOf(ir, plan.rest, opts);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// A wildcard second under a */2 minute step with a wildcard hour binds
|
|
207
|
+
// idiomatically as "every second of every other minute": "every other" is
|
|
208
|
+
// the natural English for an interval of 2, and "of" joins the two without
|
|
209
|
+
// the ambiguity of a comma, which reads as two independent cadences.
|
|
210
|
+
// Scoped to */2 only; other step sizes keep the comma form.
|
|
211
|
+
if (ir.shapes.second === 'wildcard' &&
|
|
212
|
+
plan.rest.kind === 'minuteFrequency' &&
|
|
213
|
+
plan.rest.hours.kind === 'none' &&
|
|
214
|
+
ir.pattern.minute === '*/2') {
|
|
215
|
+
return 'every second of every other minute' +
|
|
216
|
+
trailingQualifier(ir, opts);
|
|
217
|
+
}
|
|
218
|
+
|
|
195
219
|
return secondsLeadClause(ir, opts) + ', ' + render(ir, plan.rest, opts);
|
|
196
220
|
}
|
|
197
221
|
|
|
222
|
+
// The bare-hour words for a minute-0 duration confinement, joined and followed
|
|
223
|
+
// by the trailing day qualifier: "9 a.m. and 11 a.m., every day", "midnight,
|
|
224
|
+
// 2 a.m., …, every day". The hour reads as its word (noon/midnight included),
|
|
225
|
+
// never "H:00", since the "for one minute" frame already carries the minute.
|
|
226
|
+
function durationHours(ir: IR, plan: PlanOf<'clockTimes'>,
|
|
227
|
+
opts: NormalizedOptions): string {
|
|
228
|
+
const hours = plan.times.map(function clock(time) {
|
|
229
|
+
return getTime({hour: time.hour, minute: 0}, opts);
|
|
230
|
+
});
|
|
231
|
+
const trail = dayQualifier(ir, leadingWords, opts);
|
|
232
|
+
|
|
233
|
+
return joinList(hours, opts) + (trail && ', ' + trail);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// The clock times for a non-zero pinned-minute compose-seconds rest, joined
|
|
237
|
+
// and followed by the trailing day qualifier: "9:05 a.m. and 11:05 a.m.,
|
|
238
|
+
// every day". The non-zero minute reads as a clock time, never the hour.
|
|
239
|
+
function clockTimesOf(ir: IR, plan: PlanOf<'clockTimes'>,
|
|
240
|
+
opts: NormalizedOptions): string {
|
|
241
|
+
const times = plan.times.map(function clock(time) {
|
|
242
|
+
return getTime({
|
|
243
|
+
hour: time.hour,
|
|
244
|
+
minute: time.minute,
|
|
245
|
+
second: time.second,
|
|
246
|
+
explicit: true
|
|
247
|
+
}, opts);
|
|
248
|
+
});
|
|
249
|
+
const trail = dayQualifier(ir, leadingWords, opts);
|
|
250
|
+
|
|
251
|
+
return joinList(times, opts) + (trail && ', ' + trail);
|
|
252
|
+
}
|
|
253
|
+
|
|
198
254
|
// The leading clause describing a second field relative to the minute,
|
|
199
255
|
// e.g. "at 5 and 10 seconds past the minute" or "every second from zero
|
|
200
256
|
// through 30 past the minute".
|
|
@@ -289,9 +345,18 @@ function renderMinuteFrequency(ir: IR, plan: PlanOf<'minuteFrequency'>,
|
|
|
289
345
|
}
|
|
290
346
|
|
|
291
347
|
// A minute wildcard or plain range under a single specific hour fires
|
|
292
|
-
// every minute within a window inside that hour.
|
|
348
|
+
// every minute within a window inside that hour. A wildcard minute is the
|
|
349
|
+
// whole hour, so it reads as that hour itself ("every minute of the 9 a.m.
|
|
350
|
+
// hour") rather than a synthesized "from H:00 through H:59" range the source
|
|
351
|
+
// never stated; a plain range is a real window and keeps "from … through …".
|
|
293
352
|
function renderMinuteSpanInHour(ir: IR, plan: PlanOf<'minuteSpanInHour'>,
|
|
294
353
|
opts: NormalizedOptions): string {
|
|
354
|
+
if (ir.pattern.minute === '*') {
|
|
355
|
+
return 'every minute of the ' +
|
|
356
|
+
getTime({hour: plan.hour, minute: 0}, opts) + ' hour' +
|
|
357
|
+
trailingQualifier(ir, opts);
|
|
358
|
+
}
|
|
359
|
+
|
|
295
360
|
return 'every minute from ' +
|
|
296
361
|
getTime({hour: plan.hour, minute: plan.span[0]}, opts) +
|
|
297
362
|
through(opts) + getTime({hour: plan.hour, minute: plan.span[1]}, opts) +
|
|
@@ -547,7 +612,10 @@ const renderers = {
|
|
|
547
612
|
// Phrase a `start/interval` step segment for a field that cycles every 60
|
|
548
613
|
// units (seconds and minutes). `unit` is the singular noun and `anchor` is
|
|
549
614
|
// the larger unit the values are counted against. Interval-one steps never
|
|
550
|
-
// arrive here: normalization collapses them to ranges or `*`.
|
|
615
|
+
// arrive here: normalization collapses them to ranges or `*`. Nor do uneven
|
|
616
|
+
// steps that fail to tile the cycle: normalization rewrites those to the
|
|
617
|
+
// literal list of their fires, so only a clean cadence (interval dividing
|
|
618
|
+
// 60, start within the first interval) reaches a step renderer.
|
|
551
619
|
function stepCycle60(segment: StepSegment, unit: string,
|
|
552
620
|
anchor: string, opts: NormalizedOptions): string {
|
|
553
621
|
// A bounded start (`a-b/n`) applies the interval within the range.
|
|
@@ -559,6 +627,8 @@ function stepCycle60(segment: StepSegment, unit: string,
|
|
|
559
627
|
const interval = segment.interval;
|
|
560
628
|
|
|
561
629
|
if (start !== 0) {
|
|
630
|
+
// A short offset cadence lists its fires; a longer one names the
|
|
631
|
+
// interval and its starting offset ("every six minutes from five …").
|
|
562
632
|
if (segment.fires.length <= 3) {
|
|
563
633
|
return listPastThe(numberWords(segment.fires, opts), unit, anchor,
|
|
564
634
|
opts);
|
|
@@ -569,18 +639,8 @@ function stepCycle60(segment: StepSegment, unit: string,
|
|
|
569
639
|
' past the ' + anchor;
|
|
570
640
|
}
|
|
571
641
|
|
|
572
|
-
// A
|
|
573
|
-
|
|
574
|
-
if (60 % interval === 0) {
|
|
575
|
-
return 'every ' + getNumber(interval, opts) + ' ' + unit + 's';
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
if (segment.fires.length <= 2) {
|
|
579
|
-
return listPastThe(numberWords(segment.fires, opts), unit, anchor, opts);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return 'every ' + getNumber(interval, opts) + ' ' + unit +
|
|
583
|
-
's past the ' + anchor;
|
|
642
|
+
// A clean stride from the top of the cycle is the bare cadence.
|
|
643
|
+
return 'every ' + getNumber(interval, opts) + ' ' + unit + 's';
|
|
584
644
|
}
|
|
585
645
|
|
|
586
646
|
// Phrase a `start/interval` step segment for the hour field (cycles every
|
|
@@ -594,18 +654,18 @@ function stepHours(segment: StepSegment, opts: NormalizedOptions): string {
|
|
|
594
654
|
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
595
655
|
const interval = segment.interval;
|
|
596
656
|
|
|
597
|
-
|
|
657
|
+
// A clean stride from midnight is the bare cadence. (An uneven stride is
|
|
658
|
+
// rewritten to its fires upstream and never reaches here.)
|
|
659
|
+
if (start === 0) {
|
|
598
660
|
return 'every ' + getNumber(interval, opts) + ' hours';
|
|
599
661
|
}
|
|
600
662
|
|
|
663
|
+
// A short offset cadence lists its fires; a longer one names the interval
|
|
664
|
+
// and its start ("every three hours from 2 a.m.").
|
|
601
665
|
if (segment.fires.length <= 3) {
|
|
602
666
|
return 'at ' + hourTimes(segment.fires, opts);
|
|
603
667
|
}
|
|
604
668
|
|
|
605
|
-
if (start === 0) {
|
|
606
|
-
return 'every ' + getNumber(interval, opts) + ' hours from midnight';
|
|
607
|
-
}
|
|
608
|
-
|
|
609
669
|
return 'every ' + getNumber(interval, opts) + ' hours from ' +
|
|
610
670
|
getTime({hour: start, minute: 0}, opts);
|
|
611
671
|
}
|
|
@@ -1174,7 +1234,7 @@ function stepYears(yearField: string, opts: NormalizedOptions): string {
|
|
|
1174
1234
|
// "3.45pm" / "9am" / "midday" for UK (Guardian), or "15:45" / "15.45" in
|
|
1175
1235
|
// 24-hour mode.
|
|
1176
1236
|
function getTime(time: TimeEntry, opts: NormalizedOptions): string {
|
|
1177
|
-
const {hour, minute, plain} = time;
|
|
1237
|
+
const {hour, minute, plain, explicit} = time;
|
|
1178
1238
|
// Seconds are only shown when a specific non-zero value is supplied.
|
|
1179
1239
|
const second = typeof time.second === 'number' && time.second > 0 ?
|
|
1180
1240
|
time.second :
|
|
@@ -1184,12 +1244,13 @@ function getTime(time: TimeEntry, opts: NormalizedOptions): string {
|
|
|
1184
1244
|
// Hour/minute arrive as numbers or raw field tokens (a range bound or
|
|
1185
1245
|
// single value is a string); `clockDigits` types them as numbers but
|
|
1186
1246
|
// `pad` stringifies either form to the same digits. Cast to keep the
|
|
1187
|
-
// value byte-identical rather than coercing it.
|
|
1247
|
+
// value byte-identical rather than coercing it. The 24-hour form always
|
|
1248
|
+
// shows the minute, so it is already explicit.
|
|
1188
1249
|
return clockDigits({hour: hour as number, minute: minute as number,
|
|
1189
1250
|
second}, {pad: true, sep: opts.style.sep});
|
|
1190
1251
|
}
|
|
1191
1252
|
|
|
1192
|
-
return twelveHourTime({hour, minute, second, plain}, opts);
|
|
1253
|
+
return twelveHourTime({hour, minute, second, plain, explicit}, opts);
|
|
1193
1254
|
}
|
|
1194
1255
|
|
|
1195
1256
|
// The 12-hour form of a clock time: "9:30 a.m.", "9 a.m." on the hour, or
|
|
@@ -1198,13 +1259,13 @@ function getTime(time: TimeEntry, opts: NormalizedOptions): string {
|
|
|
1198
1259
|
// stays in one number style.
|
|
1199
1260
|
function twelveHourTime(
|
|
1200
1261
|
time: {hour: number | string; minute: number | string; second: number;
|
|
1201
|
-
plain?: boolean},
|
|
1262
|
+
plain?: boolean; explicit?: boolean},
|
|
1202
1263
|
opts: NormalizedOptions
|
|
1203
1264
|
): string {
|
|
1204
|
-
const {hour, minute, second, plain} = time;
|
|
1265
|
+
const {hour, minute, second, plain, explicit} = time;
|
|
1205
1266
|
const style = opts.style;
|
|
1206
1267
|
|
|
1207
|
-
if (!plain && +minute === 0 && !second) {
|
|
1268
|
+
if (!plain && !explicit && +minute === 0 && !second) {
|
|
1208
1269
|
if (+hour === 0) {
|
|
1209
1270
|
return style.midnight;
|
|
1210
1271
|
}
|
|
@@ -1216,9 +1277,11 @@ function twelveHourTime(
|
|
|
1216
1277
|
|
|
1217
1278
|
// `hour`/`minute` may be raw field tokens; the arithmetic below coerces
|
|
1218
1279
|
// them numerically, matching `clockDigits`. Cast for the modulo/compare.
|
|
1280
|
+
// `explicit` keeps the minute (":00") rather than leaning down to the bare
|
|
1281
|
+
// hour, so a pinned minute-0 stays visible.
|
|
1219
1282
|
const digits = clockDigits(
|
|
1220
1283
|
{hour: (hour as number) % 12 || 12, minute: minute as number, second},
|
|
1221
|
-
{lean:
|
|
1284
|
+
{lean: !explicit, sep: style.sep});
|
|
1222
1285
|
|
|
1223
1286
|
return digits + (style.closeUp ? '' : ' ') +
|
|
1224
1287
|
((hour as number) < 12 ? style.am : style.pm);
|
|
@@ -1260,11 +1323,10 @@ function getOrdinal(n: number | string): string {
|
|
|
1260
1323
|
return n + suffix;
|
|
1261
1324
|
}
|
|
1262
1325
|
|
|
1263
|
-
// Get English month names from a number
|
|
1326
|
+
// Get English month names from a canonical month number (months are never
|
|
1327
|
+
// Quartz, so the field is always number-canonicalized by the core).
|
|
1264
1328
|
function getMonth(m: number | string, opts: NormalizedOptions): string {
|
|
1265
|
-
|
|
1266
|
-
// (indexing `monthAbbreviations`); the unmatched table yields undefined.
|
|
1267
|
-
const month = monthNames[m as number] || monthAbbreviations[m];
|
|
1329
|
+
const month = monthNames[+m];
|
|
1268
1330
|
|
|
1269
1331
|
// A valid month always resolves to a name pair, so the guarded lookup is
|
|
1270
1332
|
// a string; the cast keeps the original null-guard expression intact.
|
|
@@ -1292,7 +1354,10 @@ const en: Language = {
|
|
|
1292
1354
|
fallback: 'an unrecognizable cron pattern',
|
|
1293
1355
|
options: normalizeOptions,
|
|
1294
1356
|
reboot: 'at system startup',
|
|
1295
|
-
|
|
1357
|
+
// A description ending in an abbreviation already carries its period
|
|
1358
|
+
// ("…9 a.m."), so closing the sentence must not double it.
|
|
1359
|
+
sentence: (description) =>
|
|
1360
|
+
'Runs ' + description + (description.endsWith('.') ? '' : '.')
|
|
1296
1361
|
};
|
|
1297
1362
|
|
|
1298
1363
|
export default en;
|
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.
|
|
@@ -1015,17 +1126,9 @@ function stepCycle60(
|
|
|
1015
1126
|
unit + ' ' + start + ' de cada ' + anchor;
|
|
1016
1127
|
}
|
|
1017
1128
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
if (segment.fires.length <= 2) {
|
|
1023
|
-
return 'en los ' + unit + 's ' + joinList(wordList(segment.fires)) +
|
|
1024
|
-
' de cada ' + anchor;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
return 'cada ' + numero(interval, opts) + ' ' + unit + 's de cada ' +
|
|
1028
|
-
anchor;
|
|
1129
|
+
// A clean stride from the top of the cycle is the bare cadence. (An uneven
|
|
1130
|
+
// stride is rewritten to its fires upstream and never reaches here.)
|
|
1131
|
+
return 'cada ' + numero(interval, opts) + ' ' + unit + 's';
|
|
1029
1132
|
}
|
|
1030
1133
|
|
|
1031
1134
|
// "cada seis horas", "a las 9:00, a las 11:00 y a la 1:00", or "cada
|
|
@@ -1038,7 +1141,9 @@ function stepHours(segment: StepSegment, opts: Opts): string {
|
|
|
1038
1141
|
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
1039
1142
|
const interval = segment.interval;
|
|
1040
1143
|
|
|
1041
|
-
|
|
1144
|
+
// A clean stride from midnight is the bare cadence. (An uneven stride is
|
|
1145
|
+
// rewritten to its fires upstream and never reaches here.)
|
|
1146
|
+
if (start === 0) {
|
|
1042
1147
|
return 'cada ' + numero(interval, opts) + ' horas';
|
|
1043
1148
|
}
|
|
1044
1149
|
|
|
@@ -1046,10 +1151,6 @@ function stepHours(segment: StepSegment, opts: Opts): string {
|
|
|
1046
1151
|
return groupClockTimesByArticle(atTimes(segment.fires, opts));
|
|
1047
1152
|
}
|
|
1048
1153
|
|
|
1049
|
-
if (start === 0) {
|
|
1050
|
-
return 'cada ' + numero(interval, opts) + ' horas desde medianoche';
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
1154
|
return 'cada ' + numero(interval, opts) + ' horas a partir de ' +
|
|
1054
1155
|
timePhrase(start, 0, null, opts);
|
|
1055
1156
|
}
|
|
@@ -1699,16 +1800,13 @@ function numero(n: number, opts: Opts): string | number {
|
|
|
1699
1800
|
return numeral(n, numeros, opts);
|
|
1700
1801
|
}
|
|
1701
1802
|
|
|
1702
|
-
// 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.
|
|
1703
1806
|
function weekdayName(token: NameToken): string {
|
|
1704
|
-
|
|
1705
|
-
return weekdayNames[0];
|
|
1706
|
-
}
|
|
1807
|
+
const number = toFieldNumber('' + token, weekdayNumbers);
|
|
1707
1808
|
|
|
1708
|
-
|
|
1709
|
-
// the numeric path indexes the name array, the name path the token map.
|
|
1710
|
-
return weekdayNames[token as number] ||
|
|
1711
|
-
weekdayNames[weekdayTokens[token as string]];
|
|
1809
|
+
return weekdayNames[number === 7 ? 0 : number];
|
|
1712
1810
|
}
|
|
1713
1811
|
|
|
1714
1812
|
// The plural weekday form: días ending in -s are invariant ("los lunes");
|
|
@@ -1719,12 +1817,10 @@ function pluralWeekday(token: NameToken): string {
|
|
|
1719
1817
|
return name.endsWith('s') ? name : name + 's';
|
|
1720
1818
|
}
|
|
1721
1819
|
|
|
1722
|
-
// 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.
|
|
1723
1822
|
function monthName(token: NameToken): string {
|
|
1724
|
-
|
|
1725
|
-
// token map. The name array has a leading null hole for the 1-based index.
|
|
1726
|
-
return (monthNames[token as number] ||
|
|
1727
|
-
monthNames[monthTokens[token as string]]) as string;
|
|
1823
|
+
return monthNames[+token] as string;
|
|
1728
1824
|
}
|
|
1729
1825
|
|
|
1730
1826
|
// Whether a canonical field value is an open step (`*/n` or `a/n`).
|
|
@@ -1740,7 +1836,10 @@ const es: Language<SpanishStyle> = {
|
|
|
1740
1836
|
fallback: 'un patrón cron irreconocible',
|
|
1741
1837
|
options: normalizeOptions,
|
|
1742
1838
|
reboot: 'al arrancar el sistema',
|
|
1743
|
-
|
|
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('.') ? '' : '.')
|
|
1744
1843
|
};
|
|
1745
1844
|
|
|
1746
1845
|
export default es;
|