ts-time-utils 1.0.0 → 2.0.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.
Files changed (102) hide show
  1. package/README.md +682 -11
  2. package/dist/calculate.d.ts +7 -2
  3. package/dist/calculate.d.ts.map +1 -1
  4. package/dist/calculate.js +37 -13
  5. package/dist/calendar.d.ts +103 -0
  6. package/dist/calendar.d.ts.map +1 -1
  7. package/dist/calendar.js +224 -0
  8. package/dist/compare.d.ts +217 -0
  9. package/dist/compare.d.ts.map +1 -0
  10. package/dist/compare.js +417 -0
  11. package/dist/countdown.d.ts +217 -0
  12. package/dist/countdown.d.ts.map +1 -0
  13. package/dist/countdown.js +298 -0
  14. package/dist/cron.d.ts +82 -0
  15. package/dist/cron.d.ts.map +1 -0
  16. package/dist/cron.js +294 -0
  17. package/dist/dateRange.d.ts +266 -0
  18. package/dist/dateRange.d.ts.map +1 -0
  19. package/dist/dateRange.js +433 -0
  20. package/dist/esm/calculate.d.ts +7 -2
  21. package/dist/esm/calculate.d.ts.map +1 -1
  22. package/dist/esm/calculate.js +37 -13
  23. package/dist/esm/calendar.d.ts +103 -0
  24. package/dist/esm/calendar.d.ts.map +1 -1
  25. package/dist/esm/calendar.js +224 -0
  26. package/dist/esm/compare.d.ts +217 -0
  27. package/dist/esm/compare.d.ts.map +1 -0
  28. package/dist/esm/compare.js +417 -0
  29. package/dist/esm/countdown.d.ts +217 -0
  30. package/dist/esm/countdown.d.ts.map +1 -0
  31. package/dist/esm/countdown.js +298 -0
  32. package/dist/esm/cron.d.ts +82 -0
  33. package/dist/esm/cron.d.ts.map +1 -0
  34. package/dist/esm/cron.js +294 -0
  35. package/dist/esm/dateRange.d.ts +266 -0
  36. package/dist/esm/dateRange.d.ts.map +1 -0
  37. package/dist/esm/dateRange.js +433 -0
  38. package/dist/esm/fiscal.d.ts +195 -0
  39. package/dist/esm/fiscal.d.ts.map +1 -0
  40. package/dist/esm/fiscal.js +295 -0
  41. package/dist/esm/format.d.ts +65 -0
  42. package/dist/esm/format.d.ts.map +1 -1
  43. package/dist/esm/format.js +202 -0
  44. package/dist/esm/index.d.ts +18 -7
  45. package/dist/esm/index.d.ts.map +1 -1
  46. package/dist/esm/index.js +22 -6
  47. package/dist/esm/iterate.d.ts +212 -0
  48. package/dist/esm/iterate.d.ts.map +1 -0
  49. package/dist/esm/iterate.js +409 -0
  50. package/dist/esm/naturalLanguage.d.ts +107 -0
  51. package/dist/esm/naturalLanguage.d.ts.map +1 -0
  52. package/dist/esm/naturalLanguage.js +344 -0
  53. package/dist/esm/parse.d.ts +45 -0
  54. package/dist/esm/parse.d.ts.map +1 -1
  55. package/dist/esm/parse.js +207 -0
  56. package/dist/esm/recurrence.d.ts +149 -0
  57. package/dist/esm/recurrence.d.ts.map +1 -0
  58. package/dist/esm/recurrence.js +404 -0
  59. package/dist/esm/timezone.d.ts +52 -0
  60. package/dist/esm/timezone.d.ts.map +1 -1
  61. package/dist/esm/timezone.js +171 -0
  62. package/dist/esm/types.d.ts +21 -0
  63. package/dist/esm/types.d.ts.map +1 -1
  64. package/dist/esm/validate.d.ts +51 -0
  65. package/dist/esm/validate.d.ts.map +1 -1
  66. package/dist/esm/validate.js +92 -0
  67. package/dist/esm/workingHours.d.ts +70 -0
  68. package/dist/esm/workingHours.d.ts.map +1 -1
  69. package/dist/esm/workingHours.js +161 -0
  70. package/dist/fiscal.d.ts +195 -0
  71. package/dist/fiscal.d.ts.map +1 -0
  72. package/dist/fiscal.js +295 -0
  73. package/dist/format.d.ts +65 -0
  74. package/dist/format.d.ts.map +1 -1
  75. package/dist/format.js +202 -0
  76. package/dist/index.d.ts +18 -7
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +22 -6
  79. package/dist/iterate.d.ts +212 -0
  80. package/dist/iterate.d.ts.map +1 -0
  81. package/dist/iterate.js +409 -0
  82. package/dist/naturalLanguage.d.ts +107 -0
  83. package/dist/naturalLanguage.d.ts.map +1 -0
  84. package/dist/naturalLanguage.js +344 -0
  85. package/dist/parse.d.ts +45 -0
  86. package/dist/parse.d.ts.map +1 -1
  87. package/dist/parse.js +207 -0
  88. package/dist/recurrence.d.ts +149 -0
  89. package/dist/recurrence.d.ts.map +1 -0
  90. package/dist/recurrence.js +404 -0
  91. package/dist/timezone.d.ts +52 -0
  92. package/dist/timezone.d.ts.map +1 -1
  93. package/dist/timezone.js +171 -0
  94. package/dist/types.d.ts +21 -0
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/validate.d.ts +51 -0
  97. package/dist/validate.d.ts.map +1 -1
  98. package/dist/validate.js +92 -0
  99. package/dist/workingHours.d.ts +70 -0
  100. package/dist/workingHours.d.ts.map +1 -1
  101. package/dist/workingHours.js +161 -0
  102. package/package.json +59 -12
@@ -0,0 +1,298 @@
1
+ /**
2
+ * @fileoverview Countdown and timer utilities for tracking time until/since a target date
3
+ * Provides countdown timers, remaining time calculations, and progress tracking
4
+ */
5
+ /**
6
+ * Creates a countdown timer to a target date
7
+ * @param targetDate - The date to count down to
8
+ * @param options - Countdown options and callbacks
9
+ * @returns A countdown instance with control methods
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const countdown = createCountdown(
14
+ * new Date('2024-12-31T23:59:59'),
15
+ * {
16
+ * onTick: (remaining) => {
17
+ * console.log(`${remaining.days}d ${remaining.hours}h ${remaining.minutes}m ${remaining.seconds}s`);
18
+ * },
19
+ * onComplete: () => {
20
+ * console.log('Happy New Year!');
21
+ * }
22
+ * }
23
+ * );
24
+ *
25
+ * countdown.start();
26
+ * // Later...
27
+ * countdown.stop();
28
+ * ```
29
+ */
30
+ export function createCountdown(targetDate, options = {}) {
31
+ let target = new Date(targetDate);
32
+ let intervalId = null;
33
+ let running = false;
34
+ const { onTick, onComplete, onExpired, interval = 1000, immediate = true } = options;
35
+ const getRemaining = () => {
36
+ return getRemainingTime(target);
37
+ };
38
+ const tick = () => {
39
+ const remaining = getRemaining();
40
+ if (onTick) {
41
+ onTick(remaining);
42
+ }
43
+ if (remaining.isExpired) {
44
+ stop();
45
+ if (onComplete) {
46
+ onComplete();
47
+ }
48
+ }
49
+ };
50
+ const start = () => {
51
+ if (running)
52
+ return;
53
+ const remaining = getRemaining();
54
+ if (remaining.totalMilliseconds < 0) {
55
+ if (onExpired) {
56
+ onExpired();
57
+ }
58
+ return;
59
+ }
60
+ running = true;
61
+ if (immediate) {
62
+ tick();
63
+ }
64
+ intervalId = setInterval(tick, interval);
65
+ };
66
+ const stop = () => {
67
+ if (!running)
68
+ return;
69
+ running = false;
70
+ if (intervalId !== null) {
71
+ clearInterval(intervalId);
72
+ intervalId = null;
73
+ }
74
+ };
75
+ const reset = (newTarget) => {
76
+ stop();
77
+ target = new Date(newTarget);
78
+ };
79
+ const isRunning = () => running;
80
+ const isExpiredCheck = () => {
81
+ return getRemaining().isExpired;
82
+ };
83
+ return {
84
+ start,
85
+ stop,
86
+ reset,
87
+ getRemaining,
88
+ isRunning,
89
+ isExpired: isExpiredCheck
90
+ };
91
+ }
92
+ /**
93
+ * Gets the remaining time until/since a target date
94
+ * @param targetDate - The target date
95
+ * @param fromDate - The date to calculate from (defaults to now)
96
+ * @returns Object with remaining time broken down by units
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * const remaining = getRemainingTime(new Date('2024-12-31'));
101
+ * console.log(`${remaining.days} days, ${remaining.hours} hours remaining`);
102
+ *
103
+ * // Check if expired
104
+ * if (remaining.isExpired) {
105
+ * console.log('Target date has passed');
106
+ * }
107
+ * ```
108
+ */
109
+ export function getRemainingTime(targetDate, fromDate = new Date()) {
110
+ const target = new Date(targetDate);
111
+ const from = new Date(fromDate);
112
+ const totalMilliseconds = target.getTime() - from.getTime();
113
+ const isExpired = totalMilliseconds <= 0;
114
+ // Use absolute values for calculations
115
+ const absTotalMs = Math.abs(totalMilliseconds);
116
+ const totalSeconds = Math.floor(absTotalMs / 1000);
117
+ const totalMinutes = Math.floor(totalSeconds / 60);
118
+ const totalHours = Math.floor(totalMinutes / 60);
119
+ const totalDays = Math.floor(totalHours / 24);
120
+ const milliseconds = Math.floor(absTotalMs % 1000);
121
+ const seconds = totalSeconds % 60;
122
+ const minutes = totalMinutes % 60;
123
+ const hours = totalHours % 24;
124
+ const days = totalDays; // Don't mod by 7 - let the formatter decide
125
+ const weeks = Math.floor(totalDays / 7);
126
+ return {
127
+ totalMilliseconds,
128
+ totalSeconds: totalMilliseconds >= 0 ? totalSeconds : -totalSeconds,
129
+ totalMinutes: totalMilliseconds >= 0 ? totalMinutes : -totalMinutes,
130
+ totalHours: totalMilliseconds >= 0 ? totalHours : -totalHours,
131
+ totalDays: totalMilliseconds >= 0 ? totalDays : -totalDays,
132
+ milliseconds,
133
+ seconds,
134
+ minutes,
135
+ hours,
136
+ days,
137
+ weeks,
138
+ isExpired
139
+ };
140
+ }
141
+ /**
142
+ * Formats the remaining time as a human-readable string
143
+ * @param targetDate - The target date
144
+ * @param options - Formatting options
145
+ * @returns Formatted countdown string
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * formatCountdown(new Date('2024-12-31'));
150
+ * // "45d 12h 30m 15s"
151
+ *
152
+ * formatCountdown(new Date('2024-12-31'), { units: ['days', 'hours'] });
153
+ * // "45 days, 12 hours"
154
+ *
155
+ * formatCountdown(new Date('2024-12-31'), { short: false });
156
+ * // "45 days 12 hours 30 minutes 15 seconds"
157
+ * ```
158
+ */
159
+ export function formatCountdown(targetDate, options = {}) {
160
+ const { from, units = ['days', 'hours', 'minutes', 'seconds'], short = true, maxUnits, showZero = false, separator = ' ' } = options;
161
+ const remaining = getRemainingTime(targetDate, from);
162
+ if (remaining.isExpired) {
163
+ return 'Expired';
164
+ }
165
+ const parts = [];
166
+ for (const unit of units) {
167
+ const value = remaining[unit];
168
+ if (value === 0 && !showZero && parts.length === 0) {
169
+ continue;
170
+ }
171
+ if (value === 0 && !showZero) {
172
+ continue;
173
+ }
174
+ if (maxUnits && parts.length >= maxUnits) {
175
+ break;
176
+ }
177
+ if (short) {
178
+ const shortUnit = unit[0];
179
+ parts.push(`${value}${shortUnit}`);
180
+ }
181
+ else {
182
+ const unitName = value === 1 ? unit.slice(0, -1) : unit;
183
+ parts.push(`${value} ${unitName}`);
184
+ }
185
+ }
186
+ return parts.length > 0 ? parts.join(separator) : '0s';
187
+ }
188
+ /**
189
+ * Checks if a date has expired (is in the past)
190
+ * @param date - The date to check
191
+ * @param fromDate - The reference date (defaults to now)
192
+ * @returns True if the date is in the past
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * isExpired(new Date('2020-01-01')); // true
197
+ * isExpired(new Date('2030-01-01')); // false
198
+ * ```
199
+ */
200
+ export function isExpired(date, fromDate = new Date()) {
201
+ const checkDate = new Date(date);
202
+ const from = new Date(fromDate);
203
+ return checkDate.getTime() < from.getTime();
204
+ }
205
+ /**
206
+ * Calculates the progress percentage between two dates
207
+ * @param startDate - The start date
208
+ * @param endDate - The end date
209
+ * @param currentDate - The current date (defaults to now)
210
+ * @returns Progress percentage (0-100), clamped to range
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * const progress = getProgressPercentage(
215
+ * new Date('2024-01-01'),
216
+ * new Date('2024-12-31'),
217
+ * new Date('2024-07-01')
218
+ * );
219
+ * console.log(`${progress}% complete`); // ~50% complete
220
+ * ```
221
+ */
222
+ export function getProgressPercentage(startDate, endDate, currentDate = new Date()) {
223
+ const start = new Date(startDate).getTime();
224
+ const end = new Date(endDate).getTime();
225
+ const current = new Date(currentDate).getTime();
226
+ if (start >= end) {
227
+ throw new Error('Start date must be before end date');
228
+ }
229
+ const total = end - start;
230
+ const elapsed = current - start;
231
+ const percentage = (elapsed / total) * 100;
232
+ // Clamp between 0 and 100
233
+ return Math.max(0, Math.min(100, percentage));
234
+ }
235
+ /**
236
+ * Gets time until a target date in a specific unit
237
+ * @param targetDate - The target date
238
+ * @param unit - The unit to return
239
+ * @param fromDate - The date to calculate from (defaults to now)
240
+ * @returns Time remaining in the specified unit
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * getTimeUntil(new Date('2024-12-31'), 'days'); // 45.5
245
+ * getTimeUntil(new Date('2024-12-31'), 'hours'); // 1092
246
+ * getTimeUntil(new Date('2024-12-31'), 'weeks'); // 6.5
247
+ * ```
248
+ */
249
+ export function getTimeUntil(targetDate, unit, fromDate = new Date()) {
250
+ const remaining = getRemainingTime(targetDate, fromDate);
251
+ switch (unit) {
252
+ case 'milliseconds':
253
+ return remaining.totalMilliseconds;
254
+ case 'seconds':
255
+ return remaining.totalSeconds;
256
+ case 'minutes':
257
+ return remaining.totalMinutes;
258
+ case 'hours':
259
+ return remaining.totalHours;
260
+ case 'days':
261
+ return remaining.totalDays;
262
+ case 'weeks':
263
+ return remaining.totalDays / 7;
264
+ default:
265
+ return remaining.totalMilliseconds;
266
+ }
267
+ }
268
+ /**
269
+ * Creates a deadline object with useful methods
270
+ * @param targetDate - The deadline date
271
+ * @returns An object with deadline-related methods
272
+ *
273
+ * @example
274
+ * ```ts
275
+ * const deadline = createDeadline(new Date('2024-12-31'));
276
+ *
277
+ * deadline.isExpired(); // false
278
+ * deadline.daysRemaining(); // 45
279
+ * deadline.hoursRemaining(); // 1092
280
+ * deadline.formatRemaining(); // "45d 12h 30m"
281
+ * deadline.progressFrom(new Date('2024-01-01')); // 67.5%
282
+ * ```
283
+ */
284
+ export function createDeadline(targetDate) {
285
+ const target = new Date(targetDate);
286
+ return {
287
+ target,
288
+ isExpired: () => isExpired(target),
289
+ getRemaining: () => getRemainingTime(target),
290
+ daysRemaining: () => getTimeUntil(target, 'days'),
291
+ hoursRemaining: () => getTimeUntil(target, 'hours'),
292
+ minutesRemaining: () => getTimeUntil(target, 'minutes'),
293
+ secondsRemaining: () => getTimeUntil(target, 'seconds'),
294
+ formatRemaining: (options) => formatCountdown(target, options),
295
+ progressFrom: (startDate) => getProgressPercentage(startDate, target),
296
+ countdown: (options) => createCountdown(target, options)
297
+ };
298
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Cron expression utilities for scheduling
3
+ */
4
+ export interface CronParts {
5
+ minute: string;
6
+ hour: string;
7
+ dayOfMonth: string;
8
+ month: string;
9
+ dayOfWeek: string;
10
+ }
11
+ export interface ParsedCronField {
12
+ type: 'all' | 'specific' | 'range' | 'step' | 'list';
13
+ values: number[];
14
+ }
15
+ /**
16
+ * Parse a cron expression into its parts
17
+ * @param expression - cron expression (5 fields: minute hour dayOfMonth month dayOfWeek)
18
+ */
19
+ export declare function parseCronExpression(expression: string): CronParts | null;
20
+ /**
21
+ * Parse a cron field into its numeric values
22
+ * @param field - cron field string (e.g., "star/5", "1-5", "1,2,3", "*")
23
+ * @param min - minimum valid value
24
+ * @param max - maximum valid value
25
+ */
26
+ export declare function parseCronField(field: string, min: number, max: number): ParsedCronField | null;
27
+ /**
28
+ * Check if a date matches a cron expression
29
+ * @param date - date to check
30
+ * @param expression - cron expression
31
+ */
32
+ export declare function matchesCron(date: Date, expression: string): boolean;
33
+ /**
34
+ * Get the next date that matches a cron expression
35
+ * @param expression - cron expression
36
+ * @param after - start searching after this date (default: now)
37
+ * @param maxIterations - maximum iterations to prevent infinite loops
38
+ */
39
+ export declare function getNextCronDate(expression: string, after?: Date, maxIterations?: number): Date | null;
40
+ /**
41
+ * Get the N next dates that match a cron expression
42
+ * @param expression - cron expression
43
+ * @param count - number of dates to get
44
+ * @param after - start searching after this date
45
+ */
46
+ export declare function getNextCronDates(expression: string, count: number, after?: Date): Date[];
47
+ /**
48
+ * Get the previous date that matched a cron expression
49
+ * @param expression - cron expression
50
+ * @param before - start searching before this date
51
+ * @param maxIterations - maximum iterations to prevent infinite loops
52
+ */
53
+ export declare function getPreviousCronDate(expression: string, before?: Date, maxIterations?: number): Date | null;
54
+ /**
55
+ * Validate a cron expression
56
+ * @param expression - cron expression to validate
57
+ */
58
+ export declare function isValidCron(expression: string): boolean;
59
+ /**
60
+ * Convert a cron expression to a human-readable description
61
+ * @param expression - cron expression
62
+ */
63
+ export declare function describeCron(expression: string): string | null;
64
+ /**
65
+ * Common cron expressions
66
+ */
67
+ export declare const CRON_PRESETS: {
68
+ readonly everyMinute: "* * * * *";
69
+ readonly everyHour: "0 * * * *";
70
+ readonly everyDay: "0 0 * * *";
71
+ readonly everyDayAt9am: "0 9 * * *";
72
+ readonly everyDayAt6pm: "0 18 * * *";
73
+ readonly everyWeek: "0 0 * * 0";
74
+ readonly everyMonth: "0 0 1 * *";
75
+ readonly everyYear: "0 0 1 1 *";
76
+ readonly weekdays: "0 0 * * 1-5";
77
+ readonly weekends: "0 0 * * 0,6";
78
+ readonly every5Minutes: "*/5 * * * *";
79
+ readonly every15Minutes: "*/15 * * * *";
80
+ readonly every30Minutes: "*/30 * * * *";
81
+ };
82
+ //# sourceMappingURL=cron.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../../src/cron.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,KAAK,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IACrD,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAcxE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CA+D9F;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAqBnE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,KAAK,GAAE,IAAiB,EACxB,aAAa,GAAE,MAAe,GAC7B,IAAI,GAAG,IAAI,CAiCb;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,IAAiB,GACvB,IAAI,EAAE,CAYR;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,IAAiB,EACzB,aAAa,GAAE,MAAe,GAC7B,IAAI,GAAG,IAAI,CAiCb;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAWvD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAwE9D;AAED;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;CAcf,CAAC"}
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Cron expression utilities for scheduling
3
+ */
4
+ /**
5
+ * Parse a cron expression into its parts
6
+ * @param expression - cron expression (5 fields: minute hour dayOfMonth month dayOfWeek)
7
+ */
8
+ export function parseCronExpression(expression) {
9
+ const parts = expression.trim().split(/\s+/);
10
+ if (parts.length !== 5) {
11
+ return null;
12
+ }
13
+ return {
14
+ minute: parts[0],
15
+ hour: parts[1],
16
+ dayOfMonth: parts[2],
17
+ month: parts[3],
18
+ dayOfWeek: parts[4],
19
+ };
20
+ }
21
+ /**
22
+ * Parse a cron field into its numeric values
23
+ * @param field - cron field string (e.g., "star/5", "1-5", "1,2,3", "*")
24
+ * @param min - minimum valid value
25
+ * @param max - maximum valid value
26
+ */
27
+ export function parseCronField(field, min, max) {
28
+ const values = [];
29
+ // Handle wildcard
30
+ if (field === '*') {
31
+ for (let i = min; i <= max; i++) {
32
+ values.push(i);
33
+ }
34
+ return { type: 'all', values };
35
+ }
36
+ // Handle step values (*/n or range/n)
37
+ if (field.includes('/')) {
38
+ const [range, stepStr] = field.split('/');
39
+ const step = parseInt(stepStr, 10);
40
+ if (isNaN(step) || step <= 0)
41
+ return null;
42
+ let start = min;
43
+ let end = max;
44
+ if (range !== '*') {
45
+ if (range.includes('-')) {
46
+ const [s, e] = range.split('-').map(Number);
47
+ start = s;
48
+ end = e;
49
+ }
50
+ else {
51
+ start = parseInt(range, 10);
52
+ }
53
+ }
54
+ for (let i = start; i <= end; i += step) {
55
+ values.push(i);
56
+ }
57
+ return { type: 'step', values };
58
+ }
59
+ // Handle range (n-m)
60
+ if (field.includes('-')) {
61
+ const [start, end] = field.split('-').map(Number);
62
+ if (isNaN(start) || isNaN(end) || start > end)
63
+ return null;
64
+ for (let i = start; i <= end; i++) {
65
+ values.push(i);
66
+ }
67
+ return { type: 'range', values };
68
+ }
69
+ // Handle list (n,m,o)
70
+ if (field.includes(',')) {
71
+ const items = field.split(',').map(Number);
72
+ if (items.some(isNaN))
73
+ return null;
74
+ return { type: 'list', values: items.sort((a, b) => a - b) };
75
+ }
76
+ // Handle specific value
77
+ const value = parseInt(field, 10);
78
+ if (isNaN(value) || value < min || value > max)
79
+ return null;
80
+ return { type: 'specific', values: [value] };
81
+ }
82
+ /**
83
+ * Check if a date matches a cron expression
84
+ * @param date - date to check
85
+ * @param expression - cron expression
86
+ */
87
+ export function matchesCron(date, expression) {
88
+ const parts = parseCronExpression(expression);
89
+ if (!parts)
90
+ return false;
91
+ const minute = parseCronField(parts.minute, 0, 59);
92
+ const hour = parseCronField(parts.hour, 0, 23);
93
+ const dayOfMonth = parseCronField(parts.dayOfMonth, 1, 31);
94
+ const month = parseCronField(parts.month, 1, 12);
95
+ const dayOfWeek = parseCronField(parts.dayOfWeek, 0, 6);
96
+ if (!minute || !hour || !dayOfMonth || !month || !dayOfWeek) {
97
+ return false;
98
+ }
99
+ return (minute.values.includes(date.getMinutes()) &&
100
+ hour.values.includes(date.getHours()) &&
101
+ dayOfMonth.values.includes(date.getDate()) &&
102
+ month.values.includes(date.getMonth() + 1) &&
103
+ dayOfWeek.values.includes(date.getDay()));
104
+ }
105
+ /**
106
+ * Get the next date that matches a cron expression
107
+ * @param expression - cron expression
108
+ * @param after - start searching after this date (default: now)
109
+ * @param maxIterations - maximum iterations to prevent infinite loops
110
+ */
111
+ export function getNextCronDate(expression, after = new Date(), maxIterations = 525600 // Max 1 year in minutes
112
+ ) {
113
+ const parts = parseCronExpression(expression);
114
+ if (!parts)
115
+ return null;
116
+ const minute = parseCronField(parts.minute, 0, 59);
117
+ const hour = parseCronField(parts.hour, 0, 23);
118
+ const dayOfMonth = parseCronField(parts.dayOfMonth, 1, 31);
119
+ const month = parseCronField(parts.month, 1, 12);
120
+ const dayOfWeek = parseCronField(parts.dayOfWeek, 0, 6);
121
+ if (!minute || !hour || !dayOfMonth || !month || !dayOfWeek) {
122
+ return null;
123
+ }
124
+ const candidate = new Date(after);
125
+ candidate.setSeconds(0, 0);
126
+ candidate.setMinutes(candidate.getMinutes() + 1);
127
+ for (let i = 0; i < maxIterations; i++) {
128
+ if (minute.values.includes(candidate.getMinutes()) &&
129
+ hour.values.includes(candidate.getHours()) &&
130
+ dayOfMonth.values.includes(candidate.getDate()) &&
131
+ month.values.includes(candidate.getMonth() + 1) &&
132
+ dayOfWeek.values.includes(candidate.getDay())) {
133
+ return candidate;
134
+ }
135
+ candidate.setMinutes(candidate.getMinutes() + 1);
136
+ }
137
+ return null;
138
+ }
139
+ /**
140
+ * Get the N next dates that match a cron expression
141
+ * @param expression - cron expression
142
+ * @param count - number of dates to get
143
+ * @param after - start searching after this date
144
+ */
145
+ export function getNextCronDates(expression, count, after = new Date()) {
146
+ const dates = [];
147
+ let currentAfter = after;
148
+ for (let i = 0; i < count; i++) {
149
+ const next = getNextCronDate(expression, currentAfter);
150
+ if (!next)
151
+ break;
152
+ dates.push(next);
153
+ currentAfter = next;
154
+ }
155
+ return dates;
156
+ }
157
+ /**
158
+ * Get the previous date that matched a cron expression
159
+ * @param expression - cron expression
160
+ * @param before - start searching before this date
161
+ * @param maxIterations - maximum iterations to prevent infinite loops
162
+ */
163
+ export function getPreviousCronDate(expression, before = new Date(), maxIterations = 525600) {
164
+ const parts = parseCronExpression(expression);
165
+ if (!parts)
166
+ return null;
167
+ const minute = parseCronField(parts.minute, 0, 59);
168
+ const hour = parseCronField(parts.hour, 0, 23);
169
+ const dayOfMonth = parseCronField(parts.dayOfMonth, 1, 31);
170
+ const month = parseCronField(parts.month, 1, 12);
171
+ const dayOfWeek = parseCronField(parts.dayOfWeek, 0, 6);
172
+ if (!minute || !hour || !dayOfMonth || !month || !dayOfWeek) {
173
+ return null;
174
+ }
175
+ const candidate = new Date(before);
176
+ candidate.setSeconds(0, 0);
177
+ candidate.setMinutes(candidate.getMinutes() - 1);
178
+ for (let i = 0; i < maxIterations; i++) {
179
+ if (minute.values.includes(candidate.getMinutes()) &&
180
+ hour.values.includes(candidate.getHours()) &&
181
+ dayOfMonth.values.includes(candidate.getDate()) &&
182
+ month.values.includes(candidate.getMonth() + 1) &&
183
+ dayOfWeek.values.includes(candidate.getDay())) {
184
+ return candidate;
185
+ }
186
+ candidate.setMinutes(candidate.getMinutes() - 1);
187
+ }
188
+ return null;
189
+ }
190
+ /**
191
+ * Validate a cron expression
192
+ * @param expression - cron expression to validate
193
+ */
194
+ export function isValidCron(expression) {
195
+ const parts = parseCronExpression(expression);
196
+ if (!parts)
197
+ return false;
198
+ const minute = parseCronField(parts.minute, 0, 59);
199
+ const hour = parseCronField(parts.hour, 0, 23);
200
+ const dayOfMonth = parseCronField(parts.dayOfMonth, 1, 31);
201
+ const month = parseCronField(parts.month, 1, 12);
202
+ const dayOfWeek = parseCronField(parts.dayOfWeek, 0, 6);
203
+ return !!(minute && hour && dayOfMonth && month && dayOfWeek);
204
+ }
205
+ /**
206
+ * Convert a cron expression to a human-readable description
207
+ * @param expression - cron expression
208
+ */
209
+ export function describeCron(expression) {
210
+ const parts = parseCronExpression(expression);
211
+ if (!parts)
212
+ return null;
213
+ const descriptions = [];
214
+ // Handle common patterns
215
+ if (expression === '* * * * *') {
216
+ return 'Every minute';
217
+ }
218
+ if (expression === '0 * * * *') {
219
+ return 'Every hour';
220
+ }
221
+ if (expression === '0 0 * * *') {
222
+ return 'Every day at midnight';
223
+ }
224
+ if (expression === '0 0 * * 0') {
225
+ return 'Every Sunday at midnight';
226
+ }
227
+ if (expression === '0 0 1 * *') {
228
+ return 'First day of every month at midnight';
229
+ }
230
+ // Build description
231
+ const minute = parts.minute;
232
+ const hour = parts.hour;
233
+ if (minute === '0' && hour !== '*') {
234
+ if (hour.includes('/')) {
235
+ const step = hour.split('/')[1];
236
+ descriptions.push(`Every ${step} hours`);
237
+ }
238
+ else if (hour.includes('-')) {
239
+ descriptions.push(`Every hour from ${hour} at minute 0`);
240
+ }
241
+ else {
242
+ descriptions.push(`At ${hour}:00`);
243
+ }
244
+ }
245
+ else if (minute !== '*' && hour === '*') {
246
+ descriptions.push(`At minute ${minute} of every hour`);
247
+ }
248
+ else if (minute.includes('/')) {
249
+ const step = minute.split('/')[1];
250
+ descriptions.push(`Every ${step} minutes`);
251
+ }
252
+ if (parts.dayOfMonth !== '*') {
253
+ descriptions.push(`on day ${parts.dayOfMonth} of the month`);
254
+ }
255
+ if (parts.month !== '*') {
256
+ const monthNames = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
257
+ const monthNum = parseInt(parts.month, 10);
258
+ if (!isNaN(monthNum) && monthNum >= 1 && monthNum <= 12) {
259
+ descriptions.push(`in ${monthNames[monthNum]}`);
260
+ }
261
+ else {
262
+ descriptions.push(`in month ${parts.month}`);
263
+ }
264
+ }
265
+ if (parts.dayOfWeek !== '*') {
266
+ const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
267
+ const dayNum = parseInt(parts.dayOfWeek, 10);
268
+ if (!isNaN(dayNum) && dayNum >= 0 && dayNum <= 6) {
269
+ descriptions.push(`on ${dayNames[dayNum]}`);
270
+ }
271
+ else {
272
+ descriptions.push(`on day of week ${parts.dayOfWeek}`);
273
+ }
274
+ }
275
+ return descriptions.join(' ') || expression;
276
+ }
277
+ /**
278
+ * Common cron expressions
279
+ */
280
+ export const CRON_PRESETS = {
281
+ everyMinute: '* * * * *',
282
+ everyHour: '0 * * * *',
283
+ everyDay: '0 0 * * *',
284
+ everyDayAt9am: '0 9 * * *',
285
+ everyDayAt6pm: '0 18 * * *',
286
+ everyWeek: '0 0 * * 0',
287
+ everyMonth: '0 0 1 * *',
288
+ everyYear: '0 0 1 1 *',
289
+ weekdays: '0 0 * * 1-5',
290
+ weekends: '0 0 * * 0,6',
291
+ every5Minutes: '*/5 * * * *',
292
+ every15Minutes: '*/15 * * * *',
293
+ every30Minutes: '*/30 * * * *',
294
+ };