cronli5 0.1.4 → 0.1.6

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/dist/lang/fi.cjs CHANGED
@@ -50,12 +50,28 @@ var weekdayNumbers = {
50
50
  FRI: 5,
51
51
  SAT: 6
52
52
  };
53
+ var maxClockTimes = 6;
53
54
 
54
55
  // src/core/util.ts
55
56
  function isNonNegativeInteger(value) {
56
57
  const digits = /^\d+$/;
57
58
  return digits.test(value);
58
59
  }
60
+ function arithmeticStep(values) {
61
+ if (values.length < 5) {
62
+ return null;
63
+ }
64
+ const interval = values[1] - values[0];
65
+ if (interval < 2) {
66
+ return null;
67
+ }
68
+ for (let i = 2; i < values.length; i += 1) {
69
+ if (values[i] - values[i - 1] !== interval) {
70
+ return null;
71
+ }
72
+ }
73
+ return { start: values[0], interval, last: values[values.length - 1] };
74
+ }
59
75
  function toFieldNumber(token, numberMap) {
60
76
  return isNonNegativeInteger(token) ? +token : numberMap[token.toUpperCase()];
61
77
  }
@@ -191,12 +207,14 @@ var units = {
191
207
  mark: "joka tunti",
192
208
  anchor: "jokaisen tunnin",
193
209
  ela: "minuutista",
210
+ ill: "minuuttiin",
194
211
  gen: "minuutin"
195
212
  },
196
213
  second: {
197
214
  mark: "joka minuutti",
198
215
  anchor: "jokaisen minuutin",
199
216
  ela: "sekunnista",
217
+ ill: "sekuntiin",
200
218
  gen: "sekunnin"
201
219
  }
202
220
  };
@@ -252,28 +270,55 @@ function renderSecondsWithinMinute(ir, plan, opts) {
252
270
  }
253
271
  return secondsLeadClause(ir, opts) + ", " + atMarks(minuteField, units.minute, true) + trailingQualifier(ir, opts);
254
272
  }
273
+ function composeSecondsOverMinuteStep(ir, freq, opts) {
274
+ const seg = stepSegment(ir.analyses.segments.minute);
275
+ const stepPhrase = stepCycle60(seg, units.minute, opts);
276
+ if (freq.hours.kind === "during" && minuteStepIsAnchored(seg)) {
277
+ const bareHours = kloFromTimes(ir, freq.hours.times, opts);
278
+ return hoursFirstMinutes(bareHours, ir, opts) + ", " + secondsLeadClause(ir, opts) + trailingQualifier(ir, opts);
279
+ }
280
+ let hourClause = "";
281
+ if (freq.hours.kind === "during") {
282
+ hourClause = " " + hourWindowsFromTimes(ir, freq.hours.times, opts);
283
+ } else if (freq.hours.kind === "window") {
284
+ hourClause = " " + hourWindow(freq.hours, opts);
285
+ } else if (freq.hours.kind === "step") {
286
+ hourClause = " " + everyNthHour(stepSegment(ir.analyses.segments.hour), opts);
287
+ }
288
+ return stepPhrase + ", " + secondsLeadClause(ir, opts) + hourClause + trailingQualifier(ir, opts);
289
+ }
290
+ function composeHourCadence(ir, plan, opts) {
291
+ const clockRest = plan.rest.kind === "clockTimes" || plan.rest.kind === "compactClockTimes";
292
+ if (!clockRest || ir.shapes.minute !== "single") {
293
+ return null;
294
+ }
295
+ const minute = +ir.pattern.minute;
296
+ return hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
297
+ }
255
298
  function renderComposeSeconds(ir, plan, opts) {
299
+ const cadence = composeHourCadence(ir, plan, opts);
300
+ if (cadence !== null) {
301
+ return cadence;
302
+ }
256
303
  if (plan.rest.kind === "minuteFrequency" && ir.pattern.second !== "*") {
257
- const freq = plan.rest;
258
- const seg = stepSegment(ir.analyses.segments.minute);
259
- const stepPhrase = stepCycle60(seg, units.minute, opts);
260
- let hourClause = "";
261
- if (freq.hours.kind === "during" && minuteStepIsAnchored(seg)) {
262
- const bareHours = kloFromTimes(ir, freq.hours.times, opts);
263
- return hoursFirstMinutes(bareHours, ir) + ", " + secondsLeadClause(ir, opts) + trailingQualifier(ir, opts);
264
- } else if (freq.hours.kind === "during" && !minuteStepIsAnchored(seg)) {
265
- hourClause = " " + hourWindowsFromTimes(ir, freq.hours.times, opts);
266
- } else if (freq.hours.kind === "window") {
267
- hourClause = " " + hourWindow(freq.hours, opts);
268
- } else if (freq.hours.kind === "step") {
269
- hourClause = " " + everyNthHour(stepSegment(ir.analyses.segments.hour), opts);
270
- }
271
- return stepPhrase + ", " + secondsLeadClause(ir, opts) + hourClause + trailingQualifier(ir, opts);
304
+ return composeSecondsOverMinuteStep(ir, plan.rest, opts);
272
305
  }
273
306
  if (plan.rest.kind === "clockTimes" && plan.rest.times.every((time) => +time.minute === 0)) {
274
307
  return composeMinuteZero(ir, plan.rest, opts);
275
308
  }
276
- return secondsLeadClause(ir, opts) + ", " + render(ir, plan.rest, opts);
309
+ if (isEveryOtherMinuteSeconds(ir, plan)) {
310
+ return secondsLeadClause(ir, opts) + " joka toisena minuuttina";
311
+ }
312
+ const restOwnsLead = plan.rest.kind === "compactClockTimes" && ir.analyses.clockSecond;
313
+ const lead = restOwnsLead ? "" : secondsLeadClause(ir, opts) + ", ";
314
+ return lead + render(ir, plan.rest, opts);
315
+ }
316
+ function isEveryOtherMinuteSeconds(ir, plan) {
317
+ if (plan.rest.kind !== "minuteFrequency" || ir.pattern.second !== "*" || ir.shapes.hour !== "wildcard") {
318
+ return false;
319
+ }
320
+ const seg = stepSegment(ir.analyses.segments.minute);
321
+ return seg.startToken === "*" && seg.interval === 2;
277
322
  }
278
323
  function composeMinuteZero(ir, rest, opts) {
279
324
  const clocks = rest.times.map(function clock(time) {
@@ -303,7 +348,7 @@ function secondsLeadClause(ir, opts) {
303
348
  if (shape === "single") {
304
349
  return atMarks(secondField, units.second, marked);
305
350
  }
306
- return atMarks(
351
+ return strideFromSegments(ir.analyses.segments.second, units.second, opts) ?? atMarks(
307
352
  joinList(segmentWords(ir.analyses.segments.second)),
308
353
  units.second,
309
354
  marked
@@ -316,20 +361,20 @@ function renderSingleMinute(ir, plan, opts) {
316
361
  return atMarks(ir.pattern.minute, units.minute, true) + trailingQualifier(ir, opts);
317
362
  }
318
363
  function renderRangeOfMinutes(ir, plan, opts) {
319
- return minutesList(ir) + trailingQualifier(ir, opts);
364
+ return minutesList(ir, opts) + trailingQualifier(ir, opts);
320
365
  }
321
366
  function renderMultipleMinutes(ir, plan, opts) {
322
- return minutesList(ir) + trailingQualifier(ir, opts);
367
+ return minutesList(ir, opts) + trailingQualifier(ir, opts);
323
368
  }
324
- function minutesList(ir) {
325
- return atMarks(
369
+ function minutesList(ir, opts) {
370
+ return strideFromSegments(ir.analyses.segments.minute, units.minute, opts) ?? atMarks(
326
371
  joinList(segmentWords(ir.analyses.segments.minute)),
327
372
  units.minute,
328
373
  true
329
374
  );
330
375
  }
331
- function bareMinutes(ir) {
332
- return atMarks(
376
+ function bareMinutes(ir, opts) {
377
+ return strideFromSegments(ir.analyses.segments.minute, units.minute, opts) ?? atMarks(
333
378
  joinList(segmentWords(ir.analyses.segments.minute)),
334
379
  units.minute,
335
380
  false
@@ -360,7 +405,11 @@ function hoursAreRangeIsolated(segments) {
360
405
  }
361
406
  return hasRange && hasSingle;
362
407
  }
363
- function hoursFirstMinutes(hoursStr, ir) {
408
+ function hoursFirstMinutes(hoursStr, ir, opts) {
409
+ const stride = strideFromSegments(ir.analyses.segments.minute, units.minute, opts);
410
+ if (stride) {
411
+ return hoursStr + " aina " + stride;
412
+ }
364
413
  return hoursStr + " aina minuuttien " + joinList(segmentWords(ir.analyses.segments.minute)) + " kohdalla";
365
414
  }
366
415
  function hourSegmentTimesWithSeka(ir, minute, second, opts) {
@@ -381,9 +430,13 @@ function hourSegmentTimesWithSeka(ir, minute, second, opts) {
381
430
  function renderMinuteFrequency(ir, plan, opts) {
382
431
  const seg = stepSegment(ir.analyses.segments.minute);
383
432
  if (plan.hours.kind === "during") {
433
+ const cadence = unevenHourCadence(ir, opts);
434
+ if (cadence !== null) {
435
+ return stepCycle60(seg, units.minute, opts) + ", " + cadence + trailingQualifier(ir, opts);
436
+ }
384
437
  if (minuteStepIsAnchored(seg)) {
385
438
  const bareHours = kloFromTimes(ir, plan.hours.times, opts);
386
- return hoursFirstMinutes(bareHours, ir) + trailingQualifier(ir, opts);
439
+ return hoursFirstMinutes(bareHours, ir, opts) + trailingQualifier(ir, opts);
387
440
  }
388
441
  return stepCycle60(seg, units.minute, opts) + " " + hourWindowsFromTimes(ir, plan.hours.times, opts) + trailingQualifier(ir, opts);
389
442
  }
@@ -406,25 +459,29 @@ function renderMinuteSpanInHour(ir, plan, opts) {
406
459
  ) + trailingQualifier(ir, opts);
407
460
  }
408
461
  function renderMinutesAcrossHours(ir, plan, opts) {
462
+ const cadence = unevenHourCadence(ir, opts);
409
463
  if (plan.form === "wildcard") {
410
- return "joka minuutti " + hourWindowsFromTimes(ir, plan.times, opts) + trailingQualifier(ir, opts);
464
+ return cadence ? "joka minuutti, " + cadence + trailingQualifier(ir, opts) : "joka minuutti " + hourWindowsFromTimes(ir, plan.times, opts) + trailingQualifier(ir, opts);
465
+ }
466
+ if (cadence !== null) {
467
+ return bareMinutes(ir, opts) + ", " + cadence + trailingQualifier(ir, opts);
411
468
  }
412
469
  if (hoursAreRangeIsolated(ir.analyses.segments.hour)) {
413
- return bareMinutes(ir) + " " + hourSegmentTimesWithSeka(ir, 0, null, opts) + trailingQualifier(ir, opts);
470
+ return bareMinutes(ir, opts) + " " + hourSegmentTimesWithSeka(ir, 0, null, opts) + trailingQualifier(ir, opts);
414
471
  }
415
472
  const hoursStr = kloFromTimes(ir, plan.times, opts);
416
- return hoursFirstMinutes(hoursStr, ir) + trailingQualifier(ir, opts);
473
+ return hoursFirstMinutes(hoursStr, ir, opts) + trailingQualifier(ir, opts);
417
474
  }
418
475
  function renderMinuteSpanAcrossHourStep(ir, plan, opts) {
419
476
  const segment = stepSegment(ir.analyses.segments.hour);
477
+ const cadence = unevenHourCadence(ir, opts);
420
478
  if (plan.form === "wildcard") {
421
479
  return "joka minuutti " + everyNthHour(segment, opts) + trailingQualifier(ir, opts);
422
480
  }
423
- if (segment.startToken.indexOf("-") !== -1) {
424
- const hoursStr = kloList(segment.fires, opts);
425
- return hoursFirstMinutes(hoursStr, ir) + trailingQualifier(ir, opts);
481
+ if (cadence !== null) {
482
+ return bareMinutes(ir, opts) + ", " + cadence + trailingQualifier(ir, opts);
426
483
  }
427
- return bareMinutes(ir) + hourStepTail(segment, opts) + trailingQualifier(ir, opts);
484
+ return bareMinutes(ir, opts) + hourStepTail(segment, opts) + trailingQualifier(ir, opts);
428
485
  }
429
486
  function plainHourStep(segment) {
430
487
  return segment.startToken === "*" && 24 % segment.interval === 0;
@@ -482,7 +539,7 @@ function renderHourRange(ir, plan, opts) {
482
539
  return "joka minuutti " + window + trailingQualifier(ir, opts);
483
540
  }
484
541
  if (plan.minuteForm === "range") {
485
- return hoursFirstMinutes(window, ir) + trailingQualifier(ir, opts);
542
+ return hoursFirstMinutes(window, ir, opts) + trailingQualifier(ir, opts);
486
543
  }
487
544
  if (ir.pattern.minute === "0") {
488
545
  return "joka tunti " + window + trailingQualifier(ir, opts);
@@ -494,9 +551,13 @@ function renderHourRange(ir, plan, opts) {
494
551
  opts
495
552
  ) + trailingQualifier(ir, opts);
496
553
  }
497
- return hoursFirstMinutes(window, ir) + trailingQualifier(ir, opts);
554
+ return hoursFirstMinutes(window, ir, opts) + trailingQualifier(ir, opts);
498
555
  }
499
556
  function renderHourStep(ir, plan, opts) {
557
+ const cadence = unevenHourCadence(ir, opts);
558
+ if (cadence !== null) {
559
+ return cadence + trailingQualifier(ir, opts);
560
+ }
500
561
  return stepHours(stepSegment(ir.analyses.segments.hour), opts) + trailingQualifier(ir, opts);
501
562
  }
502
563
  function boundedWindow(plan) {
@@ -510,6 +571,13 @@ function hourWindow(window, opts) {
510
571
  );
511
572
  }
512
573
  function renderClockTimes(ir, plan, opts) {
574
+ if (ir.shapes.minute === "single") {
575
+ const minute = +ir.pattern.minute;
576
+ const cadence = hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
577
+ if (cadence !== null) {
578
+ return cadence;
579
+ }
580
+ }
513
581
  if (plan.times.length === 1) {
514
582
  const time = plan.times[0];
515
583
  return leadingQualifier(ir, opts) + timeWord(time.hour, time.minute, time.second, opts);
@@ -520,6 +588,12 @@ function renderClockTimes(ir, plan, opts) {
520
588
  return leadingQualifier(ir, opts) + "klo " + joinList(digits);
521
589
  }
522
590
  function renderCompactClockTimes(ir, plan, opts) {
591
+ if (plan.fold) {
592
+ const cadence2 = hourCadence(ir, plan.minute, opts) ?? hourRangeCadence(ir, plan.minute, opts);
593
+ if (cadence2 !== null) {
594
+ return cadence2;
595
+ }
596
+ }
523
597
  const hourSegs = ir.analyses.segments.hour;
524
598
  if (hoursAreRangeIsolated(hourSegs)) {
525
599
  if (plan.fold) {
@@ -530,14 +604,18 @@ function renderCompactClockTimes(ir, plan, opts) {
530
604
  opts
531
605
  );
532
606
  }
533
- const phrase2 = bareMinutes(ir) + " " + hourSegmentTimesWithSeka(ir, 0, null, opts) + trailingQualifier(ir, opts);
607
+ const phrase2 = bareMinutes(ir, opts) + " " + hourSegmentTimesWithSeka(ir, 0, null, opts) + trailingQualifier(ir, opts);
534
608
  return ir.analyses.clockSecond ? secondsLeadClause(ir, opts) + ", " + phrase2 : phrase2;
535
609
  }
536
610
  if (plan.fold) {
537
611
  return leadingQualifier(ir, opts) + hourSegmentTimes(ir, plan.minute, ir.analyses.clockSecond, opts);
538
612
  }
539
- const hoursStr = hourSegmentTimes(ir, 0, null, opts);
540
- const phrase = hoursFirstMinutes(hoursStr, ir) + trailingQualifier(ir, opts);
613
+ const cadence = unevenHourCadence(ir, opts);
614
+ const phrase = cadence ? bareMinutes(ir, opts) + ", " + cadence + trailingQualifier(ir, opts) : (
615
+ // A minute list over purely enumerated hours (step fires, all singles) —
616
+ // hours-first, drop "joka tunti".
617
+ hoursFirstMinutes(hourSegmentTimes(ir, 0, null, opts), ir, opts) + trailingQualifier(ir, opts)
618
+ );
541
619
  return ir.analyses.clockSecond ? secondsLeadClause(ir, opts) + ", " + phrase : phrase;
542
620
  }
543
621
  var renderers = {
@@ -560,20 +638,48 @@ var renderers = {
560
638
  singleMinute: renderSingleMinute,
561
639
  standaloneSeconds: renderStandaloneSeconds
562
640
  };
641
+ function renderStride(stride, opts) {
642
+ const { interval, start, last, cycle, unit } = stride;
643
+ const cadence = genitive(interval, opts) + " " + unit.gen + " v\xE4lein";
644
+ const tiles = cycle % interval === 0;
645
+ if (start === 0 && tiles) {
646
+ return cadence;
647
+ }
648
+ if (start < interval && tiles) {
649
+ return cadence + " " + unit.anchor + " " + unit.ela + " " + start + " alkaen";
650
+ }
651
+ return cadence + " " + unit.ela + " " + start + " " + unit.ill + " " + last;
652
+ }
653
+ function strideFromSegments(segments, unit, opts) {
654
+ const values = singleValues(segments);
655
+ const step = values && arithmeticStep(values);
656
+ return step ? renderStride({ ...step, cycle: 60, unit }, opts) : null;
657
+ }
658
+ function singleValues(segments) {
659
+ const values = [];
660
+ for (const segment of segments) {
661
+ if (segment.kind !== "single") {
662
+ return null;
663
+ }
664
+ values.push(+segment.value);
665
+ }
666
+ return values;
667
+ }
563
668
  function stepCycle60(segment, unit, opts) {
564
669
  if (segment.startToken.indexOf("-") !== -1) {
565
670
  return atMarks(joinList(wordList(segment.fires)), unit, true);
566
671
  }
567
672
  const start = segment.startToken === "*" ? 0 : +segment.startToken;
568
- const interval = segment.interval;
569
- const cadence = genitive(interval, opts) + " " + unit.gen + " v\xE4lein";
570
- if (start !== 0) {
571
- if (segment.fires.length <= 3) {
572
- return atMarks(joinList(wordList(segment.fires)), unit, true);
573
- }
574
- return cadence + " " + unit.anchor + " " + unit.ela + " " + start + " alkaen";
673
+ if (start !== 0 && segment.fires.length <= 3) {
674
+ return atMarks(joinList(wordList(segment.fires)), unit, true);
575
675
  }
576
- return cadence;
676
+ return renderStride({
677
+ interval: segment.interval,
678
+ start,
679
+ last: segment.fires[segment.fires.length - 1],
680
+ cycle: 60,
681
+ unit
682
+ }, opts);
577
683
  }
578
684
  function stepHours(segment, opts) {
579
685
  if (segment.startToken.indexOf("-") !== -1) {
@@ -590,6 +696,121 @@ function stepHours(segment, opts) {
590
696
  }
591
697
  return cadence + " klo " + hourElatives[start] + " alkaen";
592
698
  }
699
+ function hourStrideCadence(stride, opts) {
700
+ const { start, interval, last } = stride;
701
+ const cadence = genitive(interval, opts) + " tunnin v\xE4lein";
702
+ const tiles = 24 % interval === 0;
703
+ if (start === 0 && tiles) {
704
+ return cadence;
705
+ }
706
+ if (start < interval && tiles) {
707
+ return cadence + " klo " + hourElatives[start] + " alkaen";
708
+ }
709
+ return cadence + " " + kloRange({ hour: start, minute: 0 }, { hour: last, minute: 0 }, opts);
710
+ }
711
+ function hourListStride(values) {
712
+ if (values.length < 2) {
713
+ return null;
714
+ }
715
+ const interval = values[1] - values[0];
716
+ if (interval < 2) {
717
+ return null;
718
+ }
719
+ for (let i = 2; i < values.length; i += 1) {
720
+ if (values[i] - values[i - 1] !== interval) {
721
+ return null;
722
+ }
723
+ }
724
+ if (values[0] !== 0 && values.length < 5) {
725
+ return null;
726
+ }
727
+ return { interval, last: values[values.length - 1], start: values[0] };
728
+ }
729
+ function offsetCleanStride(stride) {
730
+ return stride.start < stride.interval && 24 % stride.interval === 0;
731
+ }
732
+ function hourStride(ir) {
733
+ const segments = ir.analyses.segments.hour;
734
+ if (!segments) {
735
+ return null;
736
+ }
737
+ if (segments.length === 1 && segments[0].kind === "step") {
738
+ const segment = segments[0];
739
+ if (segment.fires.length < 2) {
740
+ return null;
741
+ }
742
+ const start = segment.startToken === "*" ? 0 : +segment.startToken.split("-")[0];
743
+ return { interval: segment.interval, last: segment.fires[segment.fires.length - 1], start };
744
+ }
745
+ const values = singleValues(segments);
746
+ return values && hourListStride(values);
747
+ }
748
+ function unevenHourCadence(ir, opts) {
749
+ const stride = hourStride(ir);
750
+ if (!stride || offsetCleanStride(stride)) {
751
+ return null;
752
+ }
753
+ return hourStrideCadence(stride, opts);
754
+ }
755
+ function subMinuteSecond(ir) {
756
+ return ir.pattern.second === "*" || ir.shapes.second === "step";
757
+ }
758
+ function hourCadenceLead(ir, minute, opts) {
759
+ if (minute === 0) {
760
+ if (subMinuteSecond(ir)) {
761
+ return secondsLeadClause(ir, opts) + " minuutin ajan";
762
+ }
763
+ return secondsLeadClause(ir, opts);
764
+ }
765
+ const minutePhrase = atMarks(String(minute), units.minute, false);
766
+ if (ir.pattern.second === "0") {
767
+ return minutePhrase;
768
+ }
769
+ return secondsLeadClause(ir, opts) + ", " + minutePhrase;
770
+ }
771
+ function hourCadence(ir, minute, opts) {
772
+ const stride = hourStride(ir);
773
+ if (!stride) {
774
+ return null;
775
+ }
776
+ const fires = (stride.last - stride.start) / stride.interval + 1;
777
+ if (ir.pattern.second === "0" && fires <= maxClockTimes && offsetCleanStride(stride)) {
778
+ return null;
779
+ }
780
+ const segment = ir.analyses.segments.hour[0];
781
+ const confined = minute === 0 && subMinuteSecond(ir) && ir.analyses.segments.hour.length === 1 && segment.kind === "step" && cleanHourStride(segment);
782
+ if (confined) {
783
+ return secondsLeadClause(ir, opts) + " minuutin ajan " + everyNthHour(segment, opts) + trailingQualifier(ir, opts);
784
+ }
785
+ if (minute === 0 && ir.pattern.second === "0") {
786
+ return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
787
+ }
788
+ return hourCadenceLead(ir, minute, opts) + ", " + hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
789
+ }
790
+ function cleanHourStride(segment) {
791
+ if (segment.startToken.indexOf("-") !== -1) {
792
+ return false;
793
+ }
794
+ const start = segment.startToken === "*" ? 0 : +segment.startToken;
795
+ return 24 % segment.interval === 0 && start < segment.interval;
796
+ }
797
+ function hasHourWindow(ir) {
798
+ const segments = ir.analyses.segments.hour;
799
+ return !!segments && segments.some(function range(segment) {
800
+ return segment.kind === "range";
801
+ });
802
+ }
803
+ function hourRangeWindowTail(ir, opts) {
804
+ return ir.analyses.segments.hour.length === 1 ? hourSegmentTimes(ir, 0, null, opts) : hourSegmentTimesWithSeka(ir, 0, null, opts);
805
+ }
806
+ function hourRangeCadence(ir, minute, opts) {
807
+ if (minute !== 0 || !hasHourWindow(ir) || ir.pattern.second === "0") {
808
+ return null;
809
+ }
810
+ const tail = hourRangeWindowTail(ir, opts);
811
+ const joiner = subMinuteSecond(ir) ? " " : ", ";
812
+ return hourCadenceLead(ir, minute, opts) + joiner + tail + trailingQualifier(ir, opts);
813
+ }
593
814
  function kloList(hours, opts) {
594
815
  if (hours.length === 1) {
595
816
  return timeWord(hours[0], 0, null, opts);
@@ -606,12 +827,16 @@ function kloFromTimes(ir, times, opts) {
606
827
  }
607
828
  function hourWindowsFromTimes(ir, times, opts) {
608
829
  if (times.kind === "fires") {
609
- return "klo " + joinList(times.fires.map(function window(hour) {
610
- return hourWindowDigits(hour, opts);
611
- }));
830
+ return kloList(times.fires, opts);
831
+ }
832
+ const segments = ir.analyses.segments.hour;
833
+ if (!segments.some(function ranged(segment) {
834
+ return segment.kind === "range";
835
+ })) {
836
+ return kloList(hourSegmentFires(segments), opts);
612
837
  }
613
838
  const pieces = [];
614
- ir.analyses.segments.hour.forEach(function window(segment) {
839
+ segments.forEach(function window(segment) {
615
840
  if (segment.kind === "range") {
616
841
  pieces.push(rangeDigits(
617
842
  { hour: +segment.bounds[0], minute: 0 },
@@ -628,6 +853,17 @@ function hourWindowsFromTimes(ir, times, opts) {
628
853
  });
629
854
  return "klo " + joinList(pieces);
630
855
  }
856
+ function hourSegmentFires(segments) {
857
+ const hours = [];
858
+ segments.forEach(function each(segment) {
859
+ if (segment.kind === "step") {
860
+ hours.push(...segment.fires);
861
+ } else if (segment.kind === "single") {
862
+ hours.push(+segment.value);
863
+ }
864
+ });
865
+ return hours;
866
+ }
631
867
  function hourWindowDigits(hour, opts) {
632
868
  return rangeDigits({ hour, minute: 0 }, { hour, minute: 59 }, opts);
633
869
  }