cronli5 0.1.2 → 0.1.5
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 +89 -0
- package/cli.js +9 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +280 -64
- package/dist/cronli5.js +280 -64
- package/dist/lang/de.cjs +227 -42
- package/dist/lang/de.js +227 -42
- package/dist/lang/en.cjs +216 -50
- package/dist/lang/en.js +216 -50
- package/dist/lang/es.cjs +259 -54
- package/dist/lang/es.js +259 -54
- package/dist/lang/fi.cjs +230 -69
- package/dist/lang/fi.js +230 -69
- package/dist/lang/zh.cjs +190 -19
- package/dist/lang/zh.js +190 -19
- package/package.json +3 -1
- package/src/core/analyze.ts +7 -0
- package/src/core/ir.ts +1 -1
- package/src/core/normalize.ts +94 -4
- package/src/core/util.ts +31 -1
- package/src/lang/de/index.ts +449 -46
- package/src/lang/en/index.ts +433 -63
- package/src/lang/es/index.ts +505 -63
- package/src/lang/fi/index.ts +455 -89
- package/src/lang/zh/index.ts +393 -30
- package/types/core/ir.d.ts +1 -1
- package/types/core/util.d.ts +6 -1
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 {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
14
|
+
import {arithmeticStep, toFieldNumber} from '../../core/util.js';
|
|
13
15
|
import {resolveDialect} from './dialects.js';
|
|
14
16
|
import type {
|
|
15
17
|
ClockTime, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode,
|
|
@@ -61,6 +63,7 @@ interface UnitForms {
|
|
|
61
63
|
mark: string;
|
|
62
64
|
anchor: string;
|
|
63
65
|
ela: string;
|
|
66
|
+
ill: string;
|
|
64
67
|
gen: string;
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -159,16 +162,6 @@ const monthStems: (string | null)[] = [
|
|
|
159
162
|
'joulu'
|
|
160
163
|
];
|
|
161
164
|
|
|
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
165
|
// Unit form tables for the anchored-minute/second constructions.
|
|
173
166
|
// `mark` is the frequency for the "N minuutin kohdalla" ("at the
|
|
174
167
|
// N-minute mark") form; `anchor` is the possessive for the elative
|
|
@@ -178,12 +171,14 @@ const units: {minute: UnitForms; second: UnitForms} = {
|
|
|
178
171
|
mark: 'joka tunti',
|
|
179
172
|
anchor: 'jokaisen tunnin',
|
|
180
173
|
ela: 'minuutista',
|
|
174
|
+
ill: 'minuuttiin',
|
|
181
175
|
gen: 'minuutin'
|
|
182
176
|
},
|
|
183
177
|
second: {
|
|
184
178
|
mark: 'joka minuutti',
|
|
185
179
|
anchor: 'jokaisen minuutin',
|
|
186
180
|
ela: 'sekunnista',
|
|
181
|
+
ill: 'sekuntiin',
|
|
187
182
|
gen: 'sekunnin'
|
|
188
183
|
}
|
|
189
184
|
};
|
|
@@ -304,54 +299,148 @@ function renderSecondsWithinMinute(
|
|
|
304
299
|
atMarks(minuteField, units.minute, true) + trailingQualifier(ir, opts);
|
|
305
300
|
}
|
|
306
301
|
|
|
302
|
+
// A meaningful second composed over a minute-step cadence: the step leads and
|
|
303
|
+
// the second anchor follows after a comma, with the hour clause interleaved
|
|
304
|
+
// between them ("[step], [seconds][hour clause][trailing qualifier]"). The
|
|
305
|
+
// minute-frequency phrase is reconstructed directly here so the hour clause can
|
|
306
|
+
// sit between the step and the second anchor without duplicating the full
|
|
307
|
+
// renderMinuteFrequency logic; its hours-first reorder is intentionally NOT
|
|
308
|
+
// applied (the step-leads form is the correct shape for this construction).
|
|
309
|
+
function composeSecondsOverMinuteStep(
|
|
310
|
+
ir: IR,
|
|
311
|
+
freq: Extract<PlanNode, {kind: 'minuteFrequency'}>,
|
|
312
|
+
opts: NormalizedOptions
|
|
313
|
+
): string {
|
|
314
|
+
const seg = stepSegment(ir.analyses.segments.minute!);
|
|
315
|
+
const stepPhrase = stepCycle60(seg, units.minute, opts);
|
|
316
|
+
|
|
317
|
+
if (freq.hours.kind === 'during' && minuteStepIsAnchored(seg)) {
|
|
318
|
+
// The step renders as an anchored kohdalla list rather than a cadence, so
|
|
319
|
+
// the hours-first reorder applies here too: bare hours lead, minute anchors
|
|
320
|
+
// follow, then the seconds clause.
|
|
321
|
+
const bareHours = kloFromTimes(ir, freq.hours.times, opts);
|
|
322
|
+
|
|
323
|
+
return hoursFirstMinutes(bareHours, ir, opts) + ', ' +
|
|
324
|
+
secondsLeadClause(ir, opts) + trailingQualifier(ir, opts);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let hourClause = '';
|
|
328
|
+
|
|
329
|
+
if (freq.hours.kind === 'during') {
|
|
330
|
+
hourClause = ' ' + hourWindowsFromTimes(ir, freq.hours.times, opts);
|
|
331
|
+
}
|
|
332
|
+
else if (freq.hours.kind === 'window') {
|
|
333
|
+
hourClause = ' ' + hourWindow(freq.hours, opts);
|
|
334
|
+
}
|
|
335
|
+
else if (freq.hours.kind === 'step') {
|
|
336
|
+
hourClause = ' ' +
|
|
337
|
+
everyNthHour(stepSegment(ir.analyses.segments.hour!), opts);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return stepPhrase + ', ' + secondsLeadClause(ir, opts) +
|
|
341
|
+
hourClause + trailingQualifier(ir, opts);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// The hour-cadence rendering of a compose-seconds plan whose clock-time rest
|
|
345
|
+
// would cross-multiply an hour stride under a single pinned minute, or null
|
|
346
|
+
// when that does not apply (a non-clock rest, a multi-valued minute, or an
|
|
347
|
+
// hour that is not a stride).
|
|
348
|
+
function composeHourCadence(
|
|
349
|
+
ir: IR,
|
|
350
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
351
|
+
opts: NormalizedOptions
|
|
352
|
+
): string | null {
|
|
353
|
+
const clockRest = plan.rest.kind === 'clockTimes' ||
|
|
354
|
+
plan.rest.kind === 'compactClockTimes';
|
|
355
|
+
|
|
356
|
+
return clockRest && ir.shapes.minute === 'single' ?
|
|
357
|
+
hourCadence(ir, +ir.pattern.minute, opts) :
|
|
358
|
+
null;
|
|
359
|
+
}
|
|
360
|
+
|
|
307
361
|
function renderComposeSeconds(
|
|
308
362
|
ir: IR,
|
|
309
363
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
310
364
|
opts: NormalizedOptions
|
|
311
365
|
): string {
|
|
366
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
367
|
+
// minute is a cadence, not a wall of clock times: the second/minute lead,
|
|
368
|
+
// then the hour cadence ("30 sekunnin kohdalla, kahden tunnin välein"). The
|
|
369
|
+
// clock-time rest would otherwise cross-multiply the hours.
|
|
370
|
+
const cadence = composeHourCadence(ir, plan, opts);
|
|
371
|
+
|
|
372
|
+
if (cadence !== null) {
|
|
373
|
+
return cadence;
|
|
374
|
+
}
|
|
375
|
+
|
|
312
376
|
// When the rest is a minute-step cadence, the step leads and the second
|
|
313
377
|
// anchor follows after a comma (the comma marks the granularity boundary
|
|
314
|
-
// between the two levels, not a flat list).
|
|
315
|
-
// "[step phrase], [seconds][hour clause][trailing qualifier]".
|
|
316
|
-
//
|
|
317
|
-
// The minute-frequency phrase is reconstructed directly here so the hour
|
|
318
|
-
// clause can be interleaved between the step and the second anchor without
|
|
319
|
-
// duplicating the full renderMinuteFrequency logic. The hours-first reorder
|
|
320
|
-
// that applies inside renderMinuteFrequency is intentionally NOT applied
|
|
321
|
-
// here (the step-leads form is the correct shape for this construction).
|
|
378
|
+
// between the two levels, not a flat list).
|
|
322
379
|
if (plan.rest.kind === 'minuteFrequency' && ir.pattern.second !== '*') {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const stepPhrase = stepCycle60(seg, units.minute, opts);
|
|
326
|
-
let hourClause = '';
|
|
327
|
-
|
|
328
|
-
if (freq.hours.kind === 'during' && minuteStepIsAnchored(seg)) {
|
|
329
|
-
// The step renders as an anchored kohdalla list rather than a cadence,
|
|
330
|
-
// so the hours-first reorder applies here too: bare hours lead, minute
|
|
331
|
-
// anchors follow, then the seconds clause.
|
|
332
|
-
const bareHours = kloFromTimes(ir, freq.hours.times, opts);
|
|
333
|
-
|
|
334
|
-
return hoursFirstMinutes(bareHours, ir) + ', ' +
|
|
335
|
-
secondsLeadClause(ir, opts) + trailingQualifier(ir, opts);
|
|
336
|
-
}
|
|
337
|
-
else if (freq.hours.kind === 'during' && !minuteStepIsAnchored(seg)) {
|
|
338
|
-
hourClause = ' ' + hourWindowsFromTimes(ir, freq.hours.times, opts);
|
|
339
|
-
}
|
|
340
|
-
else if (freq.hours.kind === 'window') {
|
|
341
|
-
hourClause = ' ' + hourWindow(freq.hours, opts);
|
|
342
|
-
}
|
|
343
|
-
else if (freq.hours.kind === 'step') {
|
|
344
|
-
hourClause = ' ' +
|
|
345
|
-
everyNthHour(stepSegment(ir.analyses.segments.hour!), opts);
|
|
346
|
-
}
|
|
380
|
+
return composeSecondsOverMinuteStep(ir, plan.rest, opts);
|
|
381
|
+
}
|
|
347
382
|
|
|
348
|
-
|
|
349
|
-
|
|
383
|
+
// A sub-minute second with the minute pinned to 0 and a specific hour: the
|
|
384
|
+
// clock-time rest would read "klo 9", dropping the pinned :00 and so the
|
|
385
|
+
// one-minute confinement (60 fires in :00, not 3,600 across the hour). Bind
|
|
386
|
+
// the seconds to the explicit clock minute with the "minuutin HH.00 aikana"
|
|
387
|
+
// frame (an "of"/during form, never a range) and trail the day qualifier
|
|
388
|
+
// ("joka sekunti minuutin 9.00 aikana, joka päivä").
|
|
389
|
+
if (plan.rest.kind === 'clockTimes' &&
|
|
390
|
+
plan.rest.times.every((time) => +time.minute === 0)) {
|
|
391
|
+
return composeMinuteZero(ir, plan.rest, opts);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
395
|
+
// cadences that read as contradictory ("joka sekunti, kahden minuutin
|
|
396
|
+
// välein"). Bind them as "every second of every other minute" ("joka sekunti
|
|
397
|
+
// joka toisena minuuttina"), mirroring English. Other strides, a restricted
|
|
398
|
+
// hour, and an hour cadence keep the juxtaposed form.
|
|
399
|
+
if (isEveryOtherMinuteSeconds(ir, plan)) {
|
|
400
|
+
return secondsLeadClause(ir, opts) + ' joka toisena minuuttina';
|
|
350
401
|
}
|
|
351
402
|
|
|
352
403
|
return secondsLeadClause(ir, opts) + ', ' + render(ir, plan.rest, opts);
|
|
353
404
|
}
|
|
354
405
|
|
|
406
|
+
// A wildcard second over an unoffset minute */2 with a wildcard hour: the two
|
|
407
|
+
// cadences read as contradictory side by side, so they bind into one.
|
|
408
|
+
function isEveryOtherMinuteSeconds(
|
|
409
|
+
ir: IR,
|
|
410
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
411
|
+
): boolean {
|
|
412
|
+
if (plan.rest.kind !== 'minuteFrequency' || ir.pattern.second !== '*' ||
|
|
413
|
+
ir.shapes.hour !== 'wildcard') {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const seg = stepSegment(ir.analyses.segments.minute!);
|
|
418
|
+
|
|
419
|
+
return seg.startToken === '*' && seg.interval === 2;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// The minute-0 confinement: bind the seconds to the explicit clock minute(s)
|
|
423
|
+
// in the "minuutin/minuuttien HH.00 aikana" frame (an "of"/during form, never
|
|
424
|
+
// a range — a range would round-trip back to the whole hour) and trail the day
|
|
425
|
+
// qualifier ("joka sekunti minuutin 9.00 aikana, joka päivä").
|
|
426
|
+
function composeMinuteZero(
|
|
427
|
+
ir: IR,
|
|
428
|
+
rest: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
429
|
+
opts: NormalizedOptions
|
|
430
|
+
): string {
|
|
431
|
+
const clocks = rest.times.map(function clock(time): string {
|
|
432
|
+
return clockDigits({hour: time.hour, minute: time.minute},
|
|
433
|
+
{sep: opts.style.sep});
|
|
434
|
+
});
|
|
435
|
+
const frame = clocks.length === 1 ?
|
|
436
|
+
'minuutin ' + clocks[0] :
|
|
437
|
+
'minuuttien ' + joinList(clocks);
|
|
438
|
+
const dayTrail = leadingQualifier(ir, opts).trimEnd();
|
|
439
|
+
|
|
440
|
+
return secondsLeadClause(ir, opts) + ' ' + frame + ' aikana' +
|
|
441
|
+
(dayTrail ? ', ' + dayTrail : '');
|
|
442
|
+
}
|
|
443
|
+
|
|
355
444
|
// The leading clause describing a second field relative to the minute.
|
|
356
445
|
function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
357
446
|
const secondField = ir.pattern.second;
|
|
@@ -375,8 +464,11 @@ function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
|
375
464
|
return atMarks(secondField, units.second, marked);
|
|
376
465
|
}
|
|
377
466
|
|
|
378
|
-
|
|
379
|
-
|
|
467
|
+
// An offset/uneven step the core enumerated to this list reads as a stride
|
|
468
|
+
// cadence when the fires form a long-enough progression.
|
|
469
|
+
return strideFromSegments(ir.analyses.segments.second!, units.second, opts) ??
|
|
470
|
+
atMarks(joinList(segmentWords(ir.analyses.segments.second!)),
|
|
471
|
+
units.second, marked);
|
|
380
472
|
}
|
|
381
473
|
|
|
382
474
|
// --- Minute renderers. ---
|
|
@@ -403,7 +495,7 @@ function renderRangeOfMinutes(
|
|
|
403
495
|
plan: Extract<PlanNode, {kind: 'rangeOfMinutes'}>,
|
|
404
496
|
opts: NormalizedOptions
|
|
405
497
|
): string {
|
|
406
|
-
return minutesList(ir) + trailingQualifier(ir, opts);
|
|
498
|
+
return minutesList(ir, opts) + trailingQualifier(ir, opts);
|
|
407
499
|
}
|
|
408
500
|
|
|
409
501
|
function renderMultipleMinutes(
|
|
@@ -411,21 +503,27 @@ function renderMultipleMinutes(
|
|
|
411
503
|
plan: Extract<PlanNode, {kind: 'multipleMinutes'}>,
|
|
412
504
|
opts: NormalizedOptions
|
|
413
505
|
): string {
|
|
414
|
-
return minutesList(ir) + trailingQualifier(ir, opts);
|
|
506
|
+
return minutesList(ir, opts) + trailingQualifier(ir, opts);
|
|
415
507
|
}
|
|
416
508
|
|
|
417
|
-
// "joka tunti 0, 15 ja 30 minuutin kohdalla" (or a dash range).
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
509
|
+
// "joka tunti 0, 15 ja 30 minuutin kohdalla" (or a dash range). An offset/
|
|
510
|
+
// uneven step the core enumerated to this list reads as a stride cadence when
|
|
511
|
+
// the fires form a long-enough progression ("kahden minuutin välein
|
|
512
|
+
// minuutista 3 minuuttiin 59").
|
|
513
|
+
function minutesList(ir: IR, opts: NormalizedOptions): string {
|
|
514
|
+
return strideFromSegments(ir.analyses.segments.minute!, units.minute, opts) ??
|
|
515
|
+
atMarks(joinList(segmentWords(ir.analyses.segments.minute!)),
|
|
516
|
+
units.minute, true);
|
|
421
517
|
}
|
|
422
518
|
|
|
423
519
|
// The bare minute mark, for clauses where a specific hour follows and
|
|
424
520
|
// the "joka tunti" frequency would be redundant: "0–30 minuutin
|
|
425
|
-
// kohdalla".
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
521
|
+
// kohdalla". A progression reads as its bounded cadence (which carries no
|
|
522
|
+
// per-hour frequency to drop).
|
|
523
|
+
function bareMinutes(ir: IR, opts: NormalizedOptions): string {
|
|
524
|
+
return strideFromSegments(ir.analyses.segments.minute!, units.minute, opts) ??
|
|
525
|
+
atMarks(joinList(segmentWords(ir.analyses.segments.minute!)),
|
|
526
|
+
units.minute, false);
|
|
429
527
|
}
|
|
430
528
|
|
|
431
529
|
// Whether a minute step renders as an anchored "kohdalla" clause rather
|
|
@@ -475,7 +573,21 @@ function hoursAreRangeIsolated(segments: Segment[]): boolean {
|
|
|
475
573
|
// (plural genitive "minuuttien"; replaces the leading "joka tunti"). Used
|
|
476
574
|
// when a range or multi-point minute list over enumerated hours renders
|
|
477
575
|
// hours-first.
|
|
478
|
-
function hoursFirstMinutes(
|
|
576
|
+
function hoursFirstMinutes(
|
|
577
|
+
hoursStr: string,
|
|
578
|
+
ir: IR,
|
|
579
|
+
opts: NormalizedOptions
|
|
580
|
+
): string {
|
|
581
|
+
// An offset/uneven step the core enumerated to this list reads as a stride
|
|
582
|
+
// cadence ("aina kahden minuutin välein minuutista 3 minuuttiin 59") when
|
|
583
|
+
// the fires form a long-enough progression, rather than the kohdalla list.
|
|
584
|
+
const stride =
|
|
585
|
+
strideFromSegments(ir.analyses.segments.minute!, units.minute, opts);
|
|
586
|
+
|
|
587
|
+
if (stride) {
|
|
588
|
+
return hoursStr + ' aina ' + stride;
|
|
589
|
+
}
|
|
590
|
+
|
|
479
591
|
return hoursStr + ' aina minuuttien ' +
|
|
480
592
|
joinList(segmentWords(ir.analyses.segments.minute!)) + ' kohdalla';
|
|
481
593
|
}
|
|
@@ -521,7 +633,8 @@ function renderMinuteFrequency(
|
|
|
521
633
|
if (minuteStepIsAnchored(seg)) {
|
|
522
634
|
const bareHours = kloFromTimes(ir, plan.hours.times, opts);
|
|
523
635
|
|
|
524
|
-
return hoursFirstMinutes(bareHours, ir
|
|
636
|
+
return hoursFirstMinutes(bareHours, ir, opts) +
|
|
637
|
+
trailingQualifier(ir, opts);
|
|
525
638
|
}
|
|
526
639
|
|
|
527
640
|
return stepCycle60(seg, units.minute, opts) + ' ' +
|
|
@@ -542,12 +655,20 @@ function renderMinuteFrequency(
|
|
|
542
655
|
return phrase + trailingQualifier(ir, opts);
|
|
543
656
|
}
|
|
544
657
|
|
|
545
|
-
// "joka minuutti klo 9.00–9.59".
|
|
658
|
+
// "joka minuutti klo 9.00–9.59". A wildcard minute is the whole hour, so it
|
|
659
|
+
// reads as that hour itself ("joka minuutti kello 9 aikana") rather than a
|
|
660
|
+
// synthesized "klo 9.00–9.59" range the source never stated; a plain range is
|
|
661
|
+
// a real window and keeps the dash form.
|
|
546
662
|
function renderMinuteSpanInHour(
|
|
547
663
|
ir: IR,
|
|
548
664
|
plan: Extract<PlanNode, {kind: 'minuteSpanInHour'}>,
|
|
549
665
|
opts: NormalizedOptions
|
|
550
666
|
): string {
|
|
667
|
+
if (ir.pattern.minute === '*') {
|
|
668
|
+
return 'joka minuutti kello ' + plan.hour + ' aikana' +
|
|
669
|
+
trailingQualifier(ir, opts);
|
|
670
|
+
}
|
|
671
|
+
|
|
551
672
|
return 'joka minuutti ' +
|
|
552
673
|
kloRange({hour: plan.hour, minute: plan.span[0]},
|
|
553
674
|
{hour: plan.hour, minute: plan.span[1]}, opts) +
|
|
@@ -573,7 +694,7 @@ function renderMinutesAcrossHours(
|
|
|
573
694
|
|
|
574
695
|
// Range+isolated hours: minute-first, bare minutes, sekä klo.
|
|
575
696
|
if (hoursAreRangeIsolated(ir.analyses.segments.hour!)) {
|
|
576
|
-
return bareMinutes(ir) + ' ' +
|
|
697
|
+
return bareMinutes(ir, opts) + ' ' +
|
|
577
698
|
hourSegmentTimesWithSeka(ir, 0, null, opts) +
|
|
578
699
|
trailingQualifier(ir, opts);
|
|
579
700
|
}
|
|
@@ -583,7 +704,7 @@ function renderMinutesAcrossHours(
|
|
|
583
704
|
// shows it).
|
|
584
705
|
const hoursStr = kloFromTimes(ir, plan.times, opts);
|
|
585
706
|
|
|
586
|
-
return hoursFirstMinutes(hoursStr, ir) + trailingQualifier(ir, opts);
|
|
707
|
+
return hoursFirstMinutes(hoursStr, ir, opts) + trailingQualifier(ir, opts);
|
|
587
708
|
}
|
|
588
709
|
|
|
589
710
|
function renderMinuteSpanAcrossHourStep(
|
|
@@ -611,10 +732,10 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
611
732
|
if (segment.startToken.indexOf('-') !== -1) {
|
|
612
733
|
const hoursStr = kloList(segment.fires, opts);
|
|
613
734
|
|
|
614
|
-
return hoursFirstMinutes(hoursStr, ir) + trailingQualifier(ir, opts);
|
|
735
|
+
return hoursFirstMinutes(hoursStr, ir, opts) + trailingQualifier(ir, opts);
|
|
615
736
|
}
|
|
616
737
|
|
|
617
|
-
return bareMinutes(ir) + hourStepTail(segment, opts) +
|
|
738
|
+
return bareMinutes(ir, opts) + hourStepTail(segment, opts) +
|
|
618
739
|
trailingQualifier(ir, opts);
|
|
619
740
|
}
|
|
620
741
|
|
|
@@ -692,7 +813,7 @@ function renderHourRange(
|
|
|
692
813
|
// A minute range over a single hour range renders hours-first
|
|
693
814
|
// ("klo 9.00–17.30 aina minuuttien 0–30 kohdalla").
|
|
694
815
|
if (plan.minuteForm === 'range') {
|
|
695
|
-
return hoursFirstMinutes(window, ir) + trailingQualifier(ir, opts);
|
|
816
|
+
return hoursFirstMinutes(window, ir, opts) + trailingQualifier(ir, opts);
|
|
696
817
|
}
|
|
697
818
|
|
|
698
819
|
// On the hour the window joins directly ("joka tunti klo 9–17"); a
|
|
@@ -711,7 +832,7 @@ function renderHourRange(
|
|
|
711
832
|
|
|
712
833
|
// A minute list (≥2 values) over a single hour range renders hours-first
|
|
713
834
|
// ("klo 9.00–17.30 aina minuuttien 0 ja 30 kohdalla").
|
|
714
|
-
return hoursFirstMinutes(window, ir) + trailingQualifier(ir, opts);
|
|
835
|
+
return hoursFirstMinutes(window, ir, opts) + trailingQualifier(ir, opts);
|
|
715
836
|
}
|
|
716
837
|
|
|
717
838
|
function renderHourStep(
|
|
@@ -746,6 +867,16 @@ function renderClockTimes(
|
|
|
746
867
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
747
868
|
opts: NormalizedOptions
|
|
748
869
|
): string {
|
|
870
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
871
|
+
// minute reads as a cadence rather than a cross-product of clock times.
|
|
872
|
+
if (ir.shapes.minute === 'single') {
|
|
873
|
+
const cadence = hourCadence(ir, +ir.pattern.minute, opts);
|
|
874
|
+
|
|
875
|
+
if (cadence !== null) {
|
|
876
|
+
return cadence;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
749
880
|
if (plan.times.length === 1) {
|
|
750
881
|
const time = plan.times[0];
|
|
751
882
|
|
|
@@ -769,6 +900,17 @@ function renderCompactClockTimes(
|
|
|
769
900
|
plan: Extract<PlanNode, {kind: 'compactClockTimes'}>,
|
|
770
901
|
opts: NormalizedOptions
|
|
771
902
|
): string {
|
|
903
|
+
// An hour step (or arithmetic-progression hour list) under the single pinned
|
|
904
|
+
// minute reads as a cadence, not a wall of clock times. (Returns null for an
|
|
905
|
+
// irregular list or a range, which keep folding below.)
|
|
906
|
+
if (plan.fold) {
|
|
907
|
+
const cadence = hourCadence(ir, plan.minute, opts);
|
|
908
|
+
|
|
909
|
+
if (cadence !== null) {
|
|
910
|
+
return cadence;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
772
914
|
const hourSegs = ir.analyses.segments.hour!;
|
|
773
915
|
|
|
774
916
|
// Range+isolated hours: join the isolated hour with "sekä klo" to stop it
|
|
@@ -782,7 +924,7 @@ function renderCompactClockTimes(
|
|
|
782
924
|
ir.analyses.clockSecond, opts);
|
|
783
925
|
}
|
|
784
926
|
|
|
785
|
-
const phrase = bareMinutes(ir) + ' ' +
|
|
927
|
+
const phrase = bareMinutes(ir, opts) + ' ' +
|
|
786
928
|
hourSegmentTimesWithSeka(ir, 0, null, opts) +
|
|
787
929
|
trailingQualifier(ir, opts);
|
|
788
930
|
|
|
@@ -799,7 +941,7 @@ function renderCompactClockTimes(
|
|
|
799
941
|
// A minute list over purely enumerated hours (step fires, all singles) —
|
|
800
942
|
// hours-first, drop "joka tunti".
|
|
801
943
|
const hoursStr = hourSegmentTimes(ir, 0, null, opts);
|
|
802
|
-
const phrase = hoursFirstMinutes(hoursStr, ir) +
|
|
944
|
+
const phrase = hoursFirstMinutes(hoursStr, ir, opts) +
|
|
803
945
|
trailingQualifier(ir, opts);
|
|
804
946
|
|
|
805
947
|
return ir.analyses.clockSecond ?
|
|
@@ -831,8 +973,82 @@ const renderers = {
|
|
|
831
973
|
|
|
832
974
|
// --- Step phrases. ---
|
|
833
975
|
|
|
976
|
+
// A step cadence to phrase over a `cycle`-long field (60 for minute/second),
|
|
977
|
+
// running from `start` to `last`.
|
|
978
|
+
interface Stride {
|
|
979
|
+
interval: number;
|
|
980
|
+
start: number;
|
|
981
|
+
last: number;
|
|
982
|
+
cycle: number;
|
|
983
|
+
unit: UnitForms;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Speak a step cadence over a `cycle`-long field. A clean stride from the top
|
|
987
|
+
// of the cycle is the bare cadence ("viiden minuutin välein"); a uniform
|
|
988
|
+
// offset (start within the first interval, the interval still dividing the
|
|
989
|
+
// cycle) names only its start, since it wraps cleanly with no distinct
|
|
990
|
+
// endpoint ("kuuden minuutin välein jokaisen tunnin minuutista 5 alkaen"); a
|
|
991
|
+
// non-uniform stride (start >= interval, or an interval that does not divide
|
|
992
|
+
// the cycle) pins both endpoints so the bounded, non-wrapping set reads
|
|
993
|
+
// unambiguously ("kahden minuutin välein minuutista 3 minuuttiin 59"). This is
|
|
994
|
+
// the one phrasing for every step the renderer speaks, whether the core kept
|
|
995
|
+
// it a step shape (a clean cadence) or enumerated it to a fire list (an
|
|
996
|
+
// offset/uneven set the list path recognizes as a progression).
|
|
997
|
+
function renderStride(stride: Stride, opts: NormalizedOptions): string {
|
|
998
|
+
const {interval, start, last, cycle, unit} = stride;
|
|
999
|
+
const cadence = genitive(interval, opts) + ' ' + unit.gen + ' välein';
|
|
1000
|
+
const tiles = cycle % interval === 0;
|
|
1001
|
+
|
|
1002
|
+
if (start === 0 && tiles) {
|
|
1003
|
+
return cadence;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (start < interval && tiles) {
|
|
1007
|
+
return cadence + ' ' + unit.anchor + ' ' + unit.ela + ' ' + start +
|
|
1008
|
+
' alkaen';
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return cadence + ' ' + unit.ela + ' ' + start + ' ' + unit.ill + ' ' + last;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
1015
|
+
// form an arithmetic progression long enough to beat the list (the core
|
|
1016
|
+
// enumerates an offset/uneven step to this fire list; the IR is unchanged, so
|
|
1017
|
+
// the renderer recognizes the progression). Returns null for a non-progression
|
|
1018
|
+
// or a too-short list, leaving the caller to enumerate.
|
|
1019
|
+
function strideFromSegments(
|
|
1020
|
+
segments: Segment[],
|
|
1021
|
+
unit: UnitForms,
|
|
1022
|
+
opts: NormalizedOptions
|
|
1023
|
+
): string | null {
|
|
1024
|
+
const values = singleValues(segments);
|
|
1025
|
+
const step = values && arithmeticStep(values);
|
|
1026
|
+
|
|
1027
|
+
return step ?
|
|
1028
|
+
renderStride({...step, cycle: 60, unit}, opts) :
|
|
1029
|
+
null;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// The sorted numeric values a field's segments cover, or null if any segment
|
|
1033
|
+
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
1034
|
+
function singleValues(segments: Segment[]): number[] | null {
|
|
1035
|
+
const values: number[] = [];
|
|
1036
|
+
|
|
1037
|
+
for (const segment of segments) {
|
|
1038
|
+
if (segment.kind !== 'single') {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
values.push(+segment.value);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return values;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
834
1048
|
// "viiden minuutin välein", "joka tunti 0 ja 31 minuutin kohdalla", or
|
|
835
|
-
// "kolmen minuutin välein jokaisen tunnin minuutista 1 alkaen".
|
|
1049
|
+
// "kolmen minuutin välein jokaisen tunnin minuutista 1 alkaen". A step shape
|
|
1050
|
+
// only reaches here as a clean or uniform-offset cadence; an offset/uneven set
|
|
1051
|
+
// arrives as a fire list and is recognized by the list path instead.
|
|
836
1052
|
function stepCycle60(
|
|
837
1053
|
segment: StepSegment,
|
|
838
1054
|
unit: UnitForms,
|
|
@@ -843,21 +1059,20 @@ function stepCycle60(
|
|
|
843
1059
|
}
|
|
844
1060
|
|
|
845
1061
|
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
846
|
-
const interval = segment.interval;
|
|
847
|
-
const cadence = genitive(interval, opts) + ' ' + unit.gen + ' välein';
|
|
848
1062
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return cadence + ' ' + unit.anchor + ' ' + unit.ela + ' ' + start +
|
|
855
|
-
' alkaen';
|
|
1063
|
+
// A short offset cadence still lists its fires; the stride phrasing names
|
|
1064
|
+
// the interval and offset only once there are enough fires to beat the list.
|
|
1065
|
+
if (start !== 0 && segment.fires.length <= 3) {
|
|
1066
|
+
return atMarks(joinList(wordList(segment.fires)), unit, true);
|
|
856
1067
|
}
|
|
857
1068
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1069
|
+
return renderStride({
|
|
1070
|
+
interval: segment.interval,
|
|
1071
|
+
start,
|
|
1072
|
+
last: segment.fires[segment.fires.length - 1],
|
|
1073
|
+
cycle: 60,
|
|
1074
|
+
unit
|
|
1075
|
+
}, opts);
|
|
861
1076
|
}
|
|
862
1077
|
|
|
863
1078
|
// "kahden tunnin välein", "klo 0, 10 ja 20", or "viiden tunnin välein
|
|
@@ -884,6 +1099,156 @@ function stepHours(segment: StepSegment, opts: NormalizedOptions): string {
|
|
|
884
1099
|
return cadence + ' klo ' + hourElatives[start] + ' alkaen';
|
|
885
1100
|
}
|
|
886
1101
|
|
|
1102
|
+
// --- Hour-step cadence (the 24-cycle analog of renderStride). ---
|
|
1103
|
+
|
|
1104
|
+
// Speak an hour stride as a cadence with clock-time bounds: a clean stride
|
|
1105
|
+
// from midnight is the bare cadence ("kahden tunnin välein"); a clean offset
|
|
1106
|
+
// names only its start ("kuuden tunnin välein klo 2:sta alkaen"); a bounded or
|
|
1107
|
+
// non-tiling stride pins both clock-time endpoints ("kahden tunnin välein klo
|
|
1108
|
+
// 9–17") so the bounded set reads unambiguously. Used wherever an hour step
|
|
1109
|
+
// (or arithmetic-progression hour list) would otherwise be cross-multiplied
|
|
1110
|
+
// into a wall of clock times.
|
|
1111
|
+
function hourStrideCadence(
|
|
1112
|
+
stride: {start: number; interval: number; last: number},
|
|
1113
|
+
opts: NormalizedOptions
|
|
1114
|
+
): string {
|
|
1115
|
+
const {start, interval, last} = stride;
|
|
1116
|
+
const cadence = genitive(interval, opts) + ' tunnin välein';
|
|
1117
|
+
const tiles = 24 % interval === 0;
|
|
1118
|
+
|
|
1119
|
+
if (start === 0 && tiles) {
|
|
1120
|
+
return cadence;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (start < interval && tiles) {
|
|
1124
|
+
return cadence + ' klo ' + hourElatives[start] + ' alkaen';
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return cadence + ' ' +
|
|
1128
|
+
kloRange({hour: start, minute: 0}, {hour: last, minute: 0}, opts);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1132
|
+
// segment yields its {start, interval, last} directly; an all-single hour list
|
|
1133
|
+
// yields one only when its values form a long-enough arithmetic progression
|
|
1134
|
+
// (so an irregular list like 9,17 keeps enumerating). The IR is unchanged —
|
|
1135
|
+
// the renderer recognizes the stride and speaks it as a cadence instead of the
|
|
1136
|
+
// clock-time cross-product.
|
|
1137
|
+
function hourStride(
|
|
1138
|
+
ir: IR
|
|
1139
|
+
): {start: number; interval: number; last: number} | null {
|
|
1140
|
+
const segments = ir.analyses.segments.hour;
|
|
1141
|
+
|
|
1142
|
+
// A wildcard hour carries no segments (no discrete hours to stride over).
|
|
1143
|
+
if (!segments) {
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
1148
|
+
const segment = segments[0];
|
|
1149
|
+
const start = segment.startToken === '*' ?
|
|
1150
|
+
0 :
|
|
1151
|
+
+segment.startToken.split('-')[0];
|
|
1152
|
+
|
|
1153
|
+
return {interval: segment.interval, last: segment.fires[
|
|
1154
|
+
segment.fires.length - 1], start};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const values = singleValues(segments);
|
|
1158
|
+
const step = values && arithmeticStep(values);
|
|
1159
|
+
|
|
1160
|
+
return step || null;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
1164
|
+
// fills the minute (a "minuutin ajan" frame at minute 0); a single 0 is just
|
|
1165
|
+
// the top of the minute (no clause); anything else needs its own clause.
|
|
1166
|
+
function subMinuteSecond(ir: IR): boolean {
|
|
1167
|
+
return ir.pattern.second === '*' || ir.shapes.second === 'step';
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// The lead clause for an hour-cadence rendering: the second and the pinned
|
|
1171
|
+
// minute, before the hour cadence. A pinned minute 0 folds in — a single,
|
|
1172
|
+
// list, range, or step second is counted at its own bare "kohdalla" mark (the
|
|
1173
|
+
// minute-0 is the top of the hour), and a wildcard second takes a "minuutin
|
|
1174
|
+
// ajan" frame (the whole minute-0 window). A non-zero minute is a real clock
|
|
1175
|
+
// minute: the second leads with its own clause (if any), then the minute reads
|
|
1176
|
+
// at its bare "kohdalla" mark.
|
|
1177
|
+
function hourCadenceLead(ir: IR, minute: number,
|
|
1178
|
+
opts: NormalizedOptions): string {
|
|
1179
|
+
if (minute === 0) {
|
|
1180
|
+
if (subMinuteSecond(ir)) {
|
|
1181
|
+
return secondsLeadClause(ir, opts) + ' minuutin ajan';
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return secondsLeadClause(ir, opts);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const minutePhrase = atMarks(String(minute), units.minute, false);
|
|
1188
|
+
|
|
1189
|
+
// A single 0 second is just the top of the minute, so the minute leads
|
|
1190
|
+
// alone; any other second prefixes its own clause.
|
|
1191
|
+
if (ir.pattern.second === '0') {
|
|
1192
|
+
return minutePhrase;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
return secondsLeadClause(ir, opts) + ', ' + minutePhrase;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
1199
|
+
// pinned minute and a second as a cadence — the lead clause, then the hour
|
|
1200
|
+
// cadence — instead of cross-multiplying the hours into a wall of clock times.
|
|
1201
|
+
// Returns null when the hour is not a stride (an irregular list, a single
|
|
1202
|
+
// hour, or a range), or when the cross-product is short enough that
|
|
1203
|
+
// enumeration is no longer than the cadence: a meaningful second makes every
|
|
1204
|
+
// clock time three digit-groups, so any stride is worth compacting; otherwise
|
|
1205
|
+
// the stride must exceed the clock-time cap, the same point at which the core
|
|
1206
|
+
// itself stops enumerating. Renderer-only; the IR is unchanged.
|
|
1207
|
+
function hourCadence(ir: IR, minute: number,
|
|
1208
|
+
opts: NormalizedOptions): string | null {
|
|
1209
|
+
const stride = hourStride(ir);
|
|
1210
|
+
|
|
1211
|
+
if (!stride) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const fires = (stride.last - stride.start) / stride.interval + 1;
|
|
1216
|
+
|
|
1217
|
+
if (ir.pattern.second === '0' && fires <= maxClockTimes) {
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// A wildcard or sub-minute step second confined to minute 0 of a clean hour
|
|
1222
|
+
// stride is a confinement, not a juxtaposed cadence: it reads "minuutin ajan
|
|
1223
|
+
// joka toisen tunnin aikana", reusing the every-Nth-hour idiom so the
|
|
1224
|
+
// minute-0 window is never heard as the bare hour cadence.
|
|
1225
|
+
const segment = ir.analyses.segments.hour![0];
|
|
1226
|
+
const confined = minute === 0 && subMinuteSecond(ir) &&
|
|
1227
|
+
ir.analyses.segments.hour!.length === 1 && segment.kind === 'step' &&
|
|
1228
|
+
cleanHourStride(segment);
|
|
1229
|
+
|
|
1230
|
+
if (confined) {
|
|
1231
|
+
return secondsLeadClause(ir, opts) + ' minuutin ajan ' +
|
|
1232
|
+
everyNthHour(segment, opts) + trailingQualifier(ir, opts);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
return hourCadenceLead(ir, minute, opts) + ', ' +
|
|
1236
|
+
hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Whether an hour step is a clean stride over the whole day — unbounded,
|
|
1240
|
+
// dividing 24, and starting within the first interval — so it confines to "joka
|
|
1241
|
+
// N:nnen tunnin aikana" rather than enumerating its fires.
|
|
1242
|
+
function cleanHourStride(segment: StepSegment): boolean {
|
|
1243
|
+
if (segment.startToken.indexOf('-') !== -1) {
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
1248
|
+
|
|
1249
|
+
return 24 % segment.interval === 0 && start < segment.interval;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
887
1252
|
// --- Hour-time phrasing. ---
|
|
888
1253
|
|
|
889
1254
|
// On-the-hour fires as one klo phrase: "klo 0, 10 ja 20".
|
|
@@ -1339,18 +1704,16 @@ function quartzWeekdayPhrase(weekdayField: string): string | undefined {
|
|
|
1339
1704
|
}
|
|
1340
1705
|
}
|
|
1341
1706
|
|
|
1342
|
-
// Resolve a weekday
|
|
1707
|
+
// Resolve a weekday to its table index. Weekday-field segments are already
|
|
1708
|
+
// canonical numbers; a Quartz stem (`5L`, `MON#2`) is not, so resolve any
|
|
1709
|
+
// name via the core's index (with the Sunday alias 7 folding to 0).
|
|
1343
1710
|
function weekdayNumber(token: string | number): number {
|
|
1344
|
-
|
|
1345
|
-
return weekdayTokens[token];
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
return +token % 7;
|
|
1711
|
+
return toFieldNumber('' + token, weekdayNumbers) % 7;
|
|
1349
1712
|
}
|
|
1350
1713
|
|
|
1351
|
-
// Resolve a month
|
|
1714
|
+
// Resolve a canonical month number to its table index.
|
|
1352
1715
|
function monthNumber(token: string | number): number {
|
|
1353
|
-
return
|
|
1716
|
+
return +token;
|
|
1354
1717
|
}
|
|
1355
1718
|
|
|
1356
1719
|
// --- Years. ---
|
|
@@ -1492,7 +1855,10 @@ const fi: Language = {
|
|
|
1492
1855
|
fallback: 'tunnistamaton cron-lauseke',
|
|
1493
1856
|
options: normalizeOptions,
|
|
1494
1857
|
reboot: 'järjestelmän käynnistyessä',
|
|
1495
|
-
|
|
1858
|
+
// A description ending in a period already carries it, so closing the
|
|
1859
|
+
// sentence must not double it.
|
|
1860
|
+
sentence: (description) =>
|
|
1861
|
+
'Suoritetaan ' + description + (description.endsWith('.') ? '' : '.')
|
|
1496
1862
|
};
|
|
1497
1863
|
|
|
1498
1864
|
export default fi;
|