Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [435] add support for extended cron syntax #877

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export const ALIASES = Object.freeze({
wed: 3,
thu: 4,
fri: 5,
sat: 6
sat: 6,
l: 'l'
} as const);
export const TIME_UNITS_MAP = Object.freeze({
SECOND: 'second',
Expand Down Expand Up @@ -65,3 +66,5 @@ export const PRESETS = Object.freeze({
} as const);
export const RE_WILDCARDS = /\*/g;
export const RE_RANGE = /^(\d+)(?:-(\d+))?(?:\/(\d+))?$/g;
export const RE_QUESTIONMARK = /\?/g;
export const RE_L = /[lL]/g;
39 changes: 37 additions & 2 deletions src/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
CONSTRAINTS,
PARSE_DEFAULTS,
PRESETS,
RE_L,
RE_QUESTIONMARK,
RE_RANGE,
RE_WILDCARDS,
TIME_UNITS,
Expand Down Expand Up @@ -671,6 +673,9 @@ export class CronTime {
throw new CronError('Too many fields');
}

//timestamp for eventual '?' substitution
const now = DateTime.local();

const unitsLen = units.length;
for (const unit of TIME_UNITS) {
const i = TIME_UNITS.indexOf(unit);
Expand All @@ -679,7 +684,7 @@ export class CronTime {
// This adds support for 5-digit standard cron syntax
const cur =
units[i - (TIME_UNITS_LEN - unitsLen)] ?? PARSE_DEFAULTS[unit];
this._parseField(cur, unit);
this._parseField(cur, unit, now);
}
}

Expand All @@ -694,7 +699,7 @@ export class CronTime {
* - Starting with the lower bounds of the range iterate by step up to the upper bounds and toggle the CronTime field value flag on.
*/

private _parseField(value: string, unit: TimeUnit) {
private _parseField(value: string, unit: TimeUnit, now: DateTime) {
const typeObj = this[unit] as TimeUnitField<typeof unit>;
let pointer: Ranges[typeof unit];

Expand All @@ -712,6 +717,12 @@ export class CronTime {
}
});

// "L" is a shortcut for the last day of the month
value = value.replace(RE_L, this._getLastDayOf(now, unit));

// "?" will be replaced with current timestamp value
value = value.replace(RE_QUESTIONMARK, this._getTimeUnit(now, unit));

// "*" is a shortcut to [low-high] range for the field
value = value.replace(RE_WILDCARDS, `${low}-${high}`);

Expand Down Expand Up @@ -776,4 +787,28 @@ export class CronTime {
}
}
}

private _getTimeUnit(now: DateTime, unit: TimeUnit) {
switch (unit) {
case 'second':
return now.second.toString();
case 'minute':
return now.minute.toString();
case 'hour':
return now.hour.toString();
case 'dayOfMonth':
return now.day.toString();
case 'month':
return now.month.toString();
case 'dayOfWeek':
return this._getWeekDay(now).toString();
default:
throw new CronError('Invalid time unit');
}
}

private _getLastDayOf(now: DateTime, unit: TimeUnit) {
if (unit === 'dayOfMonth') return now.endOf('month').day.toString();
return '7';
}
}
105 changes: 91 additions & 14 deletions tests/crontime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,20 +352,6 @@ describe('crontime', () => {
});
});

describe('should throw an exception because `L` not supported', () => {
it('(* * * L * *)', () => {
expect(() => {
new CronTime('* * * L * *');
}).toThrow();
});

it('(* * * * * L)', () => {
expect(() => {
new CronTime('* * * * * L');
}).toThrow();
});
});

it('should strip off millisecond', () => {
const cronTime = new CronTime('0 */10 * * * *');
const x = cronTime.getNextDateFrom(new Date('2018-08-10T02:20:00.999Z'));
Expand Down Expand Up @@ -777,4 +763,95 @@ describe('crontime', () => {
new CronTime('* * * * *', 'Asia/Amman', 120);
}).toThrow();
});

describe('should support question mark', () => {
it('should substitute minute', () => {
const clock = sinon.useFakeTimers();

const now = DateTime.local();
const ct = new CronTime('? * * * *');
const minutes = ct.sendAt().get('minute');
expect(minutes).toBe(now.get('minute'));

clock.restore();
});

it('should substitute seconds', () => {
const clock = sinon.useFakeTimers();

const now = DateTime.local();
const ct = new CronTime('? * * * * *');
const second = ct.sendAt().get('second');
expect(second).toBe(now.get('second'));

clock.restore();
});

it('should substitute hours', () => {
const clock = sinon.useFakeTimers();

const now = DateTime.local();
const ct = new CronTime('* ? * * *');
const hour = ct.sendAt().get('hour');
expect(hour).toBe(now.get('hour'));

clock.restore();
});

it('should substitute day', () => {
const clock = sinon.useFakeTimers();

const now = DateTime.local();
const ct = new CronTime('* * ? * *');
const day = ct.sendAt().get('day');
expect(day).toBe(now.get('day'));

clock.restore();
});

it('should substitute month', () => {
const clock = sinon.useFakeTimers();

const now = DateTime.local();
const ct = new CronTime('* * * ? *');
const month = ct.sendAt().get('month');
expect(month).toBe(now.get('month'));

clock.restore();
});

it('should substitute dayOfWeek', () => {
const clock = sinon.useFakeTimers();

const now = DateTime.local();
const ct = new CronTime('* * * * ?');
const month = ct.sendAt().get('weekday');
expect(month).toBe(now.get('weekday'));

clock.restore();
});
});

describe("should support 'L' for last day of the month", () => {
it('should substitute last day of the month for "* * L * *"', () => {
const clock = sinon.useFakeTimers();

const now = DateTime.local();
const ct = new CronTime('* * L * *');
const day = ct.sendAt().get('day');
expect(day).toBe(now.endOf('month').get('day'));

clock.restore();
});

it('should substitute last day of the week for "* * * * * L', () => {
const clock = sinon.useFakeTimers();

const ct = new CronTime('* * * * L');
const day = ct.sendAt().get('weekday');
expect(day).toBe(7);

clock.restore();
});
});
});
Loading