cronli5 0.7.2 → 0.8.2

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.
@@ -327,6 +327,16 @@ function renderMinutePast(schedule: Schedule): string {
327
327
  return minuteHourClause(schedule);
328
328
  }
329
329
 
330
+ // Strip the generic "每小时" (every-hour) anchor that leads a minute clause.
331
+ // Under an hour STEP the hour cadence is the sole hour authority, so the minute
332
+ // clause must not also assert "每小时" — alongside a stepped hour ("每4小时…每小
333
+ // 时…") it reads as a conflicting every-hour scope. An hour WINDOW and an
334
+ // unrestricted hour keep "每小时" (the window already names the hours; an open
335
+ // hour has no other hour statement).
336
+ function withoutHourAnchor(clause: string): string {
337
+ return clause.replace(/^每小时/, '');
338
+ }
339
+
330
340
  // One hour segment as clock words by its form: a range is a span ("9点至20点"),
331
341
  // a single is one clock word ("22点"), a step keeps its fires enumerated as
332
342
  // clock words ("9点、11点、13点"). A range stated as a list element should read
@@ -383,7 +393,14 @@ function renderMinuteFrequency(schedule: Schedule, plan: PlanNode): string {
383
393
  const hourCad = unevenHourCadence(schedule);
384
394
 
385
395
  if (hourCad !== null) {
386
- return hourCad + (hourCad.indexOf('至') === -1 ? '' : ',') + base;
396
+ // An hour STEP is the sole hour authority, so an offset minute cadence
397
+ // drops its leading "每小时" ("每4小时从5分起每10分钟"); a discrete hour
398
+ // list (during) keeps it. Only the step path reaches a non-null cadence
399
+ // here — an irregular list falls through to the enumerated frame below.
400
+ const minuteBase = hours.kind === 'step' ?
401
+ withoutHourAnchor(base) : base;
402
+
403
+ return hourCad + (hourCad.indexOf('至') === -1 ? '' : ',') + minuteBase;
387
404
  }
388
405
  }
389
406
 
@@ -445,15 +462,17 @@ function renderMinuteSpanAcrossHourStep(
445
462
  const {form} = plan as Extract<PlanNode, {kind: 'minuteSpanAcrossHourStep'}>;
446
463
 
447
464
  // A minute list reads as the hour cadence plus the minute list ("每2小时,
448
- // 每小时0、25、50分"; offset "从1点起每2小时,每小时5分和30分"), the same compaction
449
- // the wildcard/range minute already uses, rather than the enumerated hours.
465
+ // 0、25、50分"; offset "从1点起每2小时,5分和30分"), the same compaction the
466
+ // wildcard/range minute already uses, rather than the enumerated hours. The
467
+ // hour cadence scopes the hours, so the minute clause drops its "每小时".
450
468
  if (form === 'list') {
451
- return hourCadencePhrase(schedule) + ',' + renderMinutePast(schedule);
469
+ return hourCadencePhrase(schedule) + ',' +
470
+ withoutHourAnchor(renderMinutePast(schedule));
452
471
  }
453
472
 
454
473
  const minuteTail = form === 'wildcard' ?
455
474
  '每分钟' :
456
- minuteHourClause(schedule) + ',每分钟';
475
+ withoutHourAnchor(minuteHourClause(schedule)) + ',每分钟';
457
476
 
458
477
  // An offset or non-tiling stride (2/6 fires at 2,8,14,20) reads as its
459
478
  // cadence ("从2点起每6小时"). A wildcard minute hangs off it with a comma; a
@@ -518,9 +537,13 @@ function renderCompactClockTimes(schedule: Schedule, plan: PlanNode): string {
518
537
  if (!compact.fold) {
519
538
  const hourCad = unevenHourCadence(schedule);
520
539
 
540
+ // A bounded/uneven hour step leads as the cadence and is the sole hour
541
+ // authority, so the minute clause drops its generic "每小时" every-hour
542
+ // scope; an enumerated hour list (hourCad null) names specific hours and
543
+ // keeps the anchor.
521
544
  return hourCad === null ?
522
545
  minuteHourClause(schedule) + ',在' + hourList(schedule) + tail :
523
- hourCad + ',' + minuteHourClause(schedule) + tail;
546
+ hourCad + ',' + withoutHourAnchor(minuteHourClause(schedule)) + tail;
524
547
  }
525
548
 
526
549
  // A single pinned minute past 0 leads with its clause; a pinned 0 folds into
@@ -870,6 +893,15 @@ function composeSecondsOnHour(
870
893
  return composeMinuteZeroClocks(schedule, sec);
871
894
  }
872
895
 
896
+ // A single fixed (non-zero) minute under enumerated clock times fuses the
897
+ // seconds onto the composed clock time the same way ("0点2分的每一秒").
898
+ const fusedSingleMinute =
899
+ composeSingleMinuteClocks(schedule, rest, sec, opts);
900
+
901
+ if (fusedSingleMinute !== null) {
902
+ return fusedSingleMinute;
903
+ }
904
+
873
905
  const restText = render(schedule, rest, opts);
874
906
  const secTail = clockRestCarriesSecond(rest) ? '' : sec;
875
907
 
@@ -886,6 +918,29 @@ function composeSecondsOnHour(
886
918
  return restText + secTail;
887
919
  }
888
920
 
921
+ // A single fixed (non-zero) minute under enumerated clock times: each clock
922
+ // point already names the minute ("0点2分", "9点5分和17点5分"), so bind the
923
+ // seconds to it with "的" — the same fusion the minute-0 ("0分的每一秒") and
924
+ // minute-step ("5、20…分的每一秒") cases use — rather than leaving a bare
925
+ // trailing "每秒" that floats as a second, unlinked adverbial. A single second
926
+ // already folded into each clock time ("9点5分30秒") is not re-appended. The
927
+ // compactClockTimes window form states its minute separately ("每小时5分") and
928
+ // keeps its own seconds clause, so it does not qualify (returns null). minute 0
929
+ // is handled by composeMinuteZeroClocks before this point.
930
+ function composeSingleMinuteClocks(
931
+ schedule: Schedule, rest: PlanNode, sec: string, opts: Opts
932
+ ): string | null {
933
+ if (rest.kind !== 'clockTimes' || schedule.shapes.minute !== 'single' ||
934
+ clockRestCarriesSecond(rest)) {
935
+ return null;
936
+ }
937
+
938
+ const core =
939
+ render(schedule, rest, opts) + minuteZeroSecondTail(schedule, sec);
940
+
941
+ return isDaily(schedule) ? '每天' + core : core;
942
+ }
943
+
889
944
  // A minute pinned to 0 under specific clock hours (not a compacted cadence): a
890
945
  // bare clock word ("9点") would hide the :00 and leave the second dangling
891
946
  // ("…9点每秒"), reading as the whole hour. Fuse the seconds with the explicit
@@ -907,14 +962,24 @@ function composeMinuteZeroClocks(schedule: Schedule, sec: string): string {
907
962
  // midnight (凌晨0点) and other hours still need it to pin the minute.
908
963
  return hour === 12 ? '正午' : hourWord(hour) + '0分';
909
964
  });
910
- // A pinned minute makes the seconds' own "每分钟" anchor misleading (it is a
911
- // single minute, not every minute), so the stride here drops it.
965
+ const core = joinAnd(clocks) + minuteZeroSecondTail(schedule, sec);
966
+
967
+ return isDaily(schedule) ? '每天' + core : core;
968
+ }
969
+
970
+ // The "的"-fused second tail for a clock time that already names a single pinned
971
+ // minute ("…的每一秒" for a wildcard second, else "…的" + the second's clause).
972
+ // A pinned minute makes the seconds' own "每分钟" anchor misleading (it is a
973
+ // single minute, not every minute), so a stride here drops it.
974
+ function minuteZeroSecondTail(schedule: Schedule, sec: string): string {
975
+ if (sec === '每秒') {
976
+ return '的每一秒';
977
+ }
978
+
912
979
  const nested =
913
980
  strideFromSegments(segmentsOf(schedule, 'second'), '秒', '秒', '');
914
- const tail = sec === '每秒' ? '的每一秒' : '的' + (nested ?? sec);
915
- const core = joinAnd(clocks) + tail;
916
981
 
917
- return isDaily(schedule) ? '每天' + core : core;
982
+ return '' + (nested ?? sec);
918
983
  }
919
984
 
920
985
  // Whether the hour field is a range — or a list whose segments include a
@@ -1010,7 +1075,10 @@ function composeSecondsListed(schedule: Schedule): string {
1010
1075
  const hourCad = unevenHourCadence(schedule);
1011
1076
 
1012
1077
  if (hourCad !== null) {
1013
- return hourCad + ',' + minutes + ',' + sec;
1078
+ // An hour STEP cadence is the sole hour authority, so the minute clause
1079
+ // drops its "每小时" ("每2小时,0至30分,每秒"); a discrete hour list keeps it
1080
+ // (it falls through to the hourFrame branch below with a null cadence).
1081
+ return hourCad + ',' + withoutHourAnchor(minutes) + ',' + sec;
1014
1082
  }
1015
1083
 
1016
1084
  return hourFrame(schedule) + minutes + ',' + sec;
@@ -1503,6 +1571,7 @@ function normalizeOptions(options?: Cronli5Options): Opts {
1503
1571
  return {
1504
1572
  ampm: typeof options.ampm === 'boolean' ? options.ampm : false,
1505
1573
  lenient: !!options.lenient,
1574
+ quartz: !!options.quartz,
1506
1575
  seconds: !!options.seconds,
1507
1576
  short: !!options.short,
1508
1577
  style,
package/src/types.ts CHANGED
@@ -96,6 +96,20 @@ export interface Cronli5Options {
96
96
  */
97
97
  lenient?: boolean;
98
98
 
99
+ /**
100
+ * Read the pattern with Quartz semantics. Quartz numbers the day-of-week
101
+ * **1 = Sunday, 2 = Monday, … 7 = Saturday** (standard cron uses
102
+ * 0/7 = Sunday, 1 = Monday), and **requires** exactly one of day-of-month or
103
+ * day-of-week to be `?` ("no specific value"). Off by default; with it off,
104
+ * `?` is rejected (rather than silently mis-read as standard cron), since a
105
+ * `?` is the unambiguous mark of a Quartz pattern. The Quartz numbering also
106
+ * applies inside the day-of-week operators (`6L`, `2#2`) and the weekday `L`
107
+ * alias (Saturday). Day names (`MON`, `SUN`) and the day-of-month, month,
108
+ * hour, and minute fields are unaffected. Composable with `seconds`/`years`.
109
+ * Defaults to `false`.
110
+ */
111
+ quartz?: boolean;
112
+
99
113
  /**
100
114
  * Return a complete standalone sentence (`'Runs every day at midnight.'`)
101
115
  * instead of the embeddable fragment (`'every day at midnight'`). Each
@@ -122,6 +136,36 @@ export interface Cronli5Options {
122
136
  years?: boolean;
123
137
  }
124
138
 
139
+ /**
140
+ * The callable default export: a function that turns a cron pattern into a
141
+ * description, carrying two named convenience methods that are sugar over the
142
+ * `sentence` option.
143
+ *
144
+ * There is deliberately **no** `toString` method: it would shadow
145
+ * `Function.prototype.toString`, which the runtime calls arg-less for
146
+ * `String(cronli5)`, template-literal coercion, and `console`/debug output.
147
+ * The named methods avoid that collision.
148
+ */
149
+ export interface Cronli5 {
150
+
151
+ /** Describe a cron pattern (lowercase embeddable fragment by default). */
152
+ (cronPattern: CronPattern, options?: Cronli5Options): string;
153
+
154
+ /**
155
+ * Describe a cron pattern as a capitalized standalone sentence
156
+ * (`'Runs every day at midnight.'`). Sugar for
157
+ * `{...options, sentence: true}`.
158
+ */
159
+ sentence(cronPattern: CronPattern, options?: Cronli5Options): string;
160
+
161
+ /**
162
+ * Describe a cron pattern as a lowercase embeddable fragment
163
+ * (`'every day at midnight'`) — the default form. Sugar for
164
+ * `{...options, sentence: false}`.
165
+ */
166
+ fragment(cronPattern: CronPattern, options?: Cronli5Options): string;
167
+ }
168
+
125
169
  /**
126
170
  * Object form of a cron pattern. Fields may be strings or numbers; at least
127
171
  * one of `second`, `minute`, or `hour` is required.
@@ -0,0 +1,4 @@
1
+ import type { CronLike } from './specs.js';
2
+ declare const quartzTokenMessage = "`?` is a Quartz token \u2014 pass { quartz: true } to enable Quartz semantics.";
3
+ declare function applyQuartz(cronPattern: CronLike, quartz: boolean): void;
4
+ export { applyQuartz, quartzTokenMessage };
@@ -160,6 +160,7 @@ export interface DialectStyle {
160
160
  export interface NormalizedOptions<Style = DialectStyle> {
161
161
  ampm: boolean;
162
162
  lenient: boolean;
163
+ quartz: boolean;
163
164
  seconds: boolean;
164
165
  short: boolean;
165
166
  style: Style;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @license MIT, Copyright (c) 2026 Andrew Brož
3
3
  */
4
- import type { CronPattern, Cronli5Options } from './types.js';
5
- declare function cronli5(cronPattern: CronPattern, options?: Cronli5Options): string;
6
- export default cronli5;
7
- export type { Cronli5Dialect, Cronli5Language, Cronli5Options, CronPattern, CronPatternObject } from './types.js';
4
+ import type { Cronli5 } from './types.js';
5
+ declare const callable: Cronli5;
6
+ export default callable;
7
+ export type { Cronli5, Cronli5Dialect, Cronli5Language, Cronli5Options, CronPattern, CronPatternObject } from './types.js';
package/types/types.d.ts CHANGED
@@ -71,6 +71,19 @@ export interface Cronli5Options {
71
71
  * Defaults to `false`.
72
72
  */
73
73
  lenient?: boolean;
74
+ /**
75
+ * Read the pattern with Quartz semantics. Quartz numbers the day-of-week
76
+ * **1 = Sunday, 2 = Monday, … 7 = Saturday** (standard cron uses
77
+ * 0/7 = Sunday, 1 = Monday), and **requires** exactly one of day-of-month or
78
+ * day-of-week to be `?` ("no specific value"). Off by default; with it off,
79
+ * `?` is rejected (rather than silently mis-read as standard cron), since a
80
+ * `?` is the unambiguous mark of a Quartz pattern. The Quartz numbering also
81
+ * applies inside the day-of-week operators (`6L`, `2#2`) and the weekday `L`
82
+ * alias (Saturday). Day names (`MON`, `SUN`) and the day-of-month, month,
83
+ * hour, and minute fields are unaffected. Composable with `seconds`/`years`.
84
+ * Defaults to `false`.
85
+ */
86
+ quartz?: boolean;
74
87
  /**
75
88
  * Return a complete standalone sentence (`'Runs every day at midnight.'`)
76
89
  * instead of the embeddable fragment (`'every day at midnight'`). Each
@@ -93,6 +106,32 @@ export interface Cronli5Options {
93
106
  */
94
107
  years?: boolean;
95
108
  }
109
+ /**
110
+ * The callable default export: a function that turns a cron pattern into a
111
+ * description, carrying two named convenience methods that are sugar over the
112
+ * `sentence` option.
113
+ *
114
+ * There is deliberately **no** `toString` method: it would shadow
115
+ * `Function.prototype.toString`, which the runtime calls arg-less for
116
+ * `String(cronli5)`, template-literal coercion, and `console`/debug output.
117
+ * The named methods avoid that collision.
118
+ */
119
+ export interface Cronli5 {
120
+ /** Describe a cron pattern (lowercase embeddable fragment by default). */
121
+ (cronPattern: CronPattern, options?: Cronli5Options): string;
122
+ /**
123
+ * Describe a cron pattern as a capitalized standalone sentence
124
+ * (`'Runs every day at midnight.'`). Sugar for
125
+ * `{...options, sentence: true}`.
126
+ */
127
+ sentence(cronPattern: CronPattern, options?: Cronli5Options): string;
128
+ /**
129
+ * Describe a cron pattern as a lowercase embeddable fragment
130
+ * (`'every day at midnight'`) — the default form. Sugar for
131
+ * `{...options, sentence: false}`.
132
+ */
133
+ fragment(cronPattern: CronPattern, options?: Cronli5Options): string;
134
+ }
96
135
  /**
97
136
  * Object form of a cron pattern. Fields may be strings or numbers; at least
98
137
  * one of `second`, `minute`, or `hour` is required.