From e94212864945f52921abdd3bd26083362b27b0f4 Mon Sep 17 00:00:00 2001 From: James Clarke Date: Wed, 7 Aug 2019 17:49:55 +0100 Subject: [PATCH] Accept dates in CronosTask constructor (Closes #4) --- changelog.md | 4 ++++ package-lock.json | 2 +- package.json | 2 +- readme.md | 35 +++++++++++++++++++++++++++++++---- src/expression.ts | 3 ++- src/scheduler.ts | 42 ++++++++++++++++++++++++++++++++++++------ tests/api.test.js | 36 ++++++++++++++++++++++++++++++++++++ 7 files changed, 111 insertions(+), 13 deletions(-) diff --git a/changelog.md b/changelog.md index 9c4b249..b343380 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## [1.4.0] - 07 Aug 2019 +### Added + - Support for providing a date, array of dates, or a custom date sequence to `new CronosTask()` instead of a `CronosExpression` object + ## [1.3.0] - 30 Jul 2019 ### Added - Support wrap-around ranges for cyclic type fields (ie. *Second*, *Minute*, *Hour*, *Month* and *Day of Week*) diff --git a/package-lock.json b/package-lock.json index 098e40e..e7f2574 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cronosjs", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index db766ac..563eb5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cronosjs", - "version": "1.3.0", + "version": "1.4.0", "description": "A cron based task scheduler for node and the browser, with extended syntax and timezone support.", "keywords": [ "cron", diff --git a/readme.md b/readme.md index 73fe25b..b25baf4 100644 --- a/readme.md +++ b/readme.md @@ -67,6 +67,22 @@ task console.log(`No more dates matching expression`) }) .start() + +// schedule tasks from a list of dates +const taskFromDates = new CronosTask([ + new Date(2020, 7, 23, 9, 45, 0), + 1555847845000, + '5 Oct 2019 17:32', +]) + +taskFromDates + .on('run', (timestamp) => { + console.log(`Task triggered at ${timestamp}`) + }) + .on('ended', () => { + console.log(`No more dates in list`) + }) + .start() ``` @@ -283,10 +299,21 @@ import { ### CronosTask ```class CronosTask``` -#### Constructor -```new CronosTask(expression)``` - - `expression: CronosExpression` - An instance of [CronosExpression](#cronosexpression) +#### Constructor (3 overloads) + - ```new CronosTask(sequence)``` + - `sequence: DateSequence` + Either an instance of [CronosExpression](#cronosexpression) or any other object that implements the `DateSequence` interface + ```typescript + interface DateSequence { + nextDate: (afterDate: Date) => Date | null + } + ``` + - ```new CronosTask(date)``` + - `date: Date | string | number` + Either a `Date`, a timestamp, or a string repesenting a valid date, parsable by `new Date()` + - ```new CronosTask(dates)``` + - `dates: (Date | string | number)[]` + An array of dates accepted valid in above constructor #### Properties - `nextRun: Date | null` (readonly) diff --git a/src/expression.ts b/src/expression.ts index 98c9b65..ddc65ef 100644 --- a/src/expression.ts +++ b/src/expression.ts @@ -1,11 +1,12 @@ import { CronosDaysExpression, _parse } from './parser' import { CronosDate, CronosTimezone } from './date' +import { DateSequence } from './scheduler' import { sortAsc } from './utils' const hourinms = 60 * 60 * 1000 const findFirstFrom = (from: number, list: number[]) => list.findIndex(n => n >= from) -export class CronosExpression { +export class CronosExpression implements DateSequence { private timezone?: CronosTimezone private skipRepeatedHour = true private missingHour: 'insert' | 'offset' | 'skip' = 'insert' diff --git a/src/scheduler.ts b/src/scheduler.ts index 1cd7fdc..bacc0c9 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -1,5 +1,3 @@ -import { CronosExpression } from './expression' - const maxTimeout = Math.pow(2, 31) - 1 const scheduledTasks: CronosTask[] = [] @@ -46,6 +44,27 @@ function runScheduledTasks() { } else runningTimer = null } +export interface DateSequence { + nextDate: (afterDate: Date) => Date | null +} + +class DateArraySequence implements DateSequence { + private _dates: Date[] + + constructor(dateLikes: DateLike[]) { + this._dates = dateLikes.map(dateLike => { + const date = new Date(dateLike) + if (isNaN(date.getTime())) throw new Error('Invalid date') + return date + }).sort((a, b) => a.getTime() - b.getTime()) + } + + nextDate(afterDate: Date) { + const nextIndex = this._dates.findIndex(d => d > afterDate) + return nextIndex === -1 ? null : this._dates[nextIndex] + } +} + type CronosTaskListeners = { 'started': () => void 'stopped': () => void @@ -53,6 +72,8 @@ type CronosTaskListeners = { 'ended': () => void } +type DateLike = Date | string | number + export class CronosTask { private _listeners: { [K in keyof CronosTaskListeners]: Set @@ -63,12 +84,21 @@ export class CronosTask { 'ended': new Set(), } private _timestamp?: number - private _expression: CronosExpression + private _sequence: DateSequence + constructor(sequence: DateSequence) + constructor(dates: DateLike[]) + constructor(date: DateLike) constructor( - expression: CronosExpression, + sequenceOrDates: DateSequence | DateLike[] | DateLike, ) { - this._expression = expression + if (Array.isArray(sequenceOrDates)) this._sequence = new DateArraySequence(sequenceOrDates) + else if ( + typeof sequenceOrDates === 'string' || + typeof sequenceOrDates === 'number' || + sequenceOrDates instanceof Date + ) this._sequence = new DateArraySequence([sequenceOrDates]) + else this._sequence = sequenceOrDates } start() { @@ -99,7 +129,7 @@ export class CronosTask { } private _updateTimestamp() { - const nextDate = this._expression.nextDate( + const nextDate = this._sequence.nextDate( this._timestamp ? new Date(this._timestamp) : new Date() ) diff --git a/tests/api.test.js b/tests/api.test.js index 1dccc69..ec25d60 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -159,6 +159,42 @@ describe('Scheduling tests', () => { expect(task.isRunning).toEqual(false) }) + test('CronosTask with array of dates', () => { + const startedCallback = jest.fn() + const runCallback = jest.fn() + + const fromDate = '2019-04-21T11:23:45Z' + mockDate(fromDate) + + const task = new CronosTask( + [new Date(2020, 7, 23, 9, 45, 0), 1555847845000, '5 Oct 2019 17:32'] + ) + + task + .on('started', startedCallback) + .on('run', runCallback) + + expect(startedCallback).not.toBeCalled() + + task.start() + + expect(startedCallback).toBeCalled() + + mockDate('2019-04-21T11:57:25Z') + jest.runOnlyPendingTimers() + + expect(runCallback).toHaveBeenCalledTimes(1) + expect(runCallback).toHaveBeenLastCalledWith( getTimestamp('2019-04-21T11:57:25Z') ) + + task.off('run', runCallback) + }) + + test('CronosTask with invalid date', () => { + expect( + () => new CronosTask('invalid') + ).toThrow() + }) + }) describe('CronosExpression.cronString and .toString()', () => {