cronli5 0.1.5 → 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/CHANGELOG.md +28 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +117 -20
- package/dist/cronli5.js +117 -20
- package/dist/lang/de.cjs +94 -19
- package/dist/lang/de.js +94 -19
- package/dist/lang/en.cjs +117 -20
- package/dist/lang/en.js +117 -20
- package/dist/lang/es.cjs +89 -14
- package/dist/lang/es.js +89 -14
- package/dist/lang/fi.cjs +107 -19
- package/dist/lang/fi.js +107 -19
- package/dist/lang/zh.cjs +90 -53
- package/dist/lang/zh.js +90 -53
- package/package.json +2 -2
- package/src/lang/de/index.ts +238 -51
- package/src/lang/en/index.ts +280 -46
- package/src/lang/es/index.ts +222 -31
- package/src/lang/fi/index.ts +245 -39
- package/src/lang/zh/index.ts +209 -94
package/src/lang/en/index.ts
CHANGED
|
@@ -197,14 +197,41 @@ function composeHourCadence(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
|
197
197
|
const clockRest = plan.rest.kind === 'clockTimes' ||
|
|
198
198
|
plan.rest.kind === 'compactClockTimes';
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
if (!clockRest || ir.shapes.minute !== 'single') {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const minute = +ir.pattern.minute;
|
|
205
|
+
|
|
206
|
+
return hourCadence(ir, minute, opts) ?? hourRangeCadence(ir, minute, opts);
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
// A meaningful second under minute/hour shapes the earlier strategies
|
|
206
210
|
// deferred on: the second leads with its own clause and the rest of the
|
|
207
211
|
// pattern follows.
|
|
212
|
+
// A wildcard or stepped second under a fixed minute across one or more specific
|
|
213
|
+
// hours. The clock-time rest collapses the pinned minute into the hour, and on
|
|
214
|
+
// the clock a pinned minute-0 reads as the whole hour ("9 a.m." spoken ==
|
|
215
|
+
// "9:00 a.m."), losing the one-minute confinement.
|
|
216
|
+
//
|
|
217
|
+
// A SINGLE minute-0 is the one-minute window at the top of each named hour: a
|
|
218
|
+
// duration frame ("for one minute at 9 a.m.") states the confinement outright,
|
|
219
|
+
// with the hour as its word so it cannot be heard as the hour itself. A minute
|
|
220
|
+
// LIST whose first value is 0 (e.g. */25 → :00, :25, :50) is a wall of distinct
|
|
221
|
+
// clock times, not one confinement, so it names each minute via the compact
|
|
222
|
+
// form, never collapsing to the bare hour (which once repeated it, "9 a.m.,
|
|
223
|
+
// 9 a.m."). A non-zero pinned minute is an unambiguous clock time the compact
|
|
224
|
+
// "of 9:05 a.m." form reads as the minute, never the hour.
|
|
225
|
+
function clockTimesConfinement(ir: IR, rest: PlanOf<'clockTimes'>,
|
|
226
|
+
opts: NormalizedOptions): string {
|
|
227
|
+
if (+rest.times[0].minute === 0 && ir.shapes.minute === 'single') {
|
|
228
|
+
return secondsLeadClause(ir, opts) + ' for one minute at ' +
|
|
229
|
+
durationHours(ir, rest, opts);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return secondsLeadClause(ir, opts) + ' of ' + clockTimesOf(ir, rest, opts);
|
|
233
|
+
}
|
|
234
|
+
|
|
208
235
|
function renderComposeSeconds(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
209
236
|
opts: NormalizedOptions): string {
|
|
210
237
|
// An hour step (or arithmetic-progression hour list) under a single pinned
|
|
@@ -217,28 +244,11 @@ function renderComposeSeconds(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
|
217
244
|
return cadence;
|
|
218
245
|
}
|
|
219
246
|
|
|
220
|
-
// A wildcard or stepped second under a minute
|
|
221
|
-
//
|
|
222
|
-
// pinned minute into the hour, and on the clock a pinned minute-0 reads as
|
|
223
|
-
// the whole hour ("9 a.m." spoken == "9:00 a.m."), losing the one-minute
|
|
224
|
-
// confinement. (A second list/range/single leads with a "past the minute"
|
|
225
|
-
// clause that an "of"/duration frame cannot follow, so it stays generic.)
|
|
247
|
+
// A wildcard or stepped second under a fixed minute across one or more
|
|
248
|
+
// specific hours confines the seconds to the clock time(s).
|
|
226
249
|
if (plan.rest.kind === 'clockTimes' &&
|
|
227
250
|
(ir.shapes.second === 'wildcard' || ir.shapes.second === 'step')) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// Minute 0 is the one-minute window at the top of each named hour: a
|
|
231
|
-
// duration frame ("for one minute at 9 a.m.") states the confinement
|
|
232
|
-
// outright, with the hour as its word so it cannot be heard as the hour
|
|
233
|
-
// itself. A non-zero pinned minute is an unambiguous clock time, so the
|
|
234
|
-
// compact "of 9:05 a.m." form reads it as the minute, never the hour.
|
|
235
|
-
if (+minute === 0) {
|
|
236
|
-
return secondsLeadClause(ir, opts) + ' for one minute at ' +
|
|
237
|
-
durationHours(ir, plan.rest, opts);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return secondsLeadClause(ir, opts) + ' of ' +
|
|
241
|
-
clockTimesOf(ir, plan.rest, opts);
|
|
251
|
+
return clockTimesConfinement(ir, plan.rest, opts);
|
|
242
252
|
}
|
|
243
253
|
|
|
244
254
|
// A wildcard second under a */2 minute step with a wildcard hour binds
|
|
@@ -254,7 +264,15 @@ function renderComposeSeconds(ir: IR, plan: PlanOf<'composeSeconds'>,
|
|
|
254
264
|
trailingQualifier(ir, opts);
|
|
255
265
|
}
|
|
256
266
|
|
|
257
|
-
|
|
267
|
+
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
268
|
+
// leading clause, so the composer must not prepend a second lead that would
|
|
269
|
+
// double it. A wildcard or stepped second is not folded there (no
|
|
270
|
+
// clockSecond), so it still leads its own clause here.
|
|
271
|
+
const restOwnsLead = plan.rest.kind === 'compactClockTimes' &&
|
|
272
|
+
ir.analyses.clockSecond;
|
|
273
|
+
const lead = restOwnsLead ? '' : secondsLeadClause(ir, opts) + ', ';
|
|
274
|
+
|
|
275
|
+
return lead + render(ir, plan.rest, opts);
|
|
258
276
|
}
|
|
259
277
|
|
|
260
278
|
// The bare-hour words for a minute-0 duration confinement, joined and followed
|
|
@@ -381,9 +399,15 @@ function renderMinuteFrequency(ir: IR, plan: PlanOf<'minuteFrequency'>,
|
|
|
381
399
|
'minute', 'hour', opts);
|
|
382
400
|
|
|
383
401
|
if (plan.hours.kind === 'during') {
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
402
|
+
// A uneven hour stride confines the minute cadence to its own bounded hour
|
|
403
|
+
// cadence ("every 15 minutes, every five hours from midnight through 8
|
|
404
|
+
// p.m."); an irregular hour list still names each hour's window.
|
|
405
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
406
|
+
|
|
407
|
+
phrase += cadence ?
|
|
408
|
+
', ' + cadence :
|
|
409
|
+
' during the ' +
|
|
410
|
+
hourTimesFromPlan(ir, plan.hours.times, false, opts) + ' hours';
|
|
387
411
|
}
|
|
388
412
|
else if (plan.hours.kind === 'window') {
|
|
389
413
|
phrase += ' ' + hourWindow(plan.hours, opts);
|
|
@@ -422,13 +446,20 @@ function renderMinuteSpanInHour(ir: IR, plan: PlanOf<'minuteSpanInHour'>,
|
|
|
422
446
|
// during each hour.
|
|
423
447
|
function renderMinutesAcrossHours(ir: IR, plan: PlanOf<'minutesAcrossHours'>,
|
|
424
448
|
opts: NormalizedOptions): string {
|
|
449
|
+
// A uneven hour stride reads as a cadence, not a wall of hour columns: the
|
|
450
|
+
// minute lead, then "every N hours from X through Y".
|
|
451
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
452
|
+
|
|
425
453
|
if (plan.form === 'wildcard') {
|
|
454
|
+
if (cadence !== null) {
|
|
455
|
+
return 'every minute, ' + cadence + trailingQualifier(ir, opts);
|
|
456
|
+
}
|
|
457
|
+
|
|
426
458
|
return 'every minute during the ' +
|
|
427
459
|
hourTimesFromPlan(ir, plan.times, false, opts) + ' hours' +
|
|
428
460
|
trailingQualifier(ir, opts);
|
|
429
461
|
}
|
|
430
462
|
|
|
431
|
-
const times = hourTimesFromPlan(ir, plan.times, true, opts);
|
|
432
463
|
const lead = plan.form === 'range' ?
|
|
433
464
|
minuteRangeLead(ir.pattern.minute, opts) :
|
|
434
465
|
// The 'list' form is a minute list, which has segments; an offset/uneven
|
|
@@ -437,6 +468,12 @@ function renderMinutesAcrossHours(ir: IR, plan: PlanOf<'minutesAcrossHours'>,
|
|
|
437
468
|
listPastThe(segmentWords(ir.analyses.segments.minute!, opts),
|
|
438
469
|
'minute', 'hour', opts);
|
|
439
470
|
|
|
471
|
+
if (cadence !== null) {
|
|
472
|
+
return lead + ', ' + cadence + trailingQualifier(ir, opts);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const times = hourTimesFromPlan(ir, plan.times, true, opts);
|
|
476
|
+
|
|
440
477
|
return lead + ', at ' + times + trailingQualifier(ir, opts);
|
|
441
478
|
}
|
|
442
479
|
|
|
@@ -466,6 +503,9 @@ function renderMinuteSpanAcrossHourStep(ir: IR,
|
|
|
466
503
|
// segment is a step segment.
|
|
467
504
|
const segment = ir.analyses.segments.hour![0] as StepSegment;
|
|
468
505
|
|
|
506
|
+
// A wildcard minute over a stepped hour is reached only for a clean stride
|
|
507
|
+
// (a bounded or uneven step routes through minutesAcrossHours instead), so it
|
|
508
|
+
// confines to every Nth hour without a bounded-cadence case here.
|
|
469
509
|
if (plan.form === 'wildcard') {
|
|
470
510
|
return 'every minute ' + everyNthHour(segment, opts) +
|
|
471
511
|
trailingQualifier(ir, opts);
|
|
@@ -478,8 +518,13 @@ function renderMinuteSpanAcrossHourStep(ir: IR,
|
|
|
478
518
|
listPastThe(segmentWords(ir.analyses.segments.minute!, opts),
|
|
479
519
|
'minute', 'hour', opts) :
|
|
480
520
|
minuteRangeLead(ir.pattern.minute, opts);
|
|
521
|
+
// A bounded or uneven hour step reads as its endpoint-pinning cadence after
|
|
522
|
+
// the minute lead, not a wall of clock-time columns; an offset-clean step
|
|
523
|
+
// keeps its existing per-step phrasing.
|
|
524
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
481
525
|
|
|
482
|
-
return lead + ', ' +
|
|
526
|
+
return lead + ', ' +
|
|
527
|
+
(cadence ?? stepHours(segment, opts)) + trailingQualifier(ir, opts);
|
|
483
528
|
}
|
|
484
529
|
|
|
485
530
|
// Lead phrase for a plain minute range: "every minute from <a> through <b>
|
|
@@ -536,6 +581,15 @@ function rangeMinuteLead(ir: IR, opts: NormalizedOptions): string {
|
|
|
536
581
|
|
|
537
582
|
function renderHourStep(ir: IR, plan: PlanOf<'hourStep'>,
|
|
538
583
|
opts: NormalizedOptions): string {
|
|
584
|
+
// A bounded or uneven hour step reads as its endpoint-pinning cadence ("every
|
|
585
|
+
// two hours from 9 a.m. through 5 p.m."), the same form the compound paths
|
|
586
|
+
// speak; an offset-clean step keeps its bare or "from M" cadence.
|
|
587
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
588
|
+
|
|
589
|
+
if (cadence !== null) {
|
|
590
|
+
return cadence + trailingQualifier(ir, opts);
|
|
591
|
+
}
|
|
592
|
+
|
|
539
593
|
// An hour-step plan is selected only for a stepped hour field, whose
|
|
540
594
|
// first segment is a step segment.
|
|
541
595
|
return stepHours(ir.analyses.segments.hour![0] as StepSegment, opts) +
|
|
@@ -563,10 +617,13 @@ function hourWindow(window: {from: number; to: number; last: number},
|
|
|
563
617
|
// a day-level qualifier, e.g. "every day at 9 a.m. and 9:30 a.m.".
|
|
564
618
|
function renderClockTimes(ir: IR, plan: PlanOf<'clockTimes'>,
|
|
565
619
|
opts: NormalizedOptions): string {
|
|
566
|
-
// An hour step (or arithmetic-progression hour list) under a
|
|
567
|
-
// minute reads as a cadence rather than a
|
|
620
|
+
// An hour step or range (or arithmetic-progression hour list) under a
|
|
621
|
+
// single pinned minute reads as a cadence or window rather than a
|
|
622
|
+
// cross-product of clock times.
|
|
568
623
|
if (ir.shapes.minute === 'single') {
|
|
569
|
-
const
|
|
624
|
+
const minute = +ir.pattern.minute;
|
|
625
|
+
const cadence = hourCadence(ir, minute, opts) ??
|
|
626
|
+
hourRangeCadence(ir, minute, opts);
|
|
570
627
|
|
|
571
628
|
if (cadence !== null) {
|
|
572
629
|
return cadence;
|
|
@@ -592,10 +649,11 @@ function renderClockTimes(ir: IR, plan: PlanOf<'clockTimes'>,
|
|
|
592
649
|
function renderCompactClockTimes(ir: IR, plan: PlanOf<'compactClockTimes'>,
|
|
593
650
|
opts: NormalizedOptions): string {
|
|
594
651
|
if (plan.fold) {
|
|
595
|
-
// An hour step (or arithmetic-progression hour list) under the
|
|
596
|
-
// pinned minute reads as a cadence, not a wall of clock
|
|
597
|
-
// null for an irregular list
|
|
598
|
-
const cadence = hourCadence(ir, +plan.minute, opts)
|
|
652
|
+
// An hour step or range (or arithmetic-progression hour list) under the
|
|
653
|
+
// single pinned minute reads as a cadence or window, not a wall of clock
|
|
654
|
+
// times. (Returns null for an irregular list, which keeps folding below.)
|
|
655
|
+
const cadence = hourCadence(ir, +plan.minute, opts) ??
|
|
656
|
+
hourRangeCadence(ir, +plan.minute, opts);
|
|
599
657
|
|
|
600
658
|
if (cadence !== null) {
|
|
601
659
|
return cadence;
|
|
@@ -619,12 +677,18 @@ function renderCompactClockTimes(ir: IR, plan: PlanOf<'compactClockTimes'>,
|
|
|
619
677
|
hourSegmentTimes(ir, fold, true, opts);
|
|
620
678
|
}
|
|
621
679
|
|
|
622
|
-
const
|
|
680
|
+
const minuteLead =
|
|
623
681
|
// The non-fold branch is a minute list, which has segments. An
|
|
624
682
|
// offset/uneven step enumerated to that list reads as a stride.
|
|
625
|
-
|
|
683
|
+
strideFromSegments(ir.analyses.segments.minute!, 'minute', 'hour', opts) ??
|
|
626
684
|
listPastThe(segmentWords(ir.analyses.segments.minute!, opts),
|
|
627
|
-
'minute', 'hour', opts)
|
|
685
|
+
'minute', 'hour', opts);
|
|
686
|
+
// A uneven hour stride reads as a cadence after the minute lead, not a wall
|
|
687
|
+
// of clock-time columns.
|
|
688
|
+
const cadence = unevenHourCadence(ir, opts);
|
|
689
|
+
const phrase = cadence ?
|
|
690
|
+
minuteLead + ', ' + cadence + trailingQualifier(ir, opts) :
|
|
691
|
+
minuteLead +
|
|
628
692
|
', at ' + hourSegmentTimes(ir, {minute: 0, second: null}, true, opts) +
|
|
629
693
|
trailingQualifier(ir, opts);
|
|
630
694
|
|
|
@@ -847,12 +911,74 @@ function hourStrideCadence(stride: {start: number; interval: number;
|
|
|
847
911
|
through(opts) + getTime({hour: last, minute: 0}, opts);
|
|
848
912
|
}
|
|
849
913
|
|
|
914
|
+
// Whether an hour stride wraps the day cleanly from within its first interval
|
|
915
|
+
// (a `*/n` from the top, or a `m/n` offset with m < n that divides 24): such a
|
|
916
|
+
// stride has no distinct endpoint and keeps its bare or "from M" cadence. Every
|
|
917
|
+
// other stride — a uneven interval, or one starting at or past its interval
|
|
918
|
+
// (a bounded `a-b/n`) — is a bounded set the cadence pins both endpoints of.
|
|
919
|
+
function offsetCleanStride(
|
|
920
|
+
stride: {start: number; interval: number}
|
|
921
|
+
): boolean {
|
|
922
|
+
return stride.start < stride.interval && 24 % stride.interval === 0;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// The bounded cadence for an hour stride that pins both clock-time endpoints,
|
|
926
|
+
// or null when the hour is not such a stride. The core rewrites a uneven step
|
|
927
|
+
// to its fire list, so a minute window/list/step crossed with it lands in the
|
|
928
|
+
// enumerating list paths; there the bounded hour reads better as its cadence
|
|
929
|
+
// ("…, every five hours from midnight through 8 p.m.") than as a wall of
|
|
930
|
+
// clock-time columns. An offset-clean stride keeps its existing confinement
|
|
931
|
+
// form, so only the endpoint-bearing case routes here.
|
|
932
|
+
function unevenHourCadence(ir: IR, opts: NormalizedOptions): string | null {
|
|
933
|
+
const stride = hourStride(ir);
|
|
934
|
+
|
|
935
|
+
if (!stride || offsetCleanStride(stride)) {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return hourStrideCadence(stride, opts);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// An hour list's arithmetic progression, or null when its values are not a
|
|
943
|
+
// step the renderer should speak as a cadence. The core rewrites a uneven hour
|
|
944
|
+
// step (whose interval does not tile 24, e.g. `*/5` → 0,5,10,15,20) to its
|
|
945
|
+
// literal fire list, indistinguishable in the IR from a hand-written list; the
|
|
946
|
+
// renderer recovers the cadence from the values. A progression starting at
|
|
947
|
+
// zero is a `*/n` step however short (0,7,14,21 is `*/7`); a non-zero one is
|
|
948
|
+
// only a step when it is too long to be a deliberate clock-time list (e.g.
|
|
949
|
+
// 9,17 is two named times, not a cadence), the same length the minute/second
|
|
950
|
+
// list path uses. Interval one is a plain range, never a step.
|
|
951
|
+
function hourListStride(values: number[]):
|
|
952
|
+
{start: number; interval: number; last: number} | null {
|
|
953
|
+
if (values.length < 2) {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const interval = values[1] - values[0];
|
|
958
|
+
|
|
959
|
+
if (interval < 2) {
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
for (let i = 2; i < values.length; i += 1) {
|
|
964
|
+
if (values[i] - values[i - 1] !== interval) {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (values[0] !== 0 && values.length < 5) {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return {interval, last: values[values.length - 1], start: values[0]};
|
|
974
|
+
}
|
|
975
|
+
|
|
850
976
|
// The hour field's stride, or null when the hour is not a cadence: a step
|
|
851
977
|
// segment yields its {start, interval, last} directly; an all-single hour
|
|
852
|
-
// list yields one only when its values form a
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
//
|
|
978
|
+
// list yields one only when its values form a step progression (so an irregular
|
|
979
|
+
// list like 9,17 keeps enumerating). The IR is unchanged — the renderer
|
|
980
|
+
// recognizes the stride and speaks it as a cadence instead of the clock-time
|
|
981
|
+
// cross-product.
|
|
856
982
|
function hourStride(ir: IR):
|
|
857
983
|
{start: number; interval: number; last: number} | null {
|
|
858
984
|
// Reached only from the clock-time paths, which run under discrete hours
|
|
@@ -861,6 +987,13 @@ function hourStride(ir: IR):
|
|
|
861
987
|
|
|
862
988
|
if (segments.length === 1 && segments[0].kind === 'step') {
|
|
863
989
|
const segment = segments[0];
|
|
990
|
+
|
|
991
|
+
// A bounded step that fires only once (e.g. `9-10/5` → just 9) is a single
|
|
992
|
+
// value, not a stride: it has no interval to speak and no endpoint to pin.
|
|
993
|
+
if (segment.fires.length < 2) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
|
|
864
997
|
const start = segment.startToken === '*' ?
|
|
865
998
|
0 :
|
|
866
999
|
+segment.startToken.split('-')[0];
|
|
@@ -870,9 +1003,8 @@ function hourStride(ir: IR):
|
|
|
870
1003
|
}
|
|
871
1004
|
|
|
872
1005
|
const values = singleValues(segments);
|
|
873
|
-
const step = values && arithmeticStep(values);
|
|
874
1006
|
|
|
875
|
-
return
|
|
1007
|
+
return values && hourListStride(values);
|
|
876
1008
|
}
|
|
877
1009
|
|
|
878
1010
|
// The second's status against a pinned minute: a wildcard or sub-minute step
|
|
@@ -931,7 +1063,13 @@ function hourCadence(ir: IR, minute: number,
|
|
|
931
1063
|
|
|
932
1064
|
const fires = (stride.last - stride.start) / stride.interval + 1;
|
|
933
1065
|
|
|
934
|
-
|
|
1066
|
+
// A short stride that spells out as few clock times stays an enumeration only
|
|
1067
|
+
// when it wraps cleanly (an offset-clean stride with no endpoint): the bare
|
|
1068
|
+
// or "from M" form is no shorter than the list, so the list reads fine. A
|
|
1069
|
+
// bounded or uneven stride has no clean wrap, so its endpoint-pinning cadence
|
|
1070
|
+
// ("every five hours from midnight through 8 p.m.") reads better however few.
|
|
1071
|
+
if (ir.pattern.second === '0' && fires <= maxClockTimes &&
|
|
1072
|
+
offsetCleanStride(stride)) {
|
|
935
1073
|
return null;
|
|
936
1074
|
}
|
|
937
1075
|
|
|
@@ -948,6 +1086,14 @@ function hourCadence(ir: IR, minute: number,
|
|
|
948
1086
|
everyNthHour(confinement, opts) + trailingQualifier(ir, opts);
|
|
949
1087
|
}
|
|
950
1088
|
|
|
1089
|
+
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
1090
|
+
// lead clause to fold in, so the bounded cadence stands on its own ("every
|
|
1091
|
+
// five hours from midnight through 8 p.m."); only a real minute or second
|
|
1092
|
+
// prefixes its clause.
|
|
1093
|
+
if (minute === 0 && ir.pattern.second === '0') {
|
|
1094
|
+
return hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
951
1097
|
return hourCadenceLead(ir, minute, opts) + ', ' +
|
|
952
1098
|
hourStrideCadence(stride, opts) + trailingQualifier(ir, opts);
|
|
953
1099
|
}
|
|
@@ -970,6 +1116,94 @@ function cleanStrideSegment(ir: IR): StepSegment | null {
|
|
|
970
1116
|
return segment;
|
|
971
1117
|
}
|
|
972
1118
|
|
|
1119
|
+
// Whether the hour field is a range — or a list whose segments include a
|
|
1120
|
+
// range — and so forms a window rather than a cross-product of clock times.
|
|
1121
|
+
// A pure single-value list (9,17) has no range to span and still enumerates;
|
|
1122
|
+
// a step is handled by hourStride/hourCadence, so a field whose only segments
|
|
1123
|
+
// are steps and singles is left alone here.
|
|
1124
|
+
function hasHourWindow(ir: IR): boolean {
|
|
1125
|
+
// Reached only from the clock-time paths, which run under discrete hours
|
|
1126
|
+
// and so always carry hour segments.
|
|
1127
|
+
return ir.analyses.segments.hour!.some(function range(segment) {
|
|
1128
|
+
return segment.kind === 'range';
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// The hour-range window as a cadence tail at the top of each hour: each range
|
|
1133
|
+
// segment is a "from X through Y" window ("every hour from 9 a.m. through
|
|
1134
|
+
// 5 p.m."), and any non-contiguous single hour is appended ("and at 10 p.m.").
|
|
1135
|
+
// The minute has already folded into the lead, so the window closes on the
|
|
1136
|
+
// top of its final hour. Mirrors foldedHourWindows but pinned to minute 0.
|
|
1137
|
+
function hourRangeWindowTail(ir: IR, opts: NormalizedOptions): string {
|
|
1138
|
+
const windows: string[] = [];
|
|
1139
|
+
const singles: number[] = [];
|
|
1140
|
+
|
|
1141
|
+
// Reached only after hasHourWindow, so hour segments exist.
|
|
1142
|
+
ir.analyses.segments.hour!.forEach(function classify(segment) {
|
|
1143
|
+
if (segment.kind === 'range') {
|
|
1144
|
+
windows.push('from ' + getTime({hour: +segment.bounds[0], minute: 0},
|
|
1145
|
+
opts) + through(opts) +
|
|
1146
|
+
getTime({hour: +segment.bounds[1], minute: 0}, opts));
|
|
1147
|
+
}
|
|
1148
|
+
else if (segment.kind === 'step') {
|
|
1149
|
+
singles.push(...segment.fires);
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
singles.push(+segment.value);
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
let phrase = 'every hour ' + joinList(windows, opts);
|
|
1157
|
+
|
|
1158
|
+
if (singles.length) {
|
|
1159
|
+
phrase += ' and at ' + joinList(singles.map(function time(hour) {
|
|
1160
|
+
return getTime({hour, minute: 0}, opts);
|
|
1161
|
+
}), opts);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
return phrase;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Render an hour range (or a list whose segments include a range) under a
|
|
1168
|
+
// single pinned minute and a second as the hour-range window — the lead
|
|
1169
|
+
// clause, then "every hour from X through Y" — instead of cross-multiplying
|
|
1170
|
+
// the hours into a wall of clock times. Returns null when the hour has no
|
|
1171
|
+
// range (a pure single-value list, a single hour, or a step, which other
|
|
1172
|
+
// paths own), or when a plain :00 set is short enough that enumeration is no
|
|
1173
|
+
// longer than the window. Renderer-only; the IR is unchanged.
|
|
1174
|
+
function hourRangeCadence(ir: IR, minute: number,
|
|
1175
|
+
opts: NormalizedOptions): string | null {
|
|
1176
|
+
// Scoped to minute 0: the minute folds into the lead and every hour fires
|
|
1177
|
+
// at the top, so the window closes cleanly on the final hour. A non-zero
|
|
1178
|
+
// pinned minute is a real clock minute the existing clock-time window form
|
|
1179
|
+
// already speaks ("9:30:15 a.m. through 8:30:15 p.m."), unchanged.
|
|
1180
|
+
if (minute !== 0 || !hasHourWindow(ir)) {
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// A plain top-of-minute second (:00) carries no clause: the existing
|
|
1185
|
+
// hour-range and folded-window renderers already speak that window, so this
|
|
1186
|
+
// path only forms a window when there is a meaningful second to lead with.
|
|
1187
|
+
if (ir.pattern.second === '0') {
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// A wildcard or sub-minute step second confined to minute 0 is the whole
|
|
1192
|
+
// minute-0 window ("every second for one minute"), confined to the hour
|
|
1193
|
+
// range with the "during the … hours" idiom (the same idiom an hour list
|
|
1194
|
+
// uses). This is kept distinct from the bare minute-0 window ("every hour
|
|
1195
|
+
// from 9 a.m. through 5 p.m.") so the one-minute confinement is never heard
|
|
1196
|
+
// as it — the hour-range analog of "for one minute during every other hour".
|
|
1197
|
+
if (subMinuteSecond(ir)) {
|
|
1198
|
+
return secondsClause(ir, 'minute', opts) + ' for one minute during the ' +
|
|
1199
|
+
hourSegmentTimes(ir, {minute: 0, second: null}, false, opts) +
|
|
1200
|
+
' hours' + trailingQualifier(ir, opts);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
return hourCadenceLead(ir, minute, opts) + ', ' +
|
|
1204
|
+
hourRangeWindowTail(ir, opts) + trailingQualifier(ir, opts);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
973
1207
|
// --- List and segment phrasing. ---
|
|
974
1208
|
|
|
975
1209
|
// Chicago number style for a series: if any value crosses the spell-out
|