cronli5 0.8.2 → 0.8.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 +62 -0
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +69 -5
- package/dist/cronli5.js +69 -5
- package/dist/lang/de.cjs +86 -1
- package/dist/lang/de.js +86 -1
- package/dist/lang/en.cjs +69 -5
- package/dist/lang/en.js +69 -5
- package/dist/lang/es.cjs +105 -3
- package/dist/lang/es.js +105 -3
- package/dist/lang/fi.cjs +70 -0
- package/dist/lang/fi.js +70 -0
- package/dist/lang/fr.cjs +77 -0
- package/dist/lang/fr.js +77 -0
- package/dist/lang/pt.cjs +78 -0
- package/dist/lang/pt.js +78 -0
- package/dist/lang/zh.cjs +36 -4
- package/dist/lang/zh.js +36 -4
- package/package.json +1 -1
- package/src/lang/de/index.ts +190 -1
- package/src/lang/en/index.ts +154 -24
- package/src/lang/es/index.ts +236 -6
- package/src/lang/fi/index.ts +163 -0
- package/src/lang/fr/index.ts +178 -0
- package/src/lang/pt/index.ts +174 -0
- package/src/lang/zh/index.ts +97 -6
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 || {};
|
|
@@ -204,7 +215,19 @@ function renderSecondsWithinMinute(
|
|
|
204
215
|
trailingQualifier(schedule, opts);
|
|
205
216
|
}
|
|
206
217
|
|
|
207
|
-
|
|
218
|
+
// A second LIST or RANGE under a single minute confines that minute with the
|
|
219
|
+
// genitive "de" ("en los segundos 5 y 10 del minuto 30 de cada hora"), never
|
|
220
|
+
// the comma juxtaposition that reads as two independent schedules. A STEP
|
|
221
|
+
// second is a cadence ("cada 15 segundos") and keeps its own lead.
|
|
222
|
+
if (secondsConfinesMinute(schedule)) {
|
|
223
|
+
return secondsBareLead(schedule) + ' ' +
|
|
224
|
+
confinedMinutePhrase(schedule, opts) + trailingQualifier(schedule, opts);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// A cadence/stepped second leads straight into the locative "en el minuto …"
|
|
228
|
+
// with NO comma ("cada 15 segundos en el minuto 30 de cada hora"); the
|
|
229
|
+
// locative binds the two specs, matching the no-comma list/single form.
|
|
230
|
+
return secondsLeadClause(schedule, opts) + ' en el minuto ' + minuteField +
|
|
208
231
|
' de cada hora' + trailingQualifier(schedule, opts);
|
|
209
232
|
}
|
|
210
233
|
|
|
@@ -269,6 +292,201 @@ function isPinnedMinuteSeconds(
|
|
|
269
292
|
schedule.shapes.second === 'step');
|
|
270
293
|
}
|
|
271
294
|
|
|
295
|
+
// The minute field's step stride for the confinement frame, or null when the
|
|
296
|
+
// minute is not a stepped cadence. A `step`-shaped field (`*/6`) reads its
|
|
297
|
+
// segment; a `list`-shaped field the core enumerated from a uneven step (`2/7`
|
|
298
|
+
// → 2,9,…,58) recovers the progression from its values.
|
|
299
|
+
function minuteStride(
|
|
300
|
+
schedule: Schedule
|
|
301
|
+
): {start: number; interval: number; last: number} | null {
|
|
302
|
+
if (schedule.shapes.minute === 'step') {
|
|
303
|
+
const segment = stepSegment(schedule, 'minute');
|
|
304
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
305
|
+
|
|
306
|
+
return {interval: segment.interval, last:
|
|
307
|
+
segment.fires[segment.fires.length - 1], start};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
311
|
+
|
|
312
|
+
return values && arithmeticStep(values);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// A stepped minute under a wildcard second and wildcard hour: bind the second
|
|
316
|
+
// cadence to the minute cadence as a CONFINEMENT ("cada segundo en cada sexto
|
|
317
|
+
// minuto a partir del minuto 4 de cada hora"), never the comma juxtaposition
|
|
318
|
+
// that reads as two independent cadences. The cadence is ORDINAL ("cada sexto
|
|
319
|
+
// minuto") — the cardinal "cada seis minutos" is what fuels the misread — and
|
|
320
|
+
// the start/bound mirror the standalone minute cadence: a clean step from the
|
|
321
|
+
// top names no offset, an offset-clean stride names only its start, and a
|
|
322
|
+
// uneven one pins both endpoints ("del minuto 2 al 58"). An interval the
|
|
323
|
+
// ordinal table does not cover keeps the cardinal "cada N" after "en", which
|
|
324
|
+
// still confines.
|
|
325
|
+
function minuteStepConfinement(
|
|
326
|
+
schedule: Schedule,
|
|
327
|
+
stride: {start: number; interval: number; last: number},
|
|
328
|
+
opts: Opts
|
|
329
|
+
): string {
|
|
330
|
+
const ordinal = stepOrdinals[stride.interval];
|
|
331
|
+
const head = ordinal ?
|
|
332
|
+
'cada ' + ordinal + ' minuto' :
|
|
333
|
+
'cada ' + numero(stride.interval, opts) + ' minutos';
|
|
334
|
+
|
|
335
|
+
const tail = chooseStride({...stride, cycle: 60}, {
|
|
336
|
+
bare: () => '',
|
|
337
|
+
offset: () => ' a partir del minuto ' + stride.start,
|
|
338
|
+
bounded: () => ' del minuto ' + stride.start + ' al ' + stride.last
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return secondsLeadClause(schedule, opts) + ' en ' + head + tail +
|
|
342
|
+
' de cada hora' + trailingQualifier(schedule, opts);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Whether a stepped minute fills a wildcard hour under a wildcard second — the
|
|
346
|
+
// shape the confinement frame above handles.
|
|
347
|
+
function isSteppedMinuteSeconds(
|
|
348
|
+
schedule: Schedule,
|
|
349
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
350
|
+
): boolean {
|
|
351
|
+
return (plan.rest.kind === 'minuteFrequency' ||
|
|
352
|
+
plan.rest.kind === 'multipleMinutes') &&
|
|
353
|
+
(schedule.shapes.second === 'wildcard' ||
|
|
354
|
+
schedule.shapes.second === 'step') &&
|
|
355
|
+
schedule.shapes.hour === 'wildcard' &&
|
|
356
|
+
schedule.pattern.minute !== '*/2' &&
|
|
357
|
+
minuteStride(schedule) !== null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// The leading seconds words for a clock-point second, WITHOUT the trailing "de
|
|
361
|
+
// cada minuto" anchor: a confined second attaches to the CONFINED minute ("de
|
|
362
|
+
// cada sexto minuto…"), so the generic minute anchor would be redundant. The
|
|
363
|
+
// list/range/single forms mirror `secondsClause` minus that anchor.
|
|
364
|
+
function secondsBareLead(schedule: Schedule): string {
|
|
365
|
+
const secondField = schedule.pattern.second;
|
|
366
|
+
const shape = schedule.shapes.second;
|
|
367
|
+
|
|
368
|
+
if (shape === 'range') {
|
|
369
|
+
const bounds = secondField.split('-');
|
|
370
|
+
|
|
371
|
+
return 'cada segundo del ' + bounds[0] + ' al ' + bounds[1];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (shape === 'single') {
|
|
375
|
+
return 'en el segundo ' + secondField;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return 'en los segundos ' +
|
|
379
|
+
joinList(segmentWords(segmentsOf(schedule, 'second')));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// The CONFINED-minute genitive phrase a clock-point second attaches to, with
|
|
383
|
+
// its leading connector folded in ("de cada sexto minuto a partir del minuto 4
|
|
384
|
+
// de cada hora", "de los minutos 0, 15 y 30 de cada hora", "del minuto 30 de
|
|
385
|
+
// cada hora"). A stepped minute reuses the ordinal cadence form; a list, range,
|
|
386
|
+
// or single names the minute(s) directly. This is the seconds clause's anchor,
|
|
387
|
+
// so the generic "de cada minuto" is never stacked alongside it.
|
|
388
|
+
function confinedMinutePhrase(schedule: Schedule, opts: Opts): string {
|
|
389
|
+
const stride = minuteStride(schedule);
|
|
390
|
+
|
|
391
|
+
if (stride && schedule.pattern.minute !== '*/2') {
|
|
392
|
+
const ordinal = stepOrdinals[stride.interval];
|
|
393
|
+
const head = ordinal ?
|
|
394
|
+
'cada ' + ordinal + ' minuto' :
|
|
395
|
+
'cada ' + numero(stride.interval, opts) + ' minutos';
|
|
396
|
+
const tail = chooseStride({...stride, cycle: 60}, {
|
|
397
|
+
bare: () => '',
|
|
398
|
+
offset: () => ' a partir del minuto ' + stride.start,
|
|
399
|
+
bounded: () => ' del minuto ' + stride.start + ' al ' + stride.last
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return 'de ' + head + tail + ' de cada hora';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (schedule.shapes.minute === 'range') {
|
|
406
|
+
return 'de ' + minuteRangeLead(schedule.pattern.minute) + ' de cada hora';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (schedule.shapes.minute === 'list') {
|
|
410
|
+
return 'de los minutos ' +
|
|
411
|
+
joinList(segmentWords(segmentsOf(schedule, 'minute'))) + ' de cada hora';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// A single pinned minute: "del minuto 30 de cada hora".
|
|
415
|
+
return 'del minuto ' + schedule.pattern.minute + ' de cada hora';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Whether a clock-point second (list, range, or single) sits under a restricted
|
|
419
|
+
// minute and a wildcard hour — the shape that must CONFINE the minute with the
|
|
420
|
+
// genitive "de" rather than juxtapose it behind a comma (two independent
|
|
421
|
+
// schedules). The single-second + single-minute pair folds into one coherent
|
|
422
|
+
// clock point ("en el minuto 5 y el segundo 30 de cada hora") and is excluded.
|
|
423
|
+
// The minute-confinement rendering for a compose-seconds plan, or null when the
|
|
424
|
+
// plan is not one. A CADENCE second over a stepped minute uses the ordinal
|
|
425
|
+
// cadence form; a CLOCK-POINT second (list/range/single) over any restricted
|
|
426
|
+
// minute uses the genitive form anchored to the confined minute. Both bind the
|
|
427
|
+
// second beneath the minute instead of juxtaposing the two behind a comma.
|
|
428
|
+
// Folded into one helper so `renderComposeSeconds` carries a single branch.
|
|
429
|
+
function minuteConfinementRender(
|
|
430
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
431
|
+
schedule: Schedule, opts: Opts
|
|
432
|
+
): string | null {
|
|
433
|
+
if (isSteppedMinuteSeconds(schedule, plan)) {
|
|
434
|
+
return minuteStepConfinement(schedule, minuteStride(schedule)!, opts);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const minuteRest = plan.rest.kind === 'minuteFrequency' ||
|
|
438
|
+
plan.rest.kind === 'multipleMinutes' ||
|
|
439
|
+
plan.rest.kind === 'rangeOfMinutes';
|
|
440
|
+
|
|
441
|
+
if (minuteRest && secondsConfinesMinute(schedule)) {
|
|
442
|
+
return secondsBareLead(schedule) + ' ' +
|
|
443
|
+
confinedMinutePhrase(schedule, opts) + trailingQualifier(schedule, opts);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function secondsConfinesMinute(schedule: Schedule): boolean {
|
|
450
|
+
const {second, minute, hour} = schedule.shapes;
|
|
451
|
+
|
|
452
|
+
// A second LIST the core enumerated from a step (`*/15` → 0,15,30,45; `3/2` →
|
|
453
|
+
// 3,5,…) is really a stride CADENCE, spoken "cada N segundos" and confined by
|
|
454
|
+
// the cadence path, not a clock-point clause; exclude it here.
|
|
455
|
+
if (second === 'list') {
|
|
456
|
+
const values = singleValues(segmentsOf(schedule, 'second'));
|
|
457
|
+
|
|
458
|
+
if (values && arithmeticStep(values)) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const clockPoint = second === 'single' || second === 'range' ||
|
|
464
|
+
second === 'list';
|
|
465
|
+
|
|
466
|
+
return clockPoint && minute !== 'wildcard' && hour === 'wildcard' &&
|
|
467
|
+
!(second === 'single' && minute === 'single');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// The seconds lead plus its connector for a generic compose-seconds fallback:
|
|
471
|
+
// empty when the rest already owns the second (a compact clock time folding a
|
|
472
|
+
// meaningful single second); a bare space when the rest is a locative "en …"
|
|
473
|
+
// minute LIST/SINGLE under a wildcard hour (the locative binds the two specs,
|
|
474
|
+
// so no comma); otherwise the comma that sets the seconds clause apart.
|
|
475
|
+
function composeConnector(
|
|
476
|
+
schedule: Schedule,
|
|
477
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
478
|
+
opts: Opts
|
|
479
|
+
): string {
|
|
480
|
+
if (plan.rest.kind === 'compactClockTimes' && schedule.analyses.clockSecond) {
|
|
481
|
+
return '';
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const locative = (plan.rest.kind === 'multipleMinutes' ||
|
|
485
|
+
plan.rest.kind === 'singleMinute') && schedule.shapes.hour === 'wildcard';
|
|
486
|
+
|
|
487
|
+
return secondsLeadClause(schedule, opts) + (locative ? ' ' : ', ');
|
|
488
|
+
}
|
|
489
|
+
|
|
272
490
|
function renderComposeSeconds(
|
|
273
491
|
schedule: Schedule,
|
|
274
492
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
@@ -312,6 +530,17 @@ function renderComposeSeconds(
|
|
|
312
530
|
return dayFrame + ', ' + window + ', ' + cadence;
|
|
313
531
|
}
|
|
314
532
|
|
|
533
|
+
// A second confines the minute restriction (open hour), never the comma
|
|
534
|
+
// juxtaposition that reads as two independent cadences: a CADENCE second over
|
|
535
|
+
// a stepped minute uses the ordinal-cadence form ("cada segundo en cada sexto
|
|
536
|
+
// minuto …"); a CLOCK-POINT second uses the genitive form anchored to the
|
|
537
|
+
// confined minute ("en los segundos 5, 10 y 15 de cada sexto minuto …").
|
|
538
|
+
const confined = minuteConfinementRender(plan, schedule, opts);
|
|
539
|
+
|
|
540
|
+
if (confined !== null) {
|
|
541
|
+
return confined;
|
|
542
|
+
}
|
|
543
|
+
|
|
315
544
|
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
316
545
|
// cadences that read as contradictory ("cada segundo, cada dos minutos").
|
|
317
546
|
// Bind them with the genitive "de" ("cada segundo de cada dos minutos"),
|
|
@@ -324,11 +553,12 @@ function renderComposeSeconds(
|
|
|
324
553
|
|
|
325
554
|
// A compact clock-time rest folds a meaningful SINGLE second into its own
|
|
326
555
|
// leading clause, so the composer must not prepend a second lead that would
|
|
327
|
-
// double it. A
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
556
|
+
// double it (empty connector). A cadence/stepped second under a minute
|
|
557
|
+
// LIST or SINGLE and a wildcard hour leads straight into the locative "en …"
|
|
558
|
+
// minute phrase with NO comma ("cada segundo en los minutos 0, 15 y 30 de
|
|
559
|
+
// cada hora") — the locative binds the two specs, matching the no-comma
|
|
560
|
+
// stepped-minute and list-tier confinements. Every other rest takes a comma.
|
|
561
|
+
const lead = composeConnector(schedule, plan, opts);
|
|
332
562
|
|
|
333
563
|
return lead + render(schedule, plan.rest, opts);
|
|
334
564
|
}
|
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.
|
|
@@ -335,6 +346,13 @@ function renderSecondsWithinMinute(
|
|
|
335
346
|
units.second.gen + ' kohdalla' + trailingQualifier(schedule, opts);
|
|
336
347
|
}
|
|
337
348
|
|
|
349
|
+
// A second LIST or RANGE under a single minute folds both into one shared
|
|
350
|
+
// "kohdalla" ("joka tunti 30 minuutin ja 5 ja 10 sekunnin kohdalla"), never
|
|
351
|
+
// the comma juxtaposition; a STEP second is a cadence and keeps its clause.
|
|
352
|
+
if (secondsConfinesMinute(schedule)) {
|
|
353
|
+
return secondsConfinement(schedule, opts);
|
|
354
|
+
}
|
|
355
|
+
|
|
338
356
|
return secondsLeadClause(schedule, opts) + ', ' +
|
|
339
357
|
atMarks(minuteField, units.minute, true) +
|
|
340
358
|
trailingQualifier(schedule, opts);
|
|
@@ -404,6 +422,133 @@ function composeHourCadence(
|
|
|
404
422
|
hourRangeCadence(schedule, minute, opts);
|
|
405
423
|
}
|
|
406
424
|
|
|
425
|
+
// The minute field's step stride for the confinement frame, or null when the
|
|
426
|
+
// minute is not a stepped cadence. A `step`-shaped field reads its segment; a
|
|
427
|
+
// `list`-shaped field the core enumerated from a uneven step (`2/7` → 2,9,…,58)
|
|
428
|
+
// recovers the progression from its values.
|
|
429
|
+
function minuteStride(
|
|
430
|
+
schedule: Schedule
|
|
431
|
+
): {start: number; interval: number; last: number} | null {
|
|
432
|
+
if (schedule.shapes.minute === 'step') {
|
|
433
|
+
const segment = stepSegment(schedule, 'minute');
|
|
434
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
435
|
+
|
|
436
|
+
return {interval: segment.interval, last:
|
|
437
|
+
segment.fires[segment.fires.length - 1], start};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
441
|
+
|
|
442
|
+
return values && arithmeticStep(values);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// A stepped minute under a wildcard/stepped second and wildcard hour: bind the
|
|
446
|
+
// second cadence to the minute cadence as a CONFINEMENT ("joka sekunti joka
|
|
447
|
+
// kuudentena minuuttina jokaisen tunnin minuutista 4 alkaen"), never the comma
|
|
448
|
+
// juxtaposition that reads as two independent cadences. The cadence is ORDINAL
|
|
449
|
+
// ("joka kuudentena minuuttina") — the cardinal "kuuden minuutin välein" is
|
|
450
|
+
// what fuels the misread — and the start/bound mirror the standalone minute
|
|
451
|
+
// cadence: an offset-clean stride names only its start, a uneven one pins both
|
|
452
|
+
// endpoints ("minuutista 2 minuuttiin 58").
|
|
453
|
+
function minuteStepConfinement(
|
|
454
|
+
schedule: Schedule,
|
|
455
|
+
stride: {start: number; interval: number; last: number},
|
|
456
|
+
opts: NormalizedOptions
|
|
457
|
+
): string {
|
|
458
|
+
const ordinalForm = minuteStepOrdinals[stride.interval];
|
|
459
|
+
const minute = units.minute;
|
|
460
|
+
const head = ordinalForm ?
|
|
461
|
+
' joka ' + ordinalForm + ' minuuttina' :
|
|
462
|
+
' ' + genitive(stride.interval, opts) + ' ' + minute.gen + ' välein';
|
|
463
|
+
|
|
464
|
+
const tail = chooseStride({...stride, cycle: 60}, {
|
|
465
|
+
bare: () => '',
|
|
466
|
+
offset: () => ' ' + minute.anchor + ' ' + minute.ela + ' ' +
|
|
467
|
+
stride.start + ' alkaen',
|
|
468
|
+
bounded: () => ' ' + minute.ela + ' ' + stride.start + ' ' +
|
|
469
|
+
minute.ill + ' ' + stride.last
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return secondsLeadClause(schedule, opts) + head + tail +
|
|
473
|
+
trailingQualifier(schedule, opts);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Whether a stepped minute fills a wildcard hour under a wildcard/stepped
|
|
477
|
+
// second — the shape the confinement frame above handles.
|
|
478
|
+
function isSteppedMinuteSeconds(
|
|
479
|
+
schedule: Schedule,
|
|
480
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
481
|
+
): boolean {
|
|
482
|
+
return (plan.rest.kind === 'minuteFrequency' ||
|
|
483
|
+
plan.rest.kind === 'multipleMinutes') &&
|
|
484
|
+
(schedule.pattern.second === '*' || schedule.shapes.second === 'step') &&
|
|
485
|
+
schedule.shapes.hour === 'wildcard' &&
|
|
486
|
+
schedule.pattern.minute !== '*/2' &&
|
|
487
|
+
minuteStride(schedule) !== null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Whether a clock-point second (list, range, or single) sits under a restricted
|
|
491
|
+
// minute and a wildcard hour — the shape that must CONFINE the minute rather
|
|
492
|
+
// than juxtapose it behind a comma (two independent schedules). A second LIST
|
|
493
|
+
// the core enumerated from a step (`3/2`) is really a stride cadence and stays
|
|
494
|
+
// out. The single-second + single-minute pair folds into one shared "kohdalla"
|
|
495
|
+
// already ("joka tunti 30 minuutin ja 15 sekunnin kohdalla") and is excluded.
|
|
496
|
+
function secondsConfinesMinute(schedule: Schedule): boolean {
|
|
497
|
+
const {second, minute, hour} = schedule.shapes;
|
|
498
|
+
|
|
499
|
+
if (second === 'list') {
|
|
500
|
+
const values = singleValues(segmentsOf(schedule, 'second'));
|
|
501
|
+
|
|
502
|
+
if (values && arithmeticStep(values)) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const clockPoint = second === 'single' || second === 'range' ||
|
|
508
|
+
second === 'list';
|
|
509
|
+
|
|
510
|
+
return clockPoint && minute !== 'wildcard' && hour === 'wildcard' &&
|
|
511
|
+
!(second === 'single' && minute === 'single');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Whether a compose-seconds plan over a minute-cadence/list/range rest carries
|
|
515
|
+
// a clock-point second that must confine the minute — the gate for the "的"/
|
|
516
|
+
// shared-"kohdalla" fusion in `renderComposeSeconds`, kept out of that function
|
|
517
|
+
// to hold its branch count down.
|
|
518
|
+
function composeConfinesMinute(
|
|
519
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>, schedule: Schedule
|
|
520
|
+
): boolean {
|
|
521
|
+
const minuteRest = plan.rest.kind === 'minuteFrequency' ||
|
|
522
|
+
plan.rest.kind === 'multipleMinutes' ||
|
|
523
|
+
plan.rest.kind === 'rangeOfMinutes';
|
|
524
|
+
|
|
525
|
+
return minuteRest && secondsConfinesMinute(schedule);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Confine a restricted minute under a clock-point second. A STEPPED minute
|
|
529
|
+
// keeps the essive ordinal cadence frame ("joka kuudentena minuuttina jokaisen
|
|
530
|
+
// tunnin minuutista 4 alkaen") and the seconds trail as their postposition,
|
|
531
|
+
// mirroring the open-minute "joka minuutti 5, 10 ja 15 sekunnin kohdalla". A
|
|
532
|
+
// list, range, or single minute folds BOTH fields into one shared "kohdalla"
|
|
533
|
+
// ("joka tunti 0, 15 ja 30 minuutin ja 5, 10 ja 15 sekunnin kohdalla"), the
|
|
534
|
+
// same fusion the single-second case uses — never the comma juxtaposition.
|
|
535
|
+
function secondsConfinement(
|
|
536
|
+
schedule: Schedule, opts: NormalizedOptions
|
|
537
|
+
): string {
|
|
538
|
+
const stride = minuteStride(schedule);
|
|
539
|
+
|
|
540
|
+
if (stride && schedule.pattern.minute !== '*/2') {
|
|
541
|
+
return minuteStepConfinement(schedule, stride, opts);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const minuteDigits = joinList(segmentWords(segmentsOf(schedule, 'minute')));
|
|
545
|
+
const secondDigits = joinList(segmentWords(segmentsOf(schedule, 'second')));
|
|
546
|
+
|
|
547
|
+
return units.minute.mark + ' ' + minuteDigits + ' ' + units.minute.gen +
|
|
548
|
+
' ja ' + secondDigits + ' ' + units.second.gen + ' kohdalla' +
|
|
549
|
+
trailingQualifier(schedule, opts);
|
|
550
|
+
}
|
|
551
|
+
|
|
407
552
|
function renderComposeSeconds(
|
|
408
553
|
schedule: Schedule,
|
|
409
554
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
@@ -419,6 +564,24 @@ function renderComposeSeconds(
|
|
|
419
564
|
return cadence;
|
|
420
565
|
}
|
|
421
566
|
|
|
567
|
+
// A stepped minute under a wildcard/stepped second + wildcard hour confines
|
|
568
|
+
// the second cadence to the ordinal minute cadence ("joka sekunti joka
|
|
569
|
+
// kuudentena minuuttina jokaisen tunnin minuutista 4 alkaen"), never the
|
|
570
|
+
// comma juxtaposition that reads as two independent cadences. Checked before
|
|
571
|
+
// the general minute-step compose path, which keeps the comma form under a
|
|
572
|
+
// restricted hour.
|
|
573
|
+
if (isSteppedMinuteSeconds(schedule, plan)) {
|
|
574
|
+
return minuteStepConfinement(schedule, minuteStride(schedule)!, opts);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// A clock-point second (list/range/single) under a restricted minute confines
|
|
578
|
+
// that minute, never the comma juxtaposition that reads as two schedules: a
|
|
579
|
+
// stepped minute keeps its essive frame with the seconds postposition; a
|
|
580
|
+
// list, range, or single minute folds both into one shared "kohdalla".
|
|
581
|
+
if (composeConfinesMinute(plan, schedule)) {
|
|
582
|
+
return secondsConfinement(schedule, opts);
|
|
583
|
+
}
|
|
584
|
+
|
|
422
585
|
// When the rest is a minute-step cadence, the step leads and the second
|
|
423
586
|
// anchor follows after a comma (the comma marks the granularity boundary
|
|
424
587
|
// between the two levels, not a flat list).
|
package/src/lang/fr/index.ts
CHANGED
|
@@ -217,6 +217,14 @@ function renderSecondsWithinMinute(
|
|
|
217
217
|
trailingQualifier(schedule, opts);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// A second LIST or RANGE under a single minute confines that minute in the
|
|
221
|
+
// genitive ("aux secondes 5 et 10 de la minute 30 de chaque heure"), never
|
|
222
|
+
// the comma juxtaposition; a STEP second is a cadence and keeps its own lead.
|
|
223
|
+
if (secondsConfinesMinute(schedule)) {
|
|
224
|
+
return secondsBareLead(schedule) + ' ' +
|
|
225
|
+
confinedMinutePhrase(schedule) + trailingQualifier(schedule, opts);
|
|
226
|
+
}
|
|
227
|
+
|
|
220
228
|
return secondsLeadClause(schedule, opts) + ', à la minute ' + minuteField +
|
|
221
229
|
' de chaque heure' + trailingQualifier(schedule, opts);
|
|
222
230
|
}
|
|
@@ -279,6 +287,161 @@ function isPinnedMinuteSeconds(
|
|
|
279
287
|
schedule.shapes.second === 'step');
|
|
280
288
|
}
|
|
281
289
|
|
|
290
|
+
// The minute field's step stride for the confinement frame, or null when the
|
|
291
|
+
// minute is not a stepped cadence. A `step`-shaped field reads its segment; a
|
|
292
|
+
// `list`-shaped field the core enumerated from a uneven step (`2/7` → 2,9,…,58)
|
|
293
|
+
// recovers the progression from its values.
|
|
294
|
+
function minuteStride(
|
|
295
|
+
schedule: Schedule
|
|
296
|
+
): {start: number; interval: number; last: number} | null {
|
|
297
|
+
if (schedule.shapes.minute === 'step') {
|
|
298
|
+
const segment = stepSegment(schedule, 'minute');
|
|
299
|
+
const start = segment.startToken === '*' ? 0 : +segment.startToken;
|
|
300
|
+
|
|
301
|
+
return {interval: segment.interval, last:
|
|
302
|
+
segment.fires[segment.fires.length - 1], start};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const values = singleValues(segmentsOf(schedule, 'minute'));
|
|
306
|
+
|
|
307
|
+
return values && arithmeticStep(values);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// A stepped minute under a wildcard hour: the second clause leads, a COMMA,
|
|
311
|
+
// then the minute's own STANDALONE cardinal cadence ("chaque seconde, toutes
|
|
312
|
+
// les six minutes à partir de la minute 4 de chaque heure"; "aux secondes 5,
|
|
313
|
+
// 10 et 15, toutes les six minutes …"). The ordinal "à la sixième minute" read
|
|
314
|
+
// as a single minute (the 10th), not the every-sixth series; the standalone
|
|
315
|
+
// cardinal "toutes les six minutes" reads it correctly and handles every stride
|
|
316
|
+
// (offset, bounded, uneven) for free. The lead is the cadence clause for a
|
|
317
|
+
// wildcard/stepped second, the bare clock-point clause for a list/range/single.
|
|
318
|
+
function steppedMinuteConfinement(
|
|
319
|
+
schedule: Schedule,
|
|
320
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
321
|
+
lead: string,
|
|
322
|
+
opts: Opts
|
|
323
|
+
): string {
|
|
324
|
+
return lead + ', ' + render(schedule, plan.rest, opts) +
|
|
325
|
+
trailingQualifier(schedule, opts);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Whether a stepped minute fills a wildcard hour under a wildcard/stepped
|
|
329
|
+
// second — the shape the confinement frame above handles.
|
|
330
|
+
function isSteppedMinuteSeconds(
|
|
331
|
+
schedule: Schedule,
|
|
332
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>
|
|
333
|
+
): boolean {
|
|
334
|
+
return (plan.rest.kind === 'minuteFrequency' ||
|
|
335
|
+
plan.rest.kind === 'multipleMinutes') &&
|
|
336
|
+
(schedule.shapes.second === 'wildcard' ||
|
|
337
|
+
schedule.shapes.second === 'step') &&
|
|
338
|
+
schedule.shapes.hour === 'wildcard' &&
|
|
339
|
+
schedule.pattern.minute !== '*/2' &&
|
|
340
|
+
minuteStride(schedule) !== null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// The leading seconds words for a clock-point second, WITHOUT the trailing "de
|
|
344
|
+
// chaque minute" anchor: a confined second attaches to the CONFINED minute ("de
|
|
345
|
+
// la sixième minute…"), so the generic minute anchor would be redundant.
|
|
346
|
+
function secondsBareLead(schedule: Schedule): string {
|
|
347
|
+
const secondField = schedule.pattern.second;
|
|
348
|
+
const shape = schedule.shapes.second;
|
|
349
|
+
|
|
350
|
+
if (shape === 'range') {
|
|
351
|
+
const bounds = secondField.split('-');
|
|
352
|
+
|
|
353
|
+
return 'chaque seconde de ' + bounds[0] + ' à ' + bounds[1];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (shape === 'single') {
|
|
357
|
+
return 'à la seconde ' + secondField;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return 'aux secondes ' +
|
|
361
|
+
joinList(segmentWords(segmentsOf(schedule, 'second')));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// The CONFINED-minute genitive phrase a clock-point second attaches to ("des
|
|
365
|
+
// minutes 0, 15 et 30 de chaque heure", "de la minute 30 de chaque heure", "de
|
|
366
|
+
// chaque minute de 0 à 30 de chaque heure"). A stepped minute is handled by the
|
|
367
|
+
// standalone-cadence confinement before this point; a list, range, or single
|
|
368
|
+
// names the minute(s) in the genitive — so the bare seconds lead never stacks a
|
|
369
|
+
// redundant "de chaque minute".
|
|
370
|
+
function confinedMinutePhrase(schedule: Schedule): string {
|
|
371
|
+
if (schedule.shapes.minute === 'range') {
|
|
372
|
+
return 'de ' + minuteRangeLead(schedule.pattern.minute) +
|
|
373
|
+
' de chaque heure';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (schedule.shapes.minute === 'list') {
|
|
377
|
+
return 'des minutes ' +
|
|
378
|
+
joinList(segmentWords(segmentsOf(schedule, 'minute'))) +
|
|
379
|
+
' de chaque heure';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return 'de la minute ' + schedule.pattern.minute + ' de chaque heure';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Whether a clock-point second (list, range, or single) sits under a restricted
|
|
386
|
+
// minute and a wildcard hour — the shape that must CONFINE the minute in the
|
|
387
|
+
// genitive rather than juxtapose it behind a comma (two independent schedules).
|
|
388
|
+
// A second LIST the core enumerated from a step (`3/2`) is really a stride
|
|
389
|
+
// cadence and stays out. The single-second + single-minute pair folds into one
|
|
390
|
+
// coherent clock point and is excluded.
|
|
391
|
+
function secondsConfinesMinute(schedule: Schedule): boolean {
|
|
392
|
+
const {second, minute, hour} = schedule.shapes;
|
|
393
|
+
|
|
394
|
+
if (second === 'list') {
|
|
395
|
+
const values = singleValues(segmentsOf(schedule, 'second'));
|
|
396
|
+
|
|
397
|
+
if (values && arithmeticStep(values)) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const clockPoint = second === 'single' || second === 'range' ||
|
|
403
|
+
second === 'list';
|
|
404
|
+
|
|
405
|
+
return clockPoint && minute !== 'wildcard' && hour === 'wildcard' &&
|
|
406
|
+
!(second === 'single' && minute === 'single');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// The minute-confinement rendering for a compose-seconds plan, or null when the
|
|
410
|
+
// plan is not one. A STEPPED minute (cadence or clock-point second) leads with
|
|
411
|
+
// the second clause, a comma, then the minute's own standalone cardinal
|
|
412
|
+
// cadence; a CLOCK-POINT second over a LIST/RANGE/SINGLE minute uses the
|
|
413
|
+
// genitive form anchored to the confined minute. Both bind the second beneath
|
|
414
|
+
// the minute instead of juxtaposing the two behind a bare comma + "de chaque
|
|
415
|
+
// minute". Folded into one helper so `renderComposeSeconds` carries a single
|
|
416
|
+
// branch.
|
|
417
|
+
function minuteConfinementRender(
|
|
418
|
+
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
419
|
+
schedule: Schedule, opts: Opts
|
|
420
|
+
): string | null {
|
|
421
|
+
if (isSteppedMinuteSeconds(schedule, plan)) {
|
|
422
|
+
return steppedMinuteConfinement(schedule, plan,
|
|
423
|
+
secondsLeadClause(schedule, opts), opts);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const minuteRest = plan.rest.kind === 'minuteFrequency' ||
|
|
427
|
+
plan.rest.kind === 'multipleMinutes' ||
|
|
428
|
+
plan.rest.kind === 'rangeOfMinutes';
|
|
429
|
+
|
|
430
|
+
if (minuteRest && secondsConfinesMinute(schedule)) {
|
|
431
|
+
// A clock-point second over a STEPPED minute reuses the standalone cardinal
|
|
432
|
+
// cadence the same way; only a list/range/single minute keeps the genitive.
|
|
433
|
+
if (minuteStride(schedule) && schedule.pattern.minute !== '*/2') {
|
|
434
|
+
return steppedMinuteConfinement(schedule, plan,
|
|
435
|
+
secondsBareLead(schedule), opts);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return secondsBareLead(schedule) + ' ' +
|
|
439
|
+
confinedMinutePhrase(schedule) + trailingQualifier(schedule, opts);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
282
445
|
function renderComposeSeconds(
|
|
283
446
|
schedule: Schedule,
|
|
284
447
|
plan: Extract<PlanNode, {kind: 'composeSeconds'}>,
|
|
@@ -322,6 +485,21 @@ function renderComposeSeconds(
|
|
|
322
485
|
return dayFrame + ', ' + window + ', ' + cadence;
|
|
323
486
|
}
|
|
324
487
|
|
|
488
|
+
// A stepped minute under a wildcard/stepped second + wildcard hour confines
|
|
489
|
+
// the second cadence to the ordinal minute cadence ("chaque seconde à la
|
|
490
|
+
// sixième minute à partir de la minute 4 de chaque heure"), never the comma
|
|
491
|
+
// juxtaposition that reads as two independent cadences.
|
|
492
|
+
// A second confines the minute restriction (open hour), never the comma
|
|
493
|
+
// juxtaposition that reads as two independent cadences: a CADENCE second over
|
|
494
|
+
// a stepped minute uses the ordinal-cadence form ("chaque seconde à la
|
|
495
|
+
// sixième minute …"); a CLOCK-POINT second uses the genitive form anchored to
|
|
496
|
+
// the confined minute ("aux secondes 5, 10 et 15 de la sixième minute …").
|
|
497
|
+
const confined = minuteConfinementRender(plan, schedule, opts);
|
|
498
|
+
|
|
499
|
+
if (confined !== null) {
|
|
500
|
+
return confined;
|
|
501
|
+
}
|
|
502
|
+
|
|
325
503
|
// A wildcard second under a minute */2 with a wildcard hour juxtaposes two
|
|
326
504
|
// cadences that read as contradictory ("chaque seconde, toutes les deux
|
|
327
505
|
// minutes"). Bind them with the genitive "de" ("chaque seconde de chaque
|