cronli5 0.1.4 → 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 +25 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +180 -36
- package/dist/cronli5.js +180 -36
- package/dist/lang/de.cjs +172 -8
- package/dist/lang/de.js +172 -8
- package/dist/lang/en.cjs +175 -29
- package/dist/lang/en.js +175 -29
- package/dist/lang/es.cjs +180 -25
- package/dist/lang/es.js +180 -25
- package/dist/lang/fi.cjs +188 -40
- package/dist/lang/fi.js +188 -40
- package/dist/lang/zh.cjs +165 -19
- package/dist/lang/zh.js +165 -19
- package/package.json +2 -1
- package/src/core/analyze.ts +7 -0
- package/src/core/ir.ts +1 -1
- package/src/core/util.ts +31 -1
- package/src/lang/de/index.ts +360 -16
- package/src/lang/en/index.ts +333 -33
- package/src/lang/es/index.ts +373 -40
- package/src/lang/fi/index.ts +404 -72
- package/src/lang/zh/index.ts +327 -35
- package/types/core/ir.d.ts +1 -1
- package/types/core/util.d.ts +6 -1
package/src/lang/fi/index.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
// case-pair construction wherever digits appear.
|
|
11
11
|
|
|
12
12
|
import {clockDigits, numeral} from '../../core/format.js';
|
|
13
|
-
import {weekdayNumbers} from '../../core/specs.js';
|
|
14
|
-
import {toFieldNumber} from '../../core/util.js';
|
|
13
|
+
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
14
|
+
import {arithmeticStep, toFieldNumber} from '../../core/util.js';
|
|
15
15
|
import {resolveDialect} from './dialects.js';
|
|
16
16
|
import type {
|
|
17
17
|
ClockTime, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode,
|
|
@@ -63,6 +63,7 @@ interface UnitForms {
|
|
|
63
63
|
mark: string;
|
|
64
64
|
anchor: string;
|
|
65
65
|
ela: string;
|
|
66
|
+
ill: string;
|
|
66
67
|
gen: string;
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -170,12 +171,14 @@ const units: {minute: UnitForms; second: UnitForms} = {
|
|
|
170
171
|
mark: 'joka tunti',
|
|
171
172
|
anchor: 'jokaisen tunnin',
|
|
172
173
|
ela: 'minuutista',
|
|
174
|
+
ill: 'minuuttiin',
|
|
173
175
|
gen: 'minuutin'
|
|
174
176
|
},
|
|
175
177
|
second: {
|
|
176
178
|
mark: 'joka minuutti',
|
|
177
179
|
anchor: 'jokaisen minuutin',
|
|
178
180
|
ela: 'sekunnista',
|
|
181
|
+
ill: 'sekuntiin',
|
|
179
182
|
gen: 'sekunnin'
|
|
180
183
|
}
|
|
181
184
|
};
|
|
@@ -296,49 +299,85 @@ function renderSecondsWithinMinute(
|
|
|
296
299
|
atMarks(minuteField, units.minute, true) + trailingQualifier(ir, opts);
|
|
297
300
|
}
|
|
298
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
|
+
|
|
299
361
|
function renderComposeSeconds(
|
|
300
362
|
ir: IR,
|
|
301
363
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
302
364
|
opts: NormalizedOptions
|
|
303
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
|
+
|
|
304
376
|
// When the rest is a minute-step cadence, the step leads and the second
|
|
305
377
|
// anchor follows after a comma (the comma marks the granularity boundary
|
|
306
|
-
// between the two levels, not a flat list).
|
|
307
|
-
// "[step phrase], [seconds][hour clause][trailing qualifier]".
|
|
308
|
-
//
|
|
309
|
-
// The minute-frequency phrase is reconstructed directly here so the hour
|
|
310
|
-
// clause can be interleaved between the step and the second anchor without
|
|
311
|
-
// duplicating the full renderMinuteFrequency logic. The hours-first reorder
|
|
312
|
-
// that applies inside renderMinuteFrequency is intentionally NOT applied
|
|
313
|
-
// here (the step-leads form is the correct shape for this construction).
|
|
378
|
+
// between the two levels, not a flat list).
|
|
314
379
|
if (plan.rest.kind === 'minuteFrequency' && ir.pattern.second !== '*') {
|
|
315
|
-
|
|
316
|
-
const seg = stepSegment(ir.analyses.segments.minute!);
|
|
317
|
-
const stepPhrase = stepCycle60(seg, units.minute, opts);
|
|
318
|
-
let hourClause = '';
|
|
319
|
-
|
|
320
|
-
if (freq.hours.kind === 'during' && minuteStepIsAnchored(seg)) {
|
|
321
|
-
// The step renders as an anchored kohdalla list rather than a cadence,
|
|
322
|
-
// so the hours-first reorder applies here too: bare hours lead, minute
|
|
323
|
-
// anchors follow, then the seconds clause.
|
|
324
|
-
const bareHours = kloFromTimes(ir, freq.hours.times, opts);
|
|
325
|
-
|
|
326
|
-
return hoursFirstMinutes(bareHours, ir) + ', ' +
|
|
327
|
-
secondsLeadClause(ir, opts) + trailingQualifier(ir, opts);
|
|
328
|
-
}
|
|
329
|
-
else if (freq.hours.kind === 'during' && !minuteStepIsAnchored(seg)) {
|
|
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);
|
|
380
|
+
return composeSecondsOverMinuteStep(ir, plan.rest, opts);
|
|
342
381
|
}
|
|
343
382
|
|
|
344
383
|
// A sub-minute second with the minute pinned to 0 and a specific hour: the
|
|
@@ -352,9 +391,34 @@ function renderComposeSeconds(
|
|
|
352
391
|
return composeMinuteZero(ir, plan.rest, opts);
|
|
353
392
|
}
|
|
354
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';
|
|
401
|
+
}
|
|
402
|
+
|
|
355
403
|
return secondsLeadClause(ir, opts) + ', ' + render(ir, plan.rest, opts);
|
|
356
404
|
}
|
|
357
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
|
+
|
|
358
422
|
// The minute-0 confinement: bind the seconds to the explicit clock minute(s)
|
|
359
423
|
// in the "minuutin/minuuttien HH.00 aikana" frame (an "of"/during form, never
|
|
360
424
|
// a range — a range would round-trip back to the whole hour) and trail the day
|
|
@@ -400,8 +464,11 @@ function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
|
400
464
|
return atMarks(secondField, units.second, marked);
|
|
401
465
|
}
|
|
402
466
|
|
|
403
|
-
|
|
404
|
-
|
|
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);
|
|
405
472
|
}
|
|
406
473
|
|
|
407
474
|
// --- Minute renderers. ---
|
|
@@ -428,7 +495,7 @@ function renderRangeOfMinutes(
|
|
|
428
495
|
plan: Extract<PlanNode, {kind: 'rangeOfMinutes'}>,
|
|
429
496
|
opts: NormalizedOptions
|
|
430
497
|
): string {
|
|
431
|
-
return minutesList(ir) + trailingQualifier(ir, opts);
|
|
498
|
+
return minutesList(ir, opts) + trailingQualifier(ir, opts);
|
|
432
499
|
}
|
|
433
500
|
|
|
434
501
|
function renderMultipleMinutes(
|
|
@@ -436,21 +503,27 @@ function renderMultipleMinutes(
|
|
|
436
503
|
plan: Extract<PlanNode, {kind: 'multipleMinutes'}>,
|
|
437
504
|
opts: NormalizedOptions
|
|
438
505
|
): string {
|
|
439
|
-
return minutesList(ir) + trailingQualifier(ir, opts);
|
|
506
|
+
return minutesList(ir, opts) + trailingQualifier(ir, opts);
|
|
440
507
|
}
|
|
441
508
|
|
|
442
|
-
// "joka tunti 0, 15 ja 30 minuutin kohdalla" (or a dash range).
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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);
|
|
446
517
|
}
|
|
447
518
|
|
|
448
519
|
// The bare minute mark, for clauses where a specific hour follows and
|
|
449
520
|
// the "joka tunti" frequency would be redundant: "0–30 minuutin
|
|
450
|
-
// kohdalla".
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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);
|
|
454
527
|
}
|
|
455
528
|
|
|
456
529
|
// Whether a minute step renders as an anchored "kohdalla" clause rather
|
|
@@ -500,7 +573,21 @@ function hoursAreRangeIsolated(segments: Segment[]): boolean {
|
|
|
500
573
|
// (plural genitive "minuuttien"; replaces the leading "joka tunti"). Used
|
|
501
574
|
// when a range or multi-point minute list over enumerated hours renders
|
|
502
575
|
// hours-first.
|
|
503
|
-
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
|
+
|
|
504
591
|
return hoursStr + ' aina minuuttien ' +
|
|
505
592
|
joinList(segmentWords(ir.analyses.segments.minute!)) + ' kohdalla';
|
|
506
593
|
}
|
|
@@ -546,7 +633,8 @@ function renderMinuteFrequency(
|
|
|
546
633
|
if (minuteStepIsAnchored(seg)) {
|
|
547
634
|
const bareHours = kloFromTimes(ir, plan.hours.times, opts);
|
|
548
635
|
|
|
549
|
-
return hoursFirstMinutes(bareHours, ir
|
|
636
|
+
return hoursFirstMinutes(bareHours, ir, opts) +
|
|
637
|
+
trailingQualifier(ir, opts);
|
|
550
638
|
}
|
|
551
639
|
|
|
552
640
|
return stepCycle60(seg, units.minute, opts) + ' ' +
|
|
@@ -606,7 +694,7 @@ function renderMinutesAcrossHours(
|
|
|
606
694
|
|
|
607
695
|
// Range+isolated hours: minute-first, bare minutes, sekä klo.
|
|
608
696
|
if (hoursAreRangeIsolated(ir.analyses.segments.hour!)) {
|
|
609
|
-
return bareMinutes(ir) + ' ' +
|
|
697
|
+
return bareMinutes(ir, opts) + ' ' +
|
|
610
698
|
hourSegmentTimesWithSeka(ir, 0, null, opts) +
|
|
611
699
|
trailingQualifier(ir, opts);
|
|
612
700
|
}
|
|
@@ -616,7 +704,7 @@ function renderMinutesAcrossHours(
|
|
|
616
704
|
// shows it).
|
|
617
705
|
const hoursStr = kloFromTimes(ir, plan.times, opts);
|
|
618
706
|
|
|
619
|
-
return hoursFirstMinutes(hoursStr, ir) + trailingQualifier(ir, opts);
|
|
707
|
+
return hoursFirstMinutes(hoursStr, ir, opts) + trailingQualifier(ir, opts);
|
|
620
708
|
}
|
|
621
709
|
|
|
622
710
|
function renderMinuteSpanAcrossHourStep(
|
|
@@ -644,10 +732,10 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
644
732
|
if (segment.startToken.indexOf('-') !== -1) {
|
|
645
733
|
const hoursStr = kloList(segment.fires, opts);
|
|
646
734
|
|
|
647
|
-
return hoursFirstMinutes(hoursStr, ir) + trailingQualifier(ir, opts);
|
|
735
|
+
return hoursFirstMinutes(hoursStr, ir, opts) + trailingQualifier(ir, opts);
|
|
648
736
|
}
|
|
649
737
|
|
|
650
|
-
return bareMinutes(ir) + hourStepTail(segment, opts) +
|
|
738
|
+
return bareMinutes(ir, opts) + hourStepTail(segment, opts) +
|
|
651
739
|
trailingQualifier(ir, opts);
|
|
652
740
|
}
|
|
653
741
|
|
|
@@ -725,7 +813,7 @@ function renderHourRange(
|
|
|
725
813
|
// A minute range over a single hour range renders hours-first
|
|
726
814
|
// ("klo 9.00–17.30 aina minuuttien 0–30 kohdalla").
|
|
727
815
|
if (plan.minuteForm === 'range') {
|
|
728
|
-
return hoursFirstMinutes(window, ir) + trailingQualifier(ir, opts);
|
|
816
|
+
return hoursFirstMinutes(window, ir, opts) + trailingQualifier(ir, opts);
|
|
729
817
|
}
|
|
730
818
|
|
|
731
819
|
// On the hour the window joins directly ("joka tunti klo 9–17"); a
|
|
@@ -744,7 +832,7 @@ function renderHourRange(
|
|
|
744
832
|
|
|
745
833
|
// A minute list (≥2 values) over a single hour range renders hours-first
|
|
746
834
|
// ("klo 9.00–17.30 aina minuuttien 0 ja 30 kohdalla").
|
|
747
|
-
return hoursFirstMinutes(window, ir) + trailingQualifier(ir, opts);
|
|
835
|
+
return hoursFirstMinutes(window, ir, opts) + trailingQualifier(ir, opts);
|
|
748
836
|
}
|
|
749
837
|
|
|
750
838
|
function renderHourStep(
|
|
@@ -779,6 +867,16 @@ function renderClockTimes(
|
|
|
779
867
|
plan: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
780
868
|
opts: NormalizedOptions
|
|
781
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
|
+
|
|
782
880
|
if (plan.times.length === 1) {
|
|
783
881
|
const time = plan.times[0];
|
|
784
882
|
|
|
@@ -802,6 +900,17 @@ function renderCompactClockTimes(
|
|
|
802
900
|
plan: Extract<PlanNode, {kind: 'compactClockTimes'}>,
|
|
803
901
|
opts: NormalizedOptions
|
|
804
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
|
+
|
|
805
914
|
const hourSegs = ir.analyses.segments.hour!;
|
|
806
915
|
|
|
807
916
|
// Range+isolated hours: join the isolated hour with "sekä klo" to stop it
|
|
@@ -815,7 +924,7 @@ function renderCompactClockTimes(
|
|
|
815
924
|
ir.analyses.clockSecond, opts);
|
|
816
925
|
}
|
|
817
926
|
|
|
818
|
-
const phrase = bareMinutes(ir) + ' ' +
|
|
927
|
+
const phrase = bareMinutes(ir, opts) + ' ' +
|
|
819
928
|
hourSegmentTimesWithSeka(ir, 0, null, opts) +
|
|
820
929
|
trailingQualifier(ir, opts);
|
|
821
930
|
|
|
@@ -832,7 +941,7 @@ function renderCompactClockTimes(
|
|
|
832
941
|
// A minute list over purely enumerated hours (step fires, all singles) —
|
|
833
942
|
// hours-first, drop "joka tunti".
|
|
834
943
|
const hoursStr = hourSegmentTimes(ir, 0, null, opts);
|
|
835
|
-
const phrase = hoursFirstMinutes(hoursStr, ir) +
|
|
944
|
+
const phrase = hoursFirstMinutes(hoursStr, ir, opts) +
|
|
836
945
|
trailingQualifier(ir, opts);
|
|
837
946
|
|
|
838
947
|
return ir.analyses.clockSecond ?
|
|
@@ -864,8 +973,82 @@ const renderers = {
|
|
|
864
973
|
|
|
865
974
|
// --- Step phrases. ---
|
|
866
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
|
+
|
|
867
1048
|
// "viiden minuutin välein", "joka tunti 0 ja 31 minuutin kohdalla", or
|
|
868
|
-
// "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.
|
|
869
1052
|
function stepCycle60(
|
|
870
1053
|
segment: StepSegment,
|
|
871
1054
|
unit: UnitForms,
|
|
@@ -876,21 +1059,20 @@ function stepCycle60(
|
|
|
876
1059
|
}
|
|
877
1060
|
|
|
878
1061
|
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
879
|
-
const interval = segment.interval;
|
|
880
|
-
const cadence = genitive(interval, opts) + ' ' + unit.gen + ' välein';
|
|
881
1062
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
return cadence + ' ' + unit.anchor + ' ' + unit.ela + ' ' + start +
|
|
888
|
-
' 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);
|
|
889
1067
|
}
|
|
890
1068
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1069
|
+
return renderStride({
|
|
1070
|
+
interval: segment.interval,
|
|
1071
|
+
start,
|
|
1072
|
+
last: segment.fires[segment.fires.length - 1],
|
|
1073
|
+
cycle: 60,
|
|
1074
|
+
unit
|
|
1075
|
+
}, opts);
|
|
894
1076
|
}
|
|
895
1077
|
|
|
896
1078
|
// "kahden tunnin välein", "klo 0, 10 ja 20", or "viiden tunnin välein
|
|
@@ -917,6 +1099,156 @@ function stepHours(segment: StepSegment, opts: NormalizedOptions): string {
|
|
|
917
1099
|
return cadence + ' klo ' + hourElatives[start] + ' alkaen';
|
|
918
1100
|
}
|
|
919
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
|
+
|
|
920
1252
|
// --- Hour-time phrasing. ---
|
|
921
1253
|
|
|
922
1254
|
// On-the-hour fires as one klo phrase: "klo 0, 10 ja 20".
|