1 | 'use strict';
|
2 |
|
3 |
|
4 | var CronDate = require('./date');
|
5 |
|
6 |
|
7 | var safeIsNaN = require('is-nan');
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | var LOOP_LIMIT = 10000;
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | function isWildcardRange(range, constraints) {
|
22 | if (range instanceof Array && !range.length) {
|
23 | return false;
|
24 | }
|
25 |
|
26 | if (constraints.length !== 2) {
|
27 | return false;
|
28 | }
|
29 |
|
30 | return range.length === (constraints[1] - (constraints[0] < 1 ? - 1 : 0));
|
31 | }
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 | function CronExpression (fields, options) {
|
46 | this._options = options;
|
47 | this._utc = options.utc || false;
|
48 | this._tz = this._utc ? 'UTC' : options.tz;
|
49 | this._currentDate = new CronDate(options.currentDate, this._tz);
|
50 | this._startDate = options.startDate ? new CronDate(options.startDate, this._tz) : null;
|
51 | this._endDate = options.endDate ? new CronDate(options.endDate, this._tz) : null;
|
52 | this._fields = fields;
|
53 | this._isIterator = options.iterator || false;
|
54 | this._hasIterated = false;
|
55 | }
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 | CronExpression.map = [ 'second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek' ];
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 | CronExpression.predefined = {
|
68 | '@yearly': '0 0 1 1 *',
|
69 | '@monthly': '0 0 1 * *',
|
70 | '@weekly': '0 0 * * 0',
|
71 | '@daily': '0 0 * * *',
|
72 | '@hourly': '0 * * * *'
|
73 | };
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | CronExpression.constraints = [
|
80 | [ 0, 59 ],
|
81 | [ 0, 59 ],
|
82 | [ 0, 23 ],
|
83 | [ 1, 31 ],
|
84 | [ 1, 12 ],
|
85 | [ 0, 7 ]
|
86 | ];
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | CronExpression.daysInMonth = [
|
93 | 31,
|
94 | 29,
|
95 | 31,
|
96 | 30,
|
97 | 31,
|
98 | 30,
|
99 | 31,
|
100 | 31,
|
101 | 30,
|
102 | 31,
|
103 | 30,
|
104 | 31
|
105 | ];
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 | CronExpression.aliases = {
|
112 | month: {
|
113 | jan: 1,
|
114 | feb: 2,
|
115 | mar: 3,
|
116 | apr: 4,
|
117 | may: 5,
|
118 | jun: 6,
|
119 | jul: 7,
|
120 | aug: 8,
|
121 | sep: 9,
|
122 | oct: 10,
|
123 | nov: 11,
|
124 | dec: 12
|
125 | },
|
126 |
|
127 | dayOfWeek: {
|
128 | sun: 0,
|
129 | mon: 1,
|
130 | tue: 2,
|
131 | wed: 3,
|
132 | thu: 4,
|
133 | fri: 5,
|
134 | sat: 6
|
135 | }
|
136 | };
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 | CronExpression.parseDefaults = [ '0', '*', '*', '*', '*', '*' ];
|
143 |
|
144 | CronExpression.standardValidCharacters = /^[\d|/|*|\-|,]+$/;
|
145 | CronExpression.dayValidCharacters = /^[\d|/|*|\-|,|\?]+$/;
|
146 | CronExpression.validCharacters = {
|
147 | second: CronExpression.standardValidCharacters,
|
148 | minute: CronExpression.standardValidCharacters,
|
149 | hour: CronExpression.standardValidCharacters,
|
150 | dayOfMonth: CronExpression.dayValidCharacters,
|
151 | month: CronExpression.standardValidCharacters,
|
152 | dayOfWeek: CronExpression.dayValidCharacters,
|
153 | }
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 | CronExpression._parseField = function _parseField (field, value, constraints) {
|
165 |
|
166 | switch (field) {
|
167 | case 'month':
|
168 | case 'dayOfWeek':
|
169 | var aliases = CronExpression.aliases[field];
|
170 |
|
171 | value = value.replace(/[a-z]{1,3}/gi, function(match) {
|
172 | match = match.toLowerCase();
|
173 |
|
174 | if (typeof aliases[match] !== undefined) {
|
175 | return aliases[match];
|
176 | } else {
|
177 | throw new Error('Cannot resolve alias "' + match + '"')
|
178 | }
|
179 | });
|
180 | break;
|
181 | }
|
182 |
|
183 |
|
184 | if (!(CronExpression.validCharacters[field].test(value))) {
|
185 | throw new Error('Invalid characters, got value: ' + value)
|
186 | }
|
187 |
|
188 |
|
189 | if (value.indexOf('*') !== -1) {
|
190 | value = value.replace(/\*/g, constraints.join('-'));
|
191 | } else if (value.indexOf('?') !== -1) {
|
192 | value = value.replace(/\?/g, constraints.join('-'));
|
193 | }
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 | |
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 | function parseSequence (val) {
|
211 | var stack = [];
|
212 |
|
213 | function handleResult (result) {
|
214 | var max = stack.length > 0 ? Math.max.apply(Math, stack) : -1;
|
215 |
|
216 | if (result instanceof Array) {
|
217 | for (var i = 0, c = result.length; i < c; i++) {
|
218 | var value = result[i];
|
219 |
|
220 |
|
221 | if (value < constraints[0] || value > constraints[1]) {
|
222 | throw new Error(
|
223 | 'Constraint error, got value ' + value + ' expected range ' +
|
224 | constraints[0] + '-' + constraints[1]
|
225 | );
|
226 | }
|
227 |
|
228 | if (value > max) {
|
229 | stack.push(value);
|
230 | }
|
231 |
|
232 | max = Math.max.apply(Math, stack);
|
233 | }
|
234 | } else {
|
235 | result = +result;
|
236 |
|
237 |
|
238 | if (result < constraints[0] || result > constraints[1]) {
|
239 | throw new Error(
|
240 | 'Constraint error, got value ' + result + ' expected range ' +
|
241 | constraints[0] + '-' + constraints[1]
|
242 | );
|
243 | }
|
244 |
|
245 | if (field == 'dayOfWeek') {
|
246 | result = result % 7;
|
247 | }
|
248 |
|
249 | stack.push(result);
|
250 | }
|
251 | }
|
252 |
|
253 | var atoms = val.split(',');
|
254 | if (atoms.length > 1) {
|
255 | for (var i = 0, c = atoms.length; i < c; i++) {
|
256 | handleResult(parseRepeat(atoms[i]));
|
257 | }
|
258 | } else {
|
259 | handleResult(parseRepeat(val));
|
260 | }
|
261 |
|
262 | stack.sort(function(a, b) {
|
263 | return a - b;
|
264 | });
|
265 |
|
266 | return stack;
|
267 | }
|
268 |
|
269 | |
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 | function parseRepeat (val) {
|
276 | var repeatInterval = 1;
|
277 | var atoms = val.split('/');
|
278 |
|
279 | if (atoms.length > 1) {
|
280 | return parseRange(atoms[0], atoms[atoms.length - 1]);
|
281 | }
|
282 |
|
283 | return parseRange(val, repeatInterval);
|
284 | }
|
285 |
|
286 | |
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 | function parseRange (val, repeatInterval) {
|
295 | var stack = [];
|
296 | var atoms = val.split('-');
|
297 |
|
298 | if (atoms.length > 1 ) {
|
299 |
|
300 | if (atoms.length < 2) {
|
301 | return +val;
|
302 | }
|
303 |
|
304 | if (!atoms[0].length) {
|
305 | if (!atoms[1].length) {
|
306 | throw new Error('Invalid range: ' + val);
|
307 | }
|
308 |
|
309 | return +val;
|
310 | }
|
311 |
|
312 |
|
313 | var min = +atoms[0];
|
314 | var max = +atoms[1];
|
315 |
|
316 | if (safeIsNaN(min) || safeIsNaN(max) ||
|
317 | min < constraints[0] || max > constraints[1]) {
|
318 | throw new Error(
|
319 | 'Constraint error, got range ' +
|
320 | min + '-' + max +
|
321 | ' expected range ' +
|
322 | constraints[0] + '-' + constraints[1]
|
323 | );
|
324 | } else if (min >= max) {
|
325 | throw new Error('Invalid range: ' + val);
|
326 | }
|
327 |
|
328 |
|
329 | var repeatIndex = +repeatInterval;
|
330 |
|
331 | if (safeIsNaN(repeatIndex) || repeatIndex <= 0) {
|
332 | throw new Error('Constraint error, cannot repeat at every ' + repeatIndex + ' time.');
|
333 | }
|
334 |
|
335 | for (var index = min, count = max; index <= count; index++) {
|
336 | if (repeatIndex > 0 && (repeatIndex % repeatInterval) === 0) {
|
337 | repeatIndex = 1;
|
338 | stack.push(index);
|
339 | } else {
|
340 | repeatIndex++;
|
341 | }
|
342 | }
|
343 |
|
344 | return stack;
|
345 | }
|
346 |
|
347 | return +val;
|
348 | }
|
349 |
|
350 | return parseSequence(value);
|
351 | };
|
352 |
|
353 | CronExpression.prototype._applyTimezoneShift = function(currentDate, dateMathVerb, method) {
|
354 | if ((method === 'Month') || (method === 'Day')) {
|
355 | var prevTime = currentDate.getTime();
|
356 | currentDate[dateMathVerb + method]();
|
357 | var currTime = currentDate.getTime();
|
358 | if (prevTime === currTime) {
|
359 |
|
360 | if ((currentDate.getMinutes() === 0) &&
|
361 | (currentDate.getSeconds() === 0)) {
|
362 | currentDate.addHour();
|
363 | } else if ((currentDate.getMinutes() === 59) &&
|
364 | (currentDate.getSeconds() === 59)) {
|
365 | currentDate.subtractHour();
|
366 | }
|
367 | }
|
368 | } else {
|
369 | var previousHour = currentDate.getHours();
|
370 | currentDate[dateMathVerb + method]();
|
371 | var currentHour = currentDate.getHours();
|
372 | var diff = currentHour - previousHour;
|
373 | if (diff === 2) {
|
374 |
|
375 | if (this._fields.hour.length !== 24) {
|
376 |
|
377 | this._dstStart = currentHour;
|
378 | }
|
379 | } else if ((diff === 0) &&
|
380 | (currentDate.getMinutes() === 0) &&
|
381 | (currentDate.getSeconds() === 0)) {
|
382 |
|
383 | if (this._fields.hour.length !== 24) {
|
384 |
|
385 | this._dstEnd = currentHour;
|
386 | }
|
387 | }
|
388 | }
|
389 | };
|
390 |
|
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 | CronExpression.prototype._findSchedule = function _findSchedule (reverse) {
|
399 |
|
400 | |
401 |
|
402 |
|
403 |
|
404 |
|
405 |
|
406 |
|
407 |
|
408 | function matchSchedule (value, sequence) {
|
409 | for (var i = 0, c = sequence.length; i < c; i++) {
|
410 | if (sequence[i] >= value) {
|
411 | return sequence[i] === value;
|
412 | }
|
413 | }
|
414 |
|
415 | return sequence[0] === value;
|
416 | }
|
417 |
|
418 |
|
419 | reverse = reverse || false;
|
420 | var dateMathVerb = reverse ? 'subtract' : 'add';
|
421 |
|
422 | var currentDate = new CronDate(this._currentDate, this._tz);
|
423 | var startDate = this._startDate;
|
424 | var endDate = this._endDate;
|
425 |
|
426 |
|
427 | var startTimestamp = currentDate.getTime();
|
428 | var stepCount = 0;
|
429 |
|
430 | while (stepCount < LOOP_LIMIT) {
|
431 | stepCount++;
|
432 |
|
433 |
|
434 | if (reverse) {
|
435 | if (startDate && (currentDate.getTime() - startDate.getTime() < 0)) {
|
436 | throw new Error('Out of the timespan range');
|
437 | }
|
438 | } else {
|
439 | if (endDate && (endDate.getTime() - currentDate.getTime()) < 0) {
|
440 | throw new Error('Out of the timespan range');
|
441 | }
|
442 | }
|
443 |
|
444 |
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 | var dayOfMonthMatch = matchSchedule(currentDate.getDate(), this._fields.dayOfMonth);
|
456 | var dayOfWeekMatch = matchSchedule(currentDate.getDay(), this._fields.dayOfWeek);
|
457 |
|
458 | var isDayOfMonthWildcardMatch = isWildcardRange(this._fields.dayOfMonth, CronExpression.constraints[3]);
|
459 | var isDayOfWeekWildcardMatch = isWildcardRange(this._fields.dayOfWeek, CronExpression.constraints[5]);
|
460 |
|
461 | var currentHour = currentDate.getHours();
|
462 |
|
463 |
|
464 | if (!dayOfMonthMatch && !dayOfWeekMatch) {
|
465 | this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
|
466 | continue;
|
467 | }
|
468 |
|
469 |
|
470 | if (!isDayOfMonthWildcardMatch && isDayOfWeekWildcardMatch && !dayOfMonthMatch) {
|
471 | this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
|
472 | continue;
|
473 | }
|
474 |
|
475 |
|
476 | if (isDayOfMonthWildcardMatch && !isDayOfWeekWildcardMatch && !dayOfWeekMatch) {
|
477 | this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
|
478 | continue;
|
479 | }
|
480 |
|
481 |
|
482 | if (!(isDayOfMonthWildcardMatch && isDayOfWeekWildcardMatch) &&
|
483 | !dayOfMonthMatch && !dayOfWeekMatch) {
|
484 | this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
|
485 | continue;
|
486 | }
|
487 |
|
488 |
|
489 | if (!matchSchedule(currentDate.getMonth() + 1, this._fields.month)) {
|
490 | this._applyTimezoneShift(currentDate, dateMathVerb, 'Month');
|
491 | continue;
|
492 | }
|
493 |
|
494 |
|
495 | if (!matchSchedule(currentHour, this._fields.hour)) {
|
496 | if (this._dstStart !== currentHour) {
|
497 | this._dstStart = null;
|
498 | this._applyTimezoneShift(currentDate, dateMathVerb, 'Hour');
|
499 | continue;
|
500 | } else if (!matchSchedule(currentHour - 1, this._fields.hour)) {
|
501 | currentDate[dateMathVerb + 'Hour']();
|
502 | continue;
|
503 | }
|
504 | } else if (this._dstEnd === currentHour) {
|
505 | if (!reverse) {
|
506 | this._dstEnd = null;
|
507 | this._applyTimezoneShift(currentDate, 'add', 'Hour');
|
508 | continue;
|
509 | }
|
510 | }
|
511 |
|
512 |
|
513 | if (!matchSchedule(currentDate.getMinutes(), this._fields.minute)) {
|
514 | this._applyTimezoneShift(currentDate, dateMathVerb, 'Minute');
|
515 | continue;
|
516 | }
|
517 |
|
518 |
|
519 | if (!matchSchedule(currentDate.getSeconds(), this._fields.second)) {
|
520 | this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
|
521 | continue;
|
522 | }
|
523 |
|
524 |
|
525 |
|
526 | if (startTimestamp === currentDate.getTime()) {
|
527 | if ((dateMathVerb === 'add') || (currentDate.getMilliseconds() === 0)) {
|
528 | this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
|
529 | } else {
|
530 | currentDate.setMilliseconds(0);
|
531 | }
|
532 |
|
533 | continue;
|
534 | }
|
535 |
|
536 | break;
|
537 | }
|
538 |
|
539 | if (stepCount >= LOOP_LIMIT) {
|
540 | throw new Error('Invalid expression, loop limit exceeded');
|
541 | }
|
542 |
|
543 | this._currentDate = new CronDate(currentDate, this._tz);
|
544 | this._hasIterated = true;
|
545 |
|
546 | return currentDate;
|
547 | };
|
548 |
|
549 |
|
550 |
|
551 |
|
552 |
|
553 |
|
554 |
|
555 | CronExpression.prototype.next = function next () {
|
556 | var schedule = this._findSchedule();
|
557 |
|
558 |
|
559 | if (this._isIterator) {
|
560 | return {
|
561 | value: schedule,
|
562 | done: !this.hasNext()
|
563 | };
|
564 | }
|
565 |
|
566 | return schedule;
|
567 | };
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 |
|
575 | CronExpression.prototype.prev = function prev () {
|
576 | var schedule = this._findSchedule(true);
|
577 |
|
578 |
|
579 | if (this._isIterator) {
|
580 | return {
|
581 | value: schedule,
|
582 | done: !this.hasPrev()
|
583 | };
|
584 | }
|
585 |
|
586 | return schedule;
|
587 | };
|
588 |
|
589 |
|
590 |
|
591 |
|
592 |
|
593 |
|
594 |
|
595 | CronExpression.prototype.hasNext = function() {
|
596 | var current = this._currentDate;
|
597 | var hasIterated = this._hasIterated;
|
598 |
|
599 | try {
|
600 | this._findSchedule();
|
601 | return true;
|
602 | } catch (err) {
|
603 | return false;
|
604 | } finally {
|
605 | this._currentDate = current;
|
606 | this._hasIterated = hasIterated;
|
607 | }
|
608 | };
|
609 |
|
610 |
|
611 |
|
612 |
|
613 |
|
614 |
|
615 |
|
616 | CronExpression.prototype.hasPrev = function() {
|
617 | var current = this._currentDate;
|
618 | var hasIterated = this._hasIterated;
|
619 |
|
620 | try {
|
621 | this._findSchedule(true);
|
622 | return true;
|
623 | } catch (err) {
|
624 | return false;
|
625 | } finally {
|
626 | this._currentDate = current;
|
627 | this._hasIterated = hasIterated;
|
628 | }
|
629 | };
|
630 |
|
631 |
|
632 |
|
633 |
|
634 |
|
635 |
|
636 |
|
637 |
|
638 |
|
639 | CronExpression.prototype.iterate = function iterate (steps, callback) {
|
640 | var dates = [];
|
641 |
|
642 | if (steps >= 0) {
|
643 | for (var i = 0, c = steps; i < c; i++) {
|
644 | try {
|
645 | var item = this.next();
|
646 | dates.push(item);
|
647 |
|
648 |
|
649 | if (callback) {
|
650 | callback(item, i);
|
651 | }
|
652 | } catch (err) {
|
653 | break;
|
654 | }
|
655 | }
|
656 | } else {
|
657 | for (var i = 0, c = steps; i > c; i--) {
|
658 | try {
|
659 | var item = this.prev();
|
660 | dates.push(item);
|
661 |
|
662 |
|
663 | if (callback) {
|
664 | callback(item, i);
|
665 | }
|
666 | } catch (err) {
|
667 | break;
|
668 | }
|
669 | }
|
670 | }
|
671 |
|
672 | return dates;
|
673 | };
|
674 |
|
675 |
|
676 |
|
677 |
|
678 |
|
679 |
|
680 | CronExpression.prototype.reset = function reset () {
|
681 | this._currentDate = new CronDate(this._options.currentDate);
|
682 | };
|
683 |
|
684 |
|
685 |
|
686 |
|
687 |
|
688 |
|
689 |
|
690 |
|
691 |
|
692 | CronExpression.parse = function parse(expression, options, callback) {
|
693 | var self = this;
|
694 | if (typeof options === 'function') {
|
695 | callback = options;
|
696 | options = {};
|
697 | }
|
698 |
|
699 | function parse (expression, options) {
|
700 | if (!options) {
|
701 | options = {};
|
702 | }
|
703 |
|
704 | if (typeof options.currentDate === 'undefined') {
|
705 | options.currentDate = new CronDate(undefined, self._tz);
|
706 | }
|
707 |
|
708 |
|
709 | if (CronExpression.predefined[expression]) {
|
710 | expression = CronExpression.predefined[expression];
|
711 | }
|
712 |
|
713 |
|
714 | var fields = [];
|
715 | var atoms = (expression + '').trim().split(/\s+/);
|
716 |
|
717 | if (atoms.length > 6) {
|
718 | throw new Error('Invalid cron expression');
|
719 | }
|
720 |
|
721 |
|
722 | var start = (CronExpression.map.length - atoms.length);
|
723 | for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
|
724 | var field = CronExpression.map[i];
|
725 | var value = atoms[atoms.length > c ? i : i - start];
|
726 |
|
727 | if (i < start || !value) {
|
728 | fields.push(CronExpression._parseField(
|
729 | field,
|
730 | CronExpression.parseDefaults[i],
|
731 | CronExpression.constraints[i])
|
732 | );
|
733 | } else {
|
734 | fields.push(CronExpression._parseField(
|
735 | field,
|
736 | value,
|
737 | CronExpression.constraints[i])
|
738 | );
|
739 | }
|
740 | }
|
741 |
|
742 | var mappedFields = {};
|
743 | for (var i = 0, c = CronExpression.map.length; i < c; i++) {
|
744 | var key = CronExpression.map[i];
|
745 | mappedFields[key] = fields[i];
|
746 | }
|
747 |
|
748 |
|
749 | if (mappedFields.month.length === 1) {
|
750 | var daysInMonth = CronExpression.daysInMonth[mappedFields.month[0] - 1];
|
751 | var maxDayInMonthValue = Math.max.apply(null, mappedFields.dayOfMonth);
|
752 | var isWildCardDayInMonth = isWildcardRange(mappedFields.dayOfMonth, CronExpression.constraints[3]);
|
753 |
|
754 | if (!isWildCardDayInMonth && maxDayInMonthValue > daysInMonth) {
|
755 | throw new Error('Invalid explicit day of month definition');
|
756 | }
|
757 | }
|
758 |
|
759 | return new CronExpression(mappedFields, options);
|
760 | }
|
761 |
|
762 | return parse(expression, options);
|
763 | };
|
764 |
|
765 | module.exports = CronExpression;
|