expression.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  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.dayValidCharacters = /^[\d|/|*|\-|,|\?]+$/;
  132. CronExpression.validCharacters = {
  133. second: CronExpression.standardValidCharacters,
  134. minute: CronExpression.standardValidCharacters,
  135. hour: CronExpression.standardValidCharacters,
  136. dayOfMonth: CronExpression.dayValidCharacters,
  137. month: CronExpression.standardValidCharacters,
  138. dayOfWeek: CronExpression.dayValidCharacters,
  139. }
  140. /**
  141. * Parse input interval
  142. *
  143. * @param {String} field Field symbolic name
  144. * @param {String} value Field value
  145. * @param {Array} constraints Range upper and lower constraints
  146. * @return {Array} Sequence of sorted values
  147. * @private
  148. */
  149. CronExpression._parseField = function _parseField (field, value, constraints) {
  150. // Replace aliases
  151. switch (field) {
  152. case 'month':
  153. case 'dayOfWeek':
  154. var aliases = CronExpression.aliases[field];
  155. value = value.replace(/[a-z]{1,3}/gi, function(match) {
  156. match = match.toLowerCase();
  157. if (typeof aliases[match] !== undefined) {
  158. return aliases[match];
  159. } else {
  160. throw new Error('Cannot resolve alias "' + match + '"')
  161. }
  162. });
  163. break;
  164. }
  165. // Check for valid characters.
  166. if (!(CronExpression.validCharacters[field].test(value))) {
  167. throw new Error('Invalid characters, got value: ' + value)
  168. }
  169. // Replace '*' and '?'
  170. if (value.indexOf('*') !== -1) {
  171. value = value.replace(/\*/g, constraints.join('-'));
  172. } else if (value.indexOf('?') !== -1) {
  173. value = value.replace(/\?/g, constraints.join('-'));
  174. }
  175. //
  176. // Inline parsing functions
  177. //
  178. // Parser path:
  179. // - parseSequence
  180. // - parseRepeat
  181. // - parseRange
  182. /**
  183. * Parse sequence
  184. *
  185. * @param {String} val
  186. * @return {Array}
  187. * @private
  188. */
  189. function parseSequence (val) {
  190. var stack = [];
  191. function handleResult (result) {
  192. var max = stack.length > 0 ? Math.max.apply(Math, stack) : -1;
  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. if (value > max) {
  204. stack.push(value);
  205. }
  206. max = Math.max.apply(Math, stack);
  207. }
  208. } else { // Scalar value
  209. result = +result;
  210. // Check constraints
  211. if (result < constraints[0] || result > constraints[1]) {
  212. throw new Error(
  213. 'Constraint error, got value ' + result + ' expected range ' +
  214. constraints[0] + '-' + constraints[1]
  215. );
  216. }
  217. if (field == 'dayOfWeek') {
  218. result = result % 7;
  219. }
  220. stack.push(result);
  221. }
  222. }
  223. var atoms = val.split(',');
  224. if (atoms.length > 1) {
  225. for (var i = 0, c = atoms.length; i < c; i++) {
  226. handleResult(parseRepeat(atoms[i]));
  227. }
  228. } else {
  229. handleResult(parseRepeat(val));
  230. }
  231. stack.sort(function(a, b) {
  232. return a - b;
  233. });
  234. return stack;
  235. }
  236. /**
  237. * Parse repetition interval
  238. *
  239. * @param {String} val
  240. * @return {Array}
  241. */
  242. function parseRepeat (val) {
  243. var repeatInterval = 1;
  244. var atoms = val.split('/');
  245. if (atoms.length > 1) {
  246. return parseRange(atoms[0], atoms[atoms.length - 1]);
  247. }
  248. return parseRange(val, repeatInterval);
  249. }
  250. /**
  251. * Parse range
  252. *
  253. * @param {String} val
  254. * @param {Number} repeatInterval Repetition interval
  255. * @return {Array}
  256. * @private
  257. */
  258. function parseRange (val, repeatInterval) {
  259. var stack = [];
  260. var atoms = val.split('-');
  261. if (atoms.length > 1 ) {
  262. // Invalid range, return value
  263. if (atoms.length < 2) {
  264. return +val;
  265. }
  266. if (!atoms[0].length) {
  267. if (!atoms[1].length) {
  268. throw new Error('Invalid range: ' + val);
  269. }
  270. return +val;
  271. }
  272. // Validate range
  273. var min = +atoms[0];
  274. var max = +atoms[1];
  275. if (safeIsNaN(min) || safeIsNaN(max) ||
  276. min < constraints[0] || max > constraints[1]) {
  277. throw new Error(
  278. 'Constraint error, got range ' +
  279. min + '-' + max +
  280. ' expected range ' +
  281. constraints[0] + '-' + constraints[1]
  282. );
  283. } else if (min >= max) {
  284. throw new Error('Invalid range: ' + val);
  285. }
  286. // Create range
  287. var repeatIndex = +repeatInterval;
  288. if (safeIsNaN(repeatIndex) || repeatIndex <= 0) {
  289. throw new Error('Constraint error, cannot repeat at every ' + repeatIndex + ' time.');
  290. }
  291. for (var index = min, count = max; index <= count; index++) {
  292. if (repeatIndex > 0 && (repeatIndex % repeatInterval) === 0) {
  293. repeatIndex = 1;
  294. stack.push(index);
  295. } else {
  296. repeatIndex++;
  297. }
  298. }
  299. return stack;
  300. }
  301. return +val;
  302. }
  303. return parseSequence(value);
  304. };
  305. CronExpression.prototype._applyTimezoneShift = function(currentDate, dateMathVerb, method) {
  306. if ((method === 'Month') || (method === 'Day')) {
  307. var prevTime = currentDate.getTime();
  308. currentDate[dateMathVerb + method]();
  309. var currTime = currentDate.getTime();
  310. if (prevTime === currTime) {
  311. // Jumped into a not existent date due to a DST transition
  312. if ((currentDate.getMinutes() === 0) &&
  313. (currentDate.getSeconds() === 0)) {
  314. currentDate.addHour();
  315. } else if ((currentDate.getMinutes() === 59) &&
  316. (currentDate.getSeconds() === 59)) {
  317. currentDate.subtractHour();
  318. }
  319. }
  320. } else {
  321. var previousHour = currentDate.getHours();
  322. currentDate[dateMathVerb + method]();
  323. var currentHour = currentDate.getHours();
  324. var diff = currentHour - previousHour;
  325. if (diff === 2) {
  326. // Starting DST
  327. if (this._fields.hour.length !== 24) {
  328. // Hour is specified
  329. this._dstStart = currentHour;
  330. }
  331. } else if ((diff === 0) &&
  332. (currentDate.getMinutes() === 0) &&
  333. (currentDate.getSeconds() === 0)) {
  334. // Ending DST
  335. if (this._fields.hour.length !== 24) {
  336. // Hour is specified
  337. this._dstEnd = currentHour;
  338. }
  339. }
  340. }
  341. };
  342. /**
  343. * Find next or previous matching schedule date
  344. *
  345. * @return {CronDate}
  346. * @private
  347. */
  348. CronExpression.prototype._findSchedule = function _findSchedule (reverse) {
  349. /**
  350. * Match field value
  351. *
  352. * @param {String} value
  353. * @param {Array} sequence
  354. * @return {Boolean}
  355. * @private
  356. */
  357. function matchSchedule (value, sequence) {
  358. for (var i = 0, c = sequence.length; i < c; i++) {
  359. if (sequence[i] >= value) {
  360. return sequence[i] === value;
  361. }
  362. }
  363. return sequence[0] === value;
  364. }
  365. /**
  366. * Helps determine if the provided date is the correct nth occurence of the
  367. * desired day of week.
  368. *
  369. * @param {CronDate} date
  370. * @param {Number} nthDayOfWeek
  371. * @return {Boolean}
  372. * @private
  373. */
  374. function isNthDayMatch(date, nthDayOfWeek) {
  375. if (nthDayOfWeek < 6) {
  376. if (
  377. date.getDate() < 8 &&
  378. nthDayOfWeek === 1 // First occurence has to happen in first 7 days of the month
  379. ) {
  380. return true;
  381. }
  382. var offset = date.getDate() % 7 ? 1 : 0; // Math is off by 1 when dayOfWeek isn't divisible by 7
  383. var adjustedDate = date.getDate() - (date.getDate() % 7); // find the first occurance
  384. var occurrence = Math.floor(adjustedDate / 7) + offset;
  385. return occurrence === nthDayOfWeek;
  386. }
  387. return false;
  388. }
  389. // Whether to use backwards directionality when searching
  390. reverse = reverse || false;
  391. var dateMathVerb = reverse ? 'subtract' : 'add';
  392. var currentDate = new CronDate(this._currentDate, this._tz);
  393. var startDate = this._startDate;
  394. var endDate = this._endDate;
  395. // Find matching schedule
  396. var startTimestamp = currentDate.getTime();
  397. var stepCount = 0;
  398. while (stepCount < LOOP_LIMIT) {
  399. stepCount++;
  400. // Validate timespan
  401. if (reverse) {
  402. if (startDate && (currentDate.getTime() - startDate.getTime() < 0)) {
  403. throw new Error('Out of the timespan range');
  404. }
  405. } else {
  406. if (endDate && (endDate.getTime() - currentDate.getTime()) < 0) {
  407. throw new Error('Out of the timespan range');
  408. }
  409. }
  410. // Day of month and week matching:
  411. //
  412. // "The day of a command's execution can be specified by two fields --
  413. // day of month, and day of week. If both fields are restricted (ie,
  414. // aren't *), the command will be run when either field matches the cur-
  415. // rent time. For example, "30 4 1,15 * 5" would cause a command to be
  416. // run at 4:30 am on the 1st and 15th of each month, plus every Friday."
  417. //
  418. // http://unixhelp.ed.ac.uk/CGI/man-cgi?crontab+5
  419. //
  420. var dayOfMonthMatch = matchSchedule(currentDate.getDate(), this._fields.dayOfMonth);
  421. var dayOfWeekMatch = matchSchedule(currentDate.getDay(), this._fields.dayOfWeek);
  422. var isDayOfMonthWildcardMatch = isWildcardRange(this._fields.dayOfMonth, CronExpression.constraints[3]);
  423. var isDayOfWeekWildcardMatch = isWildcardRange(this._fields.dayOfWeek, CronExpression.constraints[5]);
  424. var currentHour = currentDate.getHours();
  425. // Add or subtract day if select day not match with month (according to calendar)
  426. if (!dayOfMonthMatch && !dayOfWeekMatch) {
  427. this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
  428. continue;
  429. }
  430. // Add or subtract day if not day of month is set (and no match) and day of week is wildcard
  431. if (!isDayOfMonthWildcardMatch && isDayOfWeekWildcardMatch && !dayOfMonthMatch) {
  432. this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
  433. continue;
  434. }
  435. // Add or subtract day if not day of week is set (and no match) and day of month is wildcard
  436. if (isDayOfMonthWildcardMatch && !isDayOfWeekWildcardMatch && !dayOfWeekMatch) {
  437. this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
  438. continue;
  439. }
  440. // Add or subtract day if day of month and week are non-wildcard values and both doesn't match
  441. if (!(isDayOfMonthWildcardMatch && isDayOfWeekWildcardMatch) &&
  442. !dayOfMonthMatch && !dayOfWeekMatch) {
  443. this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
  444. continue;
  445. }
  446. // Add or subtract day if day of week & nthDayOfWeek are set (and no match)
  447. if (
  448. this._nthDayOfWeek > 0 &&
  449. !isNthDayMatch(currentDate, this._nthDayOfWeek)
  450. ) {
  451. this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
  452. continue;
  453. }
  454. // Match month
  455. if (!matchSchedule(currentDate.getMonth() + 1, this._fields.month)) {
  456. this._applyTimezoneShift(currentDate, dateMathVerb, 'Month');
  457. continue;
  458. }
  459. // Match hour
  460. if (!matchSchedule(currentHour, this._fields.hour)) {
  461. if (this._dstStart !== currentHour) {
  462. this._dstStart = null;
  463. this._applyTimezoneShift(currentDate, dateMathVerb, 'Hour');
  464. continue;
  465. } else if (!matchSchedule(currentHour - 1, this._fields.hour)) {
  466. currentDate[dateMathVerb + 'Hour']();
  467. continue;
  468. }
  469. } else if (this._dstEnd === currentHour) {
  470. if (!reverse) {
  471. this._dstEnd = null;
  472. this._applyTimezoneShift(currentDate, 'add', 'Hour');
  473. continue;
  474. }
  475. }
  476. // Match minute
  477. if (!matchSchedule(currentDate.getMinutes(), this._fields.minute)) {
  478. this._applyTimezoneShift(currentDate, dateMathVerb, 'Minute');
  479. continue;
  480. }
  481. // Match second
  482. if (!matchSchedule(currentDate.getSeconds(), this._fields.second)) {
  483. this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
  484. continue;
  485. }
  486. // Increase a second in case in the first iteration the currentDate was not
  487. // modified
  488. if (startTimestamp === currentDate.getTime()) {
  489. if ((dateMathVerb === 'add') || (currentDate.getMilliseconds() === 0)) {
  490. this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
  491. } else {
  492. currentDate.setMilliseconds(0);
  493. }
  494. continue;
  495. }
  496. break;
  497. }
  498. if (stepCount >= LOOP_LIMIT) {
  499. throw new Error('Invalid expression, loop limit exceeded');
  500. }
  501. this._currentDate = new CronDate(currentDate, this._tz);
  502. this._hasIterated = true;
  503. return currentDate;
  504. };
  505. /**
  506. * Find next suitable date
  507. *
  508. * @public
  509. * @return {CronDate|Object}
  510. */
  511. CronExpression.prototype.next = function next () {
  512. var schedule = this._findSchedule();
  513. // Try to return ES6 compatible iterator
  514. if (this._isIterator) {
  515. return {
  516. value: schedule,
  517. done: !this.hasNext()
  518. };
  519. }
  520. return schedule;
  521. };
  522. /**
  523. * Find previous suitable date
  524. *
  525. * @public
  526. * @return {CronDate|Object}
  527. */
  528. CronExpression.prototype.prev = function prev () {
  529. var schedule = this._findSchedule(true);
  530. // Try to return ES6 compatible iterator
  531. if (this._isIterator) {
  532. return {
  533. value: schedule,
  534. done: !this.hasPrev()
  535. };
  536. }
  537. return schedule;
  538. };
  539. /**
  540. * Check if next suitable date exists
  541. *
  542. * @public
  543. * @return {Boolean}
  544. */
  545. CronExpression.prototype.hasNext = function() {
  546. var current = this._currentDate;
  547. var hasIterated = this._hasIterated;
  548. try {
  549. this._findSchedule();
  550. return true;
  551. } catch (err) {
  552. return false;
  553. } finally {
  554. this._currentDate = current;
  555. this._hasIterated = hasIterated;
  556. }
  557. };
  558. /**
  559. * Check if previous suitable date exists
  560. *
  561. * @public
  562. * @return {Boolean}
  563. */
  564. CronExpression.prototype.hasPrev = function() {
  565. var current = this._currentDate;
  566. var hasIterated = this._hasIterated;
  567. try {
  568. this._findSchedule(true);
  569. return true;
  570. } catch (err) {
  571. return false;
  572. } finally {
  573. this._currentDate = current;
  574. this._hasIterated = hasIterated;
  575. }
  576. };
  577. /**
  578. * Iterate over expression iterator
  579. *
  580. * @public
  581. * @param {Number} steps Numbers of steps to iterate
  582. * @param {Function} callback Optional callback
  583. * @return {Array} Array of the iterated results
  584. */
  585. CronExpression.prototype.iterate = function iterate (steps, callback) {
  586. var dates = [];
  587. if (steps >= 0) {
  588. for (var i = 0, c = steps; i < c; i++) {
  589. try {
  590. var item = this.next();
  591. dates.push(item);
  592. // Fire the callback
  593. if (callback) {
  594. callback(item, i);
  595. }
  596. } catch (err) {
  597. break;
  598. }
  599. }
  600. } else {
  601. for (var i = 0, c = steps; i > c; i--) {
  602. try {
  603. var item = this.prev();
  604. dates.push(item);
  605. // Fire the callback
  606. if (callback) {
  607. callback(item, i);
  608. }
  609. } catch (err) {
  610. break;
  611. }
  612. }
  613. }
  614. return dates;
  615. };
  616. /**
  617. * Reset expression iterator state
  618. *
  619. * @public
  620. */
  621. CronExpression.prototype.reset = function reset (newDate) {
  622. this._currentDate = new CronDate(newDate || this._options.currentDate);
  623. };
  624. /**
  625. * Parse input expression (async)
  626. *
  627. * @public
  628. * @param {String} expression Input expression
  629. * @param {Object} [options] Parsing options
  630. * @param {Function} [callback]
  631. */
  632. CronExpression.parse = function parse(expression, options, callback) {
  633. var self = this;
  634. if (typeof options === 'function') {
  635. callback = options;
  636. options = {};
  637. }
  638. function parse (expression, options) {
  639. if (!options) {
  640. options = {};
  641. }
  642. if (typeof options.currentDate === 'undefined') {
  643. options.currentDate = new CronDate(undefined, self._tz);
  644. }
  645. // Is input expression predefined?
  646. if (CronExpression.predefined[expression]) {
  647. expression = CronExpression.predefined[expression];
  648. }
  649. // Split fields
  650. var fields = [];
  651. var atoms = (expression + '').trim().split(/\s+/);
  652. if (atoms.length > 6) {
  653. throw new Error('Invalid cron expression');
  654. }
  655. // Resolve fields
  656. var start = (CronExpression.map.length - atoms.length);
  657. for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
  658. var field = CronExpression.map[i]; // Field name
  659. var value = atoms[atoms.length > c ? i : i - start]; // Field value
  660. if (i < start || !value) { // Use default value
  661. fields.push(CronExpression._parseField(
  662. field,
  663. CronExpression.parseDefaults[i],
  664. CronExpression.constraints[i])
  665. );
  666. } else {
  667. var val = field === 'dayOfWeek' ? parseNthDay(value) : value;
  668. fields.push(CronExpression._parseField(
  669. field,
  670. val,
  671. CronExpression.constraints[i])
  672. );
  673. }
  674. }
  675. var mappedFields = {};
  676. for (var i = 0, c = CronExpression.map.length; i < c; i++) {
  677. var key = CronExpression.map[i];
  678. mappedFields[key] = fields[i];
  679. }
  680. // Filter out any day of month value that is larger than given month expects
  681. if (mappedFields.month.length === 1) {
  682. var daysInMonth = CronExpression.daysInMonth[mappedFields.month[0] - 1];
  683. if (mappedFields.dayOfMonth[0] > daysInMonth) {
  684. throw new Error('Invalid explicit day of month definition');
  685. }
  686. mappedFields.dayOfMonth = mappedFields.dayOfMonth.filter(function(dayOfMonth) {
  687. return dayOfMonth <= daysInMonth;
  688. });
  689. }
  690. return new CronExpression(mappedFields, options);
  691. /**
  692. * Parses out the # special character for the dayOfWeek field & adds it to options.
  693. *
  694. * @param {String} val
  695. * @return {String}
  696. * @private
  697. */
  698. function parseNthDay(val) {
  699. var atoms = val.split('#');
  700. if (atoms.length > 1) {
  701. var nthValue = +atoms[atoms.length - 1];
  702. if(/,/.test(val)) {
  703. throw new Error('Constraint error, invalid dayOfWeek `#` and `,` '
  704. + 'special characters are incompatible');
  705. }
  706. if(/\//.test(val)) {
  707. throw new Error('Constraint error, invalid dayOfWeek `#` and `/` '
  708. + 'special characters are incompatible');
  709. }
  710. if(/-/.test(val)) {
  711. throw new Error('Constraint error, invalid dayOfWeek `#` and `-` '
  712. + 'special characters are incompatible');
  713. }
  714. if (atoms.length > 2 || safeIsNaN(nthValue) || (nthValue < 1 || nthValue > 5)) {
  715. throw new Error('Constraint error, invalid dayOfWeek occurrence number (#)');
  716. }
  717. options.nthDayOfWeek = nthValue;
  718. return atoms[0];
  719. }
  720. return val;
  721. }
  722. }
  723. return parse(expression, options);
  724. };
  725. module.exports = CronExpression;