diff --git a/src/constants.ts b/src/constants.ts index a642b6c1..a7dafa12 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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', @@ -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; diff --git a/src/time.ts b/src/time.ts index 488602e6..c9a80d20 100644 --- a/src/time.ts +++ b/src/time.ts @@ -5,6 +5,8 @@ import { CONSTRAINTS, PARSE_DEFAULTS, PRESETS, + RE_L, + RE_QUESTIONMARK, RE_RANGE, RE_WILDCARDS, TIME_UNITS, @@ -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); @@ -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); } } @@ -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; let pointer: Ranges[typeof unit]; @@ -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}`); @@ -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'; + } } diff --git a/tests/crontime.test.ts b/tests/crontime.test.ts index 8d9d2426..839de341 100644 --- a/tests/crontime.test.ts +++ b/tests/crontime.test.ts @@ -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')); @@ -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(); + }); + }); });