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/en/index.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
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 {arithmeticStep} from '../../core/util.js';
|
|
7
|
+
import {maxClockTimes} from '../../core/specs.js';
|
|
6
8
|
import {clockDigits, numeral} from '../../core/format.js';
|
|
7
9
|
import type {Cronli5Options} from '../../types.js';
|
|
8
10
|
import type {
|
|
@@ -19,6 +21,18 @@ type PlanOf<K extends PlanNode['kind']> = Extract<PlanNode, {kind: K}>;
|
|
|
19
21
|
// phrasing, where the first segment is always a step segment.
|
|
20
22
|
type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
21
23
|
|
|
24
|
+
// A step cadence to phrase: the `interval` repeats over a `cycle`-long field
|
|
25
|
+
// (60 for minute/second, 24 for hour), running from `start` to `last`. `unit`
|
|
26
|
+
// is the singular noun and `anchor` the larger unit the values count against.
|
|
27
|
+
interface Stride {
|
|
28
|
+
interval: number;
|
|
29
|
+
start: number;
|
|
30
|
+
last: number;
|
|
31
|
+
cycle: number;
|
|
32
|
+
unit: string;
|
|
33
|
+
anchor: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
// A clock-time entry assembled for rendering. Hour/minute/second arrive as
|
|
23
37
|
// numbers or as raw field tokens (a range bound or single value is a
|
|
24
38
|
// string); `plain` suppresses the noon/midnight words. `explicit` forces the
|
|
@@ -174,11 +188,35 @@ function renderSecondsWithinMinute(ir: IR, plan: PlanOf<'secondsWithinMinute'>,
|
|
|
174
188
|
trailingQualifier(ir, opts);
|
|
175
189
|
}
|
|
176
190
|
|
|
191
|
+
// The hour-cadence rendering of a compose-seconds plan whose clock-time rest
|
|
192
|
+
// would cross-multiply an hour stride under a single pinned minute, or null
|
|
193
|
+
// when that does not apply (a non-clock rest, a multi-valued minute, or an
|
|
194
|
+
// hour that is not a stride).
|
|
195
|
+
function composeHourCadence(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
196
|
+
opts: NormalizedOptions): string | null {
|
|
197
|
+
const clockRest = plan.rest.kind === 'clockTimes' ||
|
|
198
|
+
plan.rest.kind === 'compactClockTimes';
|
|
199
|
+
|
|
200
|
+
return clockRest && ir.shapes.minute === 'single' ?
|
|
201
|
+
hourCadence(ir, +ir.pattern.minute, opts) :
|
|
202
|
+
null;
|
|
203
|
+
}
|
|
204
|
+
|
|
177
205
|
// A meaningful second under minute/hour shapes the earlier strategies
|
|
178
206
|
// deferred on: the second leads with its own clause and the rest of the
|
|
179
207
|
// pattern follows.
|
|
180
208
|
function renderComposeSeconds(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
181
209
|
opts: NormalizedOptions): string {
|
|
210
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
211
|
+
// minute is a cadence, not a wall of clock times: speak the second/minute
|
|
212
|
+
// lead, then the hour cadence ("at 30 seconds past the hour, every two
|
|
213
|
+
// hours"). The clock-time rest would otherwise cross-multiply the hours.
|
|
214
|
+
const cadence = composeHourCadence(ir, plan, opts);
|
|
215
|
+
|
|
216
|
+
if (cadence !== null) {
|
|
217
|
+
return cadence;
|
|
218
|
+
}
|
|
219
|
+
|
|
182
220
|
// A wildcard or stepped second under a minute pinned to a single value
|
|
183
221
|
// across one or more specific hours. The clock-time rest collapses the
|
|
184
222
|
// pinned minute into the hour, and on the clock a pinned minute-0 reads as
|
|
@@ -255,6 +293,16 @@ function clockTimesOf(ir: IR, plan: PlanOf<'clockTimes'>,
|
|
|
255
293
|
// e.g. "at 5 and 10 seconds past the minute" or "every second from zero
|
|
256
294
|
// through 30 past the minute".
|
|
257
295
|
function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
296
|
+
return secondsClause(ir, 'minute', opts);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// The second clause counted against an arbitrary anchor. The anchor is
|
|
300
|
+
// "minute" in the standalone seconds path; the hour-cadence path folds a
|
|
301
|
+
// pinned minute 0 into the hour and counts the second "past the hour"
|
|
302
|
+
// instead ("at 30 seconds past the hour", "every second from 0 through 10
|
|
303
|
+
// past the hour"), so the minute-0 confinement is stated, not dropped.
|
|
304
|
+
function secondsClause(ir: IR, anchor: string,
|
|
305
|
+
opts: NormalizedOptions): string {
|
|
258
306
|
const secondField = ir.pattern.second;
|
|
259
307
|
const shape = ir.shapes.second;
|
|
260
308
|
|
|
@@ -266,7 +314,7 @@ function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
|
266
314
|
// The plan reached this clause only for a stepped second field, whose
|
|
267
315
|
// first segment is always a step segment.
|
|
268
316
|
return stepCycle60(ir.analyses.segments.second![0] as StepSegment,
|
|
269
|
-
'second',
|
|
317
|
+
'second', anchor, opts);
|
|
270
318
|
}
|
|
271
319
|
|
|
272
320
|
if (shape === 'range') {
|
|
@@ -274,17 +322,20 @@ function secondsLeadClause(ir: IR, opts: NormalizedOptions): string {
|
|
|
274
322
|
const num = seriesNumber(bounds, opts);
|
|
275
323
|
|
|
276
324
|
return 'every second from ' + num(bounds[0]) +
|
|
277
|
-
through(opts) + num(bounds[1]) + ' past the
|
|
325
|
+
through(opts) + num(bounds[1]) + ' past the ' + anchor;
|
|
278
326
|
}
|
|
279
327
|
|
|
280
328
|
if (shape === 'single') {
|
|
281
329
|
return 'at ' + getNumber(secondField, opts) + ' ' +
|
|
282
|
-
pluralize(secondField, 'second') + ' past the
|
|
330
|
+
pluralize(secondField, 'second') + ' past the ' + anchor;
|
|
283
331
|
}
|
|
284
332
|
|
|
285
|
-
// A non-wildcard second under the list/step path always has segments.
|
|
286
|
-
|
|
287
|
-
|
|
333
|
+
// A non-wildcard second under the list/step path always has segments. An
|
|
334
|
+
// offset/uneven step the core enumerated to a fire list reads as a stride
|
|
335
|
+
// cadence when those fires form a long-enough progression.
|
|
336
|
+
return strideFromSegments(ir.analyses.segments.second!, 'second', anchor,
|
|
337
|
+
opts) ?? listPastThe(segmentWords(ir.analyses.segments.second!, opts),
|
|
338
|
+
'second', anchor, opts);
|
|
288
339
|
}
|
|
289
340
|
|
|
290
341
|
// --- Minute renderers. ---
|
|
@@ -312,9 +363,13 @@ function renderRangeOfMinutes(ir: IR, plan: PlanOf<'rangeOfMinutes'>,
|
|
|
312
363
|
function renderMultipleMinutes(ir: IR, plan: PlanOf<'multipleMinutes'>,
|
|
313
364
|
opts: NormalizedOptions): string {
|
|
314
365
|
// A multiple-minutes plan is selected only for a minute list, which has
|
|
315
|
-
// segments.
|
|
316
|
-
|
|
317
|
-
|
|
366
|
+
// segments. An offset/uneven step the core enumerated to this list reads as
|
|
367
|
+
// a stride cadence when the fires form a long-enough progression.
|
|
368
|
+
const stride =
|
|
369
|
+
strideFromSegments(ir.analyses.segments.minute!, 'minute', 'hour', opts);
|
|
370
|
+
|
|
371
|
+
return (stride ?? listPastThe(segmentWords(ir.analyses.segments.minute!,
|
|
372
|
+
opts), 'minute', 'hour', opts)) + trailingQualifier(ir, opts);
|
|
318
373
|
}
|
|
319
374
|
|
|
320
375
|
// A repeating minute step, qualified by the active hour window(s).
|
|
@@ -376,9 +431,11 @@ function renderMinutesAcrossHours(ir: IR, plan: PlanOf<'minutesAcrossHours'>,
|
|
|
376
431
|
const times = hourTimesFromPlan(ir, plan.times, true, opts);
|
|
377
432
|
const lead = plan.form === 'range' ?
|
|
378
433
|
minuteRangeLead(ir.pattern.minute, opts) :
|
|
379
|
-
// The 'list' form is a minute list, which has segments
|
|
380
|
-
|
|
381
|
-
|
|
434
|
+
// The 'list' form is a minute list, which has segments; an offset/uneven
|
|
435
|
+
// step enumerated to that list reads as a stride.
|
|
436
|
+
strideFromSegments(ir.analyses.segments.minute!, 'minute', 'hour', opts) ??
|
|
437
|
+
listPastThe(segmentWords(ir.analyses.segments.minute!, opts),
|
|
438
|
+
'minute', 'hour', opts);
|
|
382
439
|
|
|
383
440
|
return lead + ', at ' + times + trailingQualifier(ir, opts);
|
|
384
441
|
}
|
|
@@ -414,8 +471,15 @@ function renderMinuteSpanAcrossHourStep(ir: IR,
|
|
|
414
471
|
trailingQualifier(ir, opts);
|
|
415
472
|
}
|
|
416
473
|
|
|
417
|
-
|
|
418
|
-
|
|
474
|
+
// A minute list keeps the same cadence clause; only its lead differs. An
|
|
475
|
+
// offset/uneven step the core enumerated to that list reads as a stride.
|
|
476
|
+
const lead = plan.form === 'list' ?
|
|
477
|
+
strideFromSegments(ir.analyses.segments.minute!, 'minute', 'hour', opts) ??
|
|
478
|
+
listPastThe(segmentWords(ir.analyses.segments.minute!, opts),
|
|
479
|
+
'minute', 'hour', opts) :
|
|
480
|
+
minuteRangeLead(ir.pattern.minute, opts);
|
|
481
|
+
|
|
482
|
+
return lead + ', ' + stepHours(segment, opts) + trailingQualifier(ir, opts);
|
|
419
483
|
}
|
|
420
484
|
|
|
421
485
|
// Lead phrase for a plain minute range: "every minute from <a> through <b>
|
|
@@ -463,8 +527,10 @@ function rangeMinuteLead(ir: IR, opts: NormalizedOptions): string {
|
|
|
463
527
|
return 'every hour';
|
|
464
528
|
}
|
|
465
529
|
|
|
466
|
-
// A non-"0" minute here is a discrete list, which has segments
|
|
467
|
-
|
|
530
|
+
// A non-"0" minute here is a discrete list, which has segments; an
|
|
531
|
+
// offset/uneven step enumerated to that list reads as a stride.
|
|
532
|
+
return strideFromSegments(ir.analyses.segments.minute!, 'minute', 'hour',
|
|
533
|
+
opts) ?? listPastThe(segmentWords(ir.analyses.segments.minute!, opts),
|
|
468
534
|
'minute', 'hour', opts);
|
|
469
535
|
}
|
|
470
536
|
|
|
@@ -497,6 +563,16 @@ function hourWindow(window: {from: number; to: number; last: number},
|
|
|
497
563
|
// a day-level qualifier, e.g. "every day at 9 a.m. and 9:30 a.m.".
|
|
498
564
|
function renderClockTimes(ir: IR, plan: PlanOf<'clockTimes'>,
|
|
499
565
|
opts: NormalizedOptions): string {
|
|
566
|
+
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
567
|
+
// minute reads as a cadence rather than a cross-product of clock times.
|
|
568
|
+
if (ir.shapes.minute === 'single') {
|
|
569
|
+
const cadence = hourCadence(ir, +ir.pattern.minute, opts);
|
|
570
|
+
|
|
571
|
+
if (cadence !== null) {
|
|
572
|
+
return cadence;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
500
576
|
const plain = mixedTwelve(plan.times);
|
|
501
577
|
const times = plan.times.map(function clock(time) {
|
|
502
578
|
return getTime({
|
|
@@ -516,6 +592,15 @@ function renderClockTimes(ir: IR, plan: PlanOf<'clockTimes'>,
|
|
|
516
592
|
function renderCompactClockTimes(ir: IR, plan: PlanOf<'compactClockTimes'>,
|
|
517
593
|
opts: NormalizedOptions): string {
|
|
518
594
|
if (plan.fold) {
|
|
595
|
+
// An hour step (or arithmetic-progression hour list) under the single
|
|
596
|
+
// pinned minute reads as a cadence, not a wall of clock times. (Returns
|
|
597
|
+
// null for an irregular list or a range, which keep folding below.)
|
|
598
|
+
const cadence = hourCadence(ir, +plan.minute, opts);
|
|
599
|
+
|
|
600
|
+
if (cadence !== null) {
|
|
601
|
+
return cadence;
|
|
602
|
+
}
|
|
603
|
+
|
|
519
604
|
// A compact clock-time plan is reached only for discrete hours, which
|
|
520
605
|
// have segments.
|
|
521
606
|
const hasRange = ir.analyses.segments.hour!.some(function range(segment) {
|
|
@@ -535,9 +620,11 @@ function renderCompactClockTimes(ir: IR, plan: PlanOf<'compactClockTimes'>,
|
|
|
535
620
|
}
|
|
536
621
|
|
|
537
622
|
const phrase =
|
|
538
|
-
// The non-fold branch is a minute list, which has segments.
|
|
539
|
-
|
|
540
|
-
|
|
623
|
+
// The non-fold branch is a minute list, which has segments. An
|
|
624
|
+
// offset/uneven step enumerated to that list reads as a stride.
|
|
625
|
+
(strideFromSegments(ir.analyses.segments.minute!, 'minute', 'hour', opts) ??
|
|
626
|
+
listPastThe(segmentWords(ir.analyses.segments.minute!, opts),
|
|
627
|
+
'minute', 'hour', opts)) +
|
|
541
628
|
', at ' + hourSegmentTimes(ir, {minute: 0, second: null}, true, opts) +
|
|
542
629
|
trailingQualifier(ir, opts);
|
|
543
630
|
|
|
@@ -609,6 +696,71 @@ const renderers = {
|
|
|
609
696
|
|
|
610
697
|
// --- Step phrases. ---
|
|
611
698
|
|
|
699
|
+
// Speak a step cadence over a `cycle`-long field ("every N <unit>s [from M
|
|
700
|
+
// [through K]] past the <anchor>"). A clean stride from the top of the cycle
|
|
701
|
+
// is the bare cadence; a uniform offset (start within the first interval, the
|
|
702
|
+
// interval still tiling the cycle) names only its start, since it wraps cleanly
|
|
703
|
+
// and has no distinct endpoint; a non-uniform stride (start >= interval, or an
|
|
704
|
+
// interval that does not tile the cycle) pins both endpoints so the bounded,
|
|
705
|
+
// non-wrapping set reads unambiguously. This is the one phrasing for every
|
|
706
|
+
// step the renderer speaks, whether the core kept it a step shape (a clean
|
|
707
|
+
// cadence) or enumerated it to a fire list (an offset/uneven set the list
|
|
708
|
+
// path recognizes as an arithmetic progression).
|
|
709
|
+
function renderStride(stride: Stride, opts: NormalizedOptions): string {
|
|
710
|
+
const {interval, start, last, cycle, unit, anchor} = stride;
|
|
711
|
+
const cadence = 'every ' + getNumber(interval, opts) + ' ' + unit + 's';
|
|
712
|
+
const tiles = cycle % interval === 0;
|
|
713
|
+
|
|
714
|
+
if (start === 0 && tiles) {
|
|
715
|
+
return cadence;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (start < interval && tiles) {
|
|
719
|
+
// A clean wrap from a non-zero offset: name the start, no endpoint.
|
|
720
|
+
return cadence + ' from ' + getNumber(start, opts) + ' ' +
|
|
721
|
+
pluralize(start, unit) + ' past the ' + anchor;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// A bounded, non-wrapping set: pin both endpoints. The two bounds share one
|
|
725
|
+
// number style (all spelled, or all numerals once either crosses ten),
|
|
726
|
+
// matching the range idiom ("from 0 through 30").
|
|
727
|
+
const num = seriesNumber([start, last], opts);
|
|
728
|
+
|
|
729
|
+
return cadence + ' from ' + num(start) + through(opts) + num(last) + ' ' +
|
|
730
|
+
pluralize(last, unit) + ' past the ' + anchor;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// The sorted numeric values a field's segments cover, or null if any segment
|
|
734
|
+
// is not a discrete single (a range or sub-step is not a plain fire list).
|
|
735
|
+
function singleValues(segments: Segment[]): number[] | null {
|
|
736
|
+
const values: number[] = [];
|
|
737
|
+
|
|
738
|
+
for (const segment of segments) {
|
|
739
|
+
if (segment.kind !== 'single') {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
values.push(+segment.value);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return values;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Speak a minute/second field's enumerated fires as a step cadence when they
|
|
750
|
+
// form an arithmetic progression long enough to beat the list (the core
|
|
751
|
+
// enumerates an offset/uneven step to this fire list; the IR is unchanged, so
|
|
752
|
+
// the renderer recognizes the progression). Returns null for a non-progression
|
|
753
|
+
// or a too-short list, leaving the caller to enumerate.
|
|
754
|
+
function strideFromSegments(segments: Segment[], unit: string, anchor: string,
|
|
755
|
+
opts: NormalizedOptions): string | null {
|
|
756
|
+
const values = singleValues(segments);
|
|
757
|
+
const step = values && arithmeticStep(values);
|
|
758
|
+
|
|
759
|
+
return step ?
|
|
760
|
+
renderStride({...step, cycle: 60, unit, anchor}, opts) :
|
|
761
|
+
null;
|
|
762
|
+
}
|
|
763
|
+
|
|
612
764
|
// Phrase a `start/interval` step segment for a field that cycles every 60
|
|
613
765
|
// units (seconds and minutes). `unit` is the singular noun and `anchor` is
|
|
614
766
|
// the larger unit the values are counted against. Interval-one steps never
|
|
@@ -624,23 +776,23 @@ function stepCycle60(segment: StepSegment, unit: string,
|
|
|
624
776
|
}
|
|
625
777
|
|
|
626
778
|
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
627
|
-
const interval = segment.interval;
|
|
628
779
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return 'every ' + getNumber(interval, opts) + ' ' + unit + 's from ' +
|
|
638
|
-
getNumber(start, opts) + ' ' + pluralize(start, unit) +
|
|
639
|
-
' past the ' + anchor;
|
|
780
|
+
// A short offset cadence lists its fires; otherwise the stride phrasing
|
|
781
|
+
// names the interval and its offset ("every six minutes from five …"). A
|
|
782
|
+
// step shape only reaches here as a clean cadence (the interval tiles 60),
|
|
783
|
+
// so the stride collapses to the bare or uniform-offset form.
|
|
784
|
+
if (start !== 0 && segment.fires.length <= 3) {
|
|
785
|
+
return listPastThe(numberWords(segment.fires, opts), unit, anchor, opts);
|
|
640
786
|
}
|
|
641
787
|
|
|
642
|
-
|
|
643
|
-
|
|
788
|
+
return renderStride({
|
|
789
|
+
interval: segment.interval,
|
|
790
|
+
start,
|
|
791
|
+
last: segment.fires[segment.fires.length - 1],
|
|
792
|
+
cycle: 60,
|
|
793
|
+
unit,
|
|
794
|
+
anchor
|
|
795
|
+
}, opts);
|
|
644
796
|
}
|
|
645
797
|
|
|
646
798
|
// Phrase a `start/interval` step segment for the hour field (cycles every
|
|
@@ -670,6 +822,154 @@ function stepHours(segment: StepSegment, opts: NormalizedOptions): string {
|
|
|
670
822
|
getTime({hour: start, minute: 0}, opts);
|
|
671
823
|
}
|
|
672
824
|
|
|
825
|
+
// Speak an hour stride as a cadence with clock-time bounds, the 24-cycle
|
|
826
|
+
// analog of renderStride: a clean stride from midnight is the bare cadence
|
|
827
|
+
// ("every two hours"); a clean offset names only its start ("every six hours
|
|
828
|
+
// from 2 a.m."); a bounded or non-tiling stride pins both clock-time endpoints
|
|
829
|
+
// ("every two hours from 9 a.m. through 5 p.m.") so the bounded set reads
|
|
830
|
+
// unambiguously. Used wherever an hour step (or arithmetic-progression hour
|
|
831
|
+
// list) would otherwise be cross-multiplied into a wall of clock times.
|
|
832
|
+
function hourStrideCadence(stride: {start: number; interval: number;
|
|
833
|
+
last: number}, opts: NormalizedOptions): string {
|
|
834
|
+
const {start, interval, last} = stride;
|
|
835
|
+
const cadence = 'every ' + getNumber(interval, opts) + ' hours';
|
|
836
|
+
const tiles = 24 % interval === 0;
|
|
837
|
+
|
|
838
|
+
if (start === 0 && tiles) {
|
|
839
|
+
return cadence;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (start < interval && tiles) {
|
|
843
|
+
return cadence + ' from ' + getTime({hour: start, minute: 0}, opts);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return cadence + ' from ' + getTime({hour: start, minute: 0}, opts) +
|
|
847
|
+
through(opts) + getTime({hour: last, minute: 0}, opts);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
851
|
+
// segment yields its {start, interval, last} directly; an all-single hour
|
|
852
|
+
// list yields one only when its values form a long-enough arithmetic
|
|
853
|
+
// progression (so an irregular list like 9,17 keeps enumerating). The IR is
|
|
854
|
+
// unchanged — the renderer recognizes the stride and speaks it as a cadence
|
|
855
|
+
// instead of the clock-time cross-product.
|
|
856
|
+
function hourStride(ir: IR):
|
|
857
|
+
{start: number; interval: number; last: number} | null {
|
|
858
|
+
// Reached only from the clock-time paths, which run under discrete hours
|
|
859
|
+
// and so always carry hour segments.
|
|
860
|
+
const segments = ir.analyses.segments.hour!;
|
|
861
|
+
|
|
862
|
+
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
863
|
+
const segment = segments[0];
|
|
864
|
+
const start = segment.startToken === '*' ?
|
|
865
|
+
0 :
|
|
866
|
+
+segment.startToken.split('-')[0];
|
|
867
|
+
|
|
868
|
+
return {interval: segment.interval, last: segment.fires[
|
|
869
|
+
segment.fires.length - 1], start};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const values = singleValues(segments);
|
|
873
|
+
const step = values && arithmeticStep(values);
|
|
874
|
+
|
|
875
|
+
return step || null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
879
|
+
// fills the minute (a "for one minute" frame at minute 0); a single 0 is just
|
|
880
|
+
// the top of the minute (no clause); anything else needs its own clause.
|
|
881
|
+
function subMinuteSecond(ir: IR): boolean {
|
|
882
|
+
return ir.pattern.second === '*' || ir.shapes.second === 'step';
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// The lead clause for an hour-cadence rendering: the second and the pinned
|
|
886
|
+
// minute, before the hour cadence. A pinned minute 0 folds in — a single,
|
|
887
|
+
// list, or range second is counted "past the hour" (the minute-0 is the top
|
|
888
|
+
// of the hour), and a wildcard or sub-minute step second takes a "for one
|
|
889
|
+
// minute" frame (the whole minute-0 window). A non-zero minute is a real
|
|
890
|
+
// clock minute: the second leads with its own "past the minute" clause (if
|
|
891
|
+
// any), then the minute reads "M minutes past the hour".
|
|
892
|
+
function hourCadenceLead(ir: IR, minute: number,
|
|
893
|
+
opts: NormalizedOptions): string {
|
|
894
|
+
if (minute === 0) {
|
|
895
|
+
if (subMinuteSecond(ir)) {
|
|
896
|
+
return secondsClause(ir, 'minute', opts) + ' for one minute';
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return secondsClause(ir, 'hour', opts);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const minutePhrase = getNumber(minute, opts) + ' ' +
|
|
903
|
+
pluralize(minute, 'minute') + ' past the hour';
|
|
904
|
+
|
|
905
|
+
// A single 0 second is just the top of the minute, so the minute leads
|
|
906
|
+
// alone; any other second prefixes its own clause.
|
|
907
|
+
if (ir.pattern.second === '0') {
|
|
908
|
+
return minutePhrase;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
return secondsClause(ir, 'minute', opts) + ', ' + minutePhrase;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
915
|
+
// pinned minute and a second as a cadence — the lead clause, then the hour
|
|
916
|
+
// cadence — instead of cross-multiplying the hours into a wall of clock
|
|
917
|
+
// times. Returns null when the hour is not a stride (an irregular list, a
|
|
918
|
+
// single hour, or a range), or when the cross-product is short enough that
|
|
919
|
+
// enumeration is no longer than the cadence: a meaningful second (anything
|
|
920
|
+
// but a plain :00) makes every clock time three digit-groups, so any stride
|
|
921
|
+
// is worth compacting; otherwise the stride must exceed the clock-time cap,
|
|
922
|
+
// the same point at which the core itself stops enumerating. Renderer-only;
|
|
923
|
+
// the IR is unchanged.
|
|
924
|
+
function hourCadence(ir: IR, minute: number,
|
|
925
|
+
opts: NormalizedOptions): string | null {
|
|
926
|
+
const stride = hourStride(ir);
|
|
927
|
+
|
|
928
|
+
if (!stride) {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const fires = (stride.last - stride.start) / stride.interval + 1;
|
|
933
|
+
|
|
934
|
+
if (ir.pattern.second === '0' && fires <= maxClockTimes) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// A wildcard or sub-minute step second confined to minute 0 of a clean
|
|
939
|
+
// hour stride is a confinement, not a juxtaposed cadence: it reads "for one
|
|
940
|
+
// minute during every other hour", matching the "every minute during every
|
|
941
|
+
// other hour" idiom and keeping it distinct from the bare hour-step form
|
|
942
|
+
// ("every two hours") so the minute-0 confinement is never heard as it.
|
|
943
|
+
const confinement = minute === 0 && subMinuteSecond(ir) &&
|
|
944
|
+
cleanStrideSegment(ir);
|
|
945
|
+
|
|
946
|
+
if (confinement) {
|
|
947
|
+
return secondsClause(ir, 'minute', opts) + ' for one minute ' +
|
|
948
|
+
everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return hourCadenceLead(ir, minute, opts) + ', ' +
|
|
952
|
+
hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// The hour step segment when the hour is a clean stride with an idiomatic
|
|
956
|
+
// ordinal ("every other", "every sixth"), suitable for the "during every Nth
|
|
957
|
+
// hour" confinement frame; null otherwise (an uneven stride, a bounded step,
|
|
958
|
+
// or an arithmetic-progression list, which keep the bounded cadence form).
|
|
959
|
+
function cleanStrideSegment(ir: IR): StepSegment | null {
|
|
960
|
+
// Reached only after hourStride confirmed a stride, so hour segments exist.
|
|
961
|
+
const segments = ir.analyses.segments.hour!;
|
|
962
|
+
const segment = segments.length === 1 && segments[0];
|
|
963
|
+
|
|
964
|
+
if (!segment || segment.kind !== 'step' ||
|
|
965
|
+
segment.startToken.indexOf('-') !== -1 ||
|
|
966
|
+
!(segment.interval in stepOrdinals)) {
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return segment;
|
|
971
|
+
}
|
|
972
|
+
|
|
673
973
|
// --- List and segment phrasing. ---
|
|
674
974
|
|
|
675
975
|
// Chicago number style for a series: if any value crosses the spell-out
|