expression.js 22 KB

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