cronli5 0.2.0 → 0.2.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 +25 -0
- package/README.md +4 -4
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +123 -104
- package/dist/cronli5.js +123 -104
- package/dist/lang/de.cjs +65 -65
- package/dist/lang/de.js +65 -65
- package/dist/lang/en.cjs +122 -101
- package/dist/lang/en.js +122 -101
- package/dist/lang/es.cjs +71 -72
- package/dist/lang/es.js +71 -72
- package/dist/lang/fi.cjs +71 -66
- package/dist/lang/fi.js +71 -66
- package/dist/lang/zh.cjs +36 -36
- package/dist/lang/zh.js +36 -36
- package/package.json +1 -1
- package/src/core/analyze.ts +14 -13
- package/src/core/ir.ts +8 -8
- package/src/core/shapes.ts +8 -1
- package/src/core/util.ts +86 -3
- package/src/core/validate.ts +1 -1
- package/src/cronli5.ts +3 -3
- package/src/lang/de/index.ts +30 -99
- package/src/lang/en/index.ts +163 -188
- package/src/lang/es/index.ts +36 -120
- package/src/lang/fi/index.ts +33 -104
- package/src/lang/zh/index.ts +23 -48
- package/src/types.ts +2 -2
- package/types/core/analyze.d.ts +2 -2
- package/types/core/ir.d.ts +7 -7
- package/types/core/shapes.d.ts +2 -1
- package/types/core/util.d.ts +17 -2
- package/types/types.d.ts +1 -1
package/src/lang/en/index.ts
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
// the core stays semantic, and this module's only input is the IR.
|
|
4
4
|
// See docs/i18n-design.md.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
arithmeticStep, hourListStride, offsetCleanStride, orderWeekdaysForDisplay,
|
|
8
|
+
segmentsOf, singleValues, stepSegment
|
|
9
|
+
} from '../../core/util.js';
|
|
10
|
+
import {isOpenStep} from '../../core/shapes.js';
|
|
7
11
|
import {maxClockTimes} from '../../core/specs.js';
|
|
8
12
|
import {clockDigits, numeral, pad} from '../../core/format.js';
|
|
9
13
|
import type {Cronli5Options} from '../../types.js';
|
|
@@ -33,6 +37,17 @@ interface Stride {
|
|
|
33
37
|
anchor: string;
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
// A contiguous hour range to phrase as a window. `from`/`to` are the bounding
|
|
41
|
+
// hours; `throughMinute` is the close minute used by the "through" span;
|
|
42
|
+
// `continuous` is true only when the run fills every minute of the final hour
|
|
43
|
+
// (a wildcard minute), which earns the default dialect's until-window.
|
|
44
|
+
interface HourWindowSpec {
|
|
45
|
+
from: number;
|
|
46
|
+
to: number;
|
|
47
|
+
throughMinute: number | string;
|
|
48
|
+
continuous: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
// A clock-time entry assembled for rendering. Hour/minute/second arrive as
|
|
37
52
|
// numbers or as raw field tokens (a range bound or single value is a
|
|
38
53
|
// string); `plain` suppresses the noon/midnight words. `explicit` forces the
|
|
@@ -215,9 +230,6 @@ function composeHourCadence(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
|
215
230
|
return hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
|
|
216
231
|
}
|
|
217
232
|
|
|
218
|
-
// A meaningful second under minute/hour shapes the earlier strategies
|
|
219
|
-
// deferred on: the second leads with its own clause and the rest of the
|
|
220
|
-
// pattern follows.
|
|
221
233
|
// A wildcard or stepped second under a fixed minute across one or more specific
|
|
222
234
|
// hours. The clock-time rest collapses the pinned minute into the hour, and on
|
|
223
235
|
// the clock a pinned minute-0 reads as the whole hour ("9 a.m." spoken ==
|
|
@@ -340,7 +352,7 @@ function secondsClause(ir: IR, anchor: string,
|
|
|
340
352
|
if (shape === 'step') {
|
|
341
353
|
// The plan reached this clause only for a stepped second field, whose
|
|
342
354
|
// first segment is always a step segment.
|
|
343
|
-
return stepCycle60(ir
|
|
355
|
+
return stepCycle60(stepSegment(ir, 'second'),
|
|
344
356
|
'second', anchor, opts);
|
|
345
357
|
}
|
|
346
358
|
|
|
@@ -360,8 +372,8 @@ function secondsClause(ir: IR, anchor: string,
|
|
|
360
372
|
// A non-wildcard second under the list/step path always has segments. An
|
|
361
373
|
// offset/uneven step the core enumerated to a fire list reads as a stride
|
|
362
374
|
// cadence when those fires form a long-enough progression.
|
|
363
|
-
return strideFromSegments(ir
|
|
364
|
-
opts) ?? listPastThe(segmentWords(ir
|
|
375
|
+
return strideFromSegments(segmentsOf(ir, 'second'), 'second', anchor,
|
|
376
|
+
opts) ?? listPastThe(segmentWords(segmentsOf(ir, 'second'), opts),
|
|
365
377
|
'second', anchor, opts);
|
|
366
378
|
}
|
|
367
379
|
|
|
@@ -393,9 +405,9 @@ function renderMultipleMinutes(ir: IR, plan: PlanOf<'multipleMinutes'>,
|
|
|
393
405
|
// segments. An offset/uneven step the core enumerated to this list reads as
|
|
394
406
|
// a stride cadence when the fires form a long-enough progression.
|
|
395
407
|
const stride =
|
|
396
|
-
strideFromSegments(ir
|
|
408
|
+
strideFromSegments(segmentsOf(ir, 'minute'), 'minute', 'hour', opts);
|
|
397
409
|
|
|
398
|
-
return (stride ?? listPastThe(segmentWords(ir
|
|
410
|
+
return (stride ?? listPastThe(segmentWords(segmentsOf(ir, 'minute'),
|
|
399
411
|
opts), 'minute', 'hour', opts)) + trailingQualifier(ir, opts);
|
|
400
412
|
}
|
|
401
413
|
|
|
@@ -404,7 +416,7 @@ function renderMinuteFrequency(ir: IR, plan: PlanOf<'minuteFrequency'>,
|
|
|
404
416
|
opts: NormalizedOptions): string {
|
|
405
417
|
// A minute-frequency plan is selected only for a stepped minute field,
|
|
406
418
|
// which has segments.
|
|
407
|
-
let phrase = stepCycle60(ir
|
|
419
|
+
let phrase = stepCycle60(stepSegment(ir, 'minute'),
|
|
408
420
|
'minute', 'hour', opts);
|
|
409
421
|
|
|
410
422
|
if (plan.hours.kind === 'during') {
|
|
@@ -419,14 +431,23 @@ function renderMinuteFrequency(ir: IR, plan: PlanOf<'minuteFrequency'>,
|
|
|
419
431
|
hourTimesFromPlan(ir, plan.hours.times, false, opts) + ' hours';
|
|
420
432
|
}
|
|
421
433
|
else if (plan.hours.kind === 'window') {
|
|
422
|
-
|
|
434
|
+
// A minute-frequency cadence ("every 15 minutes") fills the hours from a
|
|
435
|
+
// STEPPED minute, never a wildcard one, so its run is not continuous to the
|
|
436
|
+
// top of the next hour: the default dialect reads "through <last hour>" and
|
|
437
|
+
// every other dialect closes on the step's last fire (`last`).
|
|
438
|
+
phrase += ' ' + rangeWindow({
|
|
439
|
+
continuous: false,
|
|
440
|
+
from: plan.hours.from,
|
|
441
|
+
throughMinute: plan.hours.last,
|
|
442
|
+
to: plan.hours.to
|
|
443
|
+
}, opts);
|
|
423
444
|
}
|
|
424
445
|
else if (plan.hours.kind === 'step') {
|
|
425
446
|
// The plan carries a step only for a clean stride (dividing the day),
|
|
426
447
|
// which confines the cadence to every Nth hour; a stepped hour field's
|
|
427
448
|
// first segment is a step segment.
|
|
428
449
|
phrase += ' ' +
|
|
429
|
-
everyNthHour(ir
|
|
450
|
+
everyNthHour(stepSegment(ir, 'hour'), opts);
|
|
430
451
|
}
|
|
431
452
|
|
|
432
453
|
return phrase + trailingQualifier(ir, opts);
|
|
@@ -497,8 +518,8 @@ function renderMinutesAcrossHours(ir: IR, plan: PlanOf<'minutesAcrossHours'>,
|
|
|
497
518
|
// step enumerated to that list reads as a stride. A list is a set of
|
|
498
519
|
// discrete fire minutes, not a cadence, so it keeps the "at <times>" frame.
|
|
499
520
|
const lead =
|
|
500
|
-
strideFromSegments(ir
|
|
501
|
-
listPastThe(segmentWords(ir
|
|
521
|
+
strideFromSegments(segmentsOf(ir, 'minute'), 'minute', 'hour', opts) ??
|
|
522
|
+
listPastThe(segmentWords(segmentsOf(ir, 'minute'), opts),
|
|
502
523
|
'minute', 'hour', opts);
|
|
503
524
|
|
|
504
525
|
if (cadence !== null) {
|
|
@@ -534,7 +555,7 @@ function renderMinuteSpanAcrossHourStep(ir: IR,
|
|
|
534
555
|
plan: PlanOf<'minuteSpanAcrossHourStep'>, opts: NormalizedOptions): string {
|
|
535
556
|
// This plan is reached only under a stepped hour field, whose first
|
|
536
557
|
// segment is a step segment.
|
|
537
|
-
const segment = ir
|
|
558
|
+
const segment = stepSegment(ir, 'hour');
|
|
538
559
|
|
|
539
560
|
// A wildcard minute over a stepped hour is reached only for a clean stride
|
|
540
561
|
// (a bounded or uneven step routes through minutesAcrossHours instead), so it
|
|
@@ -547,8 +568,8 @@ function renderMinuteSpanAcrossHourStep(ir: IR,
|
|
|
547
568
|
// A minute list keeps the same cadence clause; only its lead differs. An
|
|
548
569
|
// offset/uneven step the core enumerated to that list reads as a stride.
|
|
549
570
|
const lead = plan.form === 'list' ?
|
|
550
|
-
strideFromSegments(ir
|
|
551
|
-
listPastThe(segmentWords(ir
|
|
571
|
+
strideFromSegments(segmentsOf(ir, 'minute'), 'minute', 'hour', opts) ??
|
|
572
|
+
listPastThe(segmentWords(segmentsOf(ir, 'minute'), opts),
|
|
552
573
|
'minute', 'hour', opts) :
|
|
553
574
|
minuteRangeLead(ir.pattern.minute, opts);
|
|
554
575
|
// A bounded or uneven hour step reads as its endpoint-pinning cadence after
|
|
@@ -607,8 +628,8 @@ function rangeMinuteLead(ir: IR, opts: NormalizedOptions): string {
|
|
|
607
628
|
|
|
608
629
|
// A non-"0" minute here is a discrete list, which has segments; an
|
|
609
630
|
// offset/uneven step enumerated to that list reads as a stride.
|
|
610
|
-
return strideFromSegments(ir
|
|
611
|
-
opts) ?? listPastThe(segmentWords(ir
|
|
631
|
+
return strideFromSegments(segmentsOf(ir, 'minute'), 'minute', 'hour',
|
|
632
|
+
opts) ?? listPastThe(segmentWords(segmentsOf(ir, 'minute'), opts),
|
|
612
633
|
'minute', 'hour', opts);
|
|
613
634
|
}
|
|
614
635
|
|
|
@@ -625,7 +646,7 @@ function renderHourStep(ir: IR, plan: PlanOf<'hourStep'>,
|
|
|
625
646
|
|
|
626
647
|
// An hour-step plan is selected only for a stepped hour field, whose
|
|
627
648
|
// first segment is a step segment.
|
|
628
|
-
return stepHours(ir
|
|
649
|
+
return stepHours(stepSegment(ir, 'hour'), opts) +
|
|
629
650
|
trailingQualifier(ir, opts);
|
|
630
651
|
}
|
|
631
652
|
|
|
@@ -634,31 +655,42 @@ function renderHourStep(ir: IR, plan: PlanOf<'hourStep'>,
|
|
|
634
655
|
// a wildcard minute, which fills every minute and states no separate clause.
|
|
635
656
|
// A pinned/listed/ranged minute is named in its own lead clause, so folding it
|
|
636
657
|
// into the close too would read as a span ("through 5:05 p.m.") that
|
|
637
|
-
// contradicts the minute clause; the window stays bare ("through 5 p.m.").
|
|
658
|
+
// contradicts the minute clause; the window stays bare ("through 5 p.m."). The
|
|
659
|
+
// same wildcard minute is what makes the run CONTINUOUS to the top of the next
|
|
660
|
+
// hour, so it also drives the until-window choice in `rangeWindow`.
|
|
638
661
|
function boundedWindow(plan: PlanOf<'hourRange'>):
|
|
639
|
-
{from: number; to: number;
|
|
640
|
-
const
|
|
662
|
+
{from: number; to: number; closeMinute: number; continuous: boolean} {
|
|
663
|
+
const continuous = plan.minuteForm === 'wildcard';
|
|
664
|
+
const closeMinute = continuous ? plan.boundMinute ?? 0 : 0;
|
|
641
665
|
|
|
642
|
-
return {from: plan.from,
|
|
666
|
+
return {from: plan.from, closeMinute, to: plan.to, continuous};
|
|
643
667
|
}
|
|
644
668
|
|
|
645
669
|
// A contiguous hour range as a window phrase. The default English dialect
|
|
646
|
-
// reads a MULTI-hour range
|
|
647
|
-
// until 6 p.m." (the close
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
|
|
670
|
+
// reads a MULTI-hour range whose run is CONTINUOUS to the top of the next hour
|
|
671
|
+
// as an up-to-but-not-including window — "from 9 a.m. until 6 p.m." (the close
|
|
672
|
+
// is the top of the hour after the last, the sense English uses for time
|
|
673
|
+
// windows: 9-17 runs until 6 p.m.); 23 wraps to midnight. The run is continuous
|
|
674
|
+
// only when the minute is wildcard, so every minute of the final hour fires; a
|
|
675
|
+
// restricted minute fires at discrete points (e.g. only `:00`), so the run
|
|
676
|
+
// stops within the final hour and the default dialect reverts to the bare
|
|
677
|
+
// "through <last hour>" span (the minute is named in its own lead clause, so
|
|
678
|
+
// the close stays on the top of the final hour rather than restating a last
|
|
679
|
+
// fire). Every other dialect (and the compact `short` form) always speaks the
|
|
680
|
+
// span, closing on the minute field's last fire within the final hour. A
|
|
681
|
+
// single-hour sub-hour window (`from === to`, e.g. */15 9 firing 9:00 through
|
|
682
|
+
// 9:45) is NOT a multi-hour range: its close is a real fire inside the hour, so
|
|
683
|
+
// it always keeps "through" — naming "until 10 a.m." would overstate the span
|
|
684
|
+
// past the last fire.
|
|
685
|
+
function rangeWindow(window: HourWindowSpec,
|
|
656
686
|
opts: NormalizedOptions): string {
|
|
687
|
+
const {from, to, throughMinute, continuous} = window;
|
|
657
688
|
const open = 'from ' + getTime({hour: from, minute: 0}, opts);
|
|
658
689
|
|
|
659
690
|
if (opts.style.untilWindow && !opts.short && from !== to) {
|
|
660
|
-
return
|
|
661
|
-
getTime({hour: (to + 1) % 24, minute: 0}, opts)
|
|
691
|
+
return continuous ?
|
|
692
|
+
open + ' until ' + getTime({hour: (to + 1) % 24, minute: 0}, opts) :
|
|
693
|
+
open + through(opts) + getTime({hour: to, minute: 0}, opts);
|
|
662
694
|
}
|
|
663
695
|
|
|
664
696
|
return open + through(opts) +
|
|
@@ -666,11 +698,18 @@ function rangeWindow(from: number, to: number, throughMinute: number | string,
|
|
|
666
698
|
}
|
|
667
699
|
|
|
668
700
|
// An hour window phrase, e.g. "from 9 a.m. through 5:45 p.m." (or "from 9 a.m.
|
|
669
|
-
// until 6 p.m." in the default dialect
|
|
670
|
-
// hour and close at the minute field's last fire
|
|
671
|
-
|
|
701
|
+
// until 6 p.m." in the default dialect, when the minute is wildcard). Windows
|
|
702
|
+
// open at the top of the first hour and close at the minute field's last fire
|
|
703
|
+
// within the final hour.
|
|
704
|
+
function hourWindow(
|
|
705
|
+
window: {from: number; to: number; closeMinute: number; continuous: boolean},
|
|
672
706
|
opts: NormalizedOptions): string {
|
|
673
|
-
return rangeWindow(
|
|
707
|
+
return rangeWindow({
|
|
708
|
+
continuous: window.continuous,
|
|
709
|
+
from: window.from,
|
|
710
|
+
throughMinute: window.closeMinute,
|
|
711
|
+
to: window.to
|
|
712
|
+
}, opts);
|
|
674
713
|
}
|
|
675
714
|
|
|
676
715
|
// Expand a discrete set of hours and minutes into clock times prefixed by
|
|
@@ -729,7 +768,7 @@ function renderCompactClockTimes(ir: IR, plan: PlanOf<'compactClockTimes'>,
|
|
|
729
768
|
|
|
730
769
|
// A compact clock-time plan is reached only for discrete hours, which
|
|
731
770
|
// have segments.
|
|
732
|
-
const hasRange = ir.
|
|
771
|
+
const hasRange = segmentsOf(ir, 'hour').some(function range(segment) {
|
|
733
772
|
return segment.kind === 'range';
|
|
734
773
|
});
|
|
735
774
|
|
|
@@ -748,8 +787,8 @@ function renderCompactClockTimes(ir: IR, plan: PlanOf<'compactClockTimes'>,
|
|
|
748
787
|
const minuteLead =
|
|
749
788
|
// The non-fold branch is a minute list, which has segments. An
|
|
750
789
|
// offset/uneven step enumerated to that list reads as a stride.
|
|
751
|
-
strideFromSegments(ir
|
|
752
|
-
listPastThe(segmentWords(ir
|
|
790
|
+
strideFromSegments(segmentsOf(ir, 'minute'), 'minute', 'hour', opts) ??
|
|
791
|
+
listPastThe(segmentWords(segmentsOf(ir, 'minute'), opts),
|
|
753
792
|
'minute', 'hour', opts);
|
|
754
793
|
// A uneven hour stride reads as a cadence after the minute lead, not a wall
|
|
755
794
|
// of clock-time columns.
|
|
@@ -770,72 +809,63 @@ function renderCompactClockTimes(ir: IR, plan: PlanOf<'compactClockTimes'>,
|
|
|
770
809
|
// A folded hour field that includes a contiguous range reads with the
|
|
771
810
|
// hour-range frame: a shared minute lead ("every hour" / "at 30 minutes
|
|
772
811
|
// past the hour"), each range as a window, and any non-contiguous hour
|
|
773
|
-
// appended by `outlierTail` (
|
|
774
|
-
// every other dialect keeps "and at Z").
|
|
812
|
+
// appended by `outlierTail` ("and at Z").
|
|
775
813
|
function foldedHourWindows(ir: IR, plan: PlanOf<'compactClockTimes'>,
|
|
776
814
|
opts: NormalizedOptions): string {
|
|
777
815
|
const minute = plan.minute;
|
|
778
816
|
const windows: string[] = [];
|
|
779
|
-
const
|
|
780
|
-
const times = outliers.hours.map(function time(hour) {
|
|
817
|
+
const times = collectHourOutliers(ir).map(function time(hour) {
|
|
781
818
|
return getTime({hour, minute}, opts);
|
|
782
819
|
});
|
|
783
820
|
|
|
784
|
-
// Reached only via the fold branch under discrete hours, which have
|
|
785
|
-
//
|
|
786
|
-
|
|
821
|
+
// Reached only via the fold branch under discrete hours, which have segments.
|
|
822
|
+
// A folded minute is a discrete pin/list, never a wildcard, so the run is not
|
|
823
|
+
// continuous to the top of the next hour: the window is not an until-window.
|
|
824
|
+
segmentsOf(ir, 'hour').forEach(function classify(segment) {
|
|
787
825
|
if (segment.kind === 'range') {
|
|
788
|
-
windows.push(rangeWindow(
|
|
789
|
-
|
|
826
|
+
windows.push(rangeWindow({
|
|
827
|
+
continuous: false,
|
|
828
|
+
from: +segment.bounds[0],
|
|
829
|
+
throughMinute: minute,
|
|
830
|
+
to: +segment.bounds[1]
|
|
831
|
+
}, opts));
|
|
790
832
|
}
|
|
791
833
|
});
|
|
792
834
|
|
|
793
835
|
const phrase = rangeMinuteLead(ir, opts) + ' ' + joinList(windows, opts);
|
|
794
836
|
|
|
795
|
-
return phrase + outlierTail(times,
|
|
837
|
+
return phrase + outlierTail(times, opts);
|
|
796
838
|
}
|
|
797
839
|
|
|
798
|
-
// The hours outside a contiguous run — every non-range segment's values
|
|
799
|
-
//
|
|
800
|
-
|
|
801
|
-
// "plus" idiom does not fit and the additive list keeps "and at".
|
|
802
|
-
function collectHourOutliers(ir: IR):
|
|
803
|
-
{hours: number[]; pureStrays: boolean} {
|
|
840
|
+
// The hours outside a contiguous run — every non-range segment's values, with
|
|
841
|
+
// a step contributing its whole fire set.
|
|
842
|
+
function collectHourOutliers(ir: IR): number[] {
|
|
804
843
|
const hours: number[] = [];
|
|
805
|
-
let pureStrays = true;
|
|
806
844
|
|
|
807
845
|
// Reached only under discrete hours, which carry segments.
|
|
808
|
-
ir.
|
|
846
|
+
segmentsOf(ir, 'hour').forEach(function classify(segment) {
|
|
809
847
|
if (segment.kind === 'step') {
|
|
810
848
|
hours.push(...segment.fires);
|
|
811
|
-
pureStrays = false;
|
|
812
849
|
}
|
|
813
850
|
else if (segment.kind !== 'range') {
|
|
814
851
|
hours.push(+segment.value);
|
|
815
852
|
}
|
|
816
853
|
});
|
|
817
854
|
|
|
818
|
-
return
|
|
855
|
+
return hours;
|
|
819
856
|
}
|
|
820
857
|
|
|
821
|
-
// Join the outlier hour times that follow a contiguous-run window
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
// a "through <last fire>" span rather than the until-window.
|
|
828
|
-
function outlierTail(times: string[], pureStrays: boolean,
|
|
829
|
-
opts: NormalizedOptions): string {
|
|
858
|
+
// Join the outlier hour times that follow a contiguous-run window — the hours
|
|
859
|
+
// outside the run, enumerated as "and at 10 p.m.". (A fold always carries a
|
|
860
|
+
// restricted minute, so its run reads the "through" span, never the
|
|
861
|
+
// until-window; the additive "plus" idiom that paired with the until-window no
|
|
862
|
+
// longer applies here.)
|
|
863
|
+
function outlierTail(times: string[], opts: NormalizedOptions): string {
|
|
830
864
|
if (!times.length) {
|
|
831
865
|
return '';
|
|
832
866
|
}
|
|
833
867
|
|
|
834
|
-
|
|
835
|
-
' plus ' :
|
|
836
|
-
' and at ';
|
|
837
|
-
|
|
838
|
-
return connector + joinList(times, opts);
|
|
868
|
+
return ' and at ' + joinList(times, opts);
|
|
839
869
|
}
|
|
840
870
|
|
|
841
871
|
// --- Confinement frame. ---
|
|
@@ -876,7 +906,7 @@ function leadingCadence(ir: IR, opts: NormalizedOptions):
|
|
|
876
906
|
const text = minute === '*' ?
|
|
877
907
|
'every minute' :
|
|
878
908
|
// A clean minute step's first segment is a step segment.
|
|
879
|
-
stepCycle60(ir
|
|
909
|
+
stepCycle60(stepSegment(ir, 'minute'),
|
|
880
910
|
'minute', 'hour', opts);
|
|
881
911
|
|
|
882
912
|
return {secondLead: false, text};
|
|
@@ -905,7 +935,7 @@ function minuteConfinement(ir: IR, opts: NormalizedOptions): string {
|
|
|
905
935
|
// A minute single/range/list under the seconds lead. The minute reads as a
|
|
906
936
|
// ":NN" clock-minute confinement, never "N minutes past the hour" (that is
|
|
907
937
|
// the minute-lead clock-point form).
|
|
908
|
-
const segments = ir
|
|
938
|
+
const segments = segmentsOf(ir, 'minute');
|
|
909
939
|
|
|
910
940
|
if (ir.shapes.minute === 'single') {
|
|
911
941
|
return ' during minute :' + pad(minute);
|
|
@@ -970,7 +1000,15 @@ function hourConfinement(ir: IR, opts: NormalizedOptions): string {
|
|
|
970
1000
|
if (ir.shapes.hour === 'range') {
|
|
971
1001
|
const bounds = hour.split('-');
|
|
972
1002
|
|
|
973
|
-
|
|
1003
|
+
// The until-window holds only when the run is continuous to the top of the
|
|
1004
|
+
// next hour — a wildcard minute fills every minute of the final hour; a
|
|
1005
|
+
// confined minute (":00", a step) stops within it, reading "through".
|
|
1006
|
+
return ' ' + rangeWindow({
|
|
1007
|
+
continuous: ir.pattern.minute === '*',
|
|
1008
|
+
from: +bounds[0],
|
|
1009
|
+
throughMinute: 0,
|
|
1010
|
+
to: +bounds[1]
|
|
1011
|
+
}, opts);
|
|
974
1012
|
}
|
|
975
1013
|
|
|
976
1014
|
// An hour list or stepped range reads "during the <times> hours".
|
|
@@ -989,17 +1027,17 @@ function isContiguousHourRange(ir: IR): boolean {
|
|
|
989
1027
|
|
|
990
1028
|
// Whether an hour field is confinement-eligible. An OPEN hour stride — a clean
|
|
991
1029
|
// `*/n`, an offset `m/n`, or a uneven step — reads as a cadence ("every three
|
|
992
|
-
// hours from 2 a.m."), and only the `*/2` form has a
|
|
993
|
-
// ("of every other hour"), so other open steps defer. A BOUNDED stepped
|
|
994
|
-
// (`a-b/n`, e.g. `9-17/2`) is a discrete set of named hours the
|
|
995
|
-
// frame speaks as a list ("during the 9 a.m., 11 a.m., … hours").
|
|
1030
|
+
// hours from 2 a.m."), and only the `*/2` form has a dedicated confinement
|
|
1031
|
+
// idiom ("of every other hour"), so other open steps defer. A BOUNDED stepped
|
|
1032
|
+
// range (`a-b/n`, e.g. `9-17/2`) is a discrete set of named hours the
|
|
1033
|
+
// confinement frame speaks as a list ("during the 9 a.m., 11 a.m., … hours").
|
|
996
1034
|
function confinableHour(ir: IR): boolean {
|
|
997
1035
|
if (ir.shapes.hour !== 'step') {
|
|
998
1036
|
return true;
|
|
999
1037
|
}
|
|
1000
1038
|
|
|
1001
1039
|
// Reached only under a stepped hour, whose first segment is a step segment.
|
|
1002
|
-
const segment = ir
|
|
1040
|
+
const segment = stepSegment(ir, 'hour');
|
|
1003
1041
|
|
|
1004
1042
|
return ir.pattern.hour === '*/2' || segment.startToken.indexOf('-') !== -1;
|
|
1005
1043
|
}
|
|
@@ -1012,15 +1050,15 @@ function isMinuteStride(ir: IR): boolean {
|
|
|
1012
1050
|
return false;
|
|
1013
1051
|
}
|
|
1014
1052
|
|
|
1015
|
-
const values = singleValues(ir
|
|
1053
|
+
const values = singleValues(segmentsOf(ir, 'minute'));
|
|
1016
1054
|
|
|
1017
1055
|
return values !== null && arithmeticStep(values) !== null;
|
|
1018
1056
|
}
|
|
1019
1057
|
|
|
1020
|
-
// Whether the pattern is in the
|
|
1021
|
-
// covers a finer leading cadence (seconds, or minute under a :00 second)
|
|
1022
|
-
// each coarser field as a confinement; shapes outside
|
|
1023
|
-
//
|
|
1058
|
+
// Whether the pattern is in the confinement frame's supported shape-set. The
|
|
1059
|
+
// frame covers a finer leading cadence (seconds, or minute under a :00 second)
|
|
1060
|
+
// with each coarser field as a confinement; shapes outside it defer to the
|
|
1061
|
+
// existing renderers, which already produce that phrasing for them.
|
|
1024
1062
|
function confinementEligible(ir: IR,
|
|
1025
1063
|
lead: {secondLead: boolean}): boolean {
|
|
1026
1064
|
const {minute, hour} = ir.pattern;
|
|
@@ -1032,7 +1070,7 @@ function confinementEligible(ir: IR,
|
|
|
1032
1070
|
}
|
|
1033
1071
|
|
|
1034
1072
|
if (lead.secondLead) {
|
|
1035
|
-
// A minute STEP is
|
|
1073
|
+
// A minute STEP is supported only as the `*/2` "every other minute" idiom,
|
|
1036
1074
|
// and only where it fills the coarser field: a contiguous hour range or a
|
|
1037
1075
|
// single hour both close on the minute's real last fire, which the
|
|
1038
1076
|
// windowing renderer already speaks. The `*/2` step fills both, so it keeps
|
|
@@ -1054,7 +1092,7 @@ function confinementEligible(ir: IR,
|
|
|
1054
1092
|
}
|
|
1055
1093
|
|
|
1056
1094
|
// A minute-LEAD cadence (second :00). The existing renderers already produce
|
|
1057
|
-
//
|
|
1095
|
+
// that phrasing for a single/range/list hour and for a non-`*/2` hour
|
|
1058
1096
|
// step; the confinement frame only changes the `*/2` hour ("of every other
|
|
1059
1097
|
// hour") and the single hour under an "every other minute" step ("from
|
|
1060
1098
|
// midnight until 1 a.m."). Everything else defers.
|
|
@@ -1065,9 +1103,10 @@ function confinementEligible(ir: IR,
|
|
|
1065
1103
|
return ir.shapes.hour === 'single' && minute === '*/2';
|
|
1066
1104
|
}
|
|
1067
1105
|
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
// renderers in place of the older juxtaposed-cadence and
|
|
1106
|
+
// Render the pattern with the confinement frame: a finer leading cadence with
|
|
1107
|
+
// each coarser field as a confinement, or null when it does not apply. Routed
|
|
1108
|
+
// to from the cadence renderers in place of the older juxtaposed-cadence and
|
|
1109
|
+
// duration-frame forms.
|
|
1071
1110
|
function confinement(ir: IR, opts: NormalizedOptions): string | null {
|
|
1072
1111
|
// The confinement frame is scoped to the default (US) dialect, the one that
|
|
1073
1112
|
// carries the until-window; every other dialect and the compact `short` form
|
|
@@ -1152,22 +1191,6 @@ function renderStride(stride: Stride, opts: NormalizedOptions): string {
|
|
|
1152
1191
|
pluralize(last, unit) + ' past the ' + anchor;
|
|
1153
1192
|
}
|
|
1154
1193
|
|
|
1155
|
-
// The sorted numeric values a field's segments cover, or null if any segment
|
|
1156
|
-
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
1157
|
-
function singleValues(segments: Segment[]): number[] | null {
|
|
1158
|
-
const values: number[] = [];
|
|
1159
|
-
|
|
1160
|
-
for (const segment of segments) {
|
|
1161
|
-
if (segment.kind !== 'single') {
|
|
1162
|
-
return null;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
values.push(+segment.value);
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
return values;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
1194
|
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
1172
1195
|
// form an arithmetic progression long enough to beat the list (the core
|
|
1173
1196
|
// enumerates an offset/uneven step to this fire list; the IR is unchanged, so
|
|
@@ -1269,17 +1292,6 @@ function hourStrideCadence(stride: {start: number; interval: number;
|
|
|
1269
1292
|
through(opts) + getTime({hour: last, minute: 0}, opts);
|
|
1270
1293
|
}
|
|
1271
1294
|
|
|
1272
|
-
// Whether an hour stride wraps the day cleanly from within its first interval
|
|
1273
|
-
// (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
|
|
1274
|
-
// stride has no distinct endpoint and keeps its bare or "from M" cadence. Every
|
|
1275
|
-
// other stride — a uneven interval, or one starting at or past its interval
|
|
1276
|
-
// (a bounded `a-b/n`) — is a bounded set the cadence pins both endpoints of.
|
|
1277
|
-
function offsetCleanStride(
|
|
1278
|
-
stride: {start: number; interval: number}
|
|
1279
|
-
): boolean {
|
|
1280
|
-
return stride.start < stride.interval && 24 % stride.interval === 0;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
1295
|
// The bounded cadence for an hour stride that pins both clock-time endpoints,
|
|
1284
1296
|
// or null when the hour is not such a stride. The core rewrites a uneven step
|
|
1285
1297
|
// to its fire list, so a minute window/list/step crossed with it lands in the
|
|
@@ -1297,40 +1309,6 @@ function unevenHourCadence(ir: IR, opts: NormalizedOptions): string | null {
|
|
|
1297
1309
|
return hourStrideCadence(stride, opts);
|
|
1298
1310
|
}
|
|
1299
1311
|
|
|
1300
|
-
// An hour list's arithmetic progression, or null when its values are not a
|
|
1301
|
-
// step the renderer should speak as a cadence. The core rewrites a uneven hour
|
|
1302
|
-
// step (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its
|
|
1303
|
-
// literal fire list, indistinguishable in the IR from a hand-written list; the
|
|
1304
|
-
// renderer recovers the cadence from the values. A progression starting at
|
|
1305
|
-
// zero is a `*/n` step however short (0,7,14,21 is `*/7`); a non-zero one is
|
|
1306
|
-
// only a step when it is too long to be a deliberate clock-time list (e.g.
|
|
1307
|
-
// 9,17 is two named times, not a cadence), the same length the minute/second
|
|
1308
|
-
// list path uses. Interval one is a plain range, never a step.
|
|
1309
|
-
function hourListStride(values: number[]):
|
|
1310
|
-
{start: number; interval: number; last: number} | null {
|
|
1311
|
-
if (values.length < 2) {
|
|
1312
|
-
return null;
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
const interval = values[1] - values[0];
|
|
1316
|
-
|
|
1317
|
-
if (interval < 2) {
|
|
1318
|
-
return null;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
for (let i = 2; i < values.length; i += 1) {
|
|
1322
|
-
if (values[i] - values[i - 1] !== interval) {
|
|
1323
|
-
return null;
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
if (values[0] !== 0 && values.length < 5) {
|
|
1328
|
-
return null;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
return {interval, last: values[values.length - 1], start: values[0]};
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
1312
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
1335
1313
|
// segment yields its {start, interval, last} directly; an all-single hour
|
|
1336
1314
|
// list yields one only when its values form a step progression (so an irregular
|
|
@@ -1341,7 +1319,7 @@ function hourStride(ir: IR):
|
|
|
1341
1319
|
{start: number; interval: number; last: number} | null {
|
|
1342
1320
|
// Reached only from the clock-time paths, which run under discrete hours
|
|
1343
1321
|
// and so always carry hour segments.
|
|
1344
|
-
const segments = ir
|
|
1322
|
+
const segments = segmentsOf(ir, 'hour');
|
|
1345
1323
|
|
|
1346
1324
|
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
1347
1325
|
const segment = segments[0];
|
|
@@ -1462,7 +1440,7 @@ function hourCadence(ir: IR, minute: number,
|
|
|
1462
1440
|
// or an arithmetic-progression list, which keep the bounded cadence form).
|
|
1463
1441
|
function cleanStrideSegment(ir: IR): StepSegment | null {
|
|
1464
1442
|
// Reached only after hourStride confirmed a stride, so hour segments exist.
|
|
1465
|
-
const segments = ir
|
|
1443
|
+
const segments = segmentsOf(ir, 'hour');
|
|
1466
1444
|
const segment = segments.length === 1 && segments[0];
|
|
1467
1445
|
|
|
1468
1446
|
if (!segment || segment.kind !== 'step' ||
|
|
@@ -1482,35 +1460,40 @@ function cleanStrideSegment(ir: IR): StepSegment | null {
|
|
|
1482
1460
|
function hasHourWindow(ir: IR): boolean {
|
|
1483
1461
|
// Reached only from the clock-time paths, which run under discrete hours
|
|
1484
1462
|
// and so always carry hour segments.
|
|
1485
|
-
return ir.
|
|
1463
|
+
return segmentsOf(ir, 'hour').some(function range(segment) {
|
|
1486
1464
|
return segment.kind === 'range';
|
|
1487
1465
|
});
|
|
1488
1466
|
}
|
|
1489
1467
|
|
|
1490
1468
|
// The hour-range window as a cadence tail at the top of each hour: each range
|
|
1491
|
-
// segment is a window ("every hour from 9 a.m.
|
|
1492
|
-
// non-contiguous single hour is appended by `outlierTail` ("
|
|
1493
|
-
//
|
|
1494
|
-
//
|
|
1495
|
-
// hour. Mirrors foldedHourWindows but
|
|
1469
|
+
// segment is a window ("every hour from 9 a.m. through 8 p.m."), and any
|
|
1470
|
+
// non-contiguous single hour is appended by `outlierTail` ("and at 10 p.m.").
|
|
1471
|
+
// The minute has already folded into the "every hour" lead — a single pinned
|
|
1472
|
+
// minute, never a wildcard — so the run is not continuous to the top of the
|
|
1473
|
+
// next hour and the window keeps "through". Mirrors foldedHourWindows but
|
|
1474
|
+
// pinned to minute 0.
|
|
1496
1475
|
function hourRangeWindowTail(ir: IR, opts: NormalizedOptions): string {
|
|
1497
1476
|
const windows: string[] = [];
|
|
1498
|
-
const
|
|
1477
|
+
const outlierHours = collectHourOutliers(ir);
|
|
1499
1478
|
|
|
1500
1479
|
// Reached only after hasHourWindow, so hour segments exist.
|
|
1501
|
-
ir.
|
|
1480
|
+
segmentsOf(ir, 'hour').forEach(function classify(segment) {
|
|
1502
1481
|
if (segment.kind === 'range') {
|
|
1503
|
-
windows.push(rangeWindow(
|
|
1504
|
-
|
|
1482
|
+
windows.push(rangeWindow({
|
|
1483
|
+
continuous: false,
|
|
1484
|
+
from: +segment.bounds[0],
|
|
1485
|
+
throughMinute: 0,
|
|
1486
|
+
to: +segment.bounds[1]
|
|
1487
|
+
}, opts));
|
|
1505
1488
|
}
|
|
1506
1489
|
});
|
|
1507
1490
|
|
|
1508
1491
|
const phrase = 'every hour ' + joinList(windows, opts);
|
|
1509
|
-
const times =
|
|
1492
|
+
const times = outlierHours.map(function time(hour) {
|
|
1510
1493
|
return getTime({hour, minute: 0}, opts);
|
|
1511
1494
|
});
|
|
1512
1495
|
|
|
1513
|
-
return phrase + outlierTail(times,
|
|
1496
|
+
return phrase + outlierTail(times, opts);
|
|
1514
1497
|
}
|
|
1515
1498
|
|
|
1516
1499
|
// Render an hour range (or a list whose segments include a range) under a
|
|
@@ -1694,7 +1677,7 @@ function hourSegmentTimes(ir: IR,
|
|
|
1694
1677
|
const {minute, second} = fold;
|
|
1695
1678
|
// Hour-segment rendering is reached only under discrete hours, which have
|
|
1696
1679
|
// segments.
|
|
1697
|
-
const segments = ir
|
|
1680
|
+
const segments = segmentsOf(ir, 'hour');
|
|
1698
1681
|
const plain = mixedTwelve(segments.flatMap(function entries(segment) {
|
|
1699
1682
|
return segmentHours(segment).map(function entry(hour) {
|
|
1700
1683
|
return {hour: +hour, minute, second};
|
|
@@ -1905,7 +1888,7 @@ function datePhrase(ir: IR, words: QualifierWords,
|
|
|
1905
1888
|
function monthFoldsIntoDate(ir: IR): boolean {
|
|
1906
1889
|
return !oddEvenMonth(ir.pattern.month) &&
|
|
1907
1890
|
// Reached only with a restricted month, which has segments.
|
|
1908
|
-
ir.
|
|
1891
|
+
segmentsOf(ir, 'month').every(function flat(segment) {
|
|
1909
1892
|
return segment.kind !== 'range';
|
|
1910
1893
|
});
|
|
1911
1894
|
}
|
|
@@ -1971,7 +1954,7 @@ function dayUnionDatePieces(ir: IR, opts: NormalizedOptions): string[] {
|
|
|
1971
1954
|
// spreads its enumerated fires as separate "the <ordinal>" alternatives.
|
|
1972
1955
|
const pieces: string[] = [];
|
|
1973
1956
|
|
|
1974
|
-
ir.
|
|
1957
|
+
segmentsOf(ir, 'date').forEach(function expand(segment) {
|
|
1975
1958
|
if (segment.kind === 'range') {
|
|
1976
1959
|
pieces.push('from the ' + getOrdinal(segment.bounds[0]) + through(opts) +
|
|
1977
1960
|
'the ' + getOrdinal(segment.bounds[1]));
|
|
@@ -2004,11 +1987,11 @@ function dayUnionWeekdayPieces(ir: IR, opts: NormalizedOptions): string[] {
|
|
|
2004
1987
|
|
|
2005
1988
|
// The union predicate keeps the canonical Sunday-first order (0…6) rather
|
|
2006
1989
|
// than the weekend-last display order: as a flat or-list of day kinds, the
|
|
2007
|
-
// numeric order reads as naturally as any other
|
|
2008
|
-
//
|
|
1990
|
+
// numeric order reads as naturally as any other in a flat or-list ("a
|
|
1991
|
+
// Sunday, a Tuesday, a Thursday, or a Saturday").
|
|
2009
1992
|
const pieces: string[] = [];
|
|
2010
1993
|
|
|
2011
|
-
ir.
|
|
1994
|
+
segmentsOf(ir, 'weekday').forEach(function expand(segment) {
|
|
2012
1995
|
if (segment.kind === 'range' &&
|
|
2013
1996
|
segment.bounds[0] === '1' && segment.bounds[1] === '5') {
|
|
2014
1997
|
pieces.push('a weekday');
|
|
@@ -2161,7 +2144,7 @@ function monthDatePhrase(ir: IR, opts: NormalizedOptions): string {
|
|
|
2161
2144
|
const month = monthName(ir, opts);
|
|
2162
2145
|
// A month-day phrase is reached only with a restricted date, which has
|
|
2163
2146
|
// segments.
|
|
2164
|
-
const days = renderSegments(ir
|
|
2147
|
+
const days = renderSegments(segmentsOf(ir, 'date'),
|
|
2165
2148
|
opts.style.ordinals ? getOrdinal : cardinalDay, opts);
|
|
2166
2149
|
|
|
2167
2150
|
if (opts.style.dayFirst && ir.shapes.date === 'single' &&
|
|
@@ -2238,7 +2221,7 @@ function stepDates(dateField: string): string {
|
|
|
2238
2221
|
// handled separately as a frequency phrase.
|
|
2239
2222
|
function dateOrdinals(ir: IR, opts: NormalizedOptions): string {
|
|
2240
2223
|
// Reached only with a restricted date, which has segments.
|
|
2241
|
-
return renderSegments(ir
|
|
2224
|
+
return renderSegments(segmentsOf(ir, 'date'), getOrdinal, opts);
|
|
2242
2225
|
}
|
|
2243
2226
|
|
|
2244
2227
|
// Render the month field as names. There are few, named months, so a step
|
|
@@ -2254,7 +2237,7 @@ function monthName(ir: IR, opts: NormalizedOptions): string {
|
|
|
2254
2237
|
|
|
2255
2238
|
// A restricted month has segments; open steps of interval 3+ enumerate their
|
|
2256
2239
|
// fires here too.
|
|
2257
|
-
return renderSegments(ir
|
|
2240
|
+
return renderSegments(segmentsOf(ir, 'month'), function name(value) {
|
|
2258
2241
|
return getMonth(value, opts);
|
|
2259
2242
|
}, opts);
|
|
2260
2243
|
}
|
|
@@ -2293,7 +2276,7 @@ function weekdayPhrase(ir: IR, recurring: boolean,
|
|
|
2293
2276
|
// Reached only with a restricted weekday, which has segments. Weekday lists
|
|
2294
2277
|
// display Monday-first (Sunday last) so a weekend reads naturally; the IR
|
|
2295
2278
|
// stays canonical (Sunday=0) and ranges keep their form.
|
|
2296
|
-
const segments = orderWeekdaysForDisplay(ir
|
|
2279
|
+
const segments = orderWeekdaysForDisplay(segmentsOf(ir, 'weekday'));
|
|
2297
2280
|
const hasRange = segments.some(function range(segment) {
|
|
2298
2281
|
return segment.kind === 'range';
|
|
2299
2282
|
});
|
|
@@ -2345,14 +2328,6 @@ function renderSegments(segments: Segment[],
|
|
|
2345
2328
|
return joinList(pieces, opts);
|
|
2346
2329
|
}
|
|
2347
2330
|
|
|
2348
|
-
// Whether a canonical field value is an "open" step (`*/n` or `a/n`, not a
|
|
2349
|
-
// bounded range or a list). Open steps read as a frequency rather than an
|
|
2350
|
-
// enumeration.
|
|
2351
|
-
function isOpenStep(field: string): boolean {
|
|
2352
|
-
return field.indexOf('/') !== -1 && field.indexOf('-') === -1 &&
|
|
2353
|
-
field.indexOf(',') === -1;
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
2331
|
// --- Years. ---
|
|
2357
2332
|
|
|
2358
2333
|
// Append or fold the year field into a finished description. An
|