cronli5 0.2.1 → 0.3.1
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 +65 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +471 -383
- package/dist/cronli5.js +471 -383
- package/dist/lang/de.cjs +286 -215
- package/dist/lang/de.js +286 -215
- package/dist/lang/en.cjs +413 -327
- package/dist/lang/en.js +413 -327
- package/dist/lang/es.cjs +303 -265
- package/dist/lang/es.js +303 -265
- package/dist/lang/fi.cjs +311 -266
- package/dist/lang/fi.js +311 -266
- package/dist/lang/zh.cjs +308 -236
- package/dist/lang/zh.js +308 -236
- package/package.json +1 -1
- package/src/core/analyze.ts +12 -12
- package/src/core/cadence.ts +164 -0
- package/src/core/index.ts +3 -1
- package/src/core/normalize.ts +3 -3
- package/src/core/parse.ts +1 -1
- package/src/core/{ir.ts → schedule.ts} +17 -18
- package/src/core/specs.ts +1 -1
- package/src/core/util.ts +3 -165
- package/src/core/validate.ts +1 -1
- package/src/core/weekday.ts +54 -0
- package/src/cronli5.ts +5 -5
- package/src/lang/de/index.ts +329 -219
- package/src/lang/en/dialects.ts +1 -1
- package/src/lang/en/index.ts +521 -372
- package/src/lang/es/index.ts +338 -286
- package/src/lang/es/notes.md +1 -1
- package/src/lang/fi/dialects.ts +1 -1
- package/src/lang/fi/index.ts +365 -299
- package/src/lang/fi/notes.md +23 -8
- package/src/lang/fi/status.json +1 -1
- package/src/lang/zh/index.ts +344 -237
- package/src/types.ts +6 -6
- package/types/core/analyze.d.ts +3 -3
- package/types/core/cadence.d.ts +33 -0
- package/types/core/index.d.ts +3 -1
- package/types/core/normalize.d.ts +1 -1
- package/types/core/parse.d.ts +1 -1
- package/types/core/{ir.d.ts → schedule.d.ts} +11 -16
- package/types/core/specs.d.ts +1 -1
- package/types/core/util.d.ts +1 -30
- package/types/core/weekday.d.ts +10 -0
- package/types/lang/de/index.d.ts +1 -1
- package/types/lang/en/dialects.d.ts +1 -1
- package/types/lang/en/index.d.ts +1 -1
- package/types/lang/es/index.d.ts +1 -1
- package/types/lang/fi/dialects.d.ts +1 -1
- package/types/lang/fi/index.d.ts +1 -1
- package/types/lang/zh/index.d.ts +1 -1
- package/types/types.d.ts +5 -5
package/src/lang/fi/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The Finnish language module: renders an analyzed cron pattern (the
|
|
1
|
+
// The Finnish language module: renders an analyzed cron pattern (the Schedule
|
|
2
2
|
// produced by core `analyze`) as natural Finnish. Anchored to
|
|
3
3
|
// Kielitoimiston ohjepankki and SFS 4175; see notes.md.
|
|
4
4
|
//
|
|
@@ -13,14 +13,16 @@ import {clockDigits, numeral} from '../../core/format.js';
|
|
|
13
13
|
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
14
14
|
import {isOpenStep} from '../../core/shapes.js';
|
|
15
15
|
import {
|
|
16
|
-
arithmeticStep, hourListStride, offsetCleanStride,
|
|
17
|
-
segmentsOf, singleValues, stepSegment
|
|
18
|
-
} from '../../core/
|
|
16
|
+
arithmeticStep, hourListStride, offsetCleanStride,
|
|
17
|
+
renderStride as chooseStride, segmentsOf, singleValues, stepSegment
|
|
18
|
+
} from '../../core/cadence.js';
|
|
19
|
+
import {orderWeekdaysForDisplay} from '../../core/weekday.js';
|
|
20
|
+
import {toFieldNumber} from '../../core/util.js';
|
|
19
21
|
import {resolveDialect} from './dialects.js';
|
|
20
22
|
import type {
|
|
21
|
-
ClockTime, HourTimesPlan,
|
|
23
|
+
ClockTime, HourTimesPlan, Schedule, Language, NormalizedOptions, PlanNode,
|
|
22
24
|
Segment
|
|
23
|
-
} from '../../core/
|
|
25
|
+
} from '../../core/schedule.js';
|
|
24
26
|
import type {Cronli5Options} from '../../types.js';
|
|
25
27
|
|
|
26
28
|
// A step segment, the only Segment variant carrying `startToken`,
|
|
@@ -207,94 +209,134 @@ function normalizeOptions(options?: Cronli5Options): NormalizedOptions {
|
|
|
207
209
|
|
|
208
210
|
// A restricted-month date-or-weekday union: both the date and weekday are
|
|
209
211
|
// restricted AND the month is restricted. When true, the month leads so it
|
|
210
|
-
// scopes both arms, and the
|
|
211
|
-
function restrictedMonthUnion(
|
|
212
|
-
return
|
|
213
|
-
|
|
212
|
+
// scopes both arms, and the inclusive "tai" union comes last.
|
|
213
|
+
function restrictedMonthUnion(schedule: Schedule): boolean {
|
|
214
|
+
return schedule.pattern.date !== '*' && schedule.pattern.weekday !== '*' &&
|
|
215
|
+
schedule.pattern.month !== '*';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// An open interval-2 day-of-month step covers the odd days (1, 3, 5, …, 31),
|
|
219
|
+
// resetting each month — so in a union arm it reads as the parity class
|
|
220
|
+
// "kuukauden parittomina päivinä" (the essive plural of "odd days of the
|
|
221
|
+
// month"), never the continuous "joka toinen päivä" (which implies an
|
|
222
|
+
// unbroken 48-hour cycle across month boundaries) nor a 16-date enumeration
|
|
223
|
+
// that would bury the union beside the "tai". `*/2` and `1/2` are the odd
|
|
224
|
+
// days. Mirrors en's odd-numbered-day idiom; null when not such a step.
|
|
225
|
+
function oddDayUnion(dateField: string): string | null {
|
|
226
|
+
if (!isOpenStep(dateField)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const [start, step] = dateField.split('/');
|
|
231
|
+
|
|
232
|
+
return (start === '*' || start === '1') && +step === 2 ?
|
|
233
|
+
'kuukauden parittomina päivinä' :
|
|
234
|
+
null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// The DOM arm of a restricted-month union. Under a fronted month an ordinary
|
|
238
|
+
// date drops the generic "kuukauden" anchor; a Quartz date keeps its idiom
|
|
239
|
+
// unchanged; an open `*/2` step reads as the odd-day parity class.
|
|
240
|
+
function unionDateArm(schedule: Schedule): string {
|
|
241
|
+
return quartzDatePhrase(schedule.pattern.date) ||
|
|
242
|
+
oddDayUnion(schedule.pattern.date) ||
|
|
243
|
+
dateWords(schedule) + ' päivänä';
|
|
214
244
|
}
|
|
215
245
|
|
|
216
|
-
// The
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
|
|
246
|
+
// The weekday arm of a union. A Monday-through-Friday range reads as the
|
|
247
|
+
// recurring weekday class "arkisin" (= weekdays), parallel to the recurring
|
|
248
|
+
// date arm beside it; everything else defers to the general weekday qualifier.
|
|
249
|
+
function unionWeekdayArm(schedule: Schedule): string {
|
|
250
|
+
const segments = segmentsOf(schedule, 'weekday');
|
|
251
|
+
|
|
252
|
+
if (segments.length === 1 && segments[0].kind === 'range' &&
|
|
253
|
+
segments[0].bounds[0] === '1' && segments[0].bounds[1] === '5') {
|
|
254
|
+
return 'arkisin';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return weekdayQualifier(schedule);
|
|
222
258
|
}
|
|
223
259
|
|
|
224
|
-
// Render an analyzed cron pattern (the
|
|
225
|
-
function describe(
|
|
260
|
+
// Render an analyzed cron pattern (the Schedule) as Finnish.
|
|
261
|
+
function describe(schedule: Schedule, opts: NormalizedOptions): string {
|
|
226
262
|
// A restricted-month date-or-weekday union: the month leads so it scopes
|
|
227
|
-
// both arms, then the
|
|
228
|
-
|
|
229
|
-
|
|
263
|
+
// both arms, then the inclusive "tai" union comes last. Finnish "joko … tai"
|
|
264
|
+
// is the EXCLUSIVE disjunction (only one of the two), so it cannot express
|
|
265
|
+
// cron's union — plain "tai" reads inclusively.
|
|
266
|
+
if (restrictedMonthUnion(schedule)) {
|
|
267
|
+
const timePart = render(schedule, schedule.plan, opts);
|
|
230
268
|
|
|
231
269
|
return applyYear(
|
|
232
|
-
monthPhrase(
|
|
233
|
-
'
|
|
234
|
-
|
|
270
|
+
monthPhrase(schedule) + ' ' + timePart +
|
|
271
|
+
' ' + unionDateArm(schedule) + ' tai ' +
|
|
272
|
+
unionWeekdayArm(schedule),
|
|
273
|
+
schedule,
|
|
235
274
|
opts
|
|
236
275
|
);
|
|
237
276
|
}
|
|
238
277
|
|
|
239
|
-
return applyYear(render(
|
|
278
|
+
return applyYear(render(schedule, schedule.plan, opts), schedule, opts);
|
|
240
279
|
}
|
|
241
280
|
|
|
242
281
|
// Render one plan node. `composeSeconds` recurses with its `rest` plan.
|
|
243
|
-
function render(
|
|
282
|
+
function render(
|
|
283
|
+
schedule: Schedule, plan: PlanNode, opts: NormalizedOptions
|
|
284
|
+
): string {
|
|
244
285
|
// The renderers map each handles one `kind`; the dispatch indexes the
|
|
245
286
|
// union, which TypeScript cannot narrow per-key, so the lookup is cast
|
|
246
287
|
// to a renderer accepting this node's plan.
|
|
247
288
|
const renderer = renderers[plan.kind] as
|
|
248
|
-
(
|
|
289
|
+
(schedule: Schedule, plan: PlanNode, opts: NormalizedOptions) => string;
|
|
249
290
|
|
|
250
|
-
return renderer(
|
|
291
|
+
return renderer(schedule, plan, opts);
|
|
251
292
|
}
|
|
252
293
|
|
|
253
294
|
// --- Seconds renderers. ---
|
|
254
295
|
|
|
255
296
|
function renderEverySecond(
|
|
256
|
-
|
|
297
|
+
schedule: Schedule,
|
|
257
298
|
plan: Extract<PlanNode, {kind: 'everySecond'}>,
|
|
258
299
|
opts: NormalizedOptions
|
|
259
300
|
): string {
|
|
260
|
-
return 'joka sekunti' + trailingQualifier(
|
|
301
|
+
return 'joka sekunti' + trailingQualifier(schedule, opts);
|
|
261
302
|
}
|
|
262
303
|
|
|
263
304
|
function renderStandaloneSeconds(
|
|
264
|
-
|
|
305
|
+
schedule: Schedule,
|
|
265
306
|
plan: Extract<PlanNode, {kind: 'standaloneSeconds'}>,
|
|
266
307
|
opts: NormalizedOptions
|
|
267
308
|
): string {
|
|
268
|
-
return secondsLeadClause(
|
|
309
|
+
return secondsLeadClause(schedule, opts) + trailingQualifier(schedule, opts);
|
|
269
310
|
}
|
|
270
311
|
|
|
271
312
|
function renderSecondPastMinute(
|
|
272
|
-
|
|
313
|
+
schedule: Schedule,
|
|
273
314
|
plan: Extract<PlanNode, {kind: 'secondPastMinute'}>,
|
|
274
315
|
opts: NormalizedOptions
|
|
275
316
|
): string {
|
|
276
|
-
return atMarks(
|
|
277
|
-
trailingQualifier(
|
|
317
|
+
return atMarks(schedule.pattern.second, units.second, true) +
|
|
318
|
+
trailingQualifier(schedule, opts);
|
|
278
319
|
}
|
|
279
320
|
|
|
280
321
|
// A meaningful second combined with a single specific minute (and an
|
|
281
322
|
// open hour): a single second folds into one shared "kohdalla"; a list,
|
|
282
323
|
// range, or step leads with its own clause.
|
|
283
324
|
function renderSecondsWithinMinute(
|
|
284
|
-
|
|
325
|
+
schedule: Schedule,
|
|
285
326
|
plan: Extract<PlanNode, {kind: 'secondsWithinMinute'}>,
|
|
286
327
|
opts: NormalizedOptions
|
|
287
328
|
): string {
|
|
288
|
-
const minuteField =
|
|
329
|
+
const minuteField = schedule.pattern.minute;
|
|
289
330
|
|
|
290
331
|
if (plan.singleSecond) {
|
|
291
332
|
return units.minute.mark + ' ' + minuteField + ' ' +
|
|
292
|
-
units.minute.gen + ' ja ' +
|
|
293
|
-
units.second.gen + ' kohdalla' + trailingQualifier(
|
|
333
|
+
units.minute.gen + ' ja ' + schedule.pattern.second + ' ' +
|
|
334
|
+
units.second.gen + ' kohdalla' + trailingQualifier(schedule, opts);
|
|
294
335
|
}
|
|
295
336
|
|
|
296
|
-
return secondsLeadClause(
|
|
297
|
-
atMarks(minuteField, units.minute, true) +
|
|
337
|
+
return secondsLeadClause(schedule, opts) + ', ' +
|
|
338
|
+
atMarks(minuteField, units.minute, true) +
|
|
339
|
+
trailingQualifier(schedule, opts);
|
|
298
340
|
}
|
|
299
341
|
|
|
300
342
|
// A meaningful second composed over a minute-step cadence: the step leads and
|
|
@@ -305,38 +347,38 @@ function renderSecondsWithinMinute(
|
|
|
305
347
|
// renderMinuteFrequency logic; its hours-first reorder is intentionally NOT
|
|
306
348
|
// applied (the step-leads form is the correct shape for this construction).
|
|
307
349
|
function composeSecondsOverMinuteStep(
|
|
308
|
-
|
|
350
|
+
schedule: Schedule,
|
|
309
351
|
freq: Extract<PlanNode, {kind: 'minuteFrequency'}>,
|
|
310
352
|
opts: NormalizedOptions
|
|
311
353
|
): string {
|
|
312
|
-
const seg = stepSegment(
|
|
354
|
+
const seg = stepSegment(schedule, 'minute');
|
|
313
355
|
const stepPhrase = stepCycle60(seg, units.minute, opts);
|
|
314
356
|
|
|
315
357
|
if (freq.hours.kind === 'during' && minuteStepIsAnchored(seg)) {
|
|
316
358
|
// The step renders as an anchored kohdalla list rather than a cadence, so
|
|
317
359
|
// the hours-first reorder applies here too: bare hours lead, minute anchors
|
|
318
360
|
// follow, then the seconds clause.
|
|
319
|
-
const bareHours = kloFromTimes(
|
|
361
|
+
const bareHours = kloFromTimes(schedule, freq.hours.times, opts);
|
|
320
362
|
|
|
321
|
-
return hoursFirstMinutes(bareHours,
|
|
322
|
-
secondsLeadClause(
|
|
363
|
+
return hoursFirstMinutes(bareHours, schedule, opts) + ', ' +
|
|
364
|
+
secondsLeadClause(schedule, opts) + trailingQualifier(schedule, opts);
|
|
323
365
|
}
|
|
324
366
|
|
|
325
367
|
let hourClause = '';
|
|
326
368
|
|
|
327
369
|
if (freq.hours.kind === 'during') {
|
|
328
|
-
hourClause = ' ' + hourWindowsFromTimes(
|
|
370
|
+
hourClause = ' ' + hourWindowsFromTimes(schedule, freq.hours.times, opts);
|
|
329
371
|
}
|
|
330
372
|
else if (freq.hours.kind === 'window') {
|
|
331
373
|
hourClause = ' ' + hourWindow(freq.hours, opts);
|
|
332
374
|
}
|
|
333
375
|
else if (freq.hours.kind === 'step') {
|
|
334
376
|
hourClause = ' ' +
|
|
335
|
-
everyNthHour(stepSegment(
|
|
377
|
+
everyNthHour(stepSegment(schedule, 'hour'), opts);
|
|
336
378
|
}
|
|
337
379
|
|
|
338
|
-
return stepPhrase + ', ' + secondsLeadClause(
|
|
339
|
-
hourClause + trailingQualifier(
|
|
380
|
+
return stepPhrase + ', ' + secondsLeadClause(schedule, opts) +
|
|
381
|
+
hourClause + trailingQualifier(schedule, opts);
|
|
340
382
|
}
|
|
341
383
|
|
|
342
384
|
// The hour-cadence rendering of a compose-seconds plan whose clock-time rest
|
|
@@ -344,24 +386,25 @@ function composeSecondsOverMinuteStep(
|
|
|
344
386
|
// when that does not apply (a non-clock rest, a multi-valued minute, or an
|
|
345
387
|
// hour that is not a stride).
|
|
346
388
|
function composeHourCadence(
|
|
347
|
-
|
|
389
|
+
schedule: Schedule,
|
|
348
390
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
349
391
|
opts: NormalizedOptions
|
|
350
392
|
): string | null {
|
|
351
393
|
const clockRest = plan.rest.kind === 'clockTimes' ||
|
|
352
394
|
plan.rest.kind === 'compactClockTimes';
|
|
353
395
|
|
|
354
|
-
if (!clockRest ||
|
|
396
|
+
if (!clockRest || schedule.shapes.minute !== 'single') {
|
|
355
397
|
return null;
|
|
356
398
|
}
|
|
357
399
|
|
|
358
|
-
const minute = +
|
|
400
|
+
const minute = +schedule.pattern.minute;
|
|
359
401
|
|
|
360
|
-
return hourCadence(
|
|
402
|
+
return hourCadence(schedule, minute, opts) ??
|
|
403
|
+
hourRangeCadence(schedule, minute, opts);
|
|
361
404
|
}
|
|
362
405
|
|
|
363
406
|
function renderComposeSeconds(
|
|
364
|
-
|
|
407
|
+
schedule: Schedule,
|
|
365
408
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
366
409
|
opts: NormalizedOptions
|
|
367
410
|
): string {
|
|
@@ -369,7 +412,7 @@ function renderComposeSeconds(
|
|
|
369
412
|
// minute is a cadence, not a wall of clock times: the second/minute lead,
|
|
370
413
|
// then the hour cadence ("30 sekunnin kohdalla, kahden tunnin välein"). The
|
|
371
414
|
// clock-time rest would otherwise cross-multiply the hours.
|
|
372
|
-
const cadence = composeHourCadence(
|
|
415
|
+
const cadence = composeHourCadence(schedule, plan, opts);
|
|
373
416
|
|
|
374
417
|
if (cadence !== null) {
|
|
375
418
|
return cadence;
|
|
@@ -378,8 +421,8 @@ function renderComposeSeconds(
|
|
|
378
421
|
// When the rest is a minute-step cadence, the step leads and the second
|
|
379
422
|
// anchor follows after a comma (the comma marks the granularity boundary
|
|
380
423
|
// between the two levels, not a flat list).
|
|
381
|
-
if (plan.rest.kind === 'minuteFrequency' &&
|
|
382
|
-
return composeSecondsOverMinuteStep(
|
|
424
|
+
if (plan.rest.kind === 'minuteFrequency' && schedule.pattern.second !== '*') {
|
|
425
|
+
return composeSecondsOverMinuteStep(schedule, plan.rest, opts);
|
|
383
426
|
}
|
|
384
427
|
|
|
385
428
|
// A sub-minute second with the minute pinned to 0 and a specific hour: the
|
|
@@ -390,7 +433,7 @@ function renderComposeSeconds(
|
|
|
390
433
|
// ("joka sekunti minuutin 9.00 aikana, joka päivä").
|
|
391
434
|
if (plan.rest.kind === 'clockTimes' &&
|
|
392
435
|
plan.rest.times.every((time) => +time.minute === 0)) {
|
|
393
|
-
return composeMinuteZero(
|
|
436
|
+
return composeMinuteZero(schedule, plan.rest, opts);
|
|
394
437
|
}
|
|
395
438
|
|
|
396
439
|
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
@@ -398,8 +441,8 @@ function renderComposeSeconds(
|
|
|
398
441
|
// välein"). Bind them as "every second of every other minute" ("joka sekunti
|
|
399
442
|
// joka toisena minuuttina"), mirroring English. Other strides, a restricted
|
|
400
443
|
// hour, and an hour cadence keep the juxtaposed form.
|
|
401
|
-
if (isEveryOtherMinuteSeconds(
|
|
402
|
-
return secondsLeadClause(
|
|
444
|
+
if (isEveryOtherMinuteSeconds(schedule, plan)) {
|
|
445
|
+
return secondsLeadClause(schedule, opts) + ' joka toisena minuuttina';
|
|
403
446
|
}
|
|
404
447
|
|
|
405
448
|
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
@@ -407,24 +450,24 @@ function renderComposeSeconds(
|
|
|
407
450
|
// double it. A wildcard or stepped second is not folded there (no
|
|
408
451
|
// clockSecond), so it still leads its own clause here.
|
|
409
452
|
const restOwnsLead = plan.rest.kind === 'compactClockTimes' &&
|
|
410
|
-
|
|
411
|
-
const lead = restOwnsLead ? '' : secondsLeadClause(
|
|
453
|
+
schedule.analyses.clockSecond;
|
|
454
|
+
const lead = restOwnsLead ? '' : secondsLeadClause(schedule, opts) + ', ';
|
|
412
455
|
|
|
413
|
-
return lead + render(
|
|
456
|
+
return lead + render(schedule, plan.rest, opts);
|
|
414
457
|
}
|
|
415
458
|
|
|
416
459
|
// A wildcard second over an unoffset minute */2 with a wildcard hour: the two
|
|
417
460
|
// cadences read as contradictory side by side, so they bind into one.
|
|
418
461
|
function isEveryOtherMinuteSeconds(
|
|
419
|
-
|
|
462
|
+
schedule: Schedule,
|
|
420
463
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
421
464
|
): boolean {
|
|
422
|
-
if (plan.rest.kind !== 'minuteFrequency' ||
|
|
423
|
-
|
|
465
|
+
if (plan.rest.kind !== 'minuteFrequency' || schedule.pattern.second !== '*' ||
|
|
466
|
+
schedule.shapes.hour !== 'wildcard') {
|
|
424
467
|
return false;
|
|
425
468
|
}
|
|
426
469
|
|
|
427
|
-
const seg = stepSegment(
|
|
470
|
+
const seg = stepSegment(schedule, 'minute');
|
|
428
471
|
|
|
429
472
|
return seg.startToken === '*' && seg.interval === 2;
|
|
430
473
|
}
|
|
@@ -434,7 +477,7 @@ function isEveryOtherMinuteSeconds(
|
|
|
434
477
|
// a range — a range would round-trip back to the whole hour) and trail the day
|
|
435
478
|
// qualifier ("joka sekunti minuutin 9.00 aikana, joka päivä").
|
|
436
479
|
function composeMinuteZero(
|
|
437
|
-
|
|
480
|
+
schedule: Schedule,
|
|
438
481
|
rest: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
439
482
|
opts: NormalizedOptions
|
|
440
483
|
): string {
|
|
@@ -445,16 +488,18 @@ function composeMinuteZero(
|
|
|
445
488
|
const frame = clocks.length === 1 ?
|
|
446
489
|
'minuutin ' + clocks[0] :
|
|
447
490
|
'minuuttien ' + joinList(clocks);
|
|
448
|
-
const dayTrail = leadingQualifier(
|
|
491
|
+
const dayTrail = leadingQualifier(schedule, opts).trimEnd();
|
|
449
492
|
|
|
450
|
-
return secondsLeadClause(
|
|
493
|
+
return secondsLeadClause(schedule, opts) + ' ' + frame + ' aikana' +
|
|
451
494
|
(dayTrail ? ', ' + dayTrail : '');
|
|
452
495
|
}
|
|
453
496
|
|
|
454
497
|
// The leading clause describing a second field relative to the minute.
|
|
455
|
-
function secondsLeadClause(
|
|
456
|
-
|
|
457
|
-
|
|
498
|
+
function secondsLeadClause(
|
|
499
|
+
schedule: Schedule, opts: NormalizedOptions
|
|
500
|
+
): string {
|
|
501
|
+
const secondField = schedule.pattern.second;
|
|
502
|
+
const shape = schedule.shapes.second;
|
|
458
503
|
|
|
459
504
|
if (secondField === '*') {
|
|
460
505
|
return 'joka sekunti';
|
|
@@ -462,13 +507,13 @@ function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
|
462
507
|
|
|
463
508
|
if (shape === 'step') {
|
|
464
509
|
// A step shape always has segments whose first is a step segment.
|
|
465
|
-
return stepCycle60(stepSegment(
|
|
510
|
+
return stepCycle60(stepSegment(schedule, 'second'),
|
|
466
511
|
units.second, opts);
|
|
467
512
|
}
|
|
468
513
|
|
|
469
514
|
// The "joka minuutti" frequency mark is true only when the minute is open;
|
|
470
515
|
// with a fixed minute the second fires within those minutes, not every one.
|
|
471
|
-
const marked =
|
|
516
|
+
const marked = schedule.pattern.minute === '*';
|
|
472
517
|
|
|
473
518
|
if (shape === 'single') {
|
|
474
519
|
return atMarks(secondField, units.second, marked);
|
|
@@ -476,53 +521,57 @@ function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
|
476
521
|
|
|
477
522
|
// An offset/uneven step the core enumerated to this list reads as a stride
|
|
478
523
|
// cadence when the fires form a long-enough progression.
|
|
479
|
-
return strideFromSegments(
|
|
480
|
-
|
|
524
|
+
return strideFromSegments(
|
|
525
|
+
segmentsOf(schedule, 'second'), units.second, opts
|
|
526
|
+
) ??
|
|
527
|
+
atMarks(joinList(segmentWords(segmentsOf(schedule, 'second'))),
|
|
481
528
|
units.second, marked);
|
|
482
529
|
}
|
|
483
530
|
|
|
484
531
|
// --- Minute renderers. ---
|
|
485
532
|
|
|
486
533
|
function renderEveryMinute(
|
|
487
|
-
|
|
534
|
+
schedule: Schedule,
|
|
488
535
|
plan: Extract<PlanNode, {kind: 'everyMinute'}>,
|
|
489
536
|
opts: NormalizedOptions
|
|
490
537
|
): string {
|
|
491
|
-
return 'joka minuutti' + trailingQualifier(
|
|
538
|
+
return 'joka minuutti' + trailingQualifier(schedule, opts);
|
|
492
539
|
}
|
|
493
540
|
|
|
494
541
|
function renderSingleMinute(
|
|
495
|
-
|
|
542
|
+
schedule: Schedule,
|
|
496
543
|
plan: Extract<PlanNode, {kind: 'singleMinute'}>,
|
|
497
544
|
opts: NormalizedOptions
|
|
498
545
|
): string {
|
|
499
|
-
return atMarks(
|
|
500
|
-
trailingQualifier(
|
|
546
|
+
return atMarks(schedule.pattern.minute, units.minute, true) +
|
|
547
|
+
trailingQualifier(schedule, opts);
|
|
501
548
|
}
|
|
502
549
|
|
|
503
550
|
function renderRangeOfMinutes(
|
|
504
|
-
|
|
551
|
+
schedule: Schedule,
|
|
505
552
|
plan: Extract<PlanNode, {kind: 'rangeOfMinutes'}>,
|
|
506
553
|
opts: NormalizedOptions
|
|
507
554
|
): string {
|
|
508
|
-
return minutesList(
|
|
555
|
+
return minutesList(schedule, opts) + trailingQualifier(schedule, opts);
|
|
509
556
|
}
|
|
510
557
|
|
|
511
558
|
function renderMultipleMinutes(
|
|
512
|
-
|
|
559
|
+
schedule: Schedule,
|
|
513
560
|
plan: Extract<PlanNode, {kind: 'multipleMinutes'}>,
|
|
514
561
|
opts: NormalizedOptions
|
|
515
562
|
): string {
|
|
516
|
-
return minutesList(
|
|
563
|
+
return minutesList(schedule, opts) + trailingQualifier(schedule, opts);
|
|
517
564
|
}
|
|
518
565
|
|
|
519
566
|
// "joka tunti 0, 15 ja 30 minuutin kohdalla" (or a dash range). An offset/
|
|
520
567
|
// uneven step the core enumerated to this list reads as a stride cadence when
|
|
521
568
|
// the fires form a long-enough progression ("kahden minuutin välein
|
|
522
569
|
// minuutista 3 minuuttiin 59").
|
|
523
|
-
function minutesList(
|
|
524
|
-
return strideFromSegments(
|
|
525
|
-
|
|
570
|
+
function minutesList(schedule: Schedule, opts: NormalizedOptions): string {
|
|
571
|
+
return strideFromSegments(
|
|
572
|
+
segmentsOf(schedule, 'minute'), units.minute, opts
|
|
573
|
+
) ??
|
|
574
|
+
atMarks(joinList(segmentWords(segmentsOf(schedule, 'minute'))),
|
|
526
575
|
units.minute, true);
|
|
527
576
|
}
|
|
528
577
|
|
|
@@ -530,9 +579,11 @@ function minutesList(ir: IR, opts: NormalizedOptions): string {
|
|
|
530
579
|
// the "joka tunti" frequency would be redundant: "0–30 minuutin
|
|
531
580
|
// kohdalla". A progression reads as its bounded cadence (which carries no
|
|
532
581
|
// per-hour frequency to drop).
|
|
533
|
-
function bareMinutes(
|
|
534
|
-
return strideFromSegments(
|
|
535
|
-
|
|
582
|
+
function bareMinutes(schedule: Schedule, opts: NormalizedOptions): string {
|
|
583
|
+
return strideFromSegments(
|
|
584
|
+
segmentsOf(schedule, 'minute'), units.minute, opts
|
|
585
|
+
) ??
|
|
586
|
+
atMarks(joinList(segmentWords(segmentsOf(schedule, 'minute'))),
|
|
536
587
|
units.minute, false);
|
|
537
588
|
}
|
|
538
589
|
|
|
@@ -585,35 +636,35 @@ function hoursAreRangeIsolated(segments: Segment[]): boolean {
|
|
|
585
636
|
// hours-first.
|
|
586
637
|
function hoursFirstMinutes(
|
|
587
638
|
hoursStr: string,
|
|
588
|
-
|
|
639
|
+
schedule: Schedule,
|
|
589
640
|
opts: NormalizedOptions
|
|
590
641
|
): string {
|
|
591
642
|
// An offset/uneven step the core enumerated to this list reads as a stride
|
|
592
643
|
// cadence ("aina kahden minuutin välein minuutista 3 minuuttiin 59") when
|
|
593
644
|
// the fires form a long-enough progression, rather than the kohdalla list.
|
|
594
645
|
const stride =
|
|
595
|
-
strideFromSegments(segmentsOf(
|
|
646
|
+
strideFromSegments(segmentsOf(schedule, 'minute'), units.minute, opts);
|
|
596
647
|
|
|
597
648
|
if (stride) {
|
|
598
649
|
return hoursStr + ' aina ' + stride;
|
|
599
650
|
}
|
|
600
651
|
|
|
601
652
|
return hoursStr + ' aina minuuttien ' +
|
|
602
|
-
joinList(segmentWords(segmentsOf(
|
|
653
|
+
joinList(segmentWords(segmentsOf(schedule, 'minute'))) + ' kohdalla';
|
|
603
654
|
}
|
|
604
655
|
|
|
605
656
|
// Hour segment times for a range+isolated pattern: joins the isolated hour
|
|
606
657
|
// with "sekä klo" rather than "ja", marking it as discrete rather than a
|
|
607
658
|
// range extension. Used in bare-hour context only.
|
|
608
659
|
function hourSegmentTimesWithSeka(
|
|
609
|
-
|
|
660
|
+
schedule: Schedule,
|
|
610
661
|
minute: number,
|
|
611
662
|
second: number | null | undefined,
|
|
612
663
|
opts: NormalizedOptions
|
|
613
664
|
): string {
|
|
614
665
|
const pieces: string[] = [];
|
|
615
666
|
|
|
616
|
-
segmentsOf(
|
|
667
|
+
segmentsOf(schedule, 'hour').forEach(function clock(segment: Segment) {
|
|
617
668
|
if (segment.kind === 'range') {
|
|
618
669
|
pieces.push(rangeDigits(
|
|
619
670
|
{hour: +segment.bounds[0], minute, second},
|
|
@@ -630,36 +681,36 @@ function hourSegmentTimesWithSeka(
|
|
|
630
681
|
|
|
631
682
|
// A repeating minute step, qualified by the active hour window(s).
|
|
632
683
|
function renderMinuteFrequency(
|
|
633
|
-
|
|
684
|
+
schedule: Schedule,
|
|
634
685
|
plan: Extract<PlanNode, {kind: 'minuteFrequency'}>,
|
|
635
686
|
opts: NormalizedOptions
|
|
636
687
|
): string {
|
|
637
|
-
const seg = stepSegment(
|
|
688
|
+
const seg = stepSegment(schedule, 'minute');
|
|
638
689
|
|
|
639
690
|
if (plan.hours.kind === 'during') {
|
|
640
691
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence
|
|
641
692
|
// after the minute step ("15 minuutin välein, viiden tunnin välein klo
|
|
642
693
|
// 0–20").
|
|
643
|
-
const cadence = unevenHourCadence(
|
|
694
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
644
695
|
|
|
645
696
|
if (cadence !== null) {
|
|
646
697
|
return stepCycle60(seg, units.minute, opts) + ', ' + cadence +
|
|
647
|
-
trailingQualifier(
|
|
698
|
+
trailingQualifier(schedule, opts);
|
|
648
699
|
}
|
|
649
700
|
|
|
650
701
|
// When the step renders as anchored ("kohdalla"), the per-hour windows
|
|
651
702
|
// are redundant — use bare clock hours instead, then reorder to
|
|
652
703
|
// hours-first: "klo <hours> aina minuuttien <spec> kohdalla".
|
|
653
704
|
if (minuteStepIsAnchored(seg)) {
|
|
654
|
-
const bareHours = kloFromTimes(
|
|
705
|
+
const bareHours = kloFromTimes(schedule, plan.hours.times, opts);
|
|
655
706
|
|
|
656
|
-
return hoursFirstMinutes(bareHours,
|
|
657
|
-
trailingQualifier(
|
|
707
|
+
return hoursFirstMinutes(bareHours, schedule, opts) +
|
|
708
|
+
trailingQualifier(schedule, opts);
|
|
658
709
|
}
|
|
659
710
|
|
|
660
711
|
return stepCycle60(seg, units.minute, opts) + ' ' +
|
|
661
|
-
hourWindowsFromTimes(
|
|
662
|
-
trailingQualifier(
|
|
712
|
+
hourWindowsFromTimes(schedule, plan.hours.times, opts) +
|
|
713
|
+
trailingQualifier(schedule, opts);
|
|
663
714
|
}
|
|
664
715
|
|
|
665
716
|
let phrase = stepCycle60(seg, units.minute, opts);
|
|
@@ -669,10 +720,10 @@ function renderMinuteFrequency(
|
|
|
669
720
|
}
|
|
670
721
|
else if (plan.hours.kind === 'step') {
|
|
671
722
|
phrase += ' ' +
|
|
672
|
-
everyNthHour(stepSegment(
|
|
723
|
+
everyNthHour(stepSegment(schedule, 'hour'), opts);
|
|
673
724
|
}
|
|
674
725
|
|
|
675
|
-
return phrase + trailingQualifier(
|
|
726
|
+
return phrase + trailingQualifier(schedule, opts);
|
|
676
727
|
}
|
|
677
728
|
|
|
678
729
|
// "joka minuutti klo 9.00–9.59". A wildcard minute is the whole hour, so it
|
|
@@ -680,19 +731,19 @@ function renderMinuteFrequency(
|
|
|
680
731
|
// synthesized "klo 9.00–9.59" range the source never stated; a plain range is
|
|
681
732
|
// a real window and keeps the dash form.
|
|
682
733
|
function renderMinuteSpanInHour(
|
|
683
|
-
|
|
734
|
+
schedule: Schedule,
|
|
684
735
|
plan: Extract<PlanNode, {kind: 'minuteSpanInHour'}>,
|
|
685
736
|
opts: NormalizedOptions
|
|
686
737
|
): string {
|
|
687
|
-
if (
|
|
738
|
+
if (schedule.pattern.minute === '*') {
|
|
688
739
|
return 'joka minuutti kello ' + plan.hour + ' aikana' +
|
|
689
|
-
trailingQualifier(
|
|
740
|
+
trailingQualifier(schedule, opts);
|
|
690
741
|
}
|
|
691
742
|
|
|
692
743
|
return 'joka minuutti ' +
|
|
693
744
|
kloRange({hour: plan.hour, minute: plan.span[0]},
|
|
694
745
|
{hour: plan.hour, minute: plan.span[1]}, opts) +
|
|
695
|
-
trailingQualifier(
|
|
746
|
+
trailingQualifier(schedule, opts);
|
|
696
747
|
}
|
|
697
748
|
|
|
698
749
|
// A minute window under discrete hours. Like Spanish, the wildcard form
|
|
@@ -703,51 +754,53 @@ function renderMinuteSpanInHour(
|
|
|
703
754
|
// compound instead keeps minute-first and joins the isolated hour with
|
|
704
755
|
// "sekä klo" (mirrors renderCompactClockTimes).
|
|
705
756
|
function renderMinutesAcrossHours(
|
|
706
|
-
|
|
757
|
+
schedule: Schedule,
|
|
707
758
|
plan: Extract<PlanNode, {kind: 'minutesAcrossHours'}>,
|
|
708
759
|
opts: NormalizedOptions
|
|
709
760
|
): string {
|
|
710
761
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence after
|
|
711
762
|
// the minute clause ("joka minuutti, viiden tunnin välein klo 0–20"), not a
|
|
712
763
|
// wall of hour windows.
|
|
713
|
-
const cadence = unevenHourCadence(
|
|
764
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
714
765
|
|
|
715
766
|
if (plan.form === 'wildcard') {
|
|
716
767
|
return cadence ?
|
|
717
|
-
'joka minuutti, ' + cadence + trailingQualifier(
|
|
718
|
-
'joka minuutti ' + hourWindowsFromTimes(
|
|
719
|
-
trailingQualifier(
|
|
768
|
+
'joka minuutti, ' + cadence + trailingQualifier(schedule, opts) :
|
|
769
|
+
'joka minuutti ' + hourWindowsFromTimes(schedule, plan.times, opts) +
|
|
770
|
+
trailingQualifier(schedule, opts);
|
|
720
771
|
}
|
|
721
772
|
|
|
722
773
|
if (cadence !== null) {
|
|
723
|
-
return bareMinutes(
|
|
774
|
+
return bareMinutes(schedule, opts) + ', ' + cadence +
|
|
775
|
+
trailingQualifier(schedule, opts);
|
|
724
776
|
}
|
|
725
777
|
|
|
726
778
|
// Range+isolated hours: minute-first, bare minutes, sekä klo.
|
|
727
|
-
if (hoursAreRangeIsolated(segmentsOf(
|
|
728
|
-
return bareMinutes(
|
|
729
|
-
hourSegmentTimesWithSeka(
|
|
730
|
-
trailingQualifier(
|
|
779
|
+
if (hoursAreRangeIsolated(segmentsOf(schedule, 'hour'))) {
|
|
780
|
+
return bareMinutes(schedule, opts) + ' ' +
|
|
781
|
+
hourSegmentTimesWithSeka(schedule, 0, null, opts) +
|
|
782
|
+
trailingQualifier(schedule, opts);
|
|
731
783
|
}
|
|
732
784
|
|
|
733
785
|
// Range or multi-value list (≥2 points) over enumerated hours →
|
|
734
786
|
// hours-first. A single anchored minute stays minute-first (clock already
|
|
735
787
|
// shows it).
|
|
736
|
-
const hoursStr = kloFromTimes(
|
|
788
|
+
const hoursStr = kloFromTimes(schedule, plan.times, opts);
|
|
737
789
|
|
|
738
|
-
return hoursFirstMinutes(hoursStr,
|
|
790
|
+
return hoursFirstMinutes(hoursStr, schedule, opts) +
|
|
791
|
+
trailingQualifier(schedule, opts);
|
|
739
792
|
}
|
|
740
793
|
|
|
741
794
|
function renderMinuteSpanAcrossHourStep(
|
|
742
|
-
|
|
795
|
+
schedule: Schedule,
|
|
743
796
|
plan: Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>,
|
|
744
797
|
opts: NormalizedOptions
|
|
745
798
|
): string {
|
|
746
799
|
// An hour-step plan's first hour segment is always a step segment.
|
|
747
|
-
const segment = stepSegment(
|
|
800
|
+
const segment = stepSegment(schedule, 'hour');
|
|
748
801
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence; an
|
|
749
802
|
// offset-clean stride keeps its confinement / per-step phrasing.
|
|
750
|
-
const cadence = unevenHourCadence(
|
|
803
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
751
804
|
|
|
752
805
|
// A wildcard span always sets the step off with a comma ("joka
|
|
753
806
|
// minuutti, joka toinen tunti"); a restricted span joins a plain step
|
|
@@ -757,17 +810,18 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
757
810
|
// to every Nth hour; a restricted span is a per-hour window + plain step.
|
|
758
811
|
if (plan.form === 'wildcard') {
|
|
759
812
|
return 'joka minuutti ' + everyNthHour(segment, opts) +
|
|
760
|
-
trailingQualifier(
|
|
813
|
+
trailingQualifier(schedule, opts);
|
|
761
814
|
}
|
|
762
815
|
|
|
763
816
|
// A bounded or uneven stride reads as its bounded cadence after the bare
|
|
764
817
|
// minutes ("minuuteilla 0–30, kahden tunnin välein klo 9–17").
|
|
765
818
|
if (cadence !== null) {
|
|
766
|
-
return bareMinutes(
|
|
819
|
+
return bareMinutes(schedule, opts) + ', ' + cadence +
|
|
820
|
+
trailingQualifier(schedule, opts);
|
|
767
821
|
}
|
|
768
822
|
|
|
769
|
-
return bareMinutes(
|
|
770
|
-
trailingQualifier(
|
|
823
|
+
return bareMinutes(schedule, opts) + hourStepTail(segment, opts) +
|
|
824
|
+
trailingQualifier(schedule, opts);
|
|
771
825
|
}
|
|
772
826
|
|
|
773
827
|
// Whether an hour step reads as the plain "joka toinen tunti" form: a
|
|
@@ -823,65 +877,67 @@ function hourStepTail(segment: StepSegment, opts: NormalizedOptions): string {
|
|
|
823
877
|
// --- Hour renderers. ---
|
|
824
878
|
|
|
825
879
|
function renderEveryHour(
|
|
826
|
-
|
|
880
|
+
schedule: Schedule,
|
|
827
881
|
plan: Extract<PlanNode, {kind: 'everyHour'}>,
|
|
828
882
|
opts: NormalizedOptions
|
|
829
883
|
): string {
|
|
830
|
-
return 'joka tunti' + trailingQualifier(
|
|
884
|
+
return 'joka tunti' + trailingQualifier(schedule, opts);
|
|
831
885
|
}
|
|
832
886
|
|
|
833
887
|
function renderHourRange(
|
|
834
|
-
|
|
888
|
+
schedule: Schedule,
|
|
835
889
|
plan: Extract<PlanNode, {kind: 'hourRange'}>,
|
|
836
890
|
opts: NormalizedOptions
|
|
837
891
|
): string {
|
|
838
892
|
const window = hourWindow(boundedWindow(plan), opts);
|
|
839
893
|
|
|
840
894
|
if (plan.minuteForm === 'wildcard') {
|
|
841
|
-
return 'joka minuutti ' + window + trailingQualifier(
|
|
895
|
+
return 'joka minuutti ' + window + trailingQualifier(schedule, opts);
|
|
842
896
|
}
|
|
843
897
|
|
|
844
898
|
// A minute range over a single hour range renders hours-first
|
|
845
899
|
// ("klo 9.00–17.30 aina minuuttien 0–30 kohdalla").
|
|
846
900
|
if (plan.minuteForm === 'range') {
|
|
847
|
-
return hoursFirstMinutes(window,
|
|
901
|
+
return hoursFirstMinutes(window, schedule, opts) +
|
|
902
|
+
trailingQualifier(schedule, opts);
|
|
848
903
|
}
|
|
849
904
|
|
|
850
905
|
// On the hour the window joins directly ("joka tunti klo 9–17"); a
|
|
851
906
|
// discrete minute anchors its own clause first.
|
|
852
|
-
if (
|
|
853
|
-
return 'joka tunti ' + window + trailingQualifier(
|
|
907
|
+
if (schedule.pattern.minute === '0') {
|
|
908
|
+
return 'joka tunti ' + window + trailingQualifier(schedule, opts);
|
|
854
909
|
}
|
|
855
910
|
|
|
856
911
|
// A single minute makes both window ends exact fires ("klo 9.30–17.30").
|
|
857
|
-
if (
|
|
858
|
-
return atMarks(
|
|
859
|
-
kloRange({hour: plan.from, minute: +
|
|
912
|
+
if (schedule.shapes.minute === 'single') {
|
|
913
|
+
return atMarks(schedule.pattern.minute, units.minute, false) + ' ' +
|
|
914
|
+
kloRange({hour: plan.from, minute: +schedule.pattern.minute},
|
|
860
915
|
{hour: plan.to, minute: plan.last}, opts) +
|
|
861
|
-
trailingQualifier(
|
|
916
|
+
trailingQualifier(schedule, opts);
|
|
862
917
|
}
|
|
863
918
|
|
|
864
919
|
// A minute list (≥2 values) over a single hour range renders hours-first
|
|
865
920
|
// ("klo 9.00–17.30 aina minuuttien 0 ja 30 kohdalla").
|
|
866
|
-
return hoursFirstMinutes(window,
|
|
921
|
+
return hoursFirstMinutes(window, schedule, opts) +
|
|
922
|
+
trailingQualifier(schedule, opts);
|
|
867
923
|
}
|
|
868
924
|
|
|
869
925
|
function renderHourStep(
|
|
870
|
-
|
|
926
|
+
schedule: Schedule,
|
|
871
927
|
plan: Extract<PlanNode, {kind: 'hourStep'}>,
|
|
872
928
|
opts: NormalizedOptions
|
|
873
929
|
): string {
|
|
874
930
|
// A bounded or uneven hour step reads as its endpoint-pinning cadence
|
|
875
931
|
// ("kahden tunnin välein klo 9–17"); an offset-clean step keeps its bare or
|
|
876
932
|
// "alkaen" cadence.
|
|
877
|
-
const cadence = unevenHourCadence(
|
|
933
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
878
934
|
|
|
879
935
|
if (cadence !== null) {
|
|
880
|
-
return cadence + trailingQualifier(
|
|
936
|
+
return cadence + trailingQualifier(schedule, opts);
|
|
881
937
|
}
|
|
882
938
|
|
|
883
|
-
return stepHours(stepSegment(
|
|
884
|
-
trailingQualifier(
|
|
939
|
+
return stepHours(stepSegment(schedule, 'hour'), opts) +
|
|
940
|
+
trailingQualifier(schedule, opts);
|
|
885
941
|
}
|
|
886
942
|
|
|
887
943
|
// The hour-range plan as a window whose closing minute honors `boundMinute`:
|
|
@@ -903,17 +959,17 @@ function hourWindow(window: HourWindow, opts: NormalizedOptions): string {
|
|
|
903
959
|
|
|
904
960
|
// "joka päivä klo 9.30 ja 17.30".
|
|
905
961
|
function renderClockTimes(
|
|
906
|
-
|
|
962
|
+
schedule: Schedule,
|
|
907
963
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
908
964
|
opts: NormalizedOptions
|
|
909
965
|
): string {
|
|
910
966
|
// An hour step or range (or arithmetic-progression hour list) under a single
|
|
911
967
|
// pinned minute reads as a cadence or window rather than a cross-product of
|
|
912
968
|
// clock times.
|
|
913
|
-
if (
|
|
914
|
-
const minute = +
|
|
915
|
-
const cadence = hourCadence(
|
|
916
|
-
hourRangeCadence(
|
|
969
|
+
if (schedule.shapes.minute === 'single') {
|
|
970
|
+
const minute = +schedule.pattern.minute;
|
|
971
|
+
const cadence = hourCadence(schedule, minute, opts) ??
|
|
972
|
+
hourRangeCadence(schedule, minute, opts);
|
|
917
973
|
|
|
918
974
|
if (cadence !== null) {
|
|
919
975
|
return cadence;
|
|
@@ -923,7 +979,7 @@ function renderClockTimes(
|
|
|
923
979
|
if (plan.times.length === 1) {
|
|
924
980
|
const time = plan.times[0];
|
|
925
981
|
|
|
926
|
-
return leadingQualifier(
|
|
982
|
+
return leadingQualifier(schedule, opts) +
|
|
927
983
|
timeWord(time.hour, time.minute, time.second, opts);
|
|
928
984
|
}
|
|
929
985
|
|
|
@@ -931,7 +987,7 @@ function renderClockTimes(
|
|
|
931
987
|
return timeDigits(time.hour, time.minute, time.second, opts);
|
|
932
988
|
});
|
|
933
989
|
|
|
934
|
-
return leadingQualifier(
|
|
990
|
+
return leadingQualifier(schedule, opts) + 'klo ' + joinList(digits);
|
|
935
991
|
}
|
|
936
992
|
|
|
937
993
|
// Compact form past the enumeration cap: a single minute folds into
|
|
@@ -939,7 +995,7 @@ function renderClockTimes(
|
|
|
939
995
|
// A minute list over enumerated (non-range+isolated) hours renders
|
|
940
996
|
// hours-first; a range+isolated hour pattern joins with "sekä klo".
|
|
941
997
|
function renderCompactClockTimes(
|
|
942
|
-
|
|
998
|
+
schedule: Schedule,
|
|
943
999
|
plan: Extract<PlanNode, {kind: 'compactClockTimes'}>,
|
|
944
1000
|
opts: NormalizedOptions
|
|
945
1001
|
): string {
|
|
@@ -947,15 +1003,15 @@ function renderCompactClockTimes(
|
|
|
947
1003
|
// minute reads as a cadence, not a wall of clock times. (Returns null for an
|
|
948
1004
|
// irregular list or a range, which keep folding below.)
|
|
949
1005
|
if (plan.fold) {
|
|
950
|
-
const cadence = hourCadence(
|
|
951
|
-
hourRangeCadence(
|
|
1006
|
+
const cadence = hourCadence(schedule, plan.minute, opts) ??
|
|
1007
|
+
hourRangeCadence(schedule, plan.minute, opts);
|
|
952
1008
|
|
|
953
1009
|
if (cadence !== null) {
|
|
954
1010
|
return cadence;
|
|
955
1011
|
}
|
|
956
1012
|
}
|
|
957
1013
|
|
|
958
|
-
const hourSegs = segmentsOf(
|
|
1014
|
+
const hourSegs = segmentsOf(schedule, 'hour');
|
|
959
1015
|
|
|
960
1016
|
// Range+isolated hours: join the isolated hour with "sekä klo" to stop it
|
|
961
1017
|
// reading as a range extension. For the folded path (single minute folded
|
|
@@ -963,38 +1019,43 @@ function renderCompactClockTimes(
|
|
|
963
1019
|
// path use bare-minutes-first with a trailing qualifier.
|
|
964
1020
|
if (hoursAreRangeIsolated(hourSegs)) {
|
|
965
1021
|
if (plan.fold) {
|
|
966
|
-
return leadingQualifier(
|
|
967
|
-
hourSegmentTimesWithSeka(
|
|
968
|
-
|
|
1022
|
+
return leadingQualifier(schedule, opts) +
|
|
1023
|
+
hourSegmentTimesWithSeka(schedule, plan.minute,
|
|
1024
|
+
schedule.analyses.clockSecond, opts);
|
|
969
1025
|
}
|
|
970
1026
|
|
|
971
|
-
const phrase = bareMinutes(
|
|
972
|
-
hourSegmentTimesWithSeka(
|
|
973
|
-
trailingQualifier(
|
|
1027
|
+
const phrase = bareMinutes(schedule, opts) + ' ' +
|
|
1028
|
+
hourSegmentTimesWithSeka(schedule, 0, null, opts) +
|
|
1029
|
+
trailingQualifier(schedule, opts);
|
|
974
1030
|
|
|
975
|
-
return
|
|
976
|
-
secondsLeadClause(
|
|
1031
|
+
return schedule.analyses.clockSecond ?
|
|
1032
|
+
secondsLeadClause(schedule, opts) + ', ' + phrase :
|
|
977
1033
|
phrase;
|
|
978
1034
|
}
|
|
979
1035
|
|
|
980
1036
|
if (plan.fold) {
|
|
981
|
-
return leadingQualifier(
|
|
982
|
-
hourSegmentTimes(
|
|
1037
|
+
return leadingQualifier(schedule, opts) +
|
|
1038
|
+
hourSegmentTimes(
|
|
1039
|
+
schedule, plan.minute, schedule.analyses.clockSecond, opts
|
|
1040
|
+
);
|
|
983
1041
|
}
|
|
984
1042
|
|
|
985
1043
|
// A bounded or uneven hour stride reads as its endpoint-pinning cadence after
|
|
986
1044
|
// the bare minute clause ("minuuteilla 0, 25 ja 50, viiden tunnin välein klo
|
|
987
1045
|
// 0–20"), not a wall of clock-time columns.
|
|
988
|
-
const cadence = unevenHourCadence(
|
|
1046
|
+
const cadence = unevenHourCadence(schedule, opts);
|
|
989
1047
|
const phrase = cadence ?
|
|
990
|
-
bareMinutes(
|
|
1048
|
+
bareMinutes(schedule, opts) + ', ' + cadence +
|
|
1049
|
+
trailingQualifier(schedule, opts) :
|
|
991
1050
|
// A minute list over purely enumerated hours (step fires, all singles) —
|
|
992
1051
|
// hours-first, drop "joka tunti".
|
|
993
|
-
hoursFirstMinutes(
|
|
994
|
-
|
|
1052
|
+
hoursFirstMinutes(
|
|
1053
|
+
hourSegmentTimes(schedule, 0, null, opts), schedule, opts
|
|
1054
|
+
) +
|
|
1055
|
+
trailingQualifier(schedule, opts);
|
|
995
1056
|
|
|
996
|
-
return
|
|
997
|
-
secondsLeadClause(
|
|
1057
|
+
return schedule.analyses.clockSecond ?
|
|
1058
|
+
secondsLeadClause(schedule, opts) + ', ' + phrase :
|
|
998
1059
|
phrase;
|
|
999
1060
|
}
|
|
1000
1061
|
|
|
@@ -1046,25 +1107,21 @@ interface Stride {
|
|
|
1046
1107
|
function renderStride(stride: Stride, opts: NormalizedOptions): string {
|
|
1047
1108
|
const {interval, start, last, cycle, unit} = stride;
|
|
1048
1109
|
const cadence = genitive(interval, opts) + ' ' + unit.gen + ' välein';
|
|
1049
|
-
const tiles = cycle % interval === 0;
|
|
1050
|
-
|
|
1051
|
-
if (start === 0 && tiles) {
|
|
1052
|
-
return cadence;
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
if (start < interval && tiles) {
|
|
1056
|
-
return cadence + ' ' + unit.anchor + ' ' + unit.ela + ' ' + start +
|
|
1057
|
-
' alkaen';
|
|
1058
|
-
}
|
|
1059
1110
|
|
|
1060
|
-
return
|
|
1111
|
+
return chooseStride({start, interval, cycle}, {
|
|
1112
|
+
bare: () => cadence,
|
|
1113
|
+
offset: () =>
|
|
1114
|
+
cadence + ' ' + unit.anchor + ' ' + unit.ela + ' ' + start + ' alkaen',
|
|
1115
|
+
bounded: () =>
|
|
1116
|
+
cadence + ' ' + unit.ela + ' ' + start + ' ' + unit.ill + ' ' + last
|
|
1117
|
+
});
|
|
1061
1118
|
}
|
|
1062
1119
|
|
|
1063
1120
|
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
1064
1121
|
// form an arithmetic progression long enough to beat the list (the core
|
|
1065
|
-
// enumerates an offset/uneven step to this fire list; the
|
|
1066
|
-
// the renderer recognizes the progression). Returns null for a
|
|
1067
|
-
// or a too-short list, leaving the caller to enumerate.
|
|
1122
|
+
// enumerates an offset/uneven step to this fire list; the Schedule is
|
|
1123
|
+
// unchanged, so the renderer recognizes the progression). Returns null for a
|
|
1124
|
+
// non-progression or a too-short list, leaving the caller to enumerate.
|
|
1068
1125
|
function strideFromSegments(
|
|
1069
1126
|
segments: Segment[],
|
|
1070
1127
|
unit: UnitForms,
|
|
@@ -1148,29 +1205,25 @@ function hourStrideCadence(
|
|
|
1148
1205
|
): string {
|
|
1149
1206
|
const {start, interval, last} = stride;
|
|
1150
1207
|
const cadence = genitive(interval, opts) + ' tunnin välein';
|
|
1151
|
-
const tiles = 24 % interval === 0;
|
|
1152
|
-
|
|
1153
|
-
if (start === 0 && tiles) {
|
|
1154
|
-
return cadence;
|
|
1155
|
-
}
|
|
1156
1208
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1209
|
+
return chooseStride({start, interval, cycle: 24}, {
|
|
1210
|
+
bare: () => cadence,
|
|
1211
|
+
offset: () => cadence + ' klo ' + hourElatives[start] + ' alkaen',
|
|
1212
|
+
bounded: () => cadence + ' ' +
|
|
1213
|
+
kloRange({hour: start, minute: 0}, {hour: last, minute: 0}, opts)
|
|
1214
|
+
});
|
|
1163
1215
|
}
|
|
1164
1216
|
|
|
1165
1217
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1166
1218
|
// segment yields its {start, interval, last} directly; an all-single hour list
|
|
1167
1219
|
// yields one only when its values form a step progression (so an irregular list
|
|
1168
|
-
// like 9,17 keeps enumerating). The
|
|
1169
|
-
// the stride and speaks it as a cadence, not the clock-time
|
|
1220
|
+
// like 9,17 keeps enumerating). The Schedule is unchanged — the renderer
|
|
1221
|
+
// recognizes the stride and speaks it as a cadence, not the clock-time
|
|
1222
|
+
// cross-product.
|
|
1170
1223
|
function hourStride(
|
|
1171
|
-
|
|
1224
|
+
schedule: Schedule
|
|
1172
1225
|
): {start: number; interval: number; last: number} | null {
|
|
1173
|
-
const segments =
|
|
1226
|
+
const segments = schedule.analyses.segments.hour;
|
|
1174
1227
|
|
|
1175
1228
|
// A wildcard hour carries no segments (no discrete hours to stride over).
|
|
1176
1229
|
if (!segments) {
|
|
@@ -1206,8 +1259,10 @@ function hourStride(
|
|
|
1206
1259
|
// ("…, viiden tunnin välein klo 0–20") than as a wall of clock times. An
|
|
1207
1260
|
// offset-clean stride keeps its existing confinement form, so only the
|
|
1208
1261
|
// endpoint-bearing case routes here.
|
|
1209
|
-
function unevenHourCadence(
|
|
1210
|
-
|
|
1262
|
+
function unevenHourCadence(
|
|
1263
|
+
schedule: Schedule, opts: NormalizedOptions
|
|
1264
|
+
): string | null {
|
|
1265
|
+
const stride = hourStride(schedule);
|
|
1211
1266
|
|
|
1212
1267
|
if (!stride || offsetCleanStride(stride)) {
|
|
1213
1268
|
return null;
|
|
@@ -1219,8 +1274,8 @@ function unevenHourCadence(ir: IR, opts: NormalizedOptions): string | null {
|
|
|
1219
1274
|
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
1220
1275
|
// fills the minute (a "minuutin ajan" frame at minute 0); a single 0 is just
|
|
1221
1276
|
// the top of the minute (no clause); anything else needs its own clause.
|
|
1222
|
-
function subMinuteSecond(
|
|
1223
|
-
return
|
|
1277
|
+
function subMinuteSecond(schedule: Schedule): boolean {
|
|
1278
|
+
return schedule.pattern.second === '*' || schedule.shapes.second === 'step';
|
|
1224
1279
|
}
|
|
1225
1280
|
|
|
1226
1281
|
// The lead clause for an hour-cadence rendering: the second and the pinned
|
|
@@ -1230,25 +1285,25 @@ function subMinuteSecond(ir: IR): boolean {
|
|
|
1230
1285
|
// ajan" frame (the whole minute-0 window). A non-zero minute is a real clock
|
|
1231
1286
|
// minute: the second leads with its own clause (if any), then the minute reads
|
|
1232
1287
|
// at its bare "kohdalla" mark.
|
|
1233
|
-
function hourCadenceLead(
|
|
1288
|
+
function hourCadenceLead(schedule: Schedule, minute: number,
|
|
1234
1289
|
opts: NormalizedOptions): string {
|
|
1235
1290
|
if (minute === 0) {
|
|
1236
|
-
if (subMinuteSecond(
|
|
1237
|
-
return secondsLeadClause(
|
|
1291
|
+
if (subMinuteSecond(schedule)) {
|
|
1292
|
+
return secondsLeadClause(schedule, opts) + ' minuutin ajan';
|
|
1238
1293
|
}
|
|
1239
1294
|
|
|
1240
|
-
return secondsLeadClause(
|
|
1295
|
+
return secondsLeadClause(schedule, opts);
|
|
1241
1296
|
}
|
|
1242
1297
|
|
|
1243
1298
|
const minutePhrase = atMarks(String(minute), units.minute, false);
|
|
1244
1299
|
|
|
1245
1300
|
// A single 0 second is just the top of the minute, so the minute leads
|
|
1246
1301
|
// alone; any other second prefixes its own clause.
|
|
1247
|
-
if (
|
|
1302
|
+
if (schedule.pattern.second === '0') {
|
|
1248
1303
|
return minutePhrase;
|
|
1249
1304
|
}
|
|
1250
1305
|
|
|
1251
|
-
return secondsLeadClause(
|
|
1306
|
+
return secondsLeadClause(schedule, opts) + ', ' + minutePhrase;
|
|
1252
1307
|
}
|
|
1253
1308
|
|
|
1254
1309
|
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
@@ -1259,10 +1314,10 @@ function hourCadenceLead(ir: IR, minute: number,
|
|
|
1259
1314
|
// enumeration is no longer than the cadence: a meaningful second makes every
|
|
1260
1315
|
// clock time three digit-groups, so any stride is worth compacting; otherwise
|
|
1261
1316
|
// the stride must exceed the clock-time cap, the same point at which the core
|
|
1262
|
-
// itself stops enumerating. Renderer-only; the
|
|
1263
|
-
function hourCadence(
|
|
1317
|
+
// itself stops enumerating. Renderer-only; the Schedule is unchanged.
|
|
1318
|
+
function hourCadence(schedule: Schedule, minute: number,
|
|
1264
1319
|
opts: NormalizedOptions): string | null {
|
|
1265
|
-
const stride = hourStride(
|
|
1320
|
+
const stride = hourStride(schedule);
|
|
1266
1321
|
|
|
1267
1322
|
if (!stride) {
|
|
1268
1323
|
return null;
|
|
@@ -1275,7 +1330,7 @@ function hourCadence(ir: IR, minute: number,
|
|
|
1275
1330
|
// or "alkaen" form is no shorter than the list. A bounded or uneven stride
|
|
1276
1331
|
// has no clean wrap, so its endpoint-pinning cadence ("viiden tunnin välein
|
|
1277
1332
|
// klo 0–20") reads better however short.
|
|
1278
|
-
if (
|
|
1333
|
+
if (schedule.pattern.second === '0' && fires <= maxClockTimes &&
|
|
1279
1334
|
offsetCleanStride(stride)) {
|
|
1280
1335
|
return null;
|
|
1281
1336
|
}
|
|
@@ -1284,25 +1339,25 @@ function hourCadence(ir: IR, minute: number,
|
|
|
1284
1339
|
// stride is a confinement, not a juxtaposed cadence: it reads "minuutin ajan
|
|
1285
1340
|
// joka toisen tunnin aikana", reusing the every-Nth-hour idiom so the
|
|
1286
1341
|
// minute-0 window is never heard as the bare hour cadence.
|
|
1287
|
-
const segment = segmentsOf(
|
|
1288
|
-
const confined = minute === 0 && subMinuteSecond(
|
|
1289
|
-
segmentsOf(
|
|
1342
|
+
const segment = segmentsOf(schedule, 'hour')[0];
|
|
1343
|
+
const confined = minute === 0 && subMinuteSecond(schedule) &&
|
|
1344
|
+
segmentsOf(schedule, 'hour').length === 1 && segment.kind === 'step' &&
|
|
1290
1345
|
cleanHourStride(segment);
|
|
1291
1346
|
|
|
1292
1347
|
if (confined) {
|
|
1293
|
-
return secondsLeadClause(
|
|
1294
|
-
everyNthHour(segment, opts) + trailingQualifier(
|
|
1348
|
+
return secondsLeadClause(schedule, opts) + ' minuutin ajan ' +
|
|
1349
|
+
everyNthHour(segment, opts) + trailingQualifier(schedule, opts);
|
|
1295
1350
|
}
|
|
1296
1351
|
|
|
1297
1352
|
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
1298
1353
|
// lead clause to fold in, so the bounded cadence stands on its own ("viiden
|
|
1299
1354
|
// tunnin välein klo 0–20").
|
|
1300
|
-
if (minute === 0 &&
|
|
1301
|
-
return hourStrideCadence(stride, opts) + trailingQualifier(
|
|
1355
|
+
if (minute === 0 && schedule.pattern.second === '0') {
|
|
1356
|
+
return hourStrideCadence(stride, opts) + trailingQualifier(schedule, opts);
|
|
1302
1357
|
}
|
|
1303
1358
|
|
|
1304
|
-
return hourCadenceLead(
|
|
1305
|
-
hourStrideCadence(stride, opts) + trailingQualifier(
|
|
1359
|
+
return hourCadenceLead(schedule, minute, opts) + ', ' +
|
|
1360
|
+
hourStrideCadence(stride, opts) + trailingQualifier(schedule, opts);
|
|
1306
1361
|
}
|
|
1307
1362
|
|
|
1308
1363
|
// Whether an hour step is a clean stride over the whole day — unbounded,
|
|
@@ -1322,8 +1377,8 @@ function cleanHourStride(segment: StepSegment): boolean {
|
|
|
1322
1377
|
// range — and so forms a window rather than a cross-product of clock times.
|
|
1323
1378
|
// A pure single-value list (9,17) has no range to span and still enumerates;
|
|
1324
1379
|
// a step is handled by hourStride/hourCadence.
|
|
1325
|
-
function hasHourWindow(
|
|
1326
|
-
const segments =
|
|
1380
|
+
function hasHourWindow(schedule: Schedule): boolean {
|
|
1381
|
+
const segments = schedule.analyses.segments.hour;
|
|
1327
1382
|
|
|
1328
1383
|
return !!segments && segments.some(function range(segment: Segment) {
|
|
1329
1384
|
return segment.kind === 'range';
|
|
@@ -1335,10 +1390,12 @@ function hasHourWindow(ir: IR): boolean {
|
|
|
1335
1390
|
// with "sekä klo" ("klo 9–20 sekä klo 22"), the same idiom the bare folded
|
|
1336
1391
|
// window uses. The minute has folded into the lead, so the window closes on
|
|
1337
1392
|
// the top of its final hour.
|
|
1338
|
-
function hourRangeWindowTail(
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1393
|
+
function hourRangeWindowTail(
|
|
1394
|
+
schedule: Schedule, opts: NormalizedOptions
|
|
1395
|
+
): string {
|
|
1396
|
+
return segmentsOf(schedule, 'hour').length === 1 ?
|
|
1397
|
+
hourSegmentTimes(schedule, 0, null, opts) :
|
|
1398
|
+
hourSegmentTimesWithSeka(schedule, 0, null, opts);
|
|
1342
1399
|
}
|
|
1343
1400
|
|
|
1344
1401
|
// Render an hour range (or a list whose segments include a range) under
|
|
@@ -1347,24 +1404,25 @@ function hourRangeWindowTail(ir: IR, opts: NormalizedOptions): string {
|
|
|
1347
1404
|
// clock times. The hour-RANGE analog of hourCadence. Returns null when the
|
|
1348
1405
|
// hour has no range, when the minute is non-zero (a real clock minute the
|
|
1349
1406
|
// existing window form already speaks), or when a plain :00 set carries no
|
|
1350
|
-
// clause. Renderer-only; the
|
|
1351
|
-
function hourRangeCadence(
|
|
1407
|
+
// clause. Renderer-only; the Schedule is unchanged.
|
|
1408
|
+
function hourRangeCadence(schedule: Schedule, minute: number,
|
|
1352
1409
|
opts: NormalizedOptions): string | null {
|
|
1353
|
-
if (minute !== 0 || !hasHourWindow(
|
|
1410
|
+
if (minute !== 0 || !hasHourWindow(schedule) ||
|
|
1411
|
+
schedule.pattern.second === '0') {
|
|
1354
1412
|
return null;
|
|
1355
1413
|
}
|
|
1356
1414
|
|
|
1357
|
-
const tail = hourRangeWindowTail(
|
|
1415
|
+
const tail = hourRangeWindowTail(schedule, opts);
|
|
1358
1416
|
|
|
1359
1417
|
// A wildcard or sub-minute step second is the whole minute-0 window
|
|
1360
1418
|
// ("minuutin ajan", carried by hourCadenceLead), then the window — kept
|
|
1361
1419
|
// distinct from the bare "joka tunti klo 9–17" so the confinement is never
|
|
1362
1420
|
// heard as it (the hour-range analog of "minuutin ajan joka toisen tunnin
|
|
1363
1421
|
// aikana"). A meaningful second leads at its mark, then the window.
|
|
1364
|
-
const joiner = subMinuteSecond(
|
|
1422
|
+
const joiner = subMinuteSecond(schedule) ? ' ' : ', ';
|
|
1365
1423
|
|
|
1366
|
-
return hourCadenceLead(
|
|
1367
|
-
trailingQualifier(
|
|
1424
|
+
return hourCadenceLead(schedule, minute, opts) + joiner + tail +
|
|
1425
|
+
trailingQualifier(schedule, opts);
|
|
1368
1426
|
}
|
|
1369
1427
|
|
|
1370
1428
|
// --- Hour-time phrasing. ---
|
|
@@ -1383,7 +1441,7 @@ function kloList(hours: number[], opts: NormalizedOptions): string {
|
|
|
1383
1441
|
// The hour times accompanying a lead clause, with long expansions
|
|
1384
1442
|
// rendered segment by segment.
|
|
1385
1443
|
function kloFromTimes(
|
|
1386
|
-
|
|
1444
|
+
schedule: Schedule,
|
|
1387
1445
|
times: HourTimesPlan,
|
|
1388
1446
|
opts: NormalizedOptions
|
|
1389
1447
|
): string {
|
|
@@ -1391,7 +1449,7 @@ function kloFromTimes(
|
|
|
1391
1449
|
return kloList(times.fires, opts);
|
|
1392
1450
|
}
|
|
1393
1451
|
|
|
1394
|
-
return hourSegmentTimes(
|
|
1452
|
+
return hourSegmentTimes(schedule, 0, null, opts);
|
|
1395
1453
|
}
|
|
1396
1454
|
|
|
1397
1455
|
// The hours accompanying a named-once minute clause under an hour list or
|
|
@@ -1401,7 +1459,7 @@ function kloFromTimes(
|
|
|
1401
1459
|
// per-segment window ("klo 8.00–18.59 ja 22.00–22.59"), mirroring the other
|
|
1402
1460
|
// languages, which list discrete hours but keep range windows.
|
|
1403
1461
|
function hourWindowsFromTimes(
|
|
1404
|
-
|
|
1462
|
+
schedule: Schedule,
|
|
1405
1463
|
times: HourTimesPlan,
|
|
1406
1464
|
opts: NormalizedOptions
|
|
1407
1465
|
): string {
|
|
@@ -1409,7 +1467,7 @@ function hourWindowsFromTimes(
|
|
|
1409
1467
|
return kloList(times.fires, opts);
|
|
1410
1468
|
}
|
|
1411
1469
|
|
|
1412
|
-
const segments = segmentsOf(
|
|
1470
|
+
const segments = segmentsOf(schedule, 'hour');
|
|
1413
1471
|
|
|
1414
1472
|
if (!segments.some(function ranged(segment: Segment) {
|
|
1415
1473
|
return segment.kind === 'range';
|
|
@@ -1463,14 +1521,14 @@ function hourWindowDigits(hour: number, opts: NormalizedOptions): string {
|
|
|
1463
1521
|
// klo, the minute (and optional second) folded into each:
|
|
1464
1522
|
// "klo 9.30–20.30 ja 22.30".
|
|
1465
1523
|
function hourSegmentTimes(
|
|
1466
|
-
|
|
1524
|
+
schedule: Schedule,
|
|
1467
1525
|
minute: number,
|
|
1468
1526
|
second: number | null | undefined,
|
|
1469
1527
|
opts: NormalizedOptions
|
|
1470
1528
|
): string {
|
|
1471
1529
|
const pieces: string[] = [];
|
|
1472
1530
|
|
|
1473
|
-
segmentsOf(
|
|
1531
|
+
segmentsOf(schedule, 'hour').forEach(function clock(segment: Segment) {
|
|
1474
1532
|
if (segment.kind === 'step') {
|
|
1475
1533
|
pieces.push(...segment.fires.map(function each(hour: number) {
|
|
1476
1534
|
return timeDigits(hour, minute, second, opts);
|
|
@@ -1562,30 +1620,30 @@ function timeDigits(
|
|
|
1562
1620
|
|
|
1563
1621
|
// The qualifier that precedes clock times: "joka päivä ",
|
|
1564
1622
|
// "maanantaisin ", "kuukauden 13. päivänä ".
|
|
1565
|
-
function leadingQualifier(
|
|
1566
|
-
const pattern =
|
|
1623
|
+
function leadingQualifier(schedule: Schedule, opts: NormalizedOptions): string {
|
|
1624
|
+
const pattern = schedule.pattern;
|
|
1567
1625
|
|
|
1568
1626
|
// When a restricted-month union is active, describe() assembles the full
|
|
1569
1627
|
// compound; suppress the qualifier here so render() returns only the
|
|
1570
1628
|
// time/frequency part.
|
|
1571
|
-
if (restrictedMonthUnion(
|
|
1629
|
+
if (restrictedMonthUnion(schedule)) {
|
|
1572
1630
|
return '';
|
|
1573
1631
|
}
|
|
1574
1632
|
|
|
1575
1633
|
if (pattern.date !== '*' && pattern.weekday !== '*') {
|
|
1576
|
-
return dateOrWeekday(
|
|
1634
|
+
return dateOrWeekday(schedule, opts) + ' ';
|
|
1577
1635
|
}
|
|
1578
1636
|
|
|
1579
1637
|
if (pattern.date !== '*') {
|
|
1580
|
-
return datePhrase(
|
|
1638
|
+
return datePhrase(schedule, opts) + ' ';
|
|
1581
1639
|
}
|
|
1582
1640
|
|
|
1583
1641
|
if (pattern.weekday !== '*') {
|
|
1584
|
-
return weekdayQualifier(
|
|
1642
|
+
return weekdayQualifier(schedule) + monthScope(schedule) + ' ';
|
|
1585
1643
|
}
|
|
1586
1644
|
|
|
1587
1645
|
if (pattern.month !== '*') {
|
|
1588
|
-
return 'joka päivä ' + monthPhrase(
|
|
1646
|
+
return 'joka päivä ' + monthPhrase(schedule) + ' ';
|
|
1589
1647
|
}
|
|
1590
1648
|
|
|
1591
1649
|
return 'joka päivä ';
|
|
@@ -1593,57 +1651,64 @@ function leadingQualifier(ir: IR, opts: NormalizedOptions): string {
|
|
|
1593
1651
|
|
|
1594
1652
|
// The qualifier trailing a frequency: " maanantaisin", " kesäkuussa",
|
|
1595
1653
|
// " kuukauden 13. päivänä". Empty when no day-level field is set.
|
|
1596
|
-
function trailingQualifier(
|
|
1597
|
-
|
|
1654
|
+
function trailingQualifier(
|
|
1655
|
+
schedule: Schedule, opts: NormalizedOptions
|
|
1656
|
+
): string {
|
|
1657
|
+
const pattern = schedule.pattern;
|
|
1598
1658
|
|
|
1599
1659
|
// When a restricted-month union is active, describe() assembles the full
|
|
1600
1660
|
// compound; suppress the qualifier here so render() returns only the
|
|
1601
1661
|
// time/frequency part.
|
|
1602
|
-
if (restrictedMonthUnion(
|
|
1662
|
+
if (restrictedMonthUnion(schedule)) {
|
|
1603
1663
|
return '';
|
|
1604
1664
|
}
|
|
1605
1665
|
|
|
1606
1666
|
if (pattern.date !== '*' && pattern.weekday !== '*') {
|
|
1607
|
-
return ' ' + dateOrWeekday(
|
|
1667
|
+
return ' ' + dateOrWeekday(schedule, opts);
|
|
1608
1668
|
}
|
|
1609
1669
|
|
|
1610
1670
|
if (pattern.date !== '*') {
|
|
1611
|
-
return ' ' + datePhrase(
|
|
1671
|
+
return ' ' + datePhrase(schedule, opts);
|
|
1612
1672
|
}
|
|
1613
1673
|
|
|
1614
1674
|
if (pattern.weekday !== '*') {
|
|
1615
|
-
return ' ' + weekdayQualifier(
|
|
1675
|
+
return ' ' + weekdayQualifier(schedule) + monthScope(schedule);
|
|
1616
1676
|
}
|
|
1617
1677
|
|
|
1618
1678
|
if (pattern.month !== '*') {
|
|
1619
|
-
return ' ' + monthPhrase(
|
|
1679
|
+
return ' ' + monthPhrase(schedule);
|
|
1620
1680
|
}
|
|
1621
1681
|
|
|
1622
1682
|
return '';
|
|
1623
1683
|
}
|
|
1624
1684
|
|
|
1625
1685
|
// "kuukauden 13. päivänä tai perjantaisin": cron fires when either the
|
|
1626
|
-
// date or the weekday matches. Only reachable when date≠*
|
|
1627
|
-
// AND month=* (the restricted-month union is handled in
|
|
1628
|
-
// so monthScope always returns '' here.
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1686
|
+
// date or the weekday matches (inclusive union). Only reachable when date≠*
|
|
1687
|
+
// AND weekday≠* AND month=* (the restricted-month union is handled in
|
|
1688
|
+
// describe()), so monthScope always returns '' here. An open `*/2` date
|
|
1689
|
+
// reads as the odd-day parity class (not the continuous "joka toinen
|
|
1690
|
+
// päivä"); a Mon–Fri weekday reads as the recurring class "arkisin".
|
|
1691
|
+
function dateOrWeekday(schedule: Schedule, opts: NormalizedOptions): string {
|
|
1692
|
+
const dateArm = oddDayUnion(schedule.pattern.date) ||
|
|
1693
|
+
datePhrase(schedule, opts);
|
|
1694
|
+
|
|
1695
|
+
return dateArm + ' tai ' + unionWeekdayArm(schedule) +
|
|
1696
|
+
monthScope(schedule);
|
|
1632
1697
|
}
|
|
1633
1698
|
|
|
1634
1699
|
// The weekday qualifier: distributive lists ("maanantaisin,
|
|
1635
1700
|
// keskiviikkoisin ja perjantaisin") and elative–illative ranges
|
|
1636
1701
|
// ("maanantaista perjantaihin"). Step segments flatten into their fires.
|
|
1637
|
-
function weekdayQualifier(
|
|
1638
|
-
const quartz = quartzWeekdayPhrase(
|
|
1702
|
+
function weekdayQualifier(schedule: Schedule): string {
|
|
1703
|
+
const quartz = quartzWeekdayPhrase(schedule.pattern.weekday);
|
|
1639
1704
|
|
|
1640
1705
|
if (quartz) {
|
|
1641
1706
|
return quartz;
|
|
1642
1707
|
}
|
|
1643
1708
|
|
|
1644
1709
|
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
1645
|
-
// form. The
|
|
1646
|
-
const segments = orderWeekdaysForDisplay(segmentsOf(
|
|
1710
|
+
// form. The Schedule stays canonical (Sunday=0). The helper flattens steps.
|
|
1711
|
+
const segments = orderWeekdaysForDisplay(segmentsOf(schedule, 'weekday'));
|
|
1647
1712
|
|
|
1648
1713
|
return joinList(segments.map(function piece(segment: FlatSegment) {
|
|
1649
1714
|
if (segment.kind === 'range') {
|
|
@@ -1658,8 +1723,8 @@ function weekdayQualifier(ir: IR): string {
|
|
|
1658
1723
|
// The month qualifier: inessive names ("kesäkuussa ja joulukuussa") and
|
|
1659
1724
|
// elative–illative ranges ("kesäkuusta syyskuuhun"). The case endings
|
|
1660
1725
|
// keep mixed lists unambiguous with no preposition bookkeeping.
|
|
1661
|
-
function monthPhrase(
|
|
1662
|
-
const segments = flattenSteps(segmentsOf(
|
|
1726
|
+
function monthPhrase(schedule: Schedule): string {
|
|
1727
|
+
const segments = flattenSteps(segmentsOf(schedule, 'month'));
|
|
1663
1728
|
|
|
1664
1729
|
return joinList(segments.map(function piece(segment: FlatSegment) {
|
|
1665
1730
|
if (segment.kind === 'range') {
|
|
@@ -1673,12 +1738,12 @@ function monthPhrase(ir: IR): string {
|
|
|
1673
1738
|
|
|
1674
1739
|
// A trailing month scope on weekday qualifiers ("maanantaisin
|
|
1675
1740
|
// kesäkuussa").
|
|
1676
|
-
function monthScope(
|
|
1677
|
-
if (
|
|
1741
|
+
function monthScope(schedule: Schedule): string {
|
|
1742
|
+
if (schedule.pattern.month === '*') {
|
|
1678
1743
|
return '';
|
|
1679
1744
|
}
|
|
1680
1745
|
|
|
1681
|
-
return ' ' + monthPhrase(
|
|
1746
|
+
return ' ' + monthPhrase(schedule);
|
|
1682
1747
|
}
|
|
1683
1748
|
|
|
1684
1749
|
// Expand step segments into their fires as singles: the flat fires read
|
|
@@ -1696,20 +1761,21 @@ function flattenSteps(segments: Segment[]): FlatSegment[] {
|
|
|
1696
1761
|
// The date qualifier: "kuukauden 13. päivänä", "tammikuun 1. päivänä",
|
|
1697
1762
|
// "joka kolmannen kuukauden 1. päivänä", or a Quartz phrase. A foldable
|
|
1698
1763
|
// single year joins the date ("joulukuun 25. päivänä vuonna 2030").
|
|
1699
|
-
function datePhrase(
|
|
1700
|
-
const pattern =
|
|
1764
|
+
function datePhrase(schedule: Schedule, opts: NormalizedOptions): string {
|
|
1765
|
+
const pattern = schedule.pattern;
|
|
1701
1766
|
const quartz = quartzDatePhrase(pattern.date);
|
|
1702
1767
|
|
|
1703
1768
|
if (quartz) {
|
|
1704
|
-
return quartz + monthScope(
|
|
1769
|
+
return quartz + monthScope(schedule);
|
|
1705
1770
|
}
|
|
1706
1771
|
|
|
1707
1772
|
if (isOpenStep(pattern.date)) {
|
|
1708
|
-
return stepDates(pattern.date, opts) + monthScope(
|
|
1773
|
+
return stepDates(pattern.date, opts) + monthScope(schedule);
|
|
1709
1774
|
}
|
|
1710
1775
|
|
|
1711
|
-
return monthAnchor(
|
|
1712
|
-
foldedYear(
|
|
1776
|
+
return monthAnchor(schedule, opts) + ' ' + dateWords(schedule) + ' päivänä' +
|
|
1777
|
+
foldedYear(schedule) + monthStepStart(pattern.month) +
|
|
1778
|
+
rangedMonthScope(schedule);
|
|
1713
1779
|
}
|
|
1714
1780
|
|
|
1715
1781
|
// " helmikuusta alkaen" trailing the date words when an open month step
|
|
@@ -1733,10 +1799,10 @@ function monthStepStart(monthField: string): string {
|
|
|
1733
1799
|
// "tammikuun", "kesäkuun ja joulukuun", or "joka kolmannen kuukauden". A
|
|
1734
1800
|
// ranged month cannot take the genitive, so it scopes the date from
|
|
1735
1801
|
// behind instead (rangedMonthScope).
|
|
1736
|
-
function monthAnchor(
|
|
1737
|
-
const monthField =
|
|
1802
|
+
function monthAnchor(schedule: Schedule, opts: NormalizedOptions): string {
|
|
1803
|
+
const monthField = schedule.pattern.month;
|
|
1738
1804
|
|
|
1739
|
-
if (monthField === '*' || monthRanged(
|
|
1805
|
+
if (monthField === '*' || monthRanged(schedule)) {
|
|
1740
1806
|
return 'kuukauden';
|
|
1741
1807
|
}
|
|
1742
1808
|
|
|
@@ -1744,7 +1810,7 @@ function monthAnchor(ir: IR, opts: NormalizedOptions): string {
|
|
|
1744
1810
|
return stepMonths(monthField, opts);
|
|
1745
1811
|
}
|
|
1746
1812
|
|
|
1747
|
-
const segments = flattenSteps(segmentsOf(
|
|
1813
|
+
const segments = flattenSteps(segmentsOf(schedule, 'month'));
|
|
1748
1814
|
|
|
1749
1815
|
return joinList(segments.map(function genitiveOf(segment: FlatSegment) {
|
|
1750
1816
|
// The anchor branch is only reached for non-ranged months, so every
|
|
@@ -1756,22 +1822,22 @@ function monthAnchor(ir: IR, opts: NormalizedOptions): string {
|
|
|
1756
1822
|
}
|
|
1757
1823
|
|
|
1758
1824
|
// " kesäkuusta syyskuuhun" trailing a date under a ranged month.
|
|
1759
|
-
function rangedMonthScope(
|
|
1760
|
-
return monthRanged(
|
|
1825
|
+
function rangedMonthScope(schedule: Schedule): string {
|
|
1826
|
+
return monthRanged(schedule) ? ' ' + monthPhrase(schedule) : '';
|
|
1761
1827
|
}
|
|
1762
1828
|
|
|
1763
1829
|
// Whether the month field contains a range segment.
|
|
1764
|
-
function monthRanged(
|
|
1765
|
-
return
|
|
1766
|
-
segmentsOf(
|
|
1830
|
+
function monthRanged(schedule: Schedule): boolean {
|
|
1831
|
+
return schedule.pattern.month !== '*' &&
|
|
1832
|
+
segmentsOf(schedule, 'month').some(function range(segment: Segment) {
|
|
1767
1833
|
return segment.kind === 'range';
|
|
1768
1834
|
});
|
|
1769
1835
|
}
|
|
1770
1836
|
|
|
1771
1837
|
// The day-of-month words: "13.", "1. ja 15.", "1.–15.", with step
|
|
1772
1838
|
// segments expanded into their fires.
|
|
1773
|
-
function dateWords(
|
|
1774
|
-
return joinList(segmentsOf(
|
|
1839
|
+
function dateWords(schedule: Schedule): string {
|
|
1840
|
+
return joinList(segmentsOf(schedule, 'date').flatMap(
|
|
1775
1841
|
function word(segment: Segment): string[] {
|
|
1776
1842
|
if (segment.kind === 'range') {
|
|
1777
1843
|
return [segment.bounds[0] + '.–' + segment.bounds[1] + '.'];
|
|
@@ -1868,10 +1934,10 @@ function monthNumber(token: string | number): number {
|
|
|
1868
1934
|
// rendered.
|
|
1869
1935
|
function applyYear(
|
|
1870
1936
|
description: string,
|
|
1871
|
-
|
|
1937
|
+
schedule: Schedule,
|
|
1872
1938
|
opts: NormalizedOptions
|
|
1873
1939
|
): string {
|
|
1874
|
-
const yearField =
|
|
1940
|
+
const yearField = schedule.pattern.year;
|
|
1875
1941
|
|
|
1876
1942
|
if (yearField === '*') {
|
|
1877
1943
|
return description;
|
|
@@ -1882,7 +1948,7 @@ function applyYear(
|
|
|
1882
1948
|
}
|
|
1883
1949
|
|
|
1884
1950
|
// A foldable single year already joined its date in datePhrase.
|
|
1885
|
-
if (foldedYear(
|
|
1951
|
+
if (foldedYear(schedule) && schedule.pattern.date !== '*') {
|
|
1886
1952
|
return description;
|
|
1887
1953
|
}
|
|
1888
1954
|
|
|
@@ -1915,8 +1981,8 @@ function stepYears(yearField: string, opts: NormalizedOptions): string {
|
|
|
1915
1981
|
}
|
|
1916
1982
|
|
|
1917
1983
|
// " vuonna 2030" when a single year can fold into a calendar date.
|
|
1918
|
-
function foldedYear(
|
|
1919
|
-
const yearField =
|
|
1984
|
+
function foldedYear(schedule: Schedule): string {
|
|
1985
|
+
const yearField = schedule.pattern.year;
|
|
1920
1986
|
|
|
1921
1987
|
if (yearField === '*' || yearField.indexOf('/') !== -1 ||
|
|
1922
1988
|
yearField.indexOf('-') !== -1 || yearField.indexOf(',') !== -1) {
|
|
@@ -1987,7 +2053,7 @@ function joinList(items: string[]): string {
|
|
|
1987
2053
|
return items.slice(0, -1).join(', ') + ' ja ' + items[items.length - 1];
|
|
1988
2054
|
}
|
|
1989
2055
|
|
|
1990
|
-
// The Finnish language module: the
|
|
2056
|
+
// The Finnish language module: the Schedule renderer plus the language-owned
|
|
1991
2057
|
// strings and option normalization.
|
|
1992
2058
|
const fi: Language = {
|
|
1993
2059
|
describe,
|