expression.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  1. 'use strict';
  2. // Load Date class extensions
  3. var CronDate = require('./date');
  4. var stringifyField = require('./field_stringify');
  5. /**
  6. * Cron iteration loop safety limit
  7. */
  8. var LOOP_LIMIT = 10000;
  9. /**
  10. * Construct a new expression parser
  11. *
  12. * Options:
  13. * currentDate: iterator start date
  14. * endDate: iterator end date
  15. *
  16. * @constructor
  17. * @private
  18. * @param {Object} fields Expression fields parsed values
  19. * @param {Object} options Parser options
  20. */
  21. function CronExpression (fields, options) {
  22. this._options = options;
  23. this._utc = options.utc || false;
  24. this._tz = this._utc ? 'UTC' : options.tz;
  25. this._currentDate = new CronDate(options.currentDate, this._tz);
  26. this._startDate = options.startDate ? new CronDate(options.startDate, this._tz) : null;
  27. this._endDate = options.endDate ? new CronDate(options.endDate, this._tz) : null;
  28. this._isIterator = options.iterator || false;
  29. this._hasIterated = false;
  30. this._nthDayOfWeek = options.nthDayOfWeek || 0;
  31. this.fields = CronExpression._freezeFields(fields);
  32. }
  33. /**
  34. * Field mappings
  35. * @type {Array}
  36. */
  37. CronExpression.map = [ 'second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek' ];
  38. /**
  39. * Prefined intervals
  40. * @type {Object}
  41. */
  42. CronExpression.predefined = {
  43. '@yearly': '0 0 1 1 *',
  44. '@monthly': '0 0 1 * *',
  45. '@weekly': '0 0 * * 0',
  46. '@daily': '0 0 * * *',
  47. '@hourly': '0 * * * *'
  48. };
  49. /**
  50. * Fields constraints
  51. * @type {Array}
  52. */
  53. CronExpression.constraints = [
  54. { min: 0, max: 59, chars: [] }, // Second
  55. { min: 0, max: 59, chars: [] }, // Minute
  56. { min: 0, max: 23, chars: [] }, // Hour
  57. { min: 1, max: 31, chars: ['L'] }, // Day of month
  58. { min: 1, max: 12, chars: [] }, // Month
  59. { min: 0, max: 7, chars: ['L'] }, // Day of week
  60. ];
  61. /**
  62. * Days in month
  63. * @type {number[]}
  64. */
  65. CronExpression.daysInMonth = [
  66. 31,
  67. 29,
  68. 31,
  69. 30,
  70. 31,
  71. 30,
  72. 31,
  73. 31,
  74. 30,
  75. 31,
  76. 30,
  77. 31
  78. ];
  79. /**
  80. * Field aliases
  81. * @type {Object}
  82. */
  83. CronExpression.aliases = {
  84. month: {
  85. jan: 1,
  86. feb: 2,
  87. mar: 3,
  88. apr: 4,
  89. may: 5,
  90. jun: 6,
  91. jul: 7,
  92. aug: 8,
  93. sep: 9,
  94. oct: 10,
  95. nov: 11,
  96. dec: 12
  97. },
  98. dayOfWeek: {
  99. sun: 0,
  100. mon: 1,
  101. tue: 2,
  102. wed: 3,
  103. thu: 4,
  104. fri: 5,
  105. sat: 6
  106. }
  107. };
  108. /**
  109. * Field defaults
  110. * @type {Array}
  111. */
  112. CronExpression.parseDefaults = [ '0', '*', '*', '*', '*', '*' ];
  113. CronExpression.standardValidCharacters = /^[,*\d/-]+$/;
  114. CronExpression.dayOfWeekValidCharacters = /^[?,*\dL#/-]+$/;
  115. CronExpression.dayOfMonthValidCharacters = /^[?,*\dL/-]+$/;
  116. CronExpression.validCharacters = {
  117. second: CronExpression.standardValidCharacters,
  118. minute: CronExpression.standardValidCharacters,
  119. hour: CronExpression.standardValidCharacters,
  120. dayOfMonth: CronExpression.dayOfMonthValidCharacters,
  121. month: CronExpression.standardValidCharacters,
  122. dayOfWeek: CronExpression.dayOfWeekValidCharacters,
  123. };
  124. CronExpression._isValidConstraintChar = function _isValidConstraintChar(constraints, value) {
  125. if (typeof value !== 'string') {
  126. return false;
  127. }
  128. return constraints.chars.some(function(char) {
  129. return value.indexOf(char) > -1;
  130. });
  131. };
  132. /**
  133. * Parse input interval
  134. *
  135. * @param {String} field Field symbolic name
  136. * @param {String} value Field value
  137. * @param {Array} constraints Range upper and lower constraints
  138. * @return {Array} Sequence of sorted values
  139. * @private
  140. */
  141. CronExpression._parseField = function _parseField (field, value, constraints) {
  142. // Replace aliases
  143. switch (field) {
  144. case 'month':
  145. case 'dayOfWeek':
  146. var aliases = CronExpression.aliases[field];
  147. value = value.replace(/[a-z]{3}/gi, function(match) {
  148. match = match.toLowerCase();
  149. if (typeof aliases[match] !== 'undefined') {
  150. return aliases[match];
  151. } else {
  152. throw new Error('Validation error, cannot resolve alias "' + match + '"');
  153. }
  154. });
  155. break;
  156. }
  157. // Check for valid characters.
  158. if (!(CronExpression.validCharacters[field].test(value))) {
  159. throw new Error('Invalid characters, got value: ' + value);
  160. }
  161. // Replace '*' and '?'
  162. if (value.indexOf('*') !== -1) {
  163. value = value.replace(/\*/g, constraints.min + '-' + constraints.max);
  164. } else if (value.indexOf('?') !== -1) {
  165. value = value.replace(/\?/g, constraints.min + '-' + constraints.max);
  166. }
  167. //
  168. // Inline parsing functions
  169. //
  170. // Parser path:
  171. // - parseSequence
  172. // - parseRepeat
  173. // - parseRange
  174. /**
  175. * Parse sequence
  176. *
  177. * @param {String} val
  178. * @return {Array}
  179. * @private
  180. */
  181. function parseSequence (val) {
  182. var stack = [];
  183. function handleResult (result) {
  184. if (result instanceof Array) { // Make sequence linear
  185. for (var i = 0, c = result.length; i < c; i++) {
  186. var value = result[i];
  187. if (CronExpression._isValidConstraintChar(constraints, value)) {
  188. stack.push(value);
  189. continue;
  190. }
  191. // Check constraints
  192. if (typeof value !== 'number' || Number.isNaN(value) || value < constraints.min || value > constraints.max) {
  193. throw new Error(
  194. 'Constraint error, got value ' + value + ' expected range ' +
  195. constraints.min + '-' + constraints.max
  196. );
  197. }
  198. stack.push(value);
  199. }
  200. } else { // Scalar value
  201. if (CronExpression._isValidConstraintChar(constraints, result)) {
  202. stack.push(result);
  203. return;
  204. }
  205. var numResult = +result;
  206. // Check constraints
  207. if (Number.isNaN(numResult) || numResult < constraints.min || numResult > constraints.max) {
  208. throw new Error(
  209. 'Constraint error, got value ' + result + ' expected range ' +
  210. constraints.min + '-' + constraints.max
  211. );
  212. }
  213. if (field === 'dayOfWeek') {
  214. numResult = numResult % 7;
  215. }
  216. stack.push(numResult);
  217. }
  218. }
  219. var atoms = val.split(',');
  220. if (!atoms.every(function (atom) {
  221. return atom.length > 0;
  222. })) {
  223. throw new Error('Invalid list value format');
  224. }
  225. if (atoms.length > 1) {
  226. for (var i = 0, c = atoms.length; i < c; i++) {
  227. handleResult(parseRepeat(atoms[i]));
  228. }
  229. } else {
  230. handleResult(parseRepeat(val));
  231. }
  232. stack.sort(CronExpression._sortCompareFn);
  233. return stack;
  234. }
  235. /**
  236. * Parse repetition interval
  237. *
  238. * @param {String} val
  239. * @return {Array}
  240. */
  241. function parseRepeat (val) {
  242. var repeatInterval = 1;
  243. var atoms = val.split('/');
  244. if (atoms.length > 2) {
  245. throw new Error('Invalid repeat: ' + val);
  246. }
  247. if (atoms.length > 1) {
  248. if (atoms[0] == +atoms[0]) {
  249. atoms = [atoms[0] + '-' + constraints.max, atoms[1]];
  250. }
  251. return parseRange(atoms[0], atoms[atoms.length - 1]);
  252. }
  253. return parseRange(val, repeatInterval);
  254. }
  255. /**
  256. * Parse range
  257. *
  258. * @param {String} val
  259. * @param {Number} repeatInterval Repetition interval
  260. * @return {Array}
  261. * @private
  262. */
  263. function parseRange (val, repeatInterval) {
  264. var stack = [];
  265. var atoms = val.split('-');
  266. if (atoms.length > 1 ) {
  267. // Invalid range, return value
  268. if (atoms.length < 2) {
  269. return +val;
  270. }
  271. if (!atoms[0].length) {
  272. if (!atoms[1].length) {
  273. throw new Error('Invalid range: ' + val);
  274. }
  275. return +val;
  276. }
  277. // Validate range
  278. var min = +atoms[0];
  279. var max = +atoms[1];
  280. if (Number.isNaN(min) || Number.isNaN(max) ||
  281. min < constraints.min || max > constraints.max) {
  282. throw new Error(
  283. 'Constraint error, got range ' +
  284. min + '-' + max +
  285. ' expected range ' +
  286. constraints.min + '-' + constraints.max
  287. );
  288. } else if (min > max) {
  289. throw new Error('Invalid range: ' + val);
  290. }
  291. // Create range
  292. var repeatIndex = +repeatInterval;
  293. if (Number.isNaN(repeatIndex) || repeatIndex <= 0) {
  294. throw new Error('Constraint error, cannot repeat at every ' + repeatIndex + ' time.');
  295. }
  296. // JS DOW is in range of 0-6 (SUN-SAT) but we also support 7 in the expression
  297. // Handle case when range contains 7 instead of 0 and translate this value to 0
  298. if (field === 'dayOfWeek' && max % 7 === 0) {
  299. stack.push(0);
  300. }
  301. for (var index = min, count = max; index <= count; index++) {
  302. var exists = stack.indexOf(index) !== -1;
  303. if (!exists && repeatIndex > 0 && (repeatIndex % repeatInterval) === 0) {
  304. repeatIndex = 1;
  305. stack.push(index);
  306. } else {
  307. repeatIndex++;
  308. }
  309. }
  310. return stack;
  311. }
  312. return Number.isNaN(+val) ? val : +val;
  313. }
  314. return parseSequence(value);
  315. };
  316. CronExpression._sortCompareFn = function(a, b) {
  317. var aIsNumber = typeof a === 'number';
  318. var bIsNumber = typeof b === 'number';
  319. if (aIsNumber && bIsNumber) {
  320. return a - b;
  321. }
  322. if (!aIsNumber && bIsNumber) {
  323. return 1;
  324. }
  325. if (aIsNumber && !bIsNumber) {
  326. return -1;
  327. }
  328. return a.localeCompare(b);
  329. };
  330. CronExpression._handleMaxDaysInMonth = function(mappedFields) {
  331. // Filter out any day of month value that is larger than given month expects
  332. if (mappedFields.month.length === 1) {
  333. var daysInMonth = CronExpression.daysInMonth[mappedFields.month[0] - 1];
  334. if (mappedFields.dayOfMonth[0] > daysInMonth) {
  335. throw new Error('Invalid explicit day of month definition');
  336. }
  337. return mappedFields.dayOfMonth
  338. .filter(function(dayOfMonth) {
  339. return dayOfMonth === 'L' ? true : dayOfMonth <= daysInMonth;
  340. })
  341. .sort(CronExpression._sortCompareFn);
  342. }
  343. };
  344. CronExpression._freezeFields = function(fields) {
  345. for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
  346. var field = CronExpression.map[i]; // Field name
  347. var value = fields[field];
  348. fields[field] = Object.freeze(value);
  349. }
  350. return Object.freeze(fields);
  351. };
  352. CronExpression.prototype._applyTimezoneShift = function(currentDate, dateMathVerb, method) {
  353. if ((method === 'Month') || (method === 'Day')) {
  354. var prevTime = currentDate.getTime();
  355. currentDate[dateMathVerb + method]();
  356. var currTime = currentDate.getTime();
  357. if (prevTime === currTime) {
  358. // Jumped into a not existent date due to a DST transition
  359. if ((currentDate.getMinutes() === 0) &&
  360. (currentDate.getSeconds() === 0)) {
  361. currentDate.addHour();
  362. } else if ((currentDate.getMinutes() === 59) &&
  363. (currentDate.getSeconds() === 59)) {
  364. currentDate.subtractHour();
  365. }
  366. }
  367. } else {
  368. var previousHour = currentDate.getHours();
  369. currentDate[dateMathVerb + method]();
  370. var currentHour = currentDate.getHours();
  371. var diff = currentHour - previousHour;
  372. if (diff === 2) {
  373. // Starting DST
  374. if (this.fields.hour.length !== 24) {
  375. // Hour is specified
  376. this._dstStart = currentHour;
  377. }
  378. } else if ((diff === 0) &&
  379. (currentDate.getMinutes() === 0) &&
  380. (currentDate.getSeconds() === 0)) {
  381. // Ending DST
  382. if (this.fields.hour.length !== 24) {
  383. // Hour is specified
  384. this._dstEnd = currentHour;
  385. }
  386. }
  387. }
  388. };
  389. /**
  390. * Find next or previous matching schedule date
  391. *
  392. * @return {CronDate}
  393. * @private
  394. */
  395. CronExpression.prototype._findSchedule = function _findSchedule (reverse) {
  396. /**
  397. * Match field value
  398. *
  399. * @param {String} value
  400. * @param {Array} sequence
  401. * @return {Boolean}
  402. * @private
  403. */
  404. function matchSchedule (value, sequence) {
  405. for (var i = 0, c = sequence.length; i < c; i++) {
  406. if (sequence[i] >= value) {
  407. return sequence[i] === value;
  408. }
  409. }
  410. return sequence[0] === value;
  411. }
  412. /**
  413. * Helps determine if the provided date is the correct nth occurence of the
  414. * desired day of week.
  415. *
  416. * @param {CronDate} date
  417. * @param {Number} nthDayOfWeek
  418. * @return {Boolean}
  419. * @private
  420. */
  421. function isNthDayMatch(date, nthDayOfWeek) {
  422. if (nthDayOfWeek < 6) {
  423. if (
  424. date.getDate() < 8 &&
  425. nthDayOfWeek === 1 // First occurence has to happen in first 7 days of the month
  426. ) {
  427. return true;
  428. }
  429. var offset = date.getDate() % 7 ? 1 : 0; // Math is off by 1 when dayOfWeek isn't divisible by 7
  430. var adjustedDate = date.getDate() - (date.getDate() % 7); // find the first occurance
  431. var occurrence = Math.floor(adjustedDate / 7) + offset;
  432. return occurrence === nthDayOfWeek;
  433. }
  434. return false;
  435. }
  436. /**
  437. * Helper function that checks if 'L' is in the array
  438. *
  439. * @param {Array} expressions
  440. */
  441. function isLInExpressions(expressions) {
  442. return expressions.length > 0 && expressions.some(function(expression) {
  443. return typeof expression === 'string' && expression.indexOf('L') >= 0;
  444. });
  445. }
  446. // Whether to use backwards directionality when searching
  447. reverse = reverse || false;
  448. var dateMathVerb = reverse ? 'subtract' : 'add';
  449. var currentDate = new CronDate(this._currentDate, this._tz);
  450. var startDate = this._startDate;
  451. var endDate = this._endDate;
  452. // Find matching schedule
  453. var startTimestamp = currentDate.getTime();
  454. var stepCount = 0;
  455. function isLastWeekdayOfMonthMatch(expressions) {
  456. return expressions.some(function(expression) {
  457. // There might be multiple expressions and not all of them will contain
  458. // the "L".
  459. if (!isLInExpressions([expression])) {
  460. return false;
  461. }
  462. // The first character represents the weekday
  463. var weekday = Number.parseInt(expression[0]) % 7;
  464. if (Number.isNaN(weekday)) {
  465. throw new Error('Invalid last weekday of the month expression: ' + expression);
  466. }
  467. return currentDate.getDay() === weekday && currentDate.isLastWeekdayOfMonth();
  468. });
  469. }
  470. while (stepCount < LOOP_LIMIT) {
  471. stepCount++;
  472. // Validate timespan
  473. if (reverse) {
  474. if (startDate && (currentDate.getTime() - startDate.getTime() < 0)) {
  475. throw new Error('Out of the timespan range');
  476. }
  477. } else {
  478. if (endDate && (endDate.getTime() - currentDate.getTime()) < 0) {
  479. throw new Error('Out of the timespan range');
  480. }
  481. }
  482. // Day of month and week matching:
  483. //
  484. // "The day of a command's execution can be specified by two fields --
  485. // day of month, and day of week. If both fields are restricted (ie,
  486. // aren't *), the command will be run when either field matches the cur-
  487. // rent time. For example, "30 4 1,15 * 5" would cause a command to be
  488. // run at 4:30 am on the 1st and 15th of each month, plus every Friday."
  489. //
  490. // http://unixhelp.ed.ac.uk/CGI/man-cgi?crontab+5
  491. //
  492. var dayOfMonthMatch = matchSchedule(currentDate.getDate(), this.fields.dayOfMonth);
  493. if (isLInExpressions(this.fields.dayOfMonth)) {
  494. dayOfMonthMatch = dayOfMonthMatch || currentDate.isLastDayOfMonth();
  495. }
  496. var dayOfWeekMatch = matchSchedule(currentDate.getDay(), this.fields.dayOfWeek);
  497. if (isLInExpressions(this.fields.dayOfWeek)) {
  498. dayOfWeekMatch = dayOfWeekMatch || isLastWeekdayOfMonthMatch(this.fields.dayOfWeek);
  499. }
  500. var isDayOfMonthWildcardMatch = this.fields.dayOfMonth.length >= CronExpression.daysInMonth[currentDate.getMonth()];
  501. var isDayOfWeekWildcardMatch = this.fields.dayOfWeek.length === CronExpression.constraints[5].max - CronExpression.constraints[5].min + 1;
  502. var currentHour = currentDate.getHours();
  503. // Add or subtract day if select day not match with month (according to calendar)
  504. if (!dayOfMonthMatch && (!dayOfWeekMatch || isDayOfWeekWildcardMatch)) {
  505. this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
  506. continue;
  507. }
  508. // Add or subtract day if not day of month is set (and no match) and day of week is wildcard
  509. if (!isDayOfMonthWildcardMatch && isDayOfWeekWildcardMatch && !dayOfMonthMatch) {
  510. this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
  511. continue;
  512. }
  513. // Add or subtract day if not day of week is set (and no match) and day of month is wildcard
  514. if (isDayOfMonthWildcardMatch && !isDayOfWeekWildcardMatch && !dayOfWeekMatch) {
  515. this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
  516. continue;
  517. }
  518. // Add or subtract day if day of week & nthDayOfWeek are set (and no match)
  519. if (
  520. this._nthDayOfWeek > 0 &&
  521. !isNthDayMatch(currentDate, this._nthDayOfWeek)
  522. ) {
  523. this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
  524. continue;
  525. }
  526. // Match month
  527. if (!matchSchedule(currentDate.getMonth() + 1, this.fields.month)) {
  528. this._applyTimezoneShift(currentDate, dateMathVerb, 'Month');
  529. continue;
  530. }
  531. // Match hour
  532. if (!matchSchedule(currentHour, this.fields.hour)) {
  533. if (this._dstStart !== currentHour) {
  534. this._dstStart = null;
  535. this._applyTimezoneShift(currentDate, dateMathVerb, 'Hour');
  536. continue;
  537. } else if (!matchSchedule(currentHour - 1, this.fields.hour)) {
  538. currentDate[dateMathVerb + 'Hour']();
  539. continue;
  540. }
  541. } else if (this._dstEnd === currentHour) {
  542. if (!reverse) {
  543. this._dstEnd = null;
  544. this._applyTimezoneShift(currentDate, 'add', 'Hour');
  545. continue;
  546. }
  547. }
  548. // Match minute
  549. if (!matchSchedule(currentDate.getMinutes(), this.fields.minute)) {
  550. this._applyTimezoneShift(currentDate, dateMathVerb, 'Minute');
  551. continue;
  552. }
  553. // Match second
  554. if (!matchSchedule(currentDate.getSeconds(), this.fields.second)) {
  555. this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
  556. continue;
  557. }
  558. // Increase a second in case in the first iteration the currentDate was not
  559. // modified
  560. if (startTimestamp === currentDate.getTime()) {
  561. if ((dateMathVerb === 'add') || (currentDate.getMilliseconds() === 0)) {
  562. this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
  563. } else {
  564. currentDate.setMilliseconds(0);
  565. }
  566. continue;
  567. }
  568. break;
  569. }
  570. if (stepCount >= LOOP_LIMIT) {
  571. throw new Error('Invalid expression, loop limit exceeded');
  572. }
  573. this._currentDate = new CronDate(currentDate, this._tz);
  574. this._hasIterated = true;
  575. return currentDate;
  576. };
  577. /**
  578. * Find next suitable date
  579. *
  580. * @public
  581. * @return {CronDate|Object}
  582. */
  583. CronExpression.prototype.next = function next () {
  584. var schedule = this._findSchedule();
  585. // Try to return ES6 compatible iterator
  586. if (this._isIterator) {
  587. return {
  588. value: schedule,
  589. done: !this.hasNext()
  590. };
  591. }
  592. return schedule;
  593. };
  594. /**
  595. * Find previous suitable date
  596. *
  597. * @public
  598. * @return {CronDate|Object}
  599. */
  600. CronExpression.prototype.prev = function prev () {
  601. var schedule = this._findSchedule(true);
  602. // Try to return ES6 compatible iterator
  603. if (this._isIterator) {
  604. return {
  605. value: schedule,
  606. done: !this.hasPrev()
  607. };
  608. }
  609. return schedule;
  610. };
  611. /**
  612. * Check if next suitable date exists
  613. *
  614. * @public
  615. * @return {Boolean}
  616. */
  617. CronExpression.prototype.hasNext = function() {
  618. var current = this._currentDate;
  619. var hasIterated = this._hasIterated;
  620. try {
  621. this._findSchedule();
  622. return true;
  623. } catch (err) {
  624. return false;
  625. } finally {
  626. this._currentDate = current;
  627. this._hasIterated = hasIterated;
  628. }
  629. };
  630. /**
  631. * Check if previous suitable date exists
  632. *
  633. * @public
  634. * @return {Boolean}
  635. */
  636. CronExpression.prototype.hasPrev = function() {
  637. var current = this._currentDate;
  638. var hasIterated = this._hasIterated;
  639. try {
  640. this._findSchedule(true);
  641. return true;
  642. } catch (err) {
  643. return false;
  644. } finally {
  645. this._currentDate = current;
  646. this._hasIterated = hasIterated;
  647. }
  648. };
  649. /**
  650. * Iterate over expression iterator
  651. *
  652. * @public
  653. * @param {Number} steps Numbers of steps to iterate
  654. * @param {Function} callback Optional callback
  655. * @return {Array} Array of the iterated results
  656. */
  657. CronExpression.prototype.iterate = function iterate (steps, callback) {
  658. var dates = [];
  659. if (steps >= 0) {
  660. for (var i = 0, c = steps; i < c; i++) {
  661. try {
  662. var item = this.next();
  663. dates.push(item);
  664. // Fire the callback
  665. if (callback) {
  666. callback(item, i);
  667. }
  668. } catch (err) {
  669. break;
  670. }
  671. }
  672. } else {
  673. for (var i = 0, c = steps; i > c; i--) {
  674. try {
  675. var item = this.prev();
  676. dates.push(item);
  677. // Fire the callback
  678. if (callback) {
  679. callback(item, i);
  680. }
  681. } catch (err) {
  682. break;
  683. }
  684. }
  685. }
  686. return dates;
  687. };
  688. /**
  689. * Reset expression iterator state
  690. *
  691. * @public
  692. */
  693. CronExpression.prototype.reset = function reset (newDate) {
  694. this._currentDate = new CronDate(newDate || this._options.currentDate);
  695. };
  696. /**
  697. * Stringify the expression
  698. *
  699. * @public
  700. * @param {Boolean} [includeSeconds] Should stringify seconds
  701. * @return {String}
  702. */
  703. CronExpression.prototype.stringify = function stringify(includeSeconds) {
  704. var resultArr = [];
  705. for (var i = includeSeconds ? 0 : 1, c = CronExpression.map.length; i < c; ++i) {
  706. var field = CronExpression.map[i];
  707. var value = this.fields[field];
  708. var constraint = CronExpression.constraints[i];
  709. if (field === 'dayOfMonth' && this.fields.month.length === 1) {
  710. constraint = { min: 1, max: CronExpression.daysInMonth[this.fields.month[0] - 1] };
  711. } else if (field === 'dayOfWeek') {
  712. // Prefer 0-6 range when serializing day of week field
  713. constraint = { min: 0, max: 6 };
  714. value = value[value.length - 1] === 7 ? value.slice(0, -1) : value;
  715. }
  716. resultArr.push(stringifyField(value, constraint.min, constraint.max));
  717. }
  718. return resultArr.join(' ');
  719. };
  720. /**
  721. * Parse input expression (async)
  722. *
  723. * @public
  724. * @param {String} expression Input expression
  725. * @param {Object} [options] Parsing options
  726. */
  727. CronExpression.parse = function parse(expression, options) {
  728. var self = this;
  729. if (typeof options === 'function') {
  730. options = {};
  731. }
  732. function parse (expression, options) {
  733. if (!options) {
  734. options = {};
  735. }
  736. if (typeof options.currentDate === 'undefined') {
  737. options.currentDate = new CronDate(undefined, self._tz);
  738. }
  739. // Is input expression predefined?
  740. if (CronExpression.predefined[expression]) {
  741. expression = CronExpression.predefined[expression];
  742. }
  743. // Split fields
  744. var fields = [];
  745. var atoms = (expression + '').trim().split(/\s+/);
  746. if (atoms.length > 6) {
  747. throw new Error('Invalid cron expression');
  748. }
  749. // Resolve fields
  750. var start = (CronExpression.map.length - atoms.length);
  751. for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
  752. var field = CronExpression.map[i]; // Field name
  753. var value = atoms[atoms.length > c ? i : i - start]; // Field value
  754. if (i < start || !value) { // Use default value
  755. fields.push(CronExpression._parseField(
  756. field,
  757. CronExpression.parseDefaults[i],
  758. CronExpression.constraints[i]
  759. )
  760. );
  761. } else {
  762. var val = field === 'dayOfWeek' ? parseNthDay(value) : value;
  763. fields.push(CronExpression._parseField(
  764. field,
  765. val,
  766. CronExpression.constraints[i]
  767. )
  768. );
  769. }
  770. }
  771. var mappedFields = {};
  772. for (var i = 0, c = CronExpression.map.length; i < c; i++) {
  773. var key = CronExpression.map[i];
  774. mappedFields[key] = fields[i];
  775. }
  776. var dayOfMonth = CronExpression._handleMaxDaysInMonth(mappedFields);
  777. mappedFields.dayOfMonth = dayOfMonth || mappedFields.dayOfMonth;
  778. return new CronExpression(mappedFields, options);
  779. /**
  780. * Parses out the # special character for the dayOfWeek field & adds it to options.
  781. *
  782. * @param {String} val
  783. * @return {String}
  784. * @private
  785. */
  786. function parseNthDay(val) {
  787. var atoms = val.split('#');
  788. if (atoms.length > 1) {
  789. var nthValue = +atoms[atoms.length - 1];
  790. if(/,/.test(val)) {
  791. throw new Error('Constraint error, invalid dayOfWeek `#` and `,` '
  792. + 'special characters are incompatible');
  793. }
  794. if(/\//.test(val)) {
  795. throw new Error('Constraint error, invalid dayOfWeek `#` and `/` '
  796. + 'special characters are incompatible');
  797. }
  798. if(/-/.test(val)) {
  799. throw new Error('Constraint error, invalid dayOfWeek `#` and `-` '
  800. + 'special characters are incompatible');
  801. }
  802. if (atoms.length > 2 || Number.isNaN(nthValue) || (nthValue < 1 || nthValue > 5)) {
  803. throw new Error('Constraint error, invalid dayOfWeek occurrence number (#)');
  804. }
  805. options.nthDayOfWeek = nthValue;
  806. return atoms[0];
  807. }
  808. return val;
  809. }
  810. }
  811. return parse(expression, options);
  812. };
  813. /**
  814. * Convert cron fields back to Cron Expression
  815. *
  816. * @public
  817. * @param {Object} fields Input fields
  818. * @param {Object} [options] Parsing options
  819. * @return {Object}
  820. */
  821. CronExpression.fieldsToExpression = function fieldsToExpression(fields, options) {
  822. function validateConstraints (field, values, constraints) {
  823. if (!values) {
  824. throw new Error('Validation error, Field ' + field + ' is missing');
  825. }
  826. if (values.length === 0) {
  827. throw new Error('Validation error, Field ' + field + ' contains no values');
  828. }
  829. for (var i = 0, c = values.length; i < c; i++) {
  830. var value = values[i];
  831. if (CronExpression._isValidConstraintChar(constraints, value)) {
  832. continue;
  833. }
  834. // Check constraints
  835. if (typeof value !== 'number' || Number.isNaN(value) || value < constraints.min || value > constraints.max) {
  836. throw new Error(
  837. 'Constraint error, got value ' + value + ' expected range ' +
  838. constraints.min + '-' + constraints.max
  839. );
  840. }
  841. }
  842. }
  843. var mappedFields = {};
  844. for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
  845. var field = CronExpression.map[i]; // Field name
  846. var values = fields[field];
  847. validateConstraints(
  848. field,
  849. values,
  850. CronExpression.constraints[i]
  851. );
  852. var copy = [];
  853. var j = -1;
  854. while (++j < values.length) {
  855. copy[j] = values[j];
  856. }
  857. values = copy.sort(CronExpression._sortCompareFn)
  858. .filter(function(item, pos, ary) {
  859. return !pos || item !== ary[pos - 1];
  860. });
  861. if (values.length !== copy.length) {
  862. throw new Error('Validation error, Field ' + field + ' contains duplicate values');
  863. }
  864. mappedFields[field] = values;
  865. }
  866. var dayOfMonth = CronExpression._handleMaxDaysInMonth(mappedFields);
  867. mappedFields.dayOfMonth = dayOfMonth || mappedFields.dayOfMonth;
  868. return new CronExpression(mappedFields, options || {});
  869. };
  870. module.exports = CronExpression;