cronli5 0.1.6 → 0.2.0
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 +86 -0
- package/README.md +6 -6
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +401 -81
- package/dist/cronli5.js +401 -81
- package/dist/lang/de.cjs +63 -17
- package/dist/lang/de.js +63 -17
- package/dist/lang/en.cjs +401 -81
- package/dist/lang/en.js +401 -81
- package/dist/lang/es.cjs +68 -5
- package/dist/lang/es.js +68 -5
- package/dist/lang/fi.cjs +21 -1
- package/dist/lang/fi.js +21 -1
- package/dist/lang/zh.cjs +38 -7
- package/dist/lang/zh.js +38 -7
- package/package.json +1 -1
- package/src/core/ir.ts +5 -0
- package/src/core/util.ts +52 -1
- package/src/lang/de/index.ts +95 -25
- package/src/lang/en/dialects.ts +6 -2
- package/src/lang/en/index.ts +781 -117
- package/src/lang/es/index.ts +85 -9
- package/src/lang/fi/index.ts +6 -2
- package/src/lang/zh/index.ts +44 -18
- package/types/core/ir.d.ts +1 -0
- package/types/core/util.d.ts +10 -1
package/src/lang/de/index.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
import {pad} from '../../core/format.js';
|
|
5
5
|
import {maxClockTimes, weekdayNumbers} from '../../core/specs.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
arithmeticStep, orderWeekdaysForDisplay, toFieldNumber
|
|
8
|
+
} from '../../core/util.js';
|
|
7
9
|
import type {Cronli5Options} from '../../types.js';
|
|
8
10
|
import type {
|
|
9
11
|
Field, HourTimesPlan, IR, Language, NormalizedOptions, PlanNode, Segment
|
|
@@ -51,6 +53,12 @@ function everyN(interval: number, unit: Unit): string {
|
|
|
51
53
|
return 'alle ' + interval + ' ' + unit.plural;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
// Append a scope anchor to a clause, separated by a space; an empty anchor
|
|
57
|
+
// (a context that names that field in its own clause) leaves the clause bare.
|
|
58
|
+
function withAnchor(clause: string, anchor: string): string {
|
|
59
|
+
return anchor ? clause + ' ' + anchor : clause;
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
// The first segment of a step field, which the plan guarantees is step-kinded.
|
|
55
63
|
function stepSegment(segments: Segment[] | null): StepSegment {
|
|
56
64
|
return (segments as Segment[])[0] as StepSegment;
|
|
@@ -108,8 +116,9 @@ function stepClause(segment: StepSegment, unit: Unit, anchor: string): string {
|
|
|
108
116
|
const short = start !== 0 && segment.fires.length <= 3;
|
|
109
117
|
|
|
110
118
|
if (segment.startToken.indexOf('-') !== -1 || short) {
|
|
111
|
-
return
|
|
112
|
-
' ' +
|
|
119
|
+
return withAnchor(
|
|
120
|
+
'in den ' + unit.plural + ' ' + joinList(segment.fires.map(String)),
|
|
121
|
+
anchor);
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
return renderStride({
|
|
@@ -208,7 +217,9 @@ function weekdayRange(bounds: [string, string]): string {
|
|
|
208
217
|
|
|
209
218
|
// "montags", "montags bis freitags", "montags, mittwochs und freitags".
|
|
210
219
|
function weekdayQualifier(ir: IR): string {
|
|
211
|
-
|
|
220
|
+
// Weekday lists display Monday-first (Sunday last); a lone range keeps its
|
|
221
|
+
// form. The IR stays canonical (Sunday=0). The helper flattens steps.
|
|
222
|
+
const segments = orderWeekdaysForDisplay(fieldSegments(ir, 'weekday'));
|
|
212
223
|
|
|
213
224
|
if (segments.length === 1 && segments[0].kind === 'range') {
|
|
214
225
|
return weekdayRange(segments[0].bounds);
|
|
@@ -460,10 +471,21 @@ function countedPhrase(
|
|
|
460
471
|
return 'in den ' + plural + ' ' + joinList(fieldValues(ir, field));
|
|
461
472
|
}
|
|
462
473
|
|
|
463
|
-
// The seconds clause: "
|
|
464
|
-
//
|
|
474
|
+
// The minute scope for a seconds clause: "jeder Minute" only when the minute
|
|
475
|
+
// is a wildcard (the seconds really do fire in every minute). A restricted
|
|
476
|
+
// minute (single/list/range/step) is named by its own clause, so the seconds
|
|
477
|
+
// clause drops the scope — "jeder Minute" would otherwise contradict the fixed
|
|
478
|
+
// minute ("in Sekunde 30 jeder Minute, in Minute 30" fires at second 30 of
|
|
479
|
+
// minute 30, not every minute).
|
|
480
|
+
function minuteAnchor(ir: IR): string {
|
|
481
|
+
return ir.pattern.minute === '*' ? 'jeder Minute' : '';
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// The seconds clause: "alle 30 Sekunden" for a step, "in Sekunde 15 jeder
|
|
485
|
+
// Minute" under a wildcard minute, else the bare "in Sekunde 15" when the
|
|
486
|
+
// minute is fixed (its own clause names it).
|
|
465
487
|
function secondsLead(ir: IR): string {
|
|
466
|
-
return secondsClause(ir,
|
|
488
|
+
return secondsClause(ir, minuteAnchor(ir));
|
|
467
489
|
}
|
|
468
490
|
|
|
469
491
|
// The second clause counted against an arbitrary anchor. The anchor is "jeder
|
|
@@ -486,7 +508,7 @@ function secondsClause(ir: IR, anchor: string): string {
|
|
|
486
508
|
}
|
|
487
509
|
|
|
488
510
|
return strideFromSegments(segments as Segment[], UNITS.second, anchor) ??
|
|
489
|
-
countedPhrase(ir, 'second', 'Sekunde', 'Sekunden')
|
|
511
|
+
withAnchor(countedPhrase(ir, 'second', 'Sekunde', 'Sekunden'), anchor);
|
|
490
512
|
}
|
|
491
513
|
|
|
492
514
|
// A clock time that always shows its minutes: "9:00", "9:30".
|
|
@@ -888,15 +910,22 @@ function renderMinuteFrequency(
|
|
|
888
910
|
const segment = stepSegment(ir.analyses.segments.minute);
|
|
889
911
|
const sep = opts.style.sep;
|
|
890
912
|
const clean = cleanStep(segment, 60);
|
|
891
|
-
const base = stepClause(segment, UNITS.minute, 'jeder Stunde');
|
|
892
913
|
|
|
893
914
|
if (plan.hours.kind === 'window') {
|
|
915
|
+
// A single fixed hour (from === to) drops the "jeder Stunde" tail — the
|
|
916
|
+
// window names that one hour, so "jeder Stunde" (every hour) contradicts
|
|
917
|
+
// it. A range keeps it: the cadence truly repeats across each hour.
|
|
918
|
+
const singleHour = plan.hours.from === plan.hours.to;
|
|
919
|
+
const base = stepClause(segment, UNITS.minute,
|
|
920
|
+
singleHour ? '' : 'jeder Stunde');
|
|
894
921
|
const window = hourWindow(plan.hours.from, plan.hours.to, plan.hours.last,
|
|
895
922
|
sep);
|
|
896
923
|
|
|
897
924
|
return clean ? base + ' ' + window : base + ', ' + window;
|
|
898
925
|
}
|
|
899
926
|
|
|
927
|
+
const base = stepClause(segment, UNITS.minute, 'jeder Stunde');
|
|
928
|
+
|
|
900
929
|
if (plan.hours.kind === 'during') {
|
|
901
930
|
// A bounded or uneven hour stride confines the minute cadence to its own
|
|
902
931
|
// endpoint-pinning hour cadence ("alle 15 Minuten, alle 5 Stunden von 0 bis
|
|
@@ -918,9 +947,13 @@ function renderMinuteFrequency(
|
|
|
918
947
|
return base;
|
|
919
948
|
}
|
|
920
949
|
|
|
921
|
-
// A stepped hour field as a phrase:
|
|
922
|
-
// cadence
|
|
950
|
+
// A stepped hour field as a phrase: a clean stride from midnight is the bare
|
|
951
|
+
// cadence ("alle 2 Stunden"); an open offset-clean stride names only its start
|
|
952
|
+
// ("alle 2 Stunden ab 1 Uhr") since it wraps the day with no distinct
|
|
953
|
+
// endpoint; a bounded or uneven stride pins both ends ("alle 2 Stunden von 9
|
|
923
954
|
// bis 17 Uhr"). Shared by the bare hour step and the minute-step compositions.
|
|
955
|
+
// An explicitly bounded step (`a-b/n`) keeps its enumerated hours, matching
|
|
956
|
+
// en/fi/zh; only an OPEN step (`m/n`) reads as the wrapping cadence.
|
|
924
957
|
function hourStepPhrase(ir: IR): string {
|
|
925
958
|
const cadence = unevenHourCadence(ir);
|
|
926
959
|
|
|
@@ -930,9 +963,34 @@ function hourStepPhrase(ir: IR): string {
|
|
|
930
963
|
|
|
931
964
|
const segment = stepSegment(ir.analyses.segments.hour);
|
|
932
965
|
|
|
933
|
-
|
|
934
|
-
everyN(segment.interval, UNITS.hour)
|
|
935
|
-
|
|
966
|
+
if (cleanStep(segment, 24)) {
|
|
967
|
+
return everyN(segment.interval, UNITS.hour);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// An open offset-clean step (`m/n`, m < n dividing 24) wraps the day with no
|
|
971
|
+
// endpoint: name only its start, the cadence en/fi/zh and the compose paths
|
|
972
|
+
// already speak — never the enumerated hour list. A bounded `a-b/n` keeps its
|
|
973
|
+
// explicit hours.
|
|
974
|
+
const stride = openOffsetCleanStride(ir, segment);
|
|
975
|
+
|
|
976
|
+
return stride ? hourStrideCadence(stride) : atHours(segment.fires);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// The stride of an OPEN offset-clean hour step (`m/n`, m < n dividing 24),
|
|
980
|
+
// or null for any other step: such a step wraps the day with no endpoint and
|
|
981
|
+
// reads as the "alle N Stunden ab M Uhr" cadence. An explicitly bounded step
|
|
982
|
+
// (`a-b/n`, startToken carries a `-`) is excluded so it keeps its enumerated
|
|
983
|
+
// hours, matching en/fi/zh.
|
|
984
|
+
function openOffsetCleanStride(
|
|
985
|
+
ir: IR, segment: StepSegment
|
|
986
|
+
): {start: number; interval: number; last: number} | null {
|
|
987
|
+
if (segment.startToken.indexOf('-') !== -1) {
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const stride = hourStride(ir);
|
|
992
|
+
|
|
993
|
+
return stride && offsetCleanStride(stride) ? stride : null;
|
|
936
994
|
}
|
|
937
995
|
|
|
938
996
|
// --- Hour-step cadence (the 24-cycle analog of renderStride). ---
|
|
@@ -1078,7 +1136,7 @@ function subMinuteSecond(ir: IR): boolean {
|
|
|
1078
1136
|
function hourCadenceLead(ir: IR, minute: number): string {
|
|
1079
1137
|
if (minute === 0) {
|
|
1080
1138
|
if (subMinuteSecond(ir)) {
|
|
1081
|
-
return secondsClause(ir,
|
|
1139
|
+
return withAnchor(secondsClause(ir, minuteAnchor(ir)), 'für eine Minute');
|
|
1082
1140
|
}
|
|
1083
1141
|
|
|
1084
1142
|
return secondsClause(ir, 'jeder Stunde');
|
|
@@ -1092,7 +1150,7 @@ function hourCadenceLead(ir: IR, minute: number): string {
|
|
|
1092
1150
|
return minutePhrase;
|
|
1093
1151
|
}
|
|
1094
1152
|
|
|
1095
|
-
return secondsClause(ir,
|
|
1153
|
+
return secondsClause(ir, minuteAnchor(ir)) + ', ' + minutePhrase;
|
|
1096
1154
|
}
|
|
1097
1155
|
|
|
1098
1156
|
// Render an hour step (or arithmetic-progression hour list) under a single
|
|
@@ -1134,8 +1192,8 @@ function hourCadence(ir: IR, minute: number): string | null {
|
|
|
1134
1192
|
confinedHourStride(segment);
|
|
1135
1193
|
|
|
1136
1194
|
if (confined) {
|
|
1137
|
-
return secondsClause(ir,
|
|
1138
|
-
everyNthHour(segment);
|
|
1195
|
+
return withAnchor(secondsClause(ir, minuteAnchor(ir)), 'für eine Minute') +
|
|
1196
|
+
' ' + everyNthHour(segment);
|
|
1139
1197
|
}
|
|
1140
1198
|
|
|
1141
1199
|
// A plain top-of-the-hour fire (minute 0 with no meaningful second) has no
|
|
@@ -1208,11 +1266,14 @@ function renderHourRange(
|
|
|
1208
1266
|
plan: Extract<PlanNode, {kind: 'hourRange'}>,
|
|
1209
1267
|
opts: Opts
|
|
1210
1268
|
): string {
|
|
1211
|
-
//
|
|
1212
|
-
//
|
|
1213
|
-
//
|
|
1214
|
-
|
|
1215
|
-
|
|
1269
|
+
// The close lands on the top of the final hour (minute 0) unless the minute
|
|
1270
|
+
// genuinely runs to the end of that hour — i.e. a wildcard minute, which
|
|
1271
|
+
// fills every minute and states no separate clause. A pinned/listed/ranged
|
|
1272
|
+
// minute is named in its own lead clause, so folding it into the close too
|
|
1273
|
+
// would read as a span ("bis 17:05 Uhr") that contradicts the minute clause;
|
|
1274
|
+
// the window stays bare ("bis 17 Uhr").
|
|
1275
|
+
const last = plan.minuteForm === 'wildcard' ? plan.boundMinute ?? 0 : 0;
|
|
1276
|
+
const window = hourWindow(plan.from, plan.to, last, opts.style.sep);
|
|
1216
1277
|
|
|
1217
1278
|
if (plan.minuteForm === 'wildcard') {
|
|
1218
1279
|
return 'jede Minute ' + window;
|
|
@@ -1343,8 +1404,17 @@ function needsDailyFrame(ir: IR): boolean {
|
|
|
1343
1404
|
return true;
|
|
1344
1405
|
}
|
|
1345
1406
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1407
|
+
if (ir.plan.kind !== 'hourStep') {
|
|
1408
|
+
return false;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// An hour step rendered as a cadence ("alle N Stunden [ab M Uhr]") is a
|
|
1412
|
+
// frequency, not a daily clock-time list, so it takes no "täglich" frame —
|
|
1413
|
+
// only a bounded `a-b/n` step that enumerates its hours ("um 1, 3, … Uhr")
|
|
1414
|
+
// needs the recurring frame.
|
|
1415
|
+
const segment = stepSegment(ir.analyses.segments.hour);
|
|
1416
|
+
|
|
1417
|
+
return !cleanStep(segment, 24) && !openOffsetCleanStride(ir, segment);
|
|
1348
1418
|
}
|
|
1349
1419
|
|
|
1350
1420
|
function render(ir: IR, plan: PlanNode, opts: Opts): string {
|
package/src/lang/en/dialects.ts
CHANGED
|
@@ -34,7 +34,8 @@ const dialects: {[name: string]: DialectStyle} = {
|
|
|
34
34
|
pm: 'p.m.',
|
|
35
35
|
sep: ':',
|
|
36
36
|
serialComma: true,
|
|
37
|
-
through: ' through '
|
|
37
|
+
through: ' through ',
|
|
38
|
+
untilWindow: true
|
|
38
39
|
},
|
|
39
40
|
house: {
|
|
40
41
|
am: 'AM',
|
|
@@ -58,7 +59,10 @@ function resolveDialect(
|
|
|
58
59
|
dialect?: Cronli5Options['dialect']
|
|
59
60
|
): DialectStyle {
|
|
60
61
|
if (typeof dialect === 'object' && dialect !== null) {
|
|
61
|
-
|
|
62
|
+
// A custom style inherits the US base but NOT the until-window: a custom
|
|
63
|
+
// dialect that only overrides the connective (e.g. `{through: ' until '}`)
|
|
64
|
+
// keeps the "through <last fire>" close, just spelled with its own word.
|
|
65
|
+
return {...dialects.us, untilWindow: false, ...dialect};
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
// The legacy 'uk' name resolves to 'gb'; a name another language owns
|