Skip to content

Commit

Permalink
feat!: UNIX standard alignments (#667)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheerlox committed Jul 20, 2023
1 parent 7471e95 commit a70252d
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 46 deletions.
36 changes: 29 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<details>
<summary>Migrating from v2 to v3</summary>

### 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 !

</details>

## 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).
Expand Down Expand Up @@ -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

Expand Down
69 changes: 41 additions & 28 deletions lib/time.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ const CONSTRAINTS = [
[0, 59],
[0, 23],
[1, 31],
[0, 11],
[0, 6]
[1, 12],
[0, 7]
];
const MONTH_CONSTRAINTS = [
31,
Expand All @@ -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,
Expand All @@ -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 * * *',
Expand Down Expand Up @@ -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];
Expand All @@ -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) {
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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
) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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`);
Expand Down
77 changes: 66 additions & 11 deletions tests/crontime.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand All @@ -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();
});

Expand Down Expand Up @@ -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);
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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 *');
});
});

Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit a70252d

Please sign in to comment.