croner.cjs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032
  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  3. typeof define === 'function' && define.amd ? define(factory) :
  4. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Cron = factory());
  5. })(this, (function () { 'use strict';
  6. /**
  7. * "Converts" a date to a specific time zone
  8. *
  9. * Note: This is only for specific and controlled usage,
  10. * as the internal UTC time of the resulting object will be off.
  11. *
  12. * Example:
  13. * let normalDate = new Date(); // d is a normal Date instance, with local timezone and correct utc representation
  14. * tzDate = convertTZ(d, 'America/New_York') // d is a tainted Date instance, where getHours()
  15. * (for example) will return local time in new york, but getUTCHours()
  16. * will return something irrelevant.
  17. *
  18. * @param {Date} date - Input date
  19. * @param {string} tzString - Timezone string in Europe/Stockholm format
  20. * @returns {Date}
  21. */
  22. function convertTZ(date, tzString) {
  23. return new Date(date.toLocaleString("en-US", {timeZone: tzString}));
  24. }
  25. /**
  26. * Converts date to CronDate
  27. * @constructor
  28. *
  29. * @param {CronDate|date|string} [date] - Input date, if using string representation ISO 8001 (2015-11-24T19:40:00) local timezone is expected
  30. * @param {string} [timezone] - String representation of target timezone in Europe/Stockholm format.
  31. */
  32. function CronDate (date, timezone) {
  33. this.timezone = timezone;
  34. if (date && date instanceof Date) {
  35. this.fromDate(date);
  36. } else if (date === void 0) {
  37. this.fromDate(new Date());
  38. } else if (date && typeof date === "string") {
  39. this.fromString(date);
  40. } else if (date instanceof CronDate) {
  41. this.fromCronDate(date);
  42. } else {
  43. throw new TypeError("CronDate: Invalid type (" + typeof date + ") passed as parameter to CronDate constructor");
  44. }
  45. }
  46. /**
  47. * Sets internals using a Date
  48. * @private
  49. *
  50. * @param {Date} date - Input date
  51. */
  52. CronDate.prototype.fromDate = function (date) {
  53. if (this.timezone) {
  54. date = convertTZ(date, this.timezone);
  55. }
  56. this.milliseconds = date.getMilliseconds();
  57. this.seconds = date.getSeconds();
  58. this.minutes = date.getMinutes();
  59. this.hours = date.getHours();
  60. this.days = date.getDate();
  61. this.months = date.getMonth();
  62. this.years = date.getFullYear();
  63. };
  64. /**
  65. * Sets internals by deep copying another CronDate
  66. * @private
  67. *
  68. * @param {CronDate} date - Input date
  69. */
  70. CronDate.prototype.fromCronDate = function (date) {
  71. this.timezone = date.timezone;
  72. this.milliseconds = date.milliseconds;
  73. this.seconds = date.seconds;
  74. this.minutes = date.minutes;
  75. this.hours = date.hours;
  76. this.days = date.days;
  77. this.months = date.months;
  78. this.years = date.years;
  79. };
  80. /**
  81. * Reset internal parameters (seconds, minutes, hours) that may have exceeded their ranges
  82. * @private
  83. *
  84. * @param {Date} date - Input date
  85. */
  86. CronDate.prototype.apply = function () {
  87. const newDate = new Date(this.years, this.months, this.days, this.hours, this.minutes, this.seconds, this.milliseconds);
  88. this.milliseconds = newDate.getMilliseconds();
  89. this.seconds = newDate.getSeconds();
  90. this.minutes = newDate.getMinutes();
  91. this.hours = newDate.getHours();
  92. this.days = newDate.getDate();
  93. this.months = newDate.getMonth();
  94. this.years = newDate.getFullYear();
  95. };
  96. /**
  97. * Sets internals by parsing a string
  98. * @private
  99. *
  100. * @param {Date} date - Input date
  101. */
  102. CronDate.prototype.fromString = function (str) {
  103. const parsedDate = this.parseISOLocal(str);
  104. // Throw if we did get an invalid date
  105. if( isNaN(parsedDate) ) {
  106. throw new TypeError("CronDate: Provided string value for CronDate could not be parsed as date.");
  107. }
  108. this.fromDate(parsedDate);
  109. };
  110. /**
  111. * Increment to next run time
  112. * @public
  113. *
  114. * @param {string} pattern - The pattern used to increment current state
  115. * @param {boolean} [rerun=false] - If this is an internal incremental run
  116. * @return {CronDate|null} - Returns itself for chaining, or null if increment wasnt possible
  117. */
  118. CronDate.prototype.increment = function (pattern, rerun) {
  119. if (!rerun) {
  120. this.seconds += 1;
  121. }
  122. this.milliseconds = 0;
  123. const
  124. origTime = this.getTime(),
  125. /**
  126. * Find next
  127. *
  128. * @param {string} target
  129. * @param {string} pattern
  130. * @param {string} offset
  131. * @param {string} override
  132. *
  133. * @returns {boolean}
  134. *
  135. */
  136. findNext = (target, pattern, offset, override) => {
  137. const startPos = (override === void 0) ? this[target] + offset : 0 + offset;
  138. for( let i = startPos; i < pattern[target].length; i++ ) {
  139. // If pattern matches and, in case of days, weekday matches, go on
  140. if( pattern[target][i] ) {
  141. // Special handling for L (last day of month), when we are searching for days
  142. if (target === "days" && pattern.lastDayOfMonth) {
  143. let baseDate = this.getDate(true);
  144. // Set days to one day after today, if month changes, then we are at the last day of the month
  145. baseDate.setDate(i-offset+1);
  146. if (baseDate.getMonth() !== this["months"]) {
  147. this[target] = i-offset;
  148. return true;
  149. }
  150. // Normal handling
  151. } else {
  152. this[target] = i-offset;
  153. return true;
  154. }
  155. }
  156. }
  157. return false;
  158. },
  159. resetPrevious = (offset) => {
  160. // Now when we have gone to next minute, we have to set seconds to the first match
  161. // Now we are at 00:01:05 following the same example.
  162. //
  163. // This goes all the way back to seconds, hence the reverse loop.
  164. while(doing + offset >= 0) {
  165. // Ok, reset current member(e.g. seconds) to first match in pattern, using
  166. // the same method as aerlier
  167. //
  168. // Note the fourth parameter, stating that we should start matching the pattern
  169. // from zero, instead of current time.
  170. findNext(toDo[doing + offset][0], pattern, toDo[doing + offset][2], 0);
  171. // Go back up, days -> hours -> minutes -> seconds
  172. doing--;
  173. }
  174. };
  175. // Array of work to be done, consisting of subarrays described below:
  176. // [
  177. // First item is which member to process,
  178. // Second item is which member to increment if we didn't find a mathch in current item,
  179. // Third item is an offset. if months is handled 0-11 in js date object, and we get 1-12
  180. // from pattern. Offset should be -1
  181. // ]
  182. const toDo = [
  183. ["seconds", "minutes", 0],
  184. ["minutes", "hours", 0],
  185. ["hours", "days", 0],
  186. ["days", "months", -1],
  187. ["months", "years", 0]
  188. ];
  189. // Ok, we're working our way trough the toDo array, top to bottom
  190. // If we reach 5, work is done
  191. let doing = 0;
  192. while(doing < 5) {
  193. // findNext sets the current member to next match in pattern
  194. // If time is 00:00:01 and pattern says *:*:05, seconds will
  195. // be set to 5
  196. // Store current value at current level
  197. let currentValue = this[toDo[doing][0]];
  198. // If pattern didn't provide a match, increment next value (e.g. minues)
  199. if(!findNext(toDo[doing][0], pattern, toDo[doing][2])) {
  200. this[toDo[doing][1]]++;
  201. // Reset current level and previous levels
  202. resetPrevious(0);
  203. // If pattern provided a match, but changed current value ...
  204. } else if (currentValue !== this[toDo[doing][0]]) {
  205. // Reset previous levels
  206. resetPrevious(-1);
  207. }
  208. // Bail out if an impossible pattern is used
  209. if (this.years >= 4000) {
  210. return null;
  211. }
  212. // Gp down, seconds -> minutes -> hours -> days -> months -> year
  213. doing++;
  214. }
  215. // This is a special case for weekday, as the user isn't able to combine date/month patterns
  216. // with weekday patterns, it's just to increment days until we get a match.
  217. while (!pattern.daysOfWeek[this.getDate(true).getDay()]) {
  218. this.days += 1;
  219. // Reset everything before days
  220. doing = 2;
  221. resetPrevious();
  222. }
  223. // If anything changed, recreate this CronDate and run again without incrementing
  224. if (origTime != this.getTime()) {
  225. this.apply();
  226. return this.increment(pattern, true);
  227. } else {
  228. return this;
  229. }
  230. };
  231. /**
  232. * Convert current state back to a javascript Date()
  233. * @public
  234. *
  235. * @param {boolean} internal - If this is an internal call
  236. * @returns {Date}
  237. */
  238. CronDate.prototype.getDate = function (internal) {
  239. const targetDate = new Date(this.years, this.months, this.days, this.hours, this.minutes, this.seconds, this.milliseconds);
  240. if (internal || !this.timezone) {
  241. return targetDate;
  242. } else {
  243. const offset = convertTZ(targetDate, this.timezone).getTime()-targetDate.getTime();
  244. return new Date(targetDate.getTime()-offset);
  245. }
  246. };
  247. /**
  248. * Convert current state back to a javascript Date() and return UTC milliseconds
  249. * @public
  250. *
  251. * @param {boolean} internal - If this is an internal call
  252. * @returns {Date}
  253. */
  254. CronDate.prototype.getTime = function (internal) {
  255. return this.getDate(internal).getTime();
  256. };
  257. /**
  258. * Takes a iso 8001 local date time string and creates a Date object
  259. * @private
  260. *
  261. * @param {string} dateTimeString - an ISO 8001 format date and time string
  262. * with all components, e.g. 2015-11-24T19:40:00
  263. * @returns {Date|number} - Date instance from parsing the string. May be NaN.
  264. */
  265. CronDate.prototype.parseISOLocal = function (dateTimeString) {
  266. const dateTimeStringSplit = dateTimeString.split(/\D/);
  267. // Check for completeness
  268. if (dateTimeStringSplit.length < 6) {
  269. return NaN;
  270. }
  271. const
  272. year = parseInt(dateTimeStringSplit[0], 10),
  273. month = parseInt(dateTimeStringSplit[1], 10),
  274. day = parseInt(dateTimeStringSplit[2], 10),
  275. hour = parseInt(dateTimeStringSplit[3], 10),
  276. minute = parseInt(dateTimeStringSplit[4], 10),
  277. second = parseInt(dateTimeStringSplit[5], 10);
  278. // Check parts for numeric
  279. if( isNaN(year) || isNaN(month) || isNaN(day) || isNaN(hour) || isNaN(minute) || isNaN(second) ) {
  280. return NaN;
  281. } else {
  282. let generatedDate;
  283. // Check for UTC flag
  284. if ((dateTimeString.indexOf("Z") > 0)) {
  285. // Handle date as UTC
  286. generatedDate = new Date(Date.UTC(year, month-1, day, hour, minute, second));
  287. // Check generated date
  288. if (year == generatedDate.getUTCFullYear()
  289. && month == generatedDate.getUTCMonth()+1
  290. && day == generatedDate.getUTCDate()
  291. && hour == generatedDate.getUTCHours()
  292. && minute == generatedDate.getUTCMinutes()
  293. && second == generatedDate.getUTCSeconds()) {
  294. return generatedDate;
  295. } else {
  296. return NaN;
  297. }
  298. } else {
  299. // Handle date as local time
  300. generatedDate = new Date(year, month-1, day, hour, minute, second);
  301. // Check generated date
  302. if (year == generatedDate.getFullYear()
  303. && month == generatedDate.getMonth()+1
  304. && day == generatedDate.getDate()
  305. && hour == generatedDate.getHours()
  306. && minute == generatedDate.getMinutes()
  307. && second == generatedDate.getSeconds()) {
  308. return generatedDate;
  309. } else {
  310. return NaN;
  311. }
  312. }
  313. }
  314. };
  315. /**
  316. * Name for each part of the cron pattern
  317. * @typedef {("seconds" | "minutes" | "hours" | "days" | "months" | "daysOfWeek")} CronPatternPart
  318. */
  319. /**
  320. * Offset, 0 or -1.
  321. *
  322. * 0 for seconds,minutes and hours as they start on 1.
  323. * -1 on days and months, as the start on 0
  324. *
  325. * @typedef {Number} CronIndexOffset
  326. */
  327. /**
  328. * Create a CronPattern instance from pattern string ('* * * * * *')
  329. * @constructor
  330. * @param {string} pattern - Input pattern
  331. * @param {string} timezone - Input timezone, used for '?'-substitution
  332. */
  333. function CronPattern (pattern, timezone) {
  334. this.pattern = pattern;
  335. this.timezone = timezone;
  336. this.seconds = Array(60).fill(0); // 0-59
  337. this.minutes = Array(60).fill(0); // 0-59
  338. this.hours = Array(24).fill(0); // 0-23
  339. this.days = Array(31).fill(0); // 0-30 in array, 1-31 in config
  340. this.months = Array(12).fill(0); // 0-11 in array, 1-12 in config
  341. this.daysOfWeek = Array(8).fill(0); // 0-7 Where 0 = Sunday and 7=Sunday;
  342. this.lastDayOfMonth = false;
  343. this.parse();
  344. }
  345. /**
  346. * Parse current pattern, will throw on any type of failure
  347. * @private
  348. */
  349. CronPattern.prototype.parse = function () {
  350. // Sanity check
  351. if( !(typeof this.pattern === "string" || this.pattern.constructor === String) ) {
  352. throw new TypeError("CronPattern: Pattern has to be of type string.");
  353. }
  354. // Split configuration on whitespace
  355. const parts = this.pattern.trim().replace(/\s+/g, " ").split(" ");
  356. // Validite number of configuration entries
  357. if( parts.length < 5 || parts.length > 6 ) {
  358. throw new TypeError("CronPattern: invalid configuration format ('" + this.pattern + "'), exacly five or six space separated parts required.");
  359. }
  360. // If seconds is omitted, insert 0 for seconds
  361. if( parts.length === 5) {
  362. parts.unshift("0");
  363. }
  364. // Convert 'L' to '*' and add lastDayOfMonth flag,
  365. // and set days to 28,29,30,31 as those are the only days that can be the last day of month
  366. if(parts[3].toUpperCase() == "L") {
  367. parts[3] = "28,29,30,31";
  368. this.lastDayOfMonth = true;
  369. }
  370. // Replace alpha representations
  371. parts[4] = this.replaceAlphaMonths(parts[4]);
  372. parts[5] = this.replaceAlphaDays(parts[5]);
  373. // Implement '?' in the simplest possible way - replace ? with current value, before further processing
  374. let initDate = new CronDate(new Date(),this.timezone).getDate(true);
  375. parts[0] = parts[0].replace("?", initDate.getSeconds());
  376. parts[1] = parts[1].replace("?", initDate.getMinutes());
  377. parts[2] = parts[2].replace("?", initDate.getHours());
  378. parts[3] = parts[3].replace("?", initDate.getDate());
  379. parts[4] = parts[4].replace("?", initDate.getMonth()+1); // getMonth is zero indexed while pattern starts from 1
  380. parts[5] = parts[5].replace("?", initDate.getDay());
  381. // Check part content
  382. this.throwAtIllegalCharacters(parts);
  383. // Parse parts into arrays, validates as we go
  384. this.partToArray("seconds", parts[0], 0);
  385. this.partToArray("minutes", parts[1], 0);
  386. this.partToArray("hours", parts[2], 0);
  387. this.partToArray("days", parts[3], -1);
  388. this.partToArray("months", parts[4], -1);
  389. this.partToArray("daysOfWeek", parts[5], 0);
  390. // 0 = Sunday, 7 = Sunday
  391. if( this.daysOfWeek[7] ) {
  392. this.daysOfWeek[0] = 1;
  393. }
  394. };
  395. /**
  396. * Convert current part (seconds/minutes etc) to an array of 1 or 0 depending on if the part is about to trigger a run or not.
  397. * @private
  398. *
  399. * @param {CronPatternPart} type - Seconds/minutes etc
  400. * @param {string} conf - Current pattern part - *, 0-1 etc
  401. * @param {CronIndexOffset} valueIndexOffset
  402. * @param {boolean} [recursed] - Is this a recursed call
  403. */
  404. CronPattern.prototype.partToArray = function (type, conf, valueIndexOffset, recursed) {
  405. const arr = this[type];
  406. // First off, handle wildcard
  407. if( conf === "*" ) {
  408. for( let i = 0; i < arr.length; i++ ) {
  409. arr[i] = 1;
  410. }
  411. return;
  412. }
  413. // Handle separated entries (,) by recursion
  414. const split = conf.split(",");
  415. if( split.length > 1 ) {
  416. for( let i = 0; i < split.length; i++ ) {
  417. this.partToArray(type, split[i], valueIndexOffset, true);
  418. }
  419. // Handle range with stepping (x-y/z)
  420. } else if( conf.indexOf("-") !== -1 && conf.indexOf("/") !== -1 ) {
  421. if (recursed) throw new Error("CronPattern: Range with stepping cannot coexist with ,");
  422. this.handleRangeWithStepping(conf, type, valueIndexOffset);
  423. // Handle range
  424. } else if( conf.indexOf("-") !== -1 ) {
  425. if (recursed) throw new Error("CronPattern: Range with stepping cannot coexist with ,");
  426. this.handleRange(conf, type, valueIndexOffset);
  427. // Handle stepping
  428. } else if( conf.indexOf("/") !== -1 ) {
  429. if (recursed) throw new Error("CronPattern: Range with stepping cannot coexist with ,");
  430. this.handleStepping(conf, type, valueIndexOffset);
  431. } else {
  432. this.handleNumber(conf, type, valueIndexOffset);
  433. }
  434. };
  435. /**
  436. * After converting JAN-DEC, SUN-SAT only 0-9 * , / - are allowed, throw if anything else pops up
  437. * @private
  438. *
  439. * @param {string[]} parts - Each part split as strings
  440. */
  441. CronPattern.prototype.throwAtIllegalCharacters = function (parts) {
  442. const reValidCron = /[^/*0-9,-]+/;
  443. for(let i = 0; i < parts.length; i++) {
  444. if( reValidCron.test(parts[i]) ) {
  445. throw new TypeError("CronPattern: configuration entry " + i + " (" + parts[i] + ") contains illegal characters.");
  446. }
  447. }
  448. };
  449. /**
  450. * Nothing but a number left, handle that
  451. * @private
  452. *
  453. * @param {string} conf - Current part, expected to be a number, as a string
  454. * @param {string} type - One of "seconds", "minutes" etc
  455. * @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
  456. */
  457. CronPattern.prototype.handleNumber = function (conf, type, valueIndexOffset) {
  458. const i = (parseInt(conf, 10) + valueIndexOffset);
  459. if( i < 0 || i >= this[type].length ) {
  460. throw new TypeError("CronPattern: " + type + " value out of range: '" + conf + "'");
  461. }
  462. this[type][i] = 1;
  463. };
  464. /**
  465. * Take care of ranges with stepping (e.g. 3-23/5)
  466. * @private
  467. *
  468. * @param {string} conf - Current part, expected to be a string like 3-23/5
  469. * @param {string} type - One of "seconds", "minutes" etc
  470. * @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
  471. */
  472. CronPattern.prototype.handleRangeWithStepping = function (conf, type, valueIndexOffset) {
  473. const matches = conf.match(/^(\d+)-(\d+)\/(\d+)$/);
  474. if( matches === null ) throw new TypeError("CronPattern: Syntax error, illegal range with stepping: '" + conf + "'");
  475. let [, lower, upper, steps] = matches;
  476. lower = parseInt(lower, 10) + valueIndexOffset;
  477. upper = parseInt(upper, 10) + valueIndexOffset;
  478. steps = parseInt(steps, 10);
  479. if( isNaN(lower) ) throw new TypeError("CronPattern: Syntax error, illegal lower range (NaN)");
  480. if( isNaN(upper) ) throw new TypeError("CronPattern: Syntax error, illegal upper range (NaN)");
  481. if( isNaN(steps) ) throw new TypeError("CronPattern: Syntax error, illegal stepping: (NaN)");
  482. if( steps === 0 ) throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
  483. if( steps > this[type].length ) throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part ("+this[type].length+")");
  484. if( lower < 0 || upper >= this[type].length ) throw new TypeError("CronPattern: Value out of range: '" + conf + "'");
  485. if( lower > upper ) throw new TypeError("CronPattern: From value is larger than to value: '" + conf + "'");
  486. for (let i = lower; i <= upper; i += steps) {
  487. this[type][i] = 1;
  488. }
  489. };
  490. /**
  491. * Take care of ranges (e.g. 1-20)
  492. * @private
  493. *
  494. * @param {string} conf - Current part, expected to be a string like 1-20
  495. * @param {string} type - One of "seconds", "minutes" etc
  496. * @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
  497. */
  498. CronPattern.prototype.handleRange = function (conf, type, valueIndexOffset) {
  499. const split = conf.split("-");
  500. if( split.length !== 2 ) {
  501. throw new TypeError("CronPattern: Syntax error, illegal range: '" + conf + "'");
  502. }
  503. const lower = parseInt(split[0], 10) + valueIndexOffset,
  504. upper = parseInt(split[1], 10) + valueIndexOffset;
  505. if( isNaN(lower) ) {
  506. throw new TypeError("CronPattern: Syntax error, illegal lower range (NaN)");
  507. } else if( isNaN(upper) ) {
  508. throw new TypeError("CronPattern: Syntax error, illegal upper range (NaN)");
  509. }
  510. // Check that value is within range
  511. if( lower < 0 || upper >= this[type].length ) {
  512. throw new TypeError("CronPattern: Value out of range: '" + conf + "'");
  513. }
  514. //
  515. if( lower > upper ) {
  516. throw new TypeError("CronPattern: From value is larger than to value: '" + conf + "'");
  517. }
  518. for( let i = lower; i <= upper; i++ ) {
  519. this[type][i] = 1;
  520. }
  521. };
  522. /**
  523. * Handle stepping (e.g. * / 14)
  524. * @private
  525. *
  526. * @param {string} conf - Current part, expected to be a string like * /20 (without the space)
  527. * @param {string} type - One of "seconds", "minutes" etc
  528. */
  529. CronPattern.prototype.handleStepping = function (conf, type) {
  530. const split = conf.split("/");
  531. if( split.length !== 2 ) {
  532. throw new TypeError("CronPattern: Syntax error, illegal stepping: '" + conf + "'");
  533. }
  534. let start = 0;
  535. if( split[0] !== "*" ) {
  536. start = parseInt(split[0], 10);
  537. }
  538. const steps = parseInt(split[1], 10);
  539. if( isNaN(steps) ) throw new TypeError("CronPattern: Syntax error, illegal stepping: (NaN)");
  540. if( steps === 0 ) throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
  541. if( steps > this[type].length ) throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part ("+this[type].length+")");
  542. for( let i = start; i < this[type].length; i+= steps ) {
  543. this[type][i] = 1;
  544. }
  545. };
  546. /**
  547. * Replace day name with day numbers
  548. * @private
  549. *
  550. * @param {string} conf - Current part, expected to be a string that might contain sun,mon etc.
  551. *
  552. * @returns {string} - conf with 0 instead of sun etc.
  553. */
  554. CronPattern.prototype.replaceAlphaDays = function (conf) {
  555. return conf
  556. .replace(/sun/gi, "0")
  557. .replace(/mon/gi, "1")
  558. .replace(/tue/gi, "2")
  559. .replace(/wed/gi, "3")
  560. .replace(/thu/gi, "4")
  561. .replace(/fri/gi, "5")
  562. .replace(/sat/gi, "6");
  563. };
  564. /**
  565. * Replace month name with month numbers
  566. * @private
  567. *
  568. * @param {string} conf - Current part, expected to be a string that might contain jan,feb etc.
  569. *
  570. * @returns {string} - conf with 0 instead of sun etc.
  571. */
  572. CronPattern.prototype.replaceAlphaMonths = function (conf) {
  573. return conf
  574. .replace(/jan/gi, "1")
  575. .replace(/feb/gi, "2")
  576. .replace(/mar/gi, "3")
  577. .replace(/apr/gi, "4")
  578. .replace(/may/gi, "5")
  579. .replace(/jun/gi, "6")
  580. .replace(/jul/gi, "7")
  581. .replace(/aug/gi, "8")
  582. .replace(/sep/gi, "9")
  583. .replace(/oct/gi, "10")
  584. .replace(/nov/gi, "11")
  585. .replace(/dec/gi, "12");
  586. };
  587. /* ------------------------------------------------------------------------------------
  588. Croner - MIT License - Hexagon <github.com/Hexagon>
  589. Pure JavaScript Isomorphic cron parser and scheduler without dependencies.
  590. ------------------------------------------------------------------------------------
  591. License:
  592. Copyright (c) 2015-2021 Hexagon <github.com/Hexagon>
  593. Permission is hereby granted, free of charge, to any person obtaining a copy
  594. of this software and associated documentation files (the "Software"), to deal
  595. in the Software without restriction, including without limitation the rights
  596. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  597. copies of the Software, and to permit persons to whom the Software is
  598. furnished to do so, subject to the following conditions:
  599. The above copyright notice and this permission notice shall be included in
  600. all copies or substantial portions of the Software.
  601. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  602. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  603. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  604. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  605. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  606. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  607. THE SOFTWARE.
  608. ------------------------------------------------------------------------------------ */
  609. /**
  610. * @typedef {Object} CronOptions - Cron scheduler options
  611. * @property {boolean} [paused] - Job is paused
  612. * @property {boolean} [kill] - Job is about to be killed or killed
  613. * @property {boolean} [catch] - Continue exection even if a unhandled error is thrown by triggered function
  614. * @property {number} [maxRuns] - Maximum nuber of executions
  615. * @property {string | Date} [startAt] - When to start running
  616. * @property {string | Date} [stopAt] - When to stop running
  617. * @property {string} [timezone] - Time zone in Europe/Stockholm format
  618. * @property {?} [context] - Used to pass any object to scheduled function
  619. */
  620. /**
  621. * Many JS engines stores the delay as a 32-bit signed integer internally.
  622. * This causes an integer overflow when using delays larger than 2147483647,
  623. * resulting in the timeout being executed immediately.
  624. *
  625. * All JS engines implements an immediate execution of delays larger that a 32-bit
  626. * int to keep the behaviour concistent.
  627. *
  628. * @type {number}
  629. */
  630. const maxDelay = Math.pow(2, 32 - 1) - 1;
  631. /**
  632. * Cron entrypoint
  633. *
  634. * @constructor
  635. * @param {string|Date} pattern - Input pattern, input date, or input ISO 8601 time string
  636. * @param {CronOptions|Function} [options] - Options
  637. * @param {Function} [func] - Function to be run each iteration of pattern
  638. * @returns {Cron}
  639. */
  640. function Cron (pattern, options, func) {
  641. // Optional "new" keyword
  642. if( !(this instanceof Cron) ) {
  643. return new Cron(pattern, options, func);
  644. }
  645. // Make options optional
  646. if( typeof options === "function" ) {
  647. func = options;
  648. options = void 0;
  649. }
  650. /** @type {CronOptions} */
  651. this.options = this.processOptions(options);
  652. // Check if we got a date, or a pattern supplied as first argument
  653. if (pattern && (pattern instanceof Date)) {
  654. this.once = new CronDate(pattern, this.options.timezone);
  655. } else if (pattern && (typeof pattern === "string") && pattern.indexOf(":") > 0) {
  656. /** @type {CronDate} */
  657. this.once = new CronDate(pattern, this.options.timezone);
  658. } else {
  659. /** @type {CronPattern} */
  660. this.pattern = new CronPattern(pattern, this.options.timezone);
  661. }
  662. /**
  663. * Allow shorthand scheduling
  664. */
  665. if( func !== void 0 ) {
  666. this.fn = func;
  667. this.schedule();
  668. }
  669. return this;
  670. }
  671. /**
  672. * Internal function that validates options, and sets defaults
  673. * @private
  674. *
  675. * @param {CronOptions} options
  676. * @returns {CronOptions}
  677. */
  678. Cron.prototype.processOptions = function (options) {
  679. // If no options are passed, create empty object
  680. if (options === void 0) {
  681. options = {};
  682. }
  683. // Keep options, or set defaults
  684. options.paused = (options.paused === void 0) ? false : options.paused;
  685. options.maxRuns = (options.maxRuns === void 0) ? Infinity : options.maxRuns;
  686. options.catch = (options.catch === void 0) ? false : options.catch;
  687. options.kill = false;
  688. // startAt is set, validate it
  689. if( options.startAt ) {
  690. options.startAt = new CronDate(options.startAt, options.timezone);
  691. }
  692. if( options.stopAt ) {
  693. options.stopAt = new CronDate(options.stopAt, options.timezone);
  694. }
  695. return options;
  696. };
  697. /**
  698. * Find next runtime, based on supplied date. Strips milliseconds.
  699. *
  700. * @param {Date|string} [prev] - Date to start from
  701. * @returns {Date | null} - Next run time
  702. */
  703. Cron.prototype.next = function (prev) {
  704. prev = new CronDate(prev, this.options.timezone);
  705. const next = this._next(prev);
  706. return next ? next.getDate() : null;
  707. };
  708. /**
  709. * Find next n runs, based on supplied date. Strips milliseconds.
  710. *
  711. * @param {number} n - Number of runs to enumerate
  712. * @param {Date|string} [previous] - Date to start from
  713. * @returns {Date[]} - Next n run times
  714. */
  715. Cron.prototype.enumerate = function (n, previous) {
  716. let enumeration = [];
  717. while(n-- && (previous = this.next(previous))) {
  718. enumeration.push(previous);
  719. }
  720. return enumeration;
  721. };
  722. /**
  723. * Is running?
  724. * @public
  725. *
  726. * @returns {boolean} - Running or not
  727. */
  728. Cron.prototype.running = function () {
  729. const msLeft = this.msToNext(this.previousrun);
  730. const running = !this.options.paused && this.fn !== void 0;
  731. return msLeft !== null && running;
  732. };
  733. /**
  734. * Return previous run time
  735. * @public
  736. *
  737. * @returns {Date | null} - Previous run time
  738. */
  739. Cron.prototype.previous = function () {
  740. return this.previousrun ? this.previousrun.getDate() : null;
  741. };
  742. /**
  743. * Internal version of next. Cron needs millseconds internally, hence _next.
  744. * @private
  745. *
  746. * @param {CronDate} prev - Input pattern
  747. * @returns {CronDate | null} - Next run time
  748. */
  749. Cron.prototype._next = function (prev) {
  750. // Previous run should never be before startAt
  751. if( this.options.startAt && prev && prev.getTime(true) < this.options.startAt.getTime(true) ) {
  752. prev = this.options.startAt;
  753. }
  754. // Calculate next run according to pattern or one-off timestamp
  755. const nextRun = this.once || new CronDate(prev, this.options.timezone).increment(this.pattern);
  756. if (this.once && this.once.getTime(true) <= prev.getTime(true)) {
  757. return null;
  758. } else if ((nextRun === null) ||
  759. (this.options.maxRuns <= 0) ||
  760. (this.options.kill) ||
  761. (this.options.stopAt && nextRun.getTime(true) >= this.options.stopAt.getTime(true) )) {
  762. return null;
  763. } else {
  764. // All seem good, return next run
  765. return nextRun;
  766. }
  767. };
  768. /**
  769. * Returns number of milliseconds to next run
  770. * @public
  771. *
  772. * @param {Date} [prev] - Starting date, defaults to now
  773. * @returns {number | null}
  774. */
  775. Cron.prototype.msToNext = function (prev) {
  776. prev = new CronDate(prev, this.options.timezone);
  777. const next = this._next(prev);
  778. if( next ) {
  779. return (next.getTime(true) - prev.getTime(true));
  780. } else {
  781. return null;
  782. }
  783. };
  784. /**
  785. * Stop execution
  786. * @public
  787. */
  788. Cron.prototype.stop = function () {
  789. this.options.kill = true;
  790. // Stop any awaiting call
  791. if( this.currentTimeout ) {
  792. clearTimeout( this.currentTimeout );
  793. }
  794. };
  795. /**
  796. * Pause executionR
  797. * @public
  798. *
  799. * @returns {boolean} - Wether pause was successful
  800. */
  801. Cron.prototype.pause = function () {
  802. return (this.options.paused = true) && !this.options.kill;
  803. };
  804. /**
  805. * Pause execution
  806. * @public
  807. *
  808. * @returns {boolean} - Wether resume was successful
  809. */
  810. Cron.prototype.resume = function () {
  811. return !(this.options.paused = false) && !this.options.kill;
  812. };
  813. /**
  814. * Schedule a new job
  815. * @public
  816. *
  817. * @param {Function} func - Function to be run each iteration of pattern
  818. * @returns {Cron}
  819. */
  820. Cron.prototype.schedule = function (func) {
  821. // If a function is already scheduled, bail out
  822. if (func && this.fn) {
  823. throw new Error("Cron: It is not allowed to schedule two functions using the same Croner instance.");
  824. // Update function if passed
  825. } else if (func) {
  826. this.fn = func;
  827. }
  828. // Get ms to next run, bail out early if waitMs is null (no next run)
  829. let waitMs = this.msToNext(this.previousrun);
  830. if ( waitMs === null ) return this;
  831. // setTimeout cant handle more than Math.pow(2, 32 - 1) - 1 ms
  832. if( waitMs > maxDelay ) {
  833. waitMs = maxDelay;
  834. }
  835. // Ok, go!
  836. this.currentTimeout = setTimeout(() => {
  837. if( waitMs !== maxDelay && !this.options.paused ) {
  838. this.options.maxRuns--;
  839. // Always catch errors, but only re-throw if options.catch is not set
  840. if (this.options.catch) {
  841. try {
  842. this.fn(this, this.options.context);
  843. } catch (_e) {
  844. // Ignore
  845. }
  846. } else {
  847. this.fn(this, this.options.context);
  848. }
  849. this.previousrun = new CronDate(void 0, this.options.timezone);
  850. }
  851. // Recurse
  852. this.schedule();
  853. }, waitMs );
  854. return this;
  855. };
  856. return Cron;
  857. }));