cronli5 0.8.0 → 0.8.3
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 +49 -0
- package/README.md +6 -6
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +58 -8
- package/dist/cronli5.js +58 -8
- package/dist/lang/de.cjs +48 -9
- package/dist/lang/de.js +48 -9
- package/dist/lang/en.cjs +58 -8
- package/dist/lang/en.js +58 -8
- package/dist/lang/es.cjs +45 -3
- package/dist/lang/es.js +45 -3
- package/dist/lang/fi.cjs +50 -6
- package/dist/lang/fi.js +50 -6
- package/dist/lang/fr.cjs +45 -3
- package/dist/lang/fr.js +45 -3
- package/dist/lang/pt.cjs +45 -3
- package/dist/lang/pt.js +45 -3
- package/dist/lang/zh.cjs +52 -10
- package/dist/lang/zh.js +52 -10
- package/package.json +1 -1
- package/src/lang/de/index.ts +117 -30
- package/src/lang/en/index.ts +121 -28
- package/src/lang/es/index.ts +106 -7
- package/src/lang/fi/index.ts +121 -18
- package/src/lang/fr/index.ts +101 -7
- package/src/lang/pt/index.ts +101 -6
- package/src/lang/zh/index.ts +149 -16
package/src/lang/en/index.ts
CHANGED
|
@@ -581,8 +581,11 @@ function renderMinuteFrequency(schedule: Schedule,
|
|
|
581
581
|
else if (plan.hours.kind === 'step') {
|
|
582
582
|
// The plan carries a step only for a clean stride (dividing the day),
|
|
583
583
|
// which confines the cadence to every Nth hour; a stepped hour field's
|
|
584
|
-
// first segment is a step segment.
|
|
585
|
-
|
|
584
|
+
// first segment is a step segment. The hour step scopes the hours, so an
|
|
585
|
+
// offset cadence drops "past the hour" and joins with a comma.
|
|
586
|
+
const bound = withoutHourAnchor(phrase);
|
|
587
|
+
|
|
588
|
+
phrase = bound + (bound === phrase ? ' ' : ', ') +
|
|
586
589
|
everyNthHour(stepSegment(schedule, 'hour'), opts);
|
|
587
590
|
}
|
|
588
591
|
|
|
@@ -674,6 +677,67 @@ const stepOrdinals: Record<number, string> = {
|
|
|
674
677
|
2: 'other', 3: 'third', 4: 'fourth', 6: 'sixth', 8: 'eighth', 12: 'twelfth'
|
|
675
678
|
};
|
|
676
679
|
|
|
680
|
+
// Spelled ordinals for "every Nth minute" — the step intervals a minute
|
|
681
|
+
// cadence can take (2 reads idiomatically as "other"). A lookup miss falls back
|
|
682
|
+
// to the suffixed numeric ordinal, so an unusually large interval still reads.
|
|
683
|
+
const spelledOrdinals: Record<number, string> = {
|
|
684
|
+
2: 'other', 3: 'third', 4: 'fourth', 5: 'fifth', 6: 'sixth', 7: 'seventh',
|
|
685
|
+
8: 'eighth', 9: 'ninth', 10: 'tenth', 11: 'eleventh', 12: 'twelfth',
|
|
686
|
+
15: 'fifteenth', 20: 'twentieth', 30: 'thirtieth'
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// The ordinal word for a cadence interval ("sixth", "seventh"), spelled where
|
|
690
|
+
// known and suffixed-numeric ("13th") otherwise.
|
|
691
|
+
function ordinalWord(interval: number): string {
|
|
692
|
+
return spelledOrdinals[interval] ?? getOrdinal(interval);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// A stepped minute under a seconds lead reads as a CONFINEMENT of that cadence,
|
|
696
|
+
// not a juxtaposed clause (a comma there reads as two independent cadences) nor
|
|
697
|
+
// a wall of enumerated minutes: "during every Nth minute" plus the step's
|
|
698
|
+
// offset/bound. The cadence is ORDINAL ("every sixth minute"); the cardinal
|
|
699
|
+
// ("every six minutes") is the form that reads as a separate cadence. The
|
|
700
|
+
// offset/bound mirrors the standalone minute cadence: a clean stride from the
|
|
701
|
+
// top names no offset, an offset-clean stride names only its start ("from four
|
|
702
|
+
// minutes past the hour"), and an uneven one pins both endpoints ("from 2
|
|
703
|
+
// through 58 minutes past the hour").
|
|
704
|
+
function minuteStrideConfinement(stride: {start: number; interval: number;
|
|
705
|
+
last: number}, opts: NormalizedOptions): string {
|
|
706
|
+
const base = ' during every ' + ordinalWord(stride.interval) + ' minute';
|
|
707
|
+
|
|
708
|
+
return chooseStride({...stride, cycle: 60}, {
|
|
709
|
+
bare: () => base,
|
|
710
|
+
offset: () => base + ' from ' + getNumber(stride.start, opts) + ' ' +
|
|
711
|
+
pluralize(stride.start, 'minute') + ' past the hour',
|
|
712
|
+
bounded: () => {
|
|
713
|
+
const num = seriesNumber();
|
|
714
|
+
|
|
715
|
+
return base + ' from ' + num(stride.start) + through(opts) +
|
|
716
|
+
num(stride.last) + ' ' + pluralize(stride.last, 'minute') +
|
|
717
|
+
' past the hour';
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// The minute field's step stride for the confinement frame, or null when the
|
|
723
|
+
// minute is not a stepped cadence. A `step`-shaped field (`*/6`) reads its
|
|
724
|
+
// segment directly; a `list`-shaped field the core enumerated from an uneven
|
|
725
|
+
// step (`2/7` → 2,9,…,58) recovers the progression from its values.
|
|
726
|
+
function minuteStride(schedule: Schedule):
|
|
727
|
+
{start: number; interval: number; last: number} | null {
|
|
728
|
+
if (schedule.shapes.minute === 'step') {
|
|
729
|
+
const segment = stepSegment(schedule, 'minute');
|
|
730
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
731
|
+
|
|
732
|
+
return {interval: segment.interval, last:
|
|
733
|
+
segment.fires[segment.fires.length - 1], start};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
737
|
+
|
|
738
|
+
return values && arithmeticStep(values);
|
|
739
|
+
}
|
|
740
|
+
|
|
677
741
|
// Confine a cadence to a clean hour stride: "during every other hour", with
|
|
678
742
|
// the start named when it is not midnight ("…from 1 a.m." for an odd stride).
|
|
679
743
|
function everyNthHour(segment: StepSegment, opts: NormalizedOptions): string {
|
|
@@ -703,13 +767,14 @@ function renderMinuteSpanAcrossHourStep(schedule: Schedule,
|
|
|
703
767
|
}
|
|
704
768
|
|
|
705
769
|
// A minute list keeps the same cadence clause; only its lead differs. An
|
|
706
|
-
// offset/uneven step the core enumerated to that list reads as a stride.
|
|
707
|
-
|
|
770
|
+
// offset/uneven step the core enumerated to that list reads as a stride. The
|
|
771
|
+
// hour step scopes the hours, so the lead drops its generic "past the hour".
|
|
772
|
+
const lead = withoutHourAnchor(plan.form === 'list' ?
|
|
708
773
|
strideFromSegments(segmentsOf(schedule, 'minute'), 'minute', 'hour',
|
|
709
774
|
opts) ??
|
|
710
775
|
listPastThe(segmentWords(segmentsOf(schedule, 'minute'), opts),
|
|
711
776
|
'minute', 'hour', opts) :
|
|
712
|
-
minuteRangeLead(schedule.pattern.minute, opts);
|
|
777
|
+
minuteRangeLead(schedule.pattern.minute, opts));
|
|
713
778
|
// A bounded or uneven hour step reads as its endpoint-pinning cadence after
|
|
714
779
|
// the minute lead, not a wall of clock-time columns; an offset-clean step
|
|
715
780
|
// keeps its existing per-step phrasing.
|
|
@@ -932,10 +997,14 @@ function renderCompactClockTimes(schedule: Schedule,
|
|
|
932
997
|
listPastThe(segmentWords(segmentsOf(schedule, 'minute'), opts),
|
|
933
998
|
'minute', 'hour', opts);
|
|
934
999
|
// A uneven hour stride reads as a cadence after the minute lead, not a wall
|
|
935
|
-
// of clock-time columns.
|
|
1000
|
+
// of clock-time columns. The hour step is the sole hour authority there, so
|
|
1001
|
+
// the minute lead drops its generic "past the hour" (an every-hour scope that
|
|
1002
|
+
// would conflict with the step); the clock-time branch keeps it, naming
|
|
1003
|
+
// specific hours rather than a step.
|
|
936
1004
|
const cadence = unevenHourCadence(schedule, opts);
|
|
937
1005
|
const phrase = cadence ?
|
|
938
|
-
minuteLead + ', ' + cadence +
|
|
1006
|
+
withoutHourAnchor(minuteLead) + ', ' + cadence +
|
|
1007
|
+
trailingQualifier(schedule, opts) :
|
|
939
1008
|
minuteLead +
|
|
940
1009
|
', at ' +
|
|
941
1010
|
hourSegmentTimes(schedule, {minute: 0, second: null}, true, opts) +
|
|
@@ -1070,12 +1139,22 @@ function minuteConfinement(schedule: Schedule,
|
|
|
1070
1139
|
return '';
|
|
1071
1140
|
}
|
|
1072
1141
|
|
|
1073
|
-
if (
|
|
1074
|
-
// The
|
|
1075
|
-
// minute steps
|
|
1142
|
+
if (minute === '*/2') {
|
|
1143
|
+
// The `*/2` clean step reads idiomatically as "every other minute" with no
|
|
1144
|
+
// offset; other minute steps take the ordinal stride-cadence below.
|
|
1076
1145
|
return ' of every other minute';
|
|
1077
1146
|
}
|
|
1078
1147
|
|
|
1148
|
+
// A stepped minute (a clean `*/n`, an offset `m/n`, or a uneven step the core
|
|
1149
|
+
// enumerated to an arithmetic list) confines as "during every Nth minute"
|
|
1150
|
+
// plus the step's offset/bound — the ordinal cadence, not the cardinal that
|
|
1151
|
+
// reads as a separate cadence, nor a wall of enumerated ":NN" minutes.
|
|
1152
|
+
const stride = minuteStride(schedule);
|
|
1153
|
+
|
|
1154
|
+
if (stride) {
|
|
1155
|
+
return minuteStrideConfinement(stride, opts);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1079
1158
|
// A minute single/range/list under the seconds lead. The minute reads as a
|
|
1080
1159
|
// ":NN" clock-minute confinement, never "N minutes past the hour" (that is
|
|
1081
1160
|
// the minute-lead clock-point form).
|
|
@@ -1111,10 +1190,11 @@ function hourConfinement(schedule: Schedule, opts: NormalizedOptions): string {
|
|
|
1111
1190
|
|
|
1112
1191
|
if (hour === '*') {
|
|
1113
1192
|
// A pinned minute confinement ("during minute :00") repeats across every
|
|
1114
|
-
// hour, so the hour is named as the unit of recurrence; a
|
|
1115
|
-
// ("of every other minute"
|
|
1193
|
+
// hour, so the hour is named as the unit of recurrence; a minute cadence
|
|
1194
|
+
// ("of every other minute", "during every sixth minute …") or an absent
|
|
1195
|
+
// minute already implies all hours, so the hour is not restated.
|
|
1116
1196
|
const minutePinned = schedule.pattern.minute !== '*' &&
|
|
1117
|
-
!isCadenceField(schedule.pattern.minute);
|
|
1197
|
+
!isCadenceField(schedule.pattern.minute) && !minuteStride(schedule);
|
|
1118
1198
|
|
|
1119
1199
|
return minutePinned ? ' of every hour' : '';
|
|
1120
1200
|
}
|
|
@@ -1208,24 +1288,28 @@ function confinementEligible(schedule: Schedule,
|
|
|
1208
1288
|
}
|
|
1209
1289
|
|
|
1210
1290
|
if (lead.secondLead) {
|
|
1211
|
-
// A minute STEP
|
|
1212
|
-
//
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
1215
|
-
// the
|
|
1216
|
-
//
|
|
1217
|
-
// renderer rather than this confinement frame, which closes on the top of
|
|
1218
|
-
// the next hour.
|
|
1291
|
+
// A minute STEP confines as an ordinal cadence ("during every sixth minute
|
|
1292
|
+
// from four minutes past the hour"), but only where it fills the coarser
|
|
1293
|
+
// field: under a WILDCARD hour the step repeats every hour, so the cadence
|
|
1294
|
+
// is the whole confinement. A single hour or a contiguous range closes on
|
|
1295
|
+
// the minute's real last fire, which the windowing renderer already speaks,
|
|
1296
|
+
// so those defer. The `*/2` step keeps its "of every other minute" idiom.
|
|
1219
1297
|
if (minuteStep) {
|
|
1220
|
-
return minute === '*/2'
|
|
1298
|
+
return minute === '*/2' ?
|
|
1299
|
+
schedule.shapes.hour !== 'range' :
|
|
1300
|
+
schedule.pattern.hour === '*';
|
|
1221
1301
|
}
|
|
1222
1302
|
|
|
1223
|
-
// A minute list that is really
|
|
1224
|
-
//
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
1227
|
-
|
|
1228
|
-
|
|
1303
|
+
// A minute list that is really an arithmetic stride confines as that same
|
|
1304
|
+
// ordinal cadence when it fills a wildcard hour; under a restricted hour it
|
|
1305
|
+
// keeps its existing cadence form. A short explicit minute list crossed
|
|
1306
|
+
// with a discrete hour LIST is a wall of distinct clock times ("9:00 a.m.,
|
|
1307
|
+
// 9:25 a.m., …"), not a single minute confinement, so it stays enumerated.
|
|
1308
|
+
if (isMinuteStride(schedule)) {
|
|
1309
|
+
return schedule.pattern.hour === '*';
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (schedule.shapes.minute === 'list' && schedule.shapes.hour === 'list') {
|
|
1229
1313
|
return false;
|
|
1230
1314
|
}
|
|
1231
1315
|
|
|
@@ -1748,6 +1832,15 @@ function listPastThe(words: (string | number)[], unit: string, anchor: string,
|
|
|
1748
1832
|
anchor;
|
|
1749
1833
|
}
|
|
1750
1834
|
|
|
1835
|
+
// Strip the generic "past the hour" anchor from a minute-cadence lead. When the
|
|
1836
|
+
// hour field is restricted (a step or window), the hour clause is the sole hour
|
|
1837
|
+
// authority, so the cadence must not also assert "every hour" — "past the hour"
|
|
1838
|
+
// alongside a stepped/windowed hour reads as a conflicting every-hour scope. An
|
|
1839
|
+
// unrestricted hour keeps the anchor (it is the only hour statement there).
|
|
1840
|
+
function withoutHourAnchor(lead: string): string {
|
|
1841
|
+
return lead.replace(/ past the hour$/, '');
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1751
1844
|
// A clock time reads as a word ("noon"/"midnight") only at exact 12:00 or
|
|
1752
1845
|
// 0:00 with no minute or second.
|
|
1753
1846
|
function wordTime(hour: number | string, minute: number | string,
|
package/src/lang/es/index.ts
CHANGED
|
@@ -115,6 +115,17 @@ const weekdayNames = [
|
|
|
115
115
|
const nthWeekdayNames =
|
|
116
116
|
[null, 'primer', 'segundo', 'tercer', 'cuarto', 'quinto'];
|
|
117
117
|
|
|
118
|
+
// Spanish ordinals (masculine) for a stepped-minute cadence under a seconds
|
|
119
|
+
// lead ("cada sexto minuto"). The interval-2 step never reaches here — it keeps
|
|
120
|
+
// its own "de cada dos minutos" idiom — so the colliding "segundo" is unused.
|
|
121
|
+
// A lookup miss falls back to the cardinal-with-"cada" form, which still
|
|
122
|
+
// confines (see `minuteStepOrdinal`).
|
|
123
|
+
const stepOrdinals: Record<number, string> = {
|
|
124
|
+
3: 'tercer', 4: 'cuarto', 5: 'quinto', 6: 'sexto', 7: 'séptimo',
|
|
125
|
+
8: 'octavo', 9: 'noveno', 10: 'décimo', 12: 'duodécimo', 15: 'decimoquinto',
|
|
126
|
+
20: 'vigésimo', 30: 'trigésimo'
|
|
127
|
+
};
|
|
128
|
+
|
|
118
129
|
// Normalize raw user options.
|
|
119
130
|
function normalizeOptions(options?: Cronli5Options): Opts {
|
|
120
131
|
options = options || {};
|
|
@@ -269,6 +280,71 @@ function isPinnedMinuteSeconds(
|
|
|
269
280
|
schedule.shapes.second === 'step');
|
|
270
281
|
}
|
|
271
282
|
|
|
283
|
+
// The minute field's step stride for the confinement frame, or null when the
|
|
284
|
+
// minute is not a stepped cadence. A `step`-shaped field (`*/6`) reads its
|
|
285
|
+
// segment; a `list`-shaped field the core enumerated from a uneven step (`2/7`
|
|
286
|
+
// → 2,9,…,58) recovers the progression from its values.
|
|
287
|
+
function minuteStride(
|
|
288
|
+
schedule: Schedule
|
|
289
|
+
): {start: number; interval: number; last: number} | null {
|
|
290
|
+
if (schedule.shapes.minute === 'step') {
|
|
291
|
+
const segment = stepSegment(schedule, 'minute');
|
|
292
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
293
|
+
|
|
294
|
+
return {interval: segment.interval, last:
|
|
295
|
+
segment.fires[segment.fires.length - 1], start};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
299
|
+
|
|
300
|
+
return values && arithmeticStep(values);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// A stepped minute under a wildcard second and wildcard hour: bind the second
|
|
304
|
+
// cadence to the minute cadence as a CONFINEMENT ("cada segundo en cada sexto
|
|
305
|
+
// minuto a partir del minuto 4 de cada hora"), never the comma juxtaposition
|
|
306
|
+
// that reads as two independent cadences. The cadence is ORDINAL ("cada sexto
|
|
307
|
+
// minuto") — the cardinal "cada seis minutos" is what fuels the misread — and
|
|
308
|
+
// the start/bound mirror the standalone minute cadence: a clean step from the
|
|
309
|
+
// top names no offset, an offset-clean stride names only its start, and a
|
|
310
|
+
// uneven one pins both endpoints ("del minuto 2 al 58"). An interval the
|
|
311
|
+
// ordinal table does not cover keeps the cardinal "cada N" after "en", which
|
|
312
|
+
// still confines.
|
|
313
|
+
function minuteStepConfinement(
|
|
314
|
+
schedule: Schedule,
|
|
315
|
+
stride: {start: number; interval: number; last: number},
|
|
316
|
+
opts: Opts
|
|
317
|
+
): string {
|
|
318
|
+
const ordinal = stepOrdinals[stride.interval];
|
|
319
|
+
const head = ordinal ?
|
|
320
|
+
'cada ' + ordinal + ' minuto' :
|
|
321
|
+
'cada ' + numero(stride.interval, opts) + ' minutos';
|
|
322
|
+
|
|
323
|
+
const tail = chooseStride({...stride, cycle: 60}, {
|
|
324
|
+
bare: () => '',
|
|
325
|
+
offset: () => ' a partir del minuto ' + stride.start,
|
|
326
|
+
bounded: () => ' del minuto ' + stride.start + ' al ' + stride.last
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
return secondsLeadClause(schedule, opts) + ' en ' + head + tail +
|
|
330
|
+
' de cada hora' + trailingQualifier(schedule, opts);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Whether a stepped minute fills a wildcard hour under a wildcard second — the
|
|
334
|
+
// shape the confinement frame above handles.
|
|
335
|
+
function isSteppedMinuteSeconds(
|
|
336
|
+
schedule: Schedule,
|
|
337
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
338
|
+
): boolean {
|
|
339
|
+
return (plan.rest.kind === 'minuteFrequency' ||
|
|
340
|
+
plan.rest.kind === 'multipleMinutes') &&
|
|
341
|
+
(schedule.shapes.second === 'wildcard' ||
|
|
342
|
+
schedule.shapes.second === 'step') &&
|
|
343
|
+
schedule.shapes.hour === 'wildcard' &&
|
|
344
|
+
schedule.pattern.minute !== '*/2' &&
|
|
345
|
+
minuteStride(schedule) !== null;
|
|
346
|
+
}
|
|
347
|
+
|
|
272
348
|
function renderComposeSeconds(
|
|
273
349
|
schedule: Schedule,
|
|
274
350
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
@@ -312,6 +388,14 @@ function renderComposeSeconds(
|
|
|
312
388
|
return dayFrame + ', ' + window + ', ' + cadence;
|
|
313
389
|
}
|
|
314
390
|
|
|
391
|
+
// A stepped minute under a wildcard second + wildcard hour confines the
|
|
392
|
+
// second cadence to the ordinal minute cadence ("cada segundo en cada sexto
|
|
393
|
+
// minuto a partir del minuto 4 de cada hora"), never the comma juxtaposition
|
|
394
|
+
// that reads as two independent cadences.
|
|
395
|
+
if (isSteppedMinuteSeconds(schedule, plan)) {
|
|
396
|
+
return minuteStepConfinement(schedule, minuteStride(schedule)!, opts);
|
|
397
|
+
}
|
|
398
|
+
|
|
315
399
|
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
316
400
|
// cadences that read as contradictory ("cada segundo, cada dos minutos").
|
|
317
401
|
// Bind them with the genitive "de" ("cada segundo de cada dos minutos"),
|
|
@@ -467,6 +551,16 @@ function minutesList(schedule: Schedule, opts: Opts): string {
|
|
|
467
551
|
joinList(segmentWords(segmentsOf(schedule, 'minute'))) + ' de cada hora';
|
|
468
552
|
}
|
|
469
553
|
|
|
554
|
+
// Strip the generic "de cada hora" anchor from a minute-cadence lead. Under an
|
|
555
|
+
// hour STEP the hour clause is the sole hour authority, so the cadence must not
|
|
556
|
+
// also assert "de cada hora" — alongside a stepped hour it reads as a
|
|
557
|
+
// conflicting every-hour scope ("de cada hora, cada cuatro horas"). An hour
|
|
558
|
+
// WINDOW and an unrestricted hour keep the anchor (the window already names the
|
|
559
|
+
// hours; an open hour has no other hour statement).
|
|
560
|
+
function withoutHourAnchor(lead: string): string {
|
|
561
|
+
return lead.replace(/ de cada hora$/, '');
|
|
562
|
+
}
|
|
563
|
+
|
|
470
564
|
// "cada minuto del 0 al 30". The standalone renderer adds "de cada hora";
|
|
471
565
|
// when an hour qualifier follows ("..., a las 09:00", "..., cada dos
|
|
472
566
|
// horas") it would contradict, so it is not baked in here.
|
|
@@ -594,8 +688,10 @@ function renderMinuteFrequency(
|
|
|
594
688
|
}
|
|
595
689
|
else if (plan.hours.kind === 'step') {
|
|
596
690
|
// A clean stride is a confinement ("las horas pares", or the active-hour
|
|
597
|
-
// list), never a juxtaposed cadence ("cada dos horas").
|
|
598
|
-
|
|
691
|
+
// list), never a juxtaposed cadence ("cada dos horas"). The hour step
|
|
692
|
+
// scopes the hours, so an offset cadence drops "de cada hora".
|
|
693
|
+
phrase = withoutHourAnchor(phrase) + ', ' +
|
|
694
|
+
stepHourSpan(stepSegment(schedule, 'hour'), opts);
|
|
599
695
|
}
|
|
600
696
|
|
|
601
697
|
return phrase + trailingQualifier(schedule, opts);
|
|
@@ -680,10 +776,10 @@ function renderMinuteSpanAcrossHourStep(
|
|
|
680
776
|
|
|
681
777
|
// A minute list keeps the same cadence clause as the range; only its lead
|
|
682
778
|
// differs ("en los minutos 5 y 30 de cada hora" vs "cada minuto del 0 al
|
|
683
|
-
// 30").
|
|
684
|
-
const lead = plan.form === 'list' ?
|
|
779
|
+
// 30"). The hour step scopes the hours, so the lead drops "de cada hora".
|
|
780
|
+
const lead = withoutHourAnchor(plan.form === 'list' ?
|
|
685
781
|
minutesList(schedule, opts) :
|
|
686
|
-
minuteRangeLead(schedule.pattern.minute);
|
|
782
|
+
minuteRangeLead(schedule.pattern.minute));
|
|
687
783
|
|
|
688
784
|
return lead + ', ' +
|
|
689
785
|
(cadence ?? stepHours(segment, opts)) + trailingQualifier(schedule, opts);
|
|
@@ -1271,10 +1367,13 @@ function renderCompactClockTimes(
|
|
|
1271
1367
|
}
|
|
1272
1368
|
|
|
1273
1369
|
// A uneven hour stride reads as a cadence after the minute lead, not a wall
|
|
1274
|
-
// of clock-time columns.
|
|
1370
|
+
// of clock-time columns. That hour step is the sole hour authority, so the
|
|
1371
|
+
// minute lead drops its generic "de cada hora" (an every-hour scope that
|
|
1372
|
+
// would conflict with the step); the clock-time branch keeps it, naming
|
|
1373
|
+
// specific hours rather than a step.
|
|
1275
1374
|
const cadence = unevenHourCadence(schedule, opts);
|
|
1276
1375
|
const phrase = cadence ?
|
|
1277
|
-
minutesList(schedule, opts) + ', ' + cadence +
|
|
1376
|
+
withoutHourAnchor(minutesList(schedule, opts)) + ', ' + cadence +
|
|
1278
1377
|
trailingQualifier(schedule, opts) :
|
|
1279
1378
|
minutesList(schedule, opts) + ', ' +
|
|
1280
1379
|
hourContextTimes(schedule, opts) + trailingQualifier(schedule, opts);
|
package/src/lang/fi/index.ts
CHANGED
|
@@ -123,6 +123,17 @@ const nthWeekdayNames: (string | null)[] = [
|
|
|
123
123
|
'viidentenä'
|
|
124
124
|
];
|
|
125
125
|
|
|
126
|
+
// Essive ordinals for "joka N:ntenä minuuttina" — the step intervals a minute
|
|
127
|
+
// cadence can take. The interval-2 step keeps its own "joka toisena minuuttina"
|
|
128
|
+
// idiom and never reaches the confinement helper; a lookup miss falls back to
|
|
129
|
+
// the genitive "N minuutin välein" cadence, which still confines.
|
|
130
|
+
const minuteStepOrdinals: {[interval: number]: string} = {
|
|
131
|
+
3: 'kolmantena', 4: 'neljäntenä', 5: 'viidentenä', 6: 'kuudentena',
|
|
132
|
+
7: 'seitsemäntenä', 8: 'kahdeksantena', 9: 'yhdeksäntenä',
|
|
133
|
+
10: 'kymmenentenä', 12: 'kahdentenatoista', 15: 'viidentenätoista',
|
|
134
|
+
20: 'kahdentenakymmenentenä', 30: 'kolmantenakymmenentenä'
|
|
135
|
+
};
|
|
136
|
+
|
|
126
137
|
// Weekdays as stored inflected forms (SUN..SAT): distributive -isin,
|
|
127
138
|
// elative, illative, and essive. Consonant gradation (keskiviikko →
|
|
128
139
|
// keskiviikosta) makes stem+suffix logic wrong; store the forms.
|
|
@@ -404,6 +415,71 @@ function composeHourCadence(
|
|
|
404
415
|
hourRangeCadence(schedule, minute, opts);
|
|
405
416
|
}
|
|
406
417
|
|
|
418
|
+
// The minute field's step stride for the confinement frame, or null when the
|
|
419
|
+
// minute is not a stepped cadence. A `step`-shaped field reads its segment; a
|
|
420
|
+
// `list`-shaped field the core enumerated from a uneven step (`2/7` → 2,9,…,58)
|
|
421
|
+
// recovers the progression from its values.
|
|
422
|
+
function minuteStride(
|
|
423
|
+
schedule: Schedule
|
|
424
|
+
): {start: number; interval: number; last: number} | null {
|
|
425
|
+
if (schedule.shapes.minute === 'step') {
|
|
426
|
+
const segment = stepSegment(schedule, 'minute');
|
|
427
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
428
|
+
|
|
429
|
+
return {interval: segment.interval, last:
|
|
430
|
+
segment.fires[segment.fires.length - 1], start};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
434
|
+
|
|
435
|
+
return values && arithmeticStep(values);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// A stepped minute under a wildcard/stepped second and wildcard hour: bind the
|
|
439
|
+
// second cadence to the minute cadence as a CONFINEMENT ("joka sekunti joka
|
|
440
|
+
// kuudentena minuuttina jokaisen tunnin minuutista 4 alkaen"), never the comma
|
|
441
|
+
// juxtaposition that reads as two independent cadences. The cadence is ORDINAL
|
|
442
|
+
// ("joka kuudentena minuuttina") — the cardinal "kuuden minuutin välein" is
|
|
443
|
+
// what fuels the misread — and the start/bound mirror the standalone minute
|
|
444
|
+
// cadence: an offset-clean stride names only its start, a uneven one pins both
|
|
445
|
+
// endpoints ("minuutista 2 minuuttiin 58").
|
|
446
|
+
function minuteStepConfinement(
|
|
447
|
+
schedule: Schedule,
|
|
448
|
+
stride: {start: number; interval: number; last: number},
|
|
449
|
+
opts: NormalizedOptions
|
|
450
|
+
): string {
|
|
451
|
+
const ordinalForm = minuteStepOrdinals[stride.interval];
|
|
452
|
+
const minute = units.minute;
|
|
453
|
+
const head = ordinalForm ?
|
|
454
|
+
' joka ' + ordinalForm + ' minuuttina' :
|
|
455
|
+
' ' + genitive(stride.interval, opts) + ' ' + minute.gen + ' välein';
|
|
456
|
+
|
|
457
|
+
const tail = chooseStride({...stride, cycle: 60}, {
|
|
458
|
+
bare: () => '',
|
|
459
|
+
offset: () => ' ' + minute.anchor + ' ' + minute.ela + ' ' +
|
|
460
|
+
stride.start + ' alkaen',
|
|
461
|
+
bounded: () => ' ' + minute.ela + ' ' + stride.start + ' ' +
|
|
462
|
+
minute.ill + ' ' + stride.last
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
return secondsLeadClause(schedule, opts) + head + tail +
|
|
466
|
+
trailingQualifier(schedule, opts);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Whether a stepped minute fills a wildcard hour under a wildcard/stepped
|
|
470
|
+
// second — the shape the confinement frame above handles.
|
|
471
|
+
function isSteppedMinuteSeconds(
|
|
472
|
+
schedule: Schedule,
|
|
473
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
474
|
+
): boolean {
|
|
475
|
+
return (plan.rest.kind === 'minuteFrequency' ||
|
|
476
|
+
plan.rest.kind === 'multipleMinutes') &&
|
|
477
|
+
(schedule.pattern.second === '*' || schedule.shapes.second === 'step') &&
|
|
478
|
+
schedule.shapes.hour === 'wildcard' &&
|
|
479
|
+
schedule.pattern.minute !== '*/2' &&
|
|
480
|
+
minuteStride(schedule) !== null;
|
|
481
|
+
}
|
|
482
|
+
|
|
407
483
|
function renderComposeSeconds(
|
|
408
484
|
schedule: Schedule,
|
|
409
485
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
@@ -419,6 +495,16 @@ function renderComposeSeconds(
|
|
|
419
495
|
return cadence;
|
|
420
496
|
}
|
|
421
497
|
|
|
498
|
+
// A stepped minute under a wildcard/stepped second + wildcard hour confines
|
|
499
|
+
// the second cadence to the ordinal minute cadence ("joka sekunti joka
|
|
500
|
+
// kuudentena minuuttina jokaisen tunnin minuutista 4 alkaen"), never the
|
|
501
|
+
// comma juxtaposition that reads as two independent cadences. Checked before
|
|
502
|
+
// the general minute-step compose path, which keeps the comma form under a
|
|
503
|
+
// restricted hour.
|
|
504
|
+
if (isSteppedMinuteSeconds(schedule, plan)) {
|
|
505
|
+
return minuteStepConfinement(schedule, minuteStride(schedule)!, opts);
|
|
506
|
+
}
|
|
507
|
+
|
|
422
508
|
// When the rest is a minute-step cadence, the step leads and the second
|
|
423
509
|
// anchor follows after a comma (the comma marks the granularity boundary
|
|
424
510
|
// between the two levels, not a flat list).
|
|
@@ -426,15 +512,15 @@ function renderComposeSeconds(
|
|
|
426
512
|
return composeSecondsOverMinuteStep(schedule, plan.rest, opts);
|
|
427
513
|
}
|
|
428
514
|
|
|
429
|
-
// A
|
|
430
|
-
// clock-time rest would
|
|
431
|
-
//
|
|
432
|
-
// the seconds to the explicit clock minute with the "minuutin
|
|
433
|
-
// frame (an "of"/during form, never a range) and trail the day
|
|
434
|
-
// ("joka sekunti minuutin 9.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return
|
|
515
|
+
// A second over a single fixed minute and a specific hour is a single fixed
|
|
516
|
+
// timestamp: the clock-time rest would float the seconds as a separate clause
|
|
517
|
+
// ("joka sekunti, joka päivä klo 9.02"), hiding that they belong to that one
|
|
518
|
+
// minute. Bind the seconds to the explicit clock minute with the "minuutin
|
|
519
|
+
// HH.MM aikana" frame (an "of"/during form, never a range) and trail the day
|
|
520
|
+
// qualifier ("joka sekunti minuutin 9.02 aikana, joka päivä") — the same
|
|
521
|
+
// fusion the minute-0 case ("minuutin 9.00 aikana") uses.
|
|
522
|
+
if (plan.rest.kind === 'clockTimes' && schedule.shapes.minute === 'single') {
|
|
523
|
+
return composeSingleMinute(schedule, plan.rest, opts);
|
|
438
524
|
}
|
|
439
525
|
|
|
440
526
|
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
@@ -473,11 +559,13 @@ function isEveryOtherMinuteSeconds(
|
|
|
473
559
|
return seg.startToken === '*' && seg.interval === 2;
|
|
474
560
|
}
|
|
475
561
|
|
|
476
|
-
// The minute
|
|
477
|
-
// in the "minuutin/minuuttien HH.
|
|
478
|
-
// a range — a range would round-trip back to the whole hour) and
|
|
479
|
-
// qualifier ("joka sekunti minuutin 9.
|
|
480
|
-
|
|
562
|
+
// The single-fixed-minute confinement: bind the seconds to the explicit clock
|
|
563
|
+
// minute(s) in the "minuutin/minuuttien HH.MM aikana" frame (an "of"/during
|
|
564
|
+
// form, never a range — a range would round-trip back to the whole hour) and
|
|
565
|
+
// trail the day qualifier ("joka sekunti minuutin 9.02 aikana, joka päivä").
|
|
566
|
+
// Minute 0 ("minuutin 9.00 aikana") is just this with the minute being 0; any
|
|
567
|
+
// single fixed minute fuses the same way.
|
|
568
|
+
function composeSingleMinute(
|
|
481
569
|
schedule: Schedule,
|
|
482
570
|
rest: Extract<PlanNode, {kind: 'clockTimes'}>,
|
|
483
571
|
opts: NormalizedOptions
|
|
@@ -695,8 +783,10 @@ function renderMinuteFrequency(
|
|
|
695
783
|
const cadence = unevenHourCadence(schedule, opts);
|
|
696
784
|
|
|
697
785
|
if (cadence !== null) {
|
|
698
|
-
|
|
699
|
-
|
|
786
|
+
// The hour step is the sole hour authority, so an offset minute cadence
|
|
787
|
+
// drops its generic "jokaisen tunnin" every-hour scope.
|
|
788
|
+
return withoutHourAnchor(stepCycle60(seg, units.minute, opts)) + ', ' +
|
|
789
|
+
cadence + trailingQualifier(schedule, opts);
|
|
700
790
|
}
|
|
701
791
|
|
|
702
792
|
// When the step renders as anchored ("kohdalla"), the per-hour windows
|
|
@@ -714,13 +804,16 @@ function renderMinuteFrequency(
|
|
|
714
804
|
trailingQualifier(schedule, opts);
|
|
715
805
|
}
|
|
716
806
|
|
|
717
|
-
|
|
807
|
+
const phraseBase = stepCycle60(seg, units.minute, opts);
|
|
808
|
+
let phrase = phraseBase;
|
|
718
809
|
|
|
719
810
|
if (plan.hours.kind === 'window') {
|
|
720
811
|
phrase += ' ' + hourWindow(plan.hours, opts);
|
|
721
812
|
}
|
|
722
813
|
else if (plan.hours.kind === 'step') {
|
|
723
|
-
|
|
814
|
+
// The hour step is the sole hour authority, so the minute cadence drops its
|
|
815
|
+
// generic "jokaisen tunnin" every-hour scope.
|
|
816
|
+
phrase = withoutHourAnchor(phraseBase) + ' ' +
|
|
724
817
|
everyNthHour(stepSegment(schedule, 'hour'), opts);
|
|
725
818
|
}
|
|
726
819
|
|
|
@@ -1167,6 +1260,16 @@ function stepCycle60(
|
|
|
1167
1260
|
}, opts);
|
|
1168
1261
|
}
|
|
1169
1262
|
|
|
1263
|
+
// Strip the generic "jokaisen tunnin" anchor from an offset minute-cadence
|
|
1264
|
+
// lead. When the hour field is a restricted step, the hour clause is the sole
|
|
1265
|
+
// hour authority, so the cadence must not also assert "jokaisen tunnin" (every
|
|
1266
|
+
// hour) — alongside a stepped hour it conflicts as an every-hour scope.
|
|
1267
|
+
// An unrestricted hour, and an hour WINDOW, keep the anchor (the window names
|
|
1268
|
+
// the hours without an every-hour-of-the-day conflict).
|
|
1269
|
+
function withoutHourAnchor(lead: string): string {
|
|
1270
|
+
return lead.replace(' ' + units.minute.anchor + ' ', ' ');
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1170
1273
|
// "kahden tunnin välein", "klo 0, 10 ja 20", or "viiden tunnin välein
|
|
1171
1274
|
// klo 1 alkaen".
|
|
1172
1275
|
function stepHours(segment: StepSegment, opts: NormalizedOptions): string {
|