From 47e64e6c44b7fe7dd63cb69dd79d5f36c084d9c5 Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Thu, 20 Jul 2023 20:58:33 +0200 Subject: [PATCH] feat!: UNIX standard alignments (#667) --- README.md | 36 ++++++++++++++++---- lib/time.js | 69 ++++++++++++++++++++++--------------- tests/crontime.test.js | 77 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 136 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index bd8e8cdb..28d4aa9e 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,21 @@ Cron is a tool that allows you to execute _something_ on a schedule. This is typ npm install cron ``` +## Migrating from v2 to v3 + +In version 3 of this library, we aligned our format for the cron patterns with the UNIX format. See below for the changes you need to make when upgrading: + +
+ Migrating from v2 to v3 + + ### Month & day-of-week indexing changes + + **Month indexing went from `0-11` to `1-12`. So you need to increment all numeric months by 1.** + + For day-of-week indexing, we only added support for `7` as Sunday, so you don't need to change anything ! + +
+ ## Versions and Backwards compatibility breaks As goes with semver, breaking backwards compatibility should be explicit in the versioning of your library. As such, we'll upgrade the version of this module in accordance with breaking changes (We're not always great about doing it this way so if you notice that there are breaking changes that haven't been bumped appropriately please let us know). @@ -65,14 +80,21 @@ There are tools that help when constructing your cronjobs. You might find someth ### Cron Ranges -When specifying your cron values you'll need to make sure that your values fall within the ranges. For instance, some cron's use a 0-7 range for the day of week where both 0 and 7 represent Sunday. We do not. And that is an optimisation. +This library follows the [UNIX Cron format](https://man7.org/linux/man-pages/man5/crontab.5.html), with an added field at the beginning for second granularity. + +``` +field allowed values +----- -------------- +second 0-59 +minute 0-59 +hour 0-23 +day of month 1-31 +month 1-12 (or names, see below) +day of week 0-7 (0 or 7 is Sunday, or use names) +``` -- Seconds: 0-59 -- Minutes: 0-59 -- Hours: 0-23 -- Day of Month: 1-31 -- Months: 0-11 (Jan-Dec) <-- currently different from Unix `cron`! -- Day of Week: 0-6 (Sun-Sat) +> Names can also be used for the 'month' and 'day of week' fields. Use the first three letters of the particular day or month (case does not matter). Ranges and lists of names are allowed. +> Examples: "mon,wed,fri", "jan-mar". ## Gotchas diff --git a/lib/time.js b/lib/time.js index b236ac94..76cd60ba 100644 --- a/lib/time.js +++ b/lib/time.js @@ -3,8 +3,8 @@ const CONSTRAINTS = [ [0, 59], [0, 23], [1, 31], - [0, 11], - [0, 6] + [1, 12], + [0, 7] ]; const MONTH_CONSTRAINTS = [ 31, @@ -22,18 +22,18 @@ const MONTH_CONSTRAINTS = [ ]; const PARSE_DEFAULTS = ['0', '*', '*', '*', '*', '*']; const ALIASES = { - jan: 0, - feb: 1, - mar: 2, - apr: 3, - may: 4, - jun: 5, - jul: 6, - aug: 7, - sep: 8, - oct: 9, - nov: 10, - dec: 11, + jan: 1, + feb: 2, + mar: 3, + apr: 4, + may: 5, + jun: 6, + jul: 7, + aug: 8, + sep: 9, + oct: 10, + nov: 11, + dec: 12, sun: 0, mon: 1, tue: 2, @@ -42,17 +42,18 @@ const ALIASES = { fri: 5, sat: 6 }; -const TIME_UNITS = [ - 'second', - 'minute', - 'hour', - 'dayOfMonth', - 'month', - 'dayOfWeek' -]; +const TIME_UNITS_MAP = { + SECOND: 'second', + MINUTE: 'minute', + HOUR: 'hour', + DAY_OF_MONTH: 'dayOfMonth', + MONTH: 'month', + DAY_OF_WEEK: 'dayOfWeek' +}; +const TIME_UNITS = Object.values(TIME_UNITS_MAP); const TIME_UNITS_LEN = TIME_UNITS.length; const PRESETS = { - '@yearly': '0 0 0 1 0 *', + '@yearly': '0 0 0 1 1 *', '@monthly': '0 0 0 1 * *', '@weekly': '0 0 0 * * 0', '@daily': '0 0 0 * * *', @@ -112,7 +113,7 @@ function CronTime(luxon) { let lastWrongMonth = NaN; for (let i = 0; i < months.length; i++) { const m = months[i]; - const con = MONTH_CONSTRAINTS[parseInt(m, 10)]; + const con = MONTH_CONSTRAINTS[parseInt(m, 10) - 1]; for (let j = 0; j < dom.length; j++) { const day = dom[j]; @@ -130,7 +131,7 @@ function CronTime(luxon) { // infinite loop detected (dayOfMonth is not found in all months) if (!ok) { - const notOkCon = MONTH_CONSTRAINTS[parseInt(lastWrongMonth, 10)]; + const notOkCon = MONTH_CONSTRAINTS[parseInt(lastWrongMonth, 10) - 1]; for (let k = 0; k < dom.length; k++) { const notOkDay = dom[k]; if (notOkDay > notOkCon) { @@ -278,7 +279,7 @@ function CronTime(luxon) { } if ( - !(date.month - 1 in this.month) && + !(date.month in this.month) && Object.keys(this.month).length !== 12 ) { date = date.plus({ months: 1 }); @@ -471,7 +472,7 @@ function CronTime(luxon) { const beforeJumpingPoint = afterJumpingPoint.minus({ second: 1 }); if ( - date.month in this.month && + date.month + 1 in this.month && date.day in this.dayOfMonth && date.getWeekDay() in this.dayOfWeek ) { @@ -671,8 +672,13 @@ function CronTime(luxon) { _hasAll: function (type) { const constraints = CONSTRAINTS[TIME_UNITS.indexOf(type)]; + const low = constraints[0]; + const high = + type === TIME_UNITS_MAP.DAY_OF_WEEK + ? constraints[1] - 1 + : constraints[1]; - for (let i = constraints[0], n = constraints[1]; i < n; i++) { + for (let i = low, n = high; i < n; i++) { if (!(i in this[type])) { return false; } @@ -803,6 +809,13 @@ function CronTime(luxon) { typeObj[pointer] = true; // mutates the field objects values inside CronTime pointer += step; } while (pointer <= upper); + + // merge day 7 into day 0 (both Sunday), and remove day 7 + // since we work with day-of-week 0-6 under the hood + if (type === 'dayOfWeek') { + if (!typeObj[0] && !!typeObj[7]) typeObj[0] = typeObj[7]; + delete typeObj[7]; + } }); } else { throw new Error(`Field (${type}) cannot be parsed`); diff --git a/tests/crontime.test.js b/tests/crontime.test.js index 0d962d06..b43ef3f0 100644 --- a/tests/crontime.test.js +++ b/tests/crontime.test.js @@ -64,9 +64,9 @@ describe('crontime', () => { }).not.toThrow(); }); - it('should test all hyphens (0-10 0-10 1-10 1-10 0-6 0-1)', () => { + it('should test all hyphens (0-10 0-10 1-10 1-10 1-7 0-1)', () => { expect(() => { - new cron.CronTime('0-10 0-10 1-10 1-10 0-6 0-1'); + new cron.CronTime('0-10 0-10 1-10 1-10 1-7 0-1'); }).not.toThrow(); }); @@ -82,9 +82,9 @@ describe('crontime', () => { }).not.toThrow(); }); - it('should test all commas (0,10 0,10 1,10 1,10 0,6 0,1)', () => { + it('should test all commas (0,10 0,10 1,10 1,10 1,7 0,1)', () => { expect(() => { - new cron.CronTime('0,10 0,10 1,10 1,10 0,6 0,1'); + new cron.CronTime('0,10 0,10 1,10 1,10 1,7 0,1'); }).not.toThrow(); }); @@ -118,6 +118,12 @@ describe('crontime', () => { }).toThrow(); }); + it('should be case-insensitive for aliases (* * * * JAN,FEB MON,TUE)', () => { + expect(() => { + new cron.CronTime('* * * * JAN,FEB MON,TUE', null, null); + }).not.toThrow(); + }); + it('should test too few fields', () => { expect(() => { new cron.CronTime('* * * *', null, null); @@ -130,10 +136,59 @@ describe('crontime', () => { }).toThrow(); }); - it('should test out of range values', () => { - expect(() => { - new cron.CronTime('* * * * 1234', null, null); - }).toThrow(); + it('should return the same object with 0 & 7 as Sunday (except "source" prop)', () => { + const sunday0 = new cron.CronTime('* * * * 0', null, null); + const sunday7 = new cron.CronTime('* * * * 7', null, null); + delete sunday0.source; + delete sunday7.source; + expect(sunday7).toEqual(sunday0); + }); + + describe('should test out of range values', () => { + it('should test out of range minute', () => { + expect(() => { + new cron.CronTime('-1 * * * *', null, null); + }).toThrow(); + expect(() => { + new cron.CronTime('60 * * * *', null, null); + }).toThrow(); + }); + + it('should test out of range hour', () => { + expect(() => { + new cron.CronTime('* -1 * * *', null, null); + }).toThrow(); + expect(() => { + new cron.CronTime('* 24 * * *', null, null); + }).toThrow(); + }); + + it('should test out of range day-of-month', () => { + expect(() => { + new cron.CronTime('* * 0 * *', null, null); + }).toThrow(); + expect(() => { + new cron.CronTime('* * 32 * *', null, null); + }).toThrow(); + }); + + it('should test out of range month', () => { + expect(() => { + new cron.CronTime('* * * 0 *', null, null); + }).toThrow(); + expect(() => { + new cron.CronTime('* * * 13 *', null, null); + }).toThrow(); + }); + + it('should test out of range day-of-week', () => { + expect(() => { + new cron.CronTime('* * * * -1', null, null); + }).toThrow(); + expect(() => { + new cron.CronTime('* * * * 8', null, null); + }).toThrow(); + }); }); it('should test invalid wildcard expression', () => { @@ -257,7 +312,7 @@ describe('crontime', () => { it('should parse @yearly', () => { const cronTime = new cron.CronTime('@yearly'); - expect(cronTime.toString()).toEqual('0 0 0 1 0 *'); + expect(cronTime.toString()).toEqual('0 0 0 1 1 *'); }); }); @@ -564,8 +619,8 @@ describe('crontime', () => { currentDate = nextDate; } }); - it('should test valid range of months (*/15 * * 6-11 *)', () => { - const cronTime = new cron.CronTime('*/15 * * 6-11 *'); + it('should test valid range of months (*/15 * * 7-12 *)', () => { + const cronTime = new cron.CronTime('*/15 * * 7-12 *'); const previousDate1 = new Date(Date.UTC(2018, 3, 0, 0, 0)); const nextDate1 = cronTime._getNextDateFrom(previousDate1, 'UTC'); expect(new Date(nextDate1).toUTCString()).toEqual(