Workflow funktioniert nun wieder. Es gab Probleme nach Aenderungen.
Build and Publish Site / docker (push) Successful in 23s
Build and Publish Site / docker (push) Successful in 23s
ABER: Die Applikation funktioniert nur lokal. Die deployte Version geht noch nicht.
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
# Changelog
|
||||
|
||||
## [1.7.1] - 01 Apr 2021
|
||||
### Fixed
|
||||
- Fix bug where `.nextDate` was returning incorrect dates, causing tasks to run continuously without delays, when running on Node.js v14.0.0 or greater. Bug was caused by the `Intl.DateTimeFormat` API returning '24:00:00' instead of '00:00:00' on Node.js > v14.0.0.
|
||||
|
||||
## [1.7.0] - 02 Aug 2020
|
||||
### Added
|
||||
- Added `refreshSchedulerTimer()` function, to update the next execution time of all tasks and refresh the scheduler timer. Should be called when the system time is changed to ensure tasks are run at the correct times
|
||||
|
||||
## [1.6.1] - 04 Jul 2020
|
||||
### Fixed
|
||||
- Fix bug when `task.start()` is called on a running task
|
||||
|
||||
## [1.6.0] - 17 Apr 2020
|
||||
### Added
|
||||
- `CronosExpression` now has `warnings` property that lists possible errors in the expression. Currently supports detecting cases where increment value is larger than the valid (or supplied) range for a field
|
||||
- `scheduleTask`, `CronosExpression.parse()` and `validate` now support strict option, which when enabled will throw an error if warnings were generated during parsing
|
||||
|
||||
## [1.5.0] - 01 Nov 2019
|
||||
### Added
|
||||
- Support for the `?` symbol as a alias to `*` in the *Day of Month* and *Day of Week* fields
|
||||
### Changed
|
||||
- Larger year range (now 0-275759, previously 1970-2099) allowed in year field
|
||||
- Improved documentation on cron expression syntax
|
||||
|
||||
## [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*)
|
||||
### Fixed
|
||||
- Fix bug causing task to continue to run if `task.stop()` is called in the `run` callback
|
||||
|
||||
## [1.2.1] - 22 Jul 2019
|
||||
### Fixed
|
||||
- Fix bug where when multiple tasks are scheduled, they are inserted into the task queue in the wrong order, causing the jobs to not fire at the correct times
|
||||
|
||||
## [1.2.0] - 22 Jul 2019
|
||||
### Added
|
||||
- Original cron string passed to `CronosExpression.parse()` accessable on `CronosExpression.cronString` property
|
||||
- `.toString()` returns more useful information on `CronosExpression` and `CronosTimezone`
|
||||
### Fixed
|
||||
- Added workaround for bug when `Array.prototype.find` is incorrectly polyfilled
|
||||
- Added check to ensure tasks only run at most once a second
|
||||
|
||||
## [1.1.0] - 23 Jun 2019
|
||||
### Changed
|
||||
- Switched to [@pika/pack](https://github.com/pikapkg/pack) for building, adding builds optimised for node, browsers and modern ES module support (eg. Webpack, modern browsers)
|
||||
- Improved test coverage
|
||||
|
||||
## [1.0.0] - 8 May 2019
|
||||
First release of CronosJS, featuring:
|
||||
- Extended cron syntax support, including last day (`L`), nearest weekday (`W`), nth of month (`#`), optional second and year fields, and predefined expressions
|
||||
- Fixed offset and IANA timezone support, via `Intl` api
|
||||
- Configurable daylight saving handling
|
||||
- Zero dependencies
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2019, James Clarke
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
# CronosJS
|
||||
|
||||

|
||||

|
||||
[](https://travis-ci.com/jaclarke/cronosjs)
|
||||
[](https://coveralls.io/github/jaclarke/cronosjs?branch=master)
|
||||
|
||||
A cron based task scheduler for node and the browser, with extended syntax and timezone support.
|
||||
|
||||
Features:
|
||||
- Extended cron syntax support, including [last day](#last-day--l-) (`L`), [nearest weekday](#nearest-weekday--w-) (`W`), [nth of month](#nth-of-month---) (`#`), optional second and year fields, and [predefined expressions](#predefined-expressions)
|
||||
- Fixed offset and IANA [timezone support](#timezone-support), via `Intl` api
|
||||
- Configurable [daylight saving](#daylight-savings-behaviour) handling
|
||||
- Zero dependencies
|
||||
|
||||
|
||||
## Install / Usage
|
||||
|
||||
```bash
|
||||
npm i cronosjs
|
||||
```
|
||||
|
||||
```js
|
||||
import { scheduleTask, validate, CronosExpression } from 'cronosjs'
|
||||
|
||||
// schedule task every 10 minutes
|
||||
scheduleTask('*/10 * * * *', (timestamp) => {
|
||||
console.log(`Task triggered at ${timestamp}`)
|
||||
})
|
||||
|
||||
// schedule task at 16:10, on the 4th and last day of July, 2035 in the EST timezone
|
||||
scheduleTask('0 10 16 4,L Jul * 2035', (timestamp) => {
|
||||
console.log(`Task triggered at ${timestamp}`)
|
||||
}, {
|
||||
timezone: 'America/New_York'
|
||||
})
|
||||
|
||||
// offset tasks occurring in daylight savings missing hour
|
||||
scheduleTask('5/20 1 * Mar SunL', (timestamp) => {
|
||||
console.log(`Task triggered at ${timestamp}`)
|
||||
}, {
|
||||
timezone: 'Europe/London',
|
||||
missingHour: 'offset'
|
||||
})
|
||||
|
||||
// validate cron string
|
||||
validate('* * 5 smarch *') // false
|
||||
|
||||
validate('0 1/120 * * * *', {
|
||||
strict: true
|
||||
}) // false
|
||||
|
||||
// get next cron date
|
||||
CronosExpression.parse('* * 2/5 Jan *').nextDate()
|
||||
|
||||
// get next 7 cron dates after 09:17, 12th Mar 2019
|
||||
CronosExpression.parse('* * 2/5 Jan *').nextNDates(
|
||||
new Date('2019-03-12T09:17:00.000Z'), 7)
|
||||
|
||||
// advanced usage
|
||||
const expression = CronosExpression.parse('0 10 16 4,L Jul * 2035', {
|
||||
timezone: 'America/New_York'
|
||||
})
|
||||
const task = new CronosTask(expression)
|
||||
|
||||
task
|
||||
.on('run', (timestamp) => {
|
||||
console.log(`Task triggered at ${timestamp}`)
|
||||
})
|
||||
.on('ended', () => {
|
||||
console.log(`No more dates matching expression`)
|
||||
})
|
||||
.start()
|
||||
|
||||
// strict mode / warnings
|
||||
CronosExpression.parse('0 1/120 * * * *', {
|
||||
strict: true
|
||||
}) // Error: Strict mode: Parsing failed with 1 warnings
|
||||
|
||||
const strictExpr = CronosExpression.parse('0 1/120 * * * *')
|
||||
|
||||
console.log(strictExpr.warnings)
|
||||
// [{
|
||||
// type: 'IncrementLargerThanRange',
|
||||
// message: "Increment (120) is larger than range (58) for expression '1/120'"
|
||||
// }]
|
||||
|
||||
// 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()
|
||||
```
|
||||
|
||||
|
||||
## Supported expression syntax
|
||||
|
||||
```
|
||||
* * * * * * * Field Allowed values Special symbols
|
||||
| | | | | | | ----------------- --------------- ---------------
|
||||
`--|--|--|--|--|--|-> Second (optional) 0-59 * / , -
|
||||
`--|--|--|--|--|-> Minute 0-59 * / , -
|
||||
`--|--|--|--|-> Hour 0-23 * / , -
|
||||
`--|--|--|-> Day of Month 1-31 * / , - ? L W
|
||||
`--|--|-> Month 1-12 or JAN-DEC * / , -
|
||||
`--|-> Day of Week 0-7 or SUN-SAT * / , - ? L #
|
||||
`-> Year (optional) 0-275759 * / , -
|
||||
```
|
||||
|
||||
A cron expression is defined by between 5 and 7 fields separated by whitespace, as detailed above. Each field can contain an integer value in the allowed values range for that field, a three letter abbreviation (case insensitive) for the *Day of Week* and *Month* fields, or an expression containing a symbol.
|
||||
|
||||
A [predefined expression](#predefined-expressions) can also be given.
|
||||
|
||||
For the *Day of Week* field, `0` and `7` are equivalent to `Sun` , `1 = Mon` , ... , `5 = Fri` , `6 = Sat`.
|
||||
|
||||
> **Note** If only 5 fields are given, both the optional *second* and *year* fields will be given their default values of `0` and `*` respectively.
|
||||
> If 6 fields are given, the first field is assumed to be the *second* field, and the *year* field given its default value.
|
||||
|
||||
> **Why the 0 to 275759 allowed year range?**
|
||||
> The JS Date object supports dates 8,640,000,000,000,000 milliseconds either side of the 1st Jan, 1970 UTC ([ECMAScript 2019 Specification](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-time-values-and-time-range)), giving a maximum valid date of 13th Sep, 275760. Therefore the largest full year representable as a JS Date is 275759.
|
||||
> The year 0 is chosen as the minimum, disallowing negative years, to avoid confusion with the range symbol (`-`).
|
||||
|
||||
---
|
||||
|
||||
The following symbols are valid for any field:
|
||||
|
||||
### All / Any values (`*`)
|
||||
Selects all allowed values in the *second*, *minute*, *hour*, *month* and *year* fields. If part of a list of expressions in any of those fields, `*` will effectively override any other expression in that field.
|
||||
Can also be used as part of an expression containing the special symbols `/`, `L`, `W` or `#` as detailed below, where it similarly acts as a range of all valid values for that field, eg. in the *hour* field acts as `0-23`.
|
||||
|
||||
In the *Day of Month* and *Day of Week* fields, `*` on its own acts as a placeholder, matching any day (sometimes referred to as "no specific value"), and is overridden by any other expression listed in either *"Day of ..."* field. Only if both fields are '`*`', will the symbol have an effect, selecting every day of the month.
|
||||
If part of another expression, it acts as above.
|
||||
|
||||
The `?` symbol can be used as an alias for '`*`' (on its own) in the *Day of Month* and *Day of Week* fields.
|
||||
|
||||
### List (`,`)
|
||||
Separates a list of expressions for a field.
|
||||
|
||||
The separated expressions can contain any of the allowed values and symbols for that field; however while valid, some lists may not make sense, eg. in `6-14,*`, the `6-14` part is made redundant by the `*` part.
|
||||
|
||||
### Range (`-`)
|
||||
Defines a range of values, inclusive. eg. `16-39` in the seconds field means the 16th second and every second after up to and including the 39th second.
|
||||
|
||||
For fields with a cyclic nature (ie. *Second*, *Minute*, *Hour*, *Month* and *Day of Week*), wrap-around ranges are supported, eg. `Fri-Mon` will select `Fri, Sat, Sun and Mon`. Otherwise for non-cyclic fields (ie. *Day of Month* and *Year*) the second value is required to be greater than the first value.
|
||||
|
||||
> **Note** Wrap-around ranges are purely 'syntactic sugar' to primarily make *day of week* and *month* ranges simpler to write, and do not alter the underlying behaviour of this cron library. The parser effectively translates wrap-around expressions such as `Fri-Mon` to the standard form, as though it were written as `Fri-Sat,Sun-Mon` (`5-1` and `5-6,0-1` respectively in numerical form), meaning any range expression is still able to be written in a form compatible with other cron implementations.
|
||||
>
|
||||
> It is for this reason *Day of Month* is considered non-cyclic, since the number of days in a month differs between months, leading to possibly unexpected behaviour when a wraparound range is used with an increment.
|
||||
> eg. The *Day of Month* expression `27-5/2` would select the days `27, 29, 31, 2, 4`, regardless of the number of days in the month, so in a month with only 30 days the actual scheduled days would become `27, 29, 2, 4`, creating a 3 day increment between the 29th and the 2nd. Correctly handling increments across the wraparound would create behaviour incompatible with other cron implementations, such that simple translation to a non wrap-around form would not be possible.
|
||||
|
||||
### Increments (`/`)
|
||||
Defines increments of a range. Can be used in three ways:
|
||||
- The full range can be given, eg. `4-38/3` in the minutes field means the 4th minute and every 3rd minute after upto the 38th minute, ie. [4, 7, 10, ..., 31, 34, 37]
|
||||
- The start of the range can be given, eg. `4/3`, in which case the end of the range will be the maximum allowed value for that field
|
||||
- Or can be used with the `*` symbol, eg. `*/3`, which will use the full range allowed for that field, ie. equivalent to `0-59/3` for the minutes field
|
||||
|
||||
---
|
||||
|
||||
The following symbols are valid only for the *Day of Month* and/or *Day of Week* fields, and can be combined with any valid expression above (unless specified otherwise):
|
||||
|
||||
### No Specific Value (`?`)
|
||||
An alias for '`*`' in the *Day of Month* and *Day of Week* fields.
|
||||
|
||||
> **Note** Is not valid as part of another expression, eg. `?/2`, `?W` and `?#3` are invalid.
|
||||
|
||||
### Last day (`L`)
|
||||
When used in the *Day of Month* field, must be on its own, and means the last day of that month.
|
||||
|
||||
When used in the *Day of Week* field, must be used as a suffix on another expression, and means the last specified day(s) of week of that month.
|
||||
|
||||
Examples:
|
||||
- `WedL` selects the last Wednesday of the month
|
||||
- `*L` selects the whole last week of the month
|
||||
- `Mon-WedL` selects the last Monday, Tuesday and Wednesday of the month
|
||||
|
||||
### Nearest weekday (`W`)
|
||||
The `W` symbol is only valid as a suffix for the *Day of Month* field, and will select the nearest weekday(s) (Mon-Fri) to the given day(s) if that day is a Saturday or Sunday, otherwise will select the given day.
|
||||
|
||||
Examples:
|
||||
- `14W` selects the nearest weekday to the 14th of the month
|
||||
- `*W` selects every weekday of the month (same as writing `Mon-Fri` in the *Day of Week* field)
|
||||
- `5-12W` selects the nearest weekdays to everyday from the 5th to the 12th of the month
|
||||
- `18/3W` selects the nearest weekdays to the 18th, 21st, 24th, 27th and 30th of the month
|
||||
|
||||
> **Note** If the given day is the start or end of the month, the nearest weekday will not be selected if it is in another month, instead the next nearest weekday in the same month will be selected.
|
||||
>
|
||||
> For example given the expression `* * 1W * *`, on a month starting on Saturday, the selected day would be the 3rd on the following Monday, not the Friday on the last day of the previous month:
|
||||
> ```
|
||||
> Wed Thu Fri | Sat Sun Mon
|
||||
> 29 30 31 | 1 2 3
|
||||
> x ^ ✓
|
||||
> ```
|
||||
|
||||
Last day and nearest weekday can be combined, ie. `LW`, to select the last weekday of month.
|
||||
|
||||
### Nth of month (`#`)
|
||||
The `#` symbol is only valid as a suffix for the *Day of Week* field expression, and must be followed by a number `1-5`
|
||||
|
||||
Examples:
|
||||
- `Tue#3` selects the 3rd Tuesday of the month
|
||||
- `*#2` selects the whole 2nd week of the month
|
||||
- `Thu-Mon/2#4` selects the 4th Thu, Sat and Mon of the month
|
||||
|
||||
|
||||
## Timezone support
|
||||
The timezone option supports either a string containing an [IANA timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (eg. `'America/New_York'`), or a fixed offset from UTC as either a string in the format `(+|-)hh[:]mm`, or an integer number of minutes.
|
||||
|
||||
IANA timezone support is dependent on the `Intl.DateTimeFormat` api being supported by the browser/Node.js. The `Intl` api is supported by most [modern browsers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat#Browser_compatibility) and [versions of Node.js](http://kangax.github.io/compat-table/esintl/#test-DateTimeFormat).
|
||||
|
||||
If no timezone is specified the system's local timezone is used.
|
||||
|
||||
|
||||
## Daylight savings behaviour
|
||||
If the configured timezone observes daylight savings how the missing hour when the daylight savings starts, and the repeated hour when it ends are handled can be specified by the `missingHour` and `skipRepeatedHour` options.
|
||||
|
||||
### `missingHour` option
|
||||
The `missingHour` option allows three options: `'skip'`, `'offset'` and `'insert'`. (Defaults to `'insert'`)
|
||||
|
||||
For example in `'Europe/London'` timezone, with the cron expression `5/20 1 * * *` :
|
||||
|
||||
```
|
||||
31st March 2019
|
||||
GMT(+00:00) -> BST(+01:00)
|
||||
|
||||
UTC o x x x
|
||||
|-----------------|-----------------|-----------------|-----------------|
|
||||
00:00 01:00 02:00 03:00 04:00
|
||||
|
||||
GMT(+00:00) x x x
|
||||
|-----------------+.................
|
||||
00:00 (01:00)
|
||||
|
||||
BST(+01:00)
|
||||
|-----------------|-----------------|-----------------|
|
||||
02:00 03:00 04:00 05:00
|
||||
|
||||
```
|
||||
- `'skip'` - all the marked times are skipped
|
||||
- `'offset'` - the the times that would have occurred in the missing 01:00:00 - 01:59:59 (local time) period are offset by an hour to the 02:00 hour, ie. the three times marked `x`
|
||||
- `'insert'` - if any times occur in the missing hour, a time is inserted at the instant where offset changes, ie. the time marked `o`
|
||||
|
||||
> **Note** The task is only run at most once per second, so if multiple times end up occurring at the same time the task is only run once
|
||||
|
||||
### `skipRepeatedHour` option
|
||||
The `skipRepeatedHour` option is a boolean option, that specifies whether or not a task should be scheduled for a second time in a repeated hour. (Default to `true`)
|
||||
|
||||
For example in `'Europe/London'` timezone, with the cron expression `*/20 1 * * *` :
|
||||
|
||||
```
|
||||
27th October 2019
|
||||
BST(+01:00) -> GMT(+00:00)
|
||||
|
||||
UTC o o o x x x
|
||||
|-----------------|-----------------|-----------------|-----------------|
|
||||
23:00 00:00 01:00 02:00 03:00
|
||||
|
||||
BST(+01:00) o o o
|
||||
|-----------------|-----------------+
|
||||
00:00 01:00 (02:00)
|
||||
|
||||
GMT(+00:00) x x x
|
||||
|-----------------|-----------------|
|
||||
01:00 02:00 03:00
|
||||
|
||||
```
|
||||
If `skipRepeatedHour: true` only the times marked `o` are scheduled, otherwise all times marked `o` and `x` are scheduled.
|
||||
|
||||
|
||||
## API
|
||||
```js
|
||||
import {
|
||||
scheduleTask, validate,
|
||||
CronosExpression, CronosTask,
|
||||
CronosTimezone
|
||||
} from 'cronosjs'
|
||||
```
|
||||
|
||||
### scheduleTask
|
||||
```function scheduleTask(cronString, task, options?)```
|
||||
|
||||
- `cronString: string`
|
||||
The cron expression defining the schedule on which to run the task. [Allowed syntax](#supported-expression-syntax)
|
||||
|
||||
- `task: (timestamp: number) => void`
|
||||
The function to run on each execution of the task. Is called with the timestamp of when the task was scheduled to run.
|
||||
|
||||
- `options: { timezone?, skipRepeatedHour?, missingHour?, strict? }` (optional)
|
||||
- `timezone: CronosTimezone | string | number` (optional)
|
||||
Timezone in which to schedule the tasks, can be either a `CronosTimezone` object, or any IANA timezone or offset accepted by the [`CronosTimezone` constructor](#cronostimezone)
|
||||
- `skipRepeatedHour: boolean` (optional)
|
||||
Should tasks be scheduled in the repeated hour when DST ends. [Further details](#skiprepeatedhour-option)
|
||||
- `missingHour: 'insert' | 'offset' | 'skip'` (optional)
|
||||
How tasks should be scheduled in the missing hour when DST starts. [Further details](#missinghour-option)
|
||||
- `strict: boolean | {<WarningType>: boolean, ...}` (optional)
|
||||
Should an error be thrown if warnings occur during parsing. If `true`, will throw for all `WarningType`'s, alternatively an object can be provided with `WarningType`'s as the keys and boolean values to individually select which `WarningType`'s trigger an error to be thown. `WarningTypes`'s are listed in the [`CronosExpression.warnings`](#cronosexpression) documentation.
|
||||
|
||||
- **Returns** [`CronosTask`](#cronostask)
|
||||
|
||||
|
||||
### validate
|
||||
```function validate(cronString, options?)```
|
||||
|
||||
- `cronString: string`
|
||||
Cron string to validate
|
||||
- `options: { strict? }`
|
||||
Same as `strict` option documented in [`scheduleTask`](#scheduletask)
|
||||
|
||||
- **Returns** `boolean`
|
||||
Is cron string syntax valid
|
||||
|
||||
|
||||
### CronosExpression
|
||||
```class CronosExpression```
|
||||
|
||||
#### Properties
|
||||
- `cronString: string` (readonly)
|
||||
Original cron string passed to `CronosExpression.parse`
|
||||
- `warnings: Warning[]` (readonly)
|
||||
A list of warnings that occurred during parsing the expression.
|
||||
```typescript
|
||||
interface Warning {
|
||||
type: WarningType
|
||||
message: string
|
||||
}
|
||||
|
||||
type WarningType = 'IncrementLargerThanRange'
|
||||
```
|
||||
|
||||
#### Static Methods
|
||||
- `CronosExpression.parse(cronString, options)`
|
||||
Parameters `cronString` and `options` same as for [`scheduleTask`](#scheduletask), returns `CronosExpression` instance
|
||||
|
||||
#### Methods
|
||||
- `nextDate(afterDate?)`
|
||||
- `afterDate: Date` (optional)
|
||||
The date after which to find the next date matching the cron expression, if not specified defaults to current date `new Date()`
|
||||
- **Returns** `Date`
|
||||
The next date matching the cron expression after the given date. May return null if no further matching dates exist (eg. if `year` field is specified in expression)
|
||||
|
||||
- `nextNDates(afterDate?, n?)`
|
||||
- `afterDate: Date` (optional)
|
||||
As above in `nextDate` method
|
||||
- `n: number` (optional)
|
||||
Number of dates to generate, defaults to 5
|
||||
- **Returns** `[Date]`
|
||||
An array of the next `n` dates after the given date. May return fewer dates than specified if no further dates exist
|
||||
|
||||
|
||||
### CronosTask
|
||||
```class CronosTask```
|
||||
|
||||
#### 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)
|
||||
Date when task is next scheduled to run
|
||||
- `isRunning: boolean` (readonly)
|
||||
Is the task scheduled to run
|
||||
|
||||
#### Methods
|
||||
- `start()`
|
||||
Starts scheduling executions of the task as defined by the cron expression
|
||||
- `stop()`
|
||||
Removes any scheduled executions and stops any further executions of the task
|
||||
- `on(event: string, listener: Function)`
|
||||
Adds a listener for an event
|
||||
- `off(event: string, listener: Function)`
|
||||
Removes a listener for an event
|
||||
|
||||
#### Events
|
||||
- `'started': () => void`
|
||||
Listeners called when `start()` is called on task
|
||||
- `'stopped': () => void`
|
||||
Listeners called when `stop()` is called on task
|
||||
- `'run': (timestamp: number) => void`
|
||||
Listeners called on each date matching the cron expression. Listener is passed the timestamp when the execution was scheduled to start
|
||||
- `'ended': () => void`
|
||||
Listeners called when there are no further matching dates
|
||||
|
||||
|
||||
### CronosTimezone
|
||||
```class CronosTimezone```
|
||||
|
||||
#### Constructor
|
||||
```new CronosTimezone(IANANameOrOffset)```
|
||||
- `IANANameOrOffset: string | number`
|
||||
IANA zone or fixed offset as detailed under [Timezone Support](#timezone-support)
|
||||
|
||||
|
||||
## Predefined expressions
|
||||
|
||||
| Expression | Description | Equivalent to... |
|
||||
| -------------------------| -------------------------------------------- | ---------------- |
|
||||
| `@yearly` or `@annually` | Once a year at midnight on 1st January | `0 0 1 1 *` |
|
||||
| `@monthly` | Once a month at midnight on 1st of the month | `0 0 1 * *` |
|
||||
| `@weekly` | Once a week at midnight on Sunday | `0 0 * * 0` |
|
||||
| `@daily` or `@midnight` | Once a day at midnight | `0 0 * * *` |
|
||||
| `@hourly` | Once an hour at the beginning of each hour | `0 * * * *` |
|
||||
+970
@@ -0,0 +1,970 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
const sortAsc = (a, b) => a - b;
|
||||
function flatMap(arr, mapper) {
|
||||
return arr.reduce((acc, val, i) => {
|
||||
acc.push(...mapper(val, i, arr));
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
const predefinedCronStrings = {
|
||||
'@yearly': '0 0 0 1 1 * *',
|
||||
'@annually': '0 0 0 1 1 * *',
|
||||
'@monthly': '0 0 0 1 * * *',
|
||||
'@weekly': '0 0 0 * * 0 *',
|
||||
'@daily': '0 0 0 * * * *',
|
||||
'@midnight': '0 0 0 * * * *',
|
||||
'@hourly': '0 0 * * * * *'
|
||||
};
|
||||
const monthReplacements = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
||||
const monthReplacementRegex = new RegExp(monthReplacements.join('|'), 'g');
|
||||
const dayOfWeekReplacements = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||
const dayOfWeekReplacementRegex = new RegExp(dayOfWeekReplacements.join('|'), 'g');
|
||||
/*
|
||||
"The actual range of times supported by ECMAScript Date objects is slightly smaller:
|
||||
exactly –100,000,000 days to 100,000,000 days measured relative to midnight at the
|
||||
beginning of 01 January, 1970 UTC. This gives a range of 8,640,000,000,000,000
|
||||
milliseconds to either side of 01 January, 1970 UTC."
|
||||
http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1
|
||||
|
||||
new Date(8640000000000000) => 00:00:00 13th Sep 275760
|
||||
Largest full year valid as JS date = 275759
|
||||
*/
|
||||
|
||||
const maxValidYear = 275759;
|
||||
var WarningType;
|
||||
|
||||
(function (WarningType) {
|
||||
WarningType["IncrementLargerThanRange"] = "IncrementLargerThanRange";
|
||||
})(WarningType || (WarningType = {}));
|
||||
|
||||
function _parse(cronstring) {
|
||||
let expr = cronstring.trim().toLowerCase();
|
||||
|
||||
if (predefinedCronStrings[expr]) {
|
||||
expr = predefinedCronStrings[expr];
|
||||
}
|
||||
|
||||
const fields = expr.split(/\s+/g);
|
||||
|
||||
if (fields.length < 5 || fields.length > 7) {
|
||||
throw new Error('Expression must have at least 5 fields, and no more than 7 fields');
|
||||
}
|
||||
|
||||
switch (fields.length) {
|
||||
case 5:
|
||||
fields.unshift('0');
|
||||
|
||||
case 6:
|
||||
fields.push('*');
|
||||
}
|
||||
|
||||
return [new SecondsOrMinutesField(fields[0]), new SecondsOrMinutesField(fields[1]), new HoursField(fields[2]), new DaysField(fields[3], fields[5]), new MonthsField(fields[4]), new YearsField(fields[6])];
|
||||
}
|
||||
|
||||
function getIncrementLargerThanRangeWarnings(items, first, last) {
|
||||
const warnings = [];
|
||||
|
||||
for (let item of items) {
|
||||
let rangeLength;
|
||||
|
||||
if (item.step > 1 && item.step > (rangeLength = item.rangeLength(first, last))) {
|
||||
warnings.push({
|
||||
type: WarningType.IncrementLargerThanRange,
|
||||
message: `Increment (${item.step}) is larger than range (${rangeLength}) for expression '${item.itemString}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
class Field {
|
||||
constructor(field) {
|
||||
this.field = field;
|
||||
}
|
||||
|
||||
parse() {
|
||||
return this.field.split(',').map(item => FieldItem.parse(item, this.first, this.last, true));
|
||||
}
|
||||
|
||||
get items() {
|
||||
if (!this._items) this._items = this.parse();
|
||||
return this._items;
|
||||
}
|
||||
|
||||
get values() {
|
||||
return Field.getValues(this.items, this.first, this.last);
|
||||
}
|
||||
|
||||
get warnings() {
|
||||
return getIncrementLargerThanRangeWarnings(this.items, this.first, this.last);
|
||||
}
|
||||
|
||||
static getValues(items, first, last) {
|
||||
return Array.from(new Set(flatMap(items, item => item.values(first, last)))).sort(sortAsc);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FieldItem {
|
||||
constructor(itemString) {
|
||||
this.itemString = itemString;
|
||||
this.step = 1;
|
||||
}
|
||||
|
||||
rangeLength(first, last) {
|
||||
var _a, _b, _c, _d;
|
||||
|
||||
const start = (_b = (_a = this.range) === null || _a === void 0 ? void 0 : _a.from) !== null && _b !== void 0 ? _b : first,
|
||||
end = (_d = (_c = this.range) === null || _c === void 0 ? void 0 : _c.to) !== null && _d !== void 0 ? _d : last;
|
||||
return end < start ? last - start + (end - first) + 1 : end - start;
|
||||
}
|
||||
|
||||
values(first, last) {
|
||||
const start = this.range ? this.range.from : first,
|
||||
rangeLength = this.rangeLength(first, last);
|
||||
return Array(Math.floor(rangeLength / this.step) + 1).fill(0).map((_, i) => first + (start - first + this.step * i) % (last - first + 1));
|
||||
}
|
||||
|
||||
get any() {
|
||||
return this.range === undefined && this.step === 1;
|
||||
}
|
||||
|
||||
get single() {
|
||||
return !!this.range && this.range.from === this.range.to;
|
||||
}
|
||||
|
||||
static parse(item, first, last, allowCyclicRange = false, transformer) {
|
||||
var _a;
|
||||
|
||||
const fieldItem = new FieldItem(item);
|
||||
const [match, all, startFrom, range, step] = (_a = item.match(/^(?:(\*)|([0-9]+)|([0-9]+-[0-9]+))(?:\/([1-9][0-9]*))?$/)) !== null && _a !== void 0 ? _a : [];
|
||||
if (!match) throw new Error('Field item invalid format');
|
||||
|
||||
if (step) {
|
||||
fieldItem.step = parseInt(step, 10);
|
||||
}
|
||||
|
||||
if (startFrom) {
|
||||
let start = parseInt(startFrom, 10);
|
||||
start = transformer ? transformer(start) : start;
|
||||
if (start < first || start > last) throw new Error('Field item out of valid value range');
|
||||
fieldItem.range = {
|
||||
from: start,
|
||||
to: step ? undefined : start
|
||||
};
|
||||
} else if (range) {
|
||||
const [rangeStart, rangeEnd] = range.split('-').map(x => {
|
||||
const n = parseInt(x, 10);
|
||||
return transformer ? transformer(n) : n;
|
||||
});
|
||||
|
||||
if (rangeStart < first || rangeStart > last || rangeEnd < first || rangeEnd > last || rangeEnd < rangeStart && !allowCyclicRange) {
|
||||
throw new Error('Field item range invalid, either value out of valid range or start greater than end in non wraparound field');
|
||||
}
|
||||
|
||||
fieldItem.range = {
|
||||
from: rangeStart,
|
||||
to: rangeEnd
|
||||
};
|
||||
}
|
||||
|
||||
return fieldItem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
FieldItem.asterisk = new FieldItem('*');
|
||||
class SecondsOrMinutesField extends Field {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.first = 0;
|
||||
this.last = 59;
|
||||
}
|
||||
|
||||
}
|
||||
class HoursField extends Field {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.first = 0;
|
||||
this.last = 23;
|
||||
}
|
||||
|
||||
}
|
||||
class DaysField {
|
||||
constructor(daysOfMonthField, daysOfWeekField) {
|
||||
this.lastDay = false;
|
||||
this.lastWeekday = false;
|
||||
this.daysItems = [];
|
||||
this.nearestWeekdayItems = [];
|
||||
this.daysOfWeekItems = [];
|
||||
this.lastDaysOfWeekItems = [];
|
||||
this.nthDaysOfWeekItems = [];
|
||||
|
||||
for (let item of daysOfMonthField.split(',').map(s => s === '?' ? '*' : s)) {
|
||||
if (item === 'l') {
|
||||
this.lastDay = true;
|
||||
} else if (item === 'lw') {
|
||||
this.lastWeekday = true;
|
||||
} else if (item.endsWith('w')) {
|
||||
this.nearestWeekdayItems.push(FieldItem.parse(item.slice(0, -1), 1, 31));
|
||||
} else {
|
||||
this.daysItems.push(FieldItem.parse(item, 1, 31));
|
||||
}
|
||||
}
|
||||
|
||||
const normalisedDaysOfWeekField = daysOfWeekField.replace(dayOfWeekReplacementRegex, match => dayOfWeekReplacements.indexOf(match) + '');
|
||||
|
||||
const parseDayOfWeek = item => FieldItem.parse(item, 0, 6, true, n => n === 7 ? 0 : n);
|
||||
|
||||
for (let item of normalisedDaysOfWeekField.split(',').map(s => s === '?' ? '*' : s)) {
|
||||
const nthIndex = item.lastIndexOf('#');
|
||||
|
||||
if (item.endsWith('l')) {
|
||||
this.lastDaysOfWeekItems.push(parseDayOfWeek(item.slice(0, -1)));
|
||||
} else if (nthIndex !== -1) {
|
||||
const nth = item.slice(nthIndex + 1);
|
||||
if (!/^[1-5]$/.test(nth)) throw new Error('Field item nth of month (#) invalid');
|
||||
this.nthDaysOfWeekItems.push({
|
||||
item: parseDayOfWeek(item.slice(0, nthIndex)),
|
||||
nth: parseInt(nth, 10)
|
||||
});
|
||||
} else {
|
||||
this.daysOfWeekItems.push(parseDayOfWeek(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get values() {
|
||||
return DaysFieldValues.fromField(this);
|
||||
}
|
||||
|
||||
get warnings() {
|
||||
const warnings = [],
|
||||
dayItems = [...this.daysItems, ...this.nearestWeekdayItems],
|
||||
weekItems = [...this.daysOfWeekItems, ...this.lastDaysOfWeekItems, ...this.nthDaysOfWeekItems.map(({
|
||||
item
|
||||
}) => item)];
|
||||
warnings.push(...getIncrementLargerThanRangeWarnings(dayItems, 1, 31), ...getIncrementLargerThanRangeWarnings(weekItems, 0, 6));
|
||||
return warnings;
|
||||
}
|
||||
|
||||
get allDays() {
|
||||
return !this.lastDay && !this.lastWeekday && !this.nearestWeekdayItems.length && !this.lastDaysOfWeekItems.length && !this.nthDaysOfWeekItems.length && this.daysItems.length === 1 && this.daysItems[0].any && this.daysOfWeekItems.length === 1 && this.daysOfWeekItems[0].any;
|
||||
}
|
||||
|
||||
}
|
||||
class DaysFieldValues {
|
||||
constructor() {
|
||||
this.lastDay = false;
|
||||
this.lastWeekday = false;
|
||||
this.days = [];
|
||||
this.nearestWeekday = [];
|
||||
this.daysOfWeek = [];
|
||||
this.lastDaysOfWeek = [];
|
||||
this.nthDaysOfWeek = [];
|
||||
}
|
||||
|
||||
static fromField(field) {
|
||||
const values = new DaysFieldValues();
|
||||
|
||||
const filterAnyItems = items => items.filter(item => !item.any);
|
||||
|
||||
values.lastDay = field.lastDay;
|
||||
values.lastWeekday = field.lastWeekday;
|
||||
values.days = Field.getValues(field.allDays ? [FieldItem.asterisk] : filterAnyItems(field.daysItems), 1, 31);
|
||||
values.nearestWeekday = Field.getValues(field.nearestWeekdayItems, 1, 31);
|
||||
values.daysOfWeek = Field.getValues(filterAnyItems(field.daysOfWeekItems), 0, 6);
|
||||
values.lastDaysOfWeek = Field.getValues(field.lastDaysOfWeekItems, 0, 6);
|
||||
const nthDaysHashes = new Set();
|
||||
|
||||
for (let item of field.nthDaysOfWeekItems) {
|
||||
for (let n of item.item.values(0, 6)) {
|
||||
let hash = n * 10 + item.nth;
|
||||
|
||||
if (!nthDaysHashes.has(hash)) {
|
||||
nthDaysHashes.add(hash);
|
||||
values.nthDaysOfWeek.push([n, item.nth]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
getDays(year, month) {
|
||||
const days = new Set(this.days);
|
||||
const lastDateOfMonth = new Date(year, month, 0).getDate();
|
||||
const firstDayOfWeek = new Date(year, month - 1, 1).getDay();
|
||||
|
||||
const getNearestWeekday = day => {
|
||||
if (day > lastDateOfMonth) day = lastDateOfMonth;
|
||||
const dayOfWeek = (day + firstDayOfWeek - 1) % 7;
|
||||
let weekday = day + (dayOfWeek === 0 ? 1 : dayOfWeek === 6 ? -1 : 0);
|
||||
return weekday + (weekday < 1 ? 3 : weekday > lastDateOfMonth ? -3 : 0);
|
||||
};
|
||||
|
||||
if (this.lastDay) {
|
||||
days.add(lastDateOfMonth);
|
||||
}
|
||||
|
||||
if (this.lastWeekday) {
|
||||
days.add(getNearestWeekday(lastDateOfMonth));
|
||||
}
|
||||
|
||||
for (const day of this.nearestWeekday) {
|
||||
days.add(getNearestWeekday(day));
|
||||
}
|
||||
|
||||
if (this.daysOfWeek.length || this.lastDaysOfWeek.length || this.nthDaysOfWeek.length) {
|
||||
const daysOfWeek = Array(7).fill(0).map(() => []);
|
||||
|
||||
for (let day = 1; day < 36; day++) {
|
||||
daysOfWeek[(day + firstDayOfWeek - 1) % 7].push(day);
|
||||
}
|
||||
|
||||
for (const dayOfWeek of this.daysOfWeek) {
|
||||
for (const day of daysOfWeek[dayOfWeek]) {
|
||||
days.add(day);
|
||||
}
|
||||
}
|
||||
|
||||
for (const dayOfWeek of this.lastDaysOfWeek) {
|
||||
for (let i = daysOfWeek[dayOfWeek].length - 1; i >= 0; i--) {
|
||||
if (daysOfWeek[dayOfWeek][i] <= lastDateOfMonth) {
|
||||
days.add(daysOfWeek[dayOfWeek][i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [dayOfWeek, nthOfMonth] of this.nthDaysOfWeek) {
|
||||
days.add(daysOfWeek[dayOfWeek][nthOfMonth - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(days).filter(day => day <= lastDateOfMonth).sort(sortAsc);
|
||||
}
|
||||
|
||||
}
|
||||
class MonthsField extends Field {
|
||||
constructor(field) {
|
||||
super(field.replace(monthReplacementRegex, match => {
|
||||
return monthReplacements.indexOf(match) + 1 + '';
|
||||
}));
|
||||
this.first = 1;
|
||||
this.last = 12;
|
||||
}
|
||||
|
||||
}
|
||||
class YearsField extends Field {
|
||||
constructor(field) {
|
||||
super(field);
|
||||
this.first = 1970;
|
||||
this.last = 2099;
|
||||
this.items;
|
||||
}
|
||||
|
||||
parse() {
|
||||
return this.field.split(',').map(item => FieldItem.parse(item, 0, maxValidYear));
|
||||
}
|
||||
|
||||
get warnings() {
|
||||
return getIncrementLargerThanRangeWarnings(this.items, this.first, maxValidYear);
|
||||
}
|
||||
|
||||
nextYear(fromYear) {
|
||||
var _a;
|
||||
|
||||
return (_a = this.items.reduce((years, item) => {
|
||||
var _a, _b, _c, _d;
|
||||
|
||||
if (item.any) years.push(fromYear);else if (item.single) {
|
||||
const year = item.range.from;
|
||||
if (year >= fromYear) years.push(year);
|
||||
} else {
|
||||
const start = (_b = (_a = item.range) === null || _a === void 0 ? void 0 : _a.from) !== null && _b !== void 0 ? _b : this.first;
|
||||
if (start > fromYear) years.push(start);else {
|
||||
const nextYear = start + Math.ceil((fromYear - start) / item.step) * item.step;
|
||||
if (nextYear <= ((_d = (_c = item.range) === null || _c === void 0 ? void 0 : _c.to) !== null && _d !== void 0 ? _d : maxValidYear)) years.push(nextYear);
|
||||
}
|
||||
}
|
||||
return years;
|
||||
}, []).sort(sortAsc)[0]) !== null && _a !== void 0 ? _a : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CronosDate {
|
||||
constructor(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) {
|
||||
this.year = year;
|
||||
this.month = month;
|
||||
this.day = day;
|
||||
this.hour = hour;
|
||||
this.minute = minute;
|
||||
this.second = second;
|
||||
}
|
||||
|
||||
static fromDate(date, timezone) {
|
||||
if (!timezone) {
|
||||
return new CronosDate(date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds());
|
||||
}
|
||||
|
||||
return timezone['nativeDateToCronosDate'](date);
|
||||
}
|
||||
|
||||
toDate(timezone) {
|
||||
if (!timezone) {
|
||||
return new Date(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
|
||||
}
|
||||
|
||||
return timezone['cronosDateToNativeDate'](this);
|
||||
}
|
||||
|
||||
static fromUTCTimestamp(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return new CronosDate(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
|
||||
}
|
||||
|
||||
toUTCTimestamp() {
|
||||
return Date.UTC(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
|
||||
}
|
||||
|
||||
copyWith({
|
||||
year = this.year,
|
||||
month = this.month,
|
||||
day = this.day,
|
||||
hour = this.hour,
|
||||
minute = this.minute,
|
||||
second = this.second
|
||||
} = {}) {
|
||||
return new CronosDate(year, month, day, hour, minute, second);
|
||||
}
|
||||
|
||||
} // Adapted from Intl.DateTimeFormat timezone handling in https://github.com/moment/luxon
|
||||
|
||||
const ZoneCache = new Map();
|
||||
class CronosTimezone {
|
||||
constructor(IANANameOrOffset) {
|
||||
if (typeof IANANameOrOffset === 'number') {
|
||||
if (IANANameOrOffset > 840 || IANANameOrOffset < -840) throw new Error('Invalid offset');
|
||||
this.fixedOffset = IANANameOrOffset;
|
||||
return this;
|
||||
}
|
||||
|
||||
const offsetMatch = IANANameOrOffset.match(/^([+-]?)(0[1-9]|1[0-4])(?::?([0-5][0-9]))?$/);
|
||||
|
||||
if (offsetMatch) {
|
||||
this.fixedOffset = (offsetMatch[1] === '-' ? -1 : 1) * (parseInt(offsetMatch[2], 10) * 60 + (parseInt(offsetMatch[3], 10) || 0));
|
||||
return this;
|
||||
}
|
||||
|
||||
if (ZoneCache.has(IANANameOrOffset)) {
|
||||
return ZoneCache.get(IANANameOrOffset);
|
||||
}
|
||||
|
||||
try {
|
||||
this.dateTimeFormat = new Intl.DateTimeFormat("en-US", {
|
||||
hour12: false,
|
||||
timeZone: IANANameOrOffset,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error('Invalid IANA name or offset');
|
||||
}
|
||||
|
||||
this.zoneName = IANANameOrOffset;
|
||||
const currentYear = new Date().getUTCFullYear();
|
||||
this.winterOffset = this.offset(Date.UTC(currentYear, 0, 1));
|
||||
this.summerOffset = this.offset(Date.UTC(currentYear, 5, 1));
|
||||
ZoneCache.set(IANANameOrOffset, this);
|
||||
}
|
||||
|
||||
toString() {
|
||||
if (this.fixedOffset) {
|
||||
const absOffset = Math.abs(this.fixedOffset);
|
||||
return [this.fixedOffset < 0 ? '-' : '+', Math.floor(absOffset / 60).toString().padStart(2, '0'), (absOffset % 60).toString().padStart(2, '0')].join('');
|
||||
}
|
||||
|
||||
return this.zoneName;
|
||||
}
|
||||
|
||||
offset(ts) {
|
||||
if (!this.dateTimeFormat) return this.fixedOffset || 0;
|
||||
const date = new Date(ts);
|
||||
const {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second
|
||||
} = this.nativeDateToCronosDate(date);
|
||||
const asUTC = Date.UTC(year, month - 1, day, hour, minute, second),
|
||||
asTS = ts - ts % 1000;
|
||||
return (asUTC - asTS) / 60000;
|
||||
}
|
||||
|
||||
nativeDateToCronosDate(date) {
|
||||
if (!this.dateTimeFormat) {
|
||||
return CronosDate['fromUTCTimestamp'](date.getTime() + (this.fixedOffset || 0) * 60000);
|
||||
}
|
||||
|
||||
return this.dateTimeFormat['formatToParts'] ? partsOffset(this.dateTimeFormat, date) : hackyOffset(this.dateTimeFormat, date);
|
||||
}
|
||||
|
||||
cronosDateToNativeDate(date) {
|
||||
if (!this.dateTimeFormat) {
|
||||
return new Date(date['toUTCTimestamp']() - (this.fixedOffset || 0) * 60000);
|
||||
}
|
||||
|
||||
const provisionalOffset = (date.month > 3 || date.month < 11 ? this.summerOffset : this.winterOffset) || 0;
|
||||
const UTCTimestamp = date['toUTCTimestamp'](); // Find the right offset a given local time.
|
||||
// Our UTC time is just a guess because our offset is just a guess
|
||||
|
||||
let utcGuess = UTCTimestamp - provisionalOffset * 60000; // Test whether the zone matches the offset for this ts
|
||||
|
||||
const o2 = this.offset(utcGuess); // If so, offset didn't change and we're done
|
||||
|
||||
if (provisionalOffset === o2) return new Date(utcGuess); // If not, change the ts by the difference in the offset
|
||||
|
||||
utcGuess -= (o2 - provisionalOffset) * 60000; // If that gives us the local time we want, we're done
|
||||
|
||||
const o3 = this.offset(utcGuess);
|
||||
if (o2 === o3) return new Date(utcGuess); // If it's different, we're in a hole time. The offset has changed, but the we don't adjust the time
|
||||
|
||||
return new Date(UTCTimestamp - Math.min(o2, o3) * 60000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function hackyOffset(dtf, date) {
|
||||
const formatted = dtf.format(date).replace(/\u200E/g, ""),
|
||||
parsed = formatted.match(/(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/),
|
||||
[, month, day, year, hour, minute, second] = (parsed !== null && parsed !== void 0 ? parsed : []).map(n => parseInt(n, 10));
|
||||
return new CronosDate(year, month, day, hour % 24, minute, second);
|
||||
}
|
||||
|
||||
function partsOffset(dtf, date) {
|
||||
const formatted = dtf.formatToParts(date);
|
||||
return new CronosDate(parseInt(formatted[4].value, 10), parseInt(formatted[0].value, 10), parseInt(formatted[2].value, 10), parseInt(formatted[6].value, 10) % 24, parseInt(formatted[8].value, 10), parseInt(formatted[10].value, 10));
|
||||
}
|
||||
|
||||
const hourinms = 60 * 60 * 1000;
|
||||
|
||||
const findFirstFrom = (from, list) => list.findIndex(n => n >= from);
|
||||
|
||||
class CronosExpression {
|
||||
constructor(cronString, seconds, minutes, hours, days, months, years) {
|
||||
this.cronString = cronString;
|
||||
this.seconds = seconds;
|
||||
this.minutes = minutes;
|
||||
this.hours = hours;
|
||||
this.days = days;
|
||||
this.months = months;
|
||||
this.years = years;
|
||||
this.skipRepeatedHour = true;
|
||||
this.missingHour = 'insert';
|
||||
this._warnings = null;
|
||||
}
|
||||
|
||||
static parse(cronstring, options = {}) {
|
||||
var _a;
|
||||
|
||||
const parsedFields = _parse(cronstring);
|
||||
|
||||
if (options.strict) {
|
||||
let warnings = flatMap(parsedFields, field => field.warnings);
|
||||
|
||||
if (typeof options.strict === 'object') {
|
||||
warnings = warnings.filter(warning => !!options.strict[warning.type]);
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
throw new Error(`Strict mode: Parsing failed with ${warnings.length} warnings`);
|
||||
}
|
||||
}
|
||||
|
||||
const expr = new CronosExpression(cronstring, parsedFields[0].values, parsedFields[1].values, parsedFields[2].values, parsedFields[3].values, parsedFields[4].values, parsedFields[5]);
|
||||
expr.timezone = options.timezone instanceof CronosTimezone ? options.timezone : options.timezone !== undefined ? new CronosTimezone(options.timezone) : undefined;
|
||||
expr.skipRepeatedHour = options.skipRepeatedHour !== undefined ? options.skipRepeatedHour : expr.skipRepeatedHour;
|
||||
expr.missingHour = (_a = options.missingHour) !== null && _a !== void 0 ? _a : expr.missingHour;
|
||||
return expr;
|
||||
}
|
||||
|
||||
get warnings() {
|
||||
if (!this._warnings) {
|
||||
const parsedFields = _parse(this.cronString);
|
||||
|
||||
this._warnings = flatMap(parsedFields, field => field.warnings);
|
||||
}
|
||||
|
||||
return this._warnings;
|
||||
}
|
||||
|
||||
toString() {
|
||||
var _a, _b;
|
||||
|
||||
const showTzOpts = !this.timezone || !!this.timezone.zoneName;
|
||||
const timezone = Object.entries({
|
||||
tz: (_b = (_a = this.timezone) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : 'Local',
|
||||
skipRepeatedHour: showTzOpts && this.skipRepeatedHour.toString(),
|
||||
missingHour: showTzOpts && this.missingHour
|
||||
}).map(([key, val]) => val && key + ': ' + val).filter(s => s).join(', ');
|
||||
return `${this.cronString} (${timezone})`;
|
||||
}
|
||||
|
||||
nextDate(afterDate = new Date()) {
|
||||
var _a;
|
||||
|
||||
const fromCronosDate = CronosDate.fromDate(afterDate, this.timezone);
|
||||
|
||||
if (((_a = this.timezone) === null || _a === void 0 ? void 0 : _a.fixedOffset) !== undefined) {
|
||||
return this._next(fromCronosDate).date;
|
||||
}
|
||||
|
||||
const fromTimestamp = afterDate.getTime(),
|
||||
fromLocalTimestamp = fromCronosDate['toUTCTimestamp'](),
|
||||
prevHourLocalTimestamp = CronosDate.fromDate(new Date(fromTimestamp - hourinms), this.timezone)['toUTCTimestamp'](),
|
||||
nextHourLocalTimestamp = CronosDate.fromDate(new Date(fromTimestamp + hourinms), this.timezone)['toUTCTimestamp'](),
|
||||
nextHourRepeated = nextHourLocalTimestamp - fromLocalTimestamp === 0,
|
||||
thisHourRepeated = fromLocalTimestamp - prevHourLocalTimestamp === 0,
|
||||
thisHourMissing = fromLocalTimestamp - prevHourLocalTimestamp === hourinms * 2;
|
||||
|
||||
if (this.skipRepeatedHour && thisHourRepeated) {
|
||||
return this._next(fromCronosDate.copyWith({
|
||||
minute: 59,
|
||||
second: 60
|
||||
}), false).date;
|
||||
}
|
||||
|
||||
if (this.missingHour === 'offset' && thisHourMissing) {
|
||||
const nextDate = this._next(fromCronosDate.copyWith({
|
||||
hour: fromCronosDate.hour - 1
|
||||
})).date;
|
||||
|
||||
if (!nextDate || nextDate.getTime() > fromTimestamp) return nextDate;
|
||||
}
|
||||
|
||||
let {
|
||||
date: nextDate,
|
||||
cronosDate: nextCronosDate
|
||||
} = this._next(fromCronosDate);
|
||||
|
||||
if (this.missingHour !== 'offset' && nextCronosDate && nextDate) {
|
||||
const nextDateNextHourTimestamp = nextCronosDate.copyWith({
|
||||
hour: nextCronosDate.hour + 1
|
||||
}).toDate(this.timezone).getTime();
|
||||
|
||||
if (nextDateNextHourTimestamp === nextDate.getTime()) {
|
||||
if (this.missingHour === 'insert') {
|
||||
return nextCronosDate.copyWith({
|
||||
minute: 0,
|
||||
second: 0
|
||||
}).toDate(this.timezone);
|
||||
} // this.missingHour === 'skip'
|
||||
|
||||
|
||||
return this._next(nextCronosDate.copyWith({
|
||||
minute: 59,
|
||||
second: 59
|
||||
})).date;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.skipRepeatedHour) {
|
||||
if (nextHourRepeated && (!nextDate || nextDate.getTime() > fromTimestamp + hourinms)) {
|
||||
nextDate = this._next(fromCronosDate.copyWith({
|
||||
minute: 0,
|
||||
second: 0
|
||||
}), false).date;
|
||||
}
|
||||
|
||||
if (nextDate && nextDate < afterDate) {
|
||||
nextDate = new Date(nextDate.getTime() + hourinms);
|
||||
}
|
||||
}
|
||||
|
||||
return nextDate;
|
||||
}
|
||||
|
||||
_next(date, after = true) {
|
||||
const nextDate = this._nextYear(after ? date.copyWith({
|
||||
second: date.second + 1
|
||||
}) : date);
|
||||
|
||||
return {
|
||||
cronosDate: nextDate,
|
||||
date: nextDate ? nextDate.toDate(this.timezone) : null
|
||||
};
|
||||
}
|
||||
|
||||
nextNDates(afterDate = new Date(), n = 5) {
|
||||
const dates = [];
|
||||
let lastDate = afterDate;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const date = this.nextDate(lastDate);
|
||||
if (!date) break;
|
||||
lastDate = date;
|
||||
dates.push(date);
|
||||
}
|
||||
|
||||
return dates;
|
||||
}
|
||||
|
||||
_nextYear(fromDate) {
|
||||
let year = fromDate.year;
|
||||
let nextDate = null;
|
||||
|
||||
while (!nextDate) {
|
||||
year = this.years.nextYear(year);
|
||||
if (year === null) return null;
|
||||
nextDate = this._nextMonth(year === fromDate.year ? fromDate : new CronosDate(year));
|
||||
year++;
|
||||
}
|
||||
|
||||
return nextDate;
|
||||
}
|
||||
|
||||
_nextMonth(fromDate) {
|
||||
let nextMonthIndex = findFirstFrom(fromDate.month, this.months);
|
||||
let nextDate = null;
|
||||
|
||||
while (!nextDate) {
|
||||
const nextMonth = this.months[nextMonthIndex];
|
||||
if (nextMonth === undefined) return null;
|
||||
nextDate = this._nextDay(nextMonth === fromDate.month ? fromDate : new CronosDate(fromDate.year, nextMonth));
|
||||
nextMonthIndex++;
|
||||
}
|
||||
|
||||
return nextDate;
|
||||
}
|
||||
|
||||
_nextDay(fromDate) {
|
||||
const days = this.days.getDays(fromDate.year, fromDate.month);
|
||||
let nextDayIndex = findFirstFrom(fromDate.day, days);
|
||||
let nextDate = null;
|
||||
|
||||
while (!nextDate) {
|
||||
const nextDay = days[nextDayIndex];
|
||||
if (nextDay === undefined) return null;
|
||||
nextDate = this._nextHour(nextDay === fromDate.day ? fromDate : new CronosDate(fromDate.year, fromDate.month, nextDay));
|
||||
nextDayIndex++;
|
||||
}
|
||||
|
||||
return nextDate;
|
||||
}
|
||||
|
||||
_nextHour(fromDate) {
|
||||
let nextHourIndex = findFirstFrom(fromDate.hour, this.hours);
|
||||
let nextDate = null;
|
||||
|
||||
while (!nextDate) {
|
||||
const nextHour = this.hours[nextHourIndex];
|
||||
if (nextHour === undefined) return null;
|
||||
nextDate = this._nextMinute(nextHour === fromDate.hour ? fromDate : new CronosDate(fromDate.year, fromDate.month, fromDate.day, nextHour));
|
||||
nextHourIndex++;
|
||||
}
|
||||
|
||||
return nextDate;
|
||||
}
|
||||
|
||||
_nextMinute(fromDate) {
|
||||
let nextMinuteIndex = findFirstFrom(fromDate.minute, this.minutes);
|
||||
let nextDate = null;
|
||||
|
||||
while (!nextDate) {
|
||||
const nextMinute = this.minutes[nextMinuteIndex];
|
||||
if (nextMinute === undefined) return null;
|
||||
nextDate = this._nextSecond(nextMinute === fromDate.minute ? fromDate : new CronosDate(fromDate.year, fromDate.month, fromDate.day, fromDate.hour, nextMinute));
|
||||
nextMinuteIndex++;
|
||||
}
|
||||
|
||||
return nextDate;
|
||||
}
|
||||
|
||||
_nextSecond(fromDate) {
|
||||
const nextSecondIndex = findFirstFrom(fromDate.second, this.seconds),
|
||||
nextSecond = this.seconds[nextSecondIndex];
|
||||
if (nextSecond === undefined) return null;
|
||||
return fromDate.copyWith({
|
||||
second: nextSecond
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const maxTimeout = Math.pow(2, 31) - 1;
|
||||
const scheduledTasks = [];
|
||||
let runningTimer = null;
|
||||
|
||||
function addTask(task) {
|
||||
if (task['_timestamp'] !== undefined) {
|
||||
const insertIndex = scheduledTasks.findIndex(t => t['_timestamp'] < task['_timestamp']);
|
||||
if (insertIndex >= 0) scheduledTasks.splice(insertIndex, 0, task);else scheduledTasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
function removeTask(task) {
|
||||
const removeIndex = scheduledTasks.indexOf(task);
|
||||
if (removeIndex >= 0) scheduledTasks.splice(removeIndex, 1);
|
||||
|
||||
if (scheduledTasks.length === 0 && runningTimer) {
|
||||
clearTimeout(runningTimer);
|
||||
runningTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function runScheduledTasks(skipRun = false) {
|
||||
if (runningTimer) clearTimeout(runningTimer);
|
||||
const now = Date.now();
|
||||
const removeIndex = scheduledTasks.findIndex(task => task['_timestamp'] <= now);
|
||||
const tasksToRun = removeIndex >= 0 ? scheduledTasks.splice(removeIndex) : [];
|
||||
|
||||
for (let task of tasksToRun) {
|
||||
if (!skipRun) task['_runTask']();
|
||||
|
||||
if (task.isRunning) {
|
||||
task['_updateTimestamp']();
|
||||
addTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
const nextTask = scheduledTasks[scheduledTasks.length - 1];
|
||||
|
||||
if (nextTask) {
|
||||
runningTimer = setTimeout(runScheduledTasks, Math.min(nextTask['_timestamp'] - Date.now(), maxTimeout));
|
||||
} else runningTimer = null;
|
||||
}
|
||||
|
||||
function refreshSchedulerTimer() {
|
||||
for (const task of scheduledTasks) {
|
||||
task['_updateTimestamp']();
|
||||
if (!task.isRunning) removeTask(task);
|
||||
}
|
||||
|
||||
scheduledTasks.sort((a, b) => b['_timestamp'] - a['_timestamp']);
|
||||
runScheduledTasks(true);
|
||||
}
|
||||
|
||||
class DateArraySequence {
|
||||
constructor(dateLikes) {
|
||||
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) {
|
||||
const nextIndex = this._dates.findIndex(d => d > afterDate);
|
||||
|
||||
return nextIndex === -1 ? null : this._dates[nextIndex];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CronosTask {
|
||||
constructor(sequenceOrDates) {
|
||||
this._listeners = {
|
||||
'started': new Set(),
|
||||
'stopped': new Set(),
|
||||
'run': new Set(),
|
||||
'ended': new Set()
|
||||
};
|
||||
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() {
|
||||
if (!this.isRunning) {
|
||||
this._updateTimestamp();
|
||||
|
||||
addTask(this);
|
||||
runScheduledTasks();
|
||||
if (this.isRunning) this._emit('started');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.isRunning) {
|
||||
this._timestamp = undefined;
|
||||
removeTask(this);
|
||||
|
||||
this._emit('stopped');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get nextRun() {
|
||||
return this.isRunning ? new Date(this._timestamp) : undefined;
|
||||
}
|
||||
|
||||
get isRunning() {
|
||||
return this._timestamp !== undefined;
|
||||
}
|
||||
|
||||
_runTask() {
|
||||
this._emit('run', this._timestamp);
|
||||
}
|
||||
|
||||
_updateTimestamp() {
|
||||
const nextDate = this._sequence.nextDate(new Date());
|
||||
|
||||
this._timestamp = nextDate ? nextDate.getTime() : undefined;
|
||||
if (!this.isRunning) this._emit('ended');
|
||||
}
|
||||
|
||||
on(event, listener) {
|
||||
this._listeners[event].add(listener);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
off(event, listener) {
|
||||
this._listeners[event].delete(listener);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
_emit(event, ...args) {
|
||||
this._listeners[event].forEach(listener => {
|
||||
listener.call(this, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function scheduleTask(cronString, task, options) {
|
||||
const expression = CronosExpression.parse(cronString, options);
|
||||
return new CronosTask(expression).on('run', task).start();
|
||||
}
|
||||
function validate(cronString, options) {
|
||||
try {
|
||||
CronosExpression.parse(cronString, options);
|
||||
} catch (_unused) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.CronosExpression = CronosExpression;
|
||||
exports.CronosTask = CronosTask;
|
||||
exports.CronosTimezone = CronosTimezone;
|
||||
exports.refreshSchedulerTimer = refreshSchedulerTimer;
|
||||
exports.scheduleTask = scheduleTask;
|
||||
exports.validate = validate;
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+130
@@ -0,0 +1,130 @@
|
||||
export class CronosDate {
|
||||
constructor(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) {
|
||||
this.year = year;
|
||||
this.month = month;
|
||||
this.day = day;
|
||||
this.hour = hour;
|
||||
this.minute = minute;
|
||||
this.second = second;
|
||||
}
|
||||
static fromDate(date, timezone) {
|
||||
if (!timezone) {
|
||||
return new CronosDate(date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds());
|
||||
}
|
||||
return timezone['nativeDateToCronosDate'](date);
|
||||
}
|
||||
toDate(timezone) {
|
||||
if (!timezone) {
|
||||
return new Date(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
|
||||
}
|
||||
return timezone['cronosDateToNativeDate'](this);
|
||||
}
|
||||
static fromUTCTimestamp(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return new CronosDate(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
|
||||
}
|
||||
toUTCTimestamp() {
|
||||
return Date.UTC(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
|
||||
}
|
||||
copyWith({ year = this.year, month = this.month, day = this.day, hour = this.hour, minute = this.minute, second = this.second } = {}) {
|
||||
return new CronosDate(year, month, day, hour, minute, second);
|
||||
}
|
||||
}
|
||||
// Adapted from Intl.DateTimeFormat timezone handling in https://github.com/moment/luxon
|
||||
const ZoneCache = new Map();
|
||||
export class CronosTimezone {
|
||||
constructor(IANANameOrOffset) {
|
||||
if (typeof IANANameOrOffset === 'number') {
|
||||
if (IANANameOrOffset > 840 || IANANameOrOffset < -840)
|
||||
throw new Error('Invalid offset');
|
||||
this.fixedOffset = IANANameOrOffset;
|
||||
return this;
|
||||
}
|
||||
const offsetMatch = IANANameOrOffset.match(/^([+-]?)(0[1-9]|1[0-4])(?::?([0-5][0-9]))?$/);
|
||||
if (offsetMatch) {
|
||||
this.fixedOffset = (offsetMatch[1] === '-' ? -1 : 1) * ((parseInt(offsetMatch[2], 10) * 60) + (parseInt(offsetMatch[3], 10) || 0));
|
||||
return this;
|
||||
}
|
||||
if (ZoneCache.has(IANANameOrOffset)) {
|
||||
return ZoneCache.get(IANANameOrOffset);
|
||||
}
|
||||
try {
|
||||
this.dateTimeFormat = new Intl.DateTimeFormat("en-US", {
|
||||
hour12: false,
|
||||
timeZone: IANANameOrOffset,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error('Invalid IANA name or offset');
|
||||
}
|
||||
this.zoneName = IANANameOrOffset;
|
||||
const currentYear = new Date().getUTCFullYear();
|
||||
this.winterOffset = this.offset(Date.UTC(currentYear, 0, 1));
|
||||
this.summerOffset = this.offset(Date.UTC(currentYear, 5, 1));
|
||||
ZoneCache.set(IANANameOrOffset, this);
|
||||
}
|
||||
toString() {
|
||||
if (this.fixedOffset) {
|
||||
const absOffset = Math.abs(this.fixedOffset);
|
||||
return [
|
||||
this.fixedOffset < 0 ? '-' : '+',
|
||||
Math.floor(absOffset / 60).toString().padStart(2, '0'),
|
||||
(absOffset % 60).toString().padStart(2, '0')
|
||||
].join('');
|
||||
}
|
||||
return this.zoneName;
|
||||
}
|
||||
offset(ts) {
|
||||
if (!this.dateTimeFormat)
|
||||
return this.fixedOffset || 0;
|
||||
const date = new Date(ts);
|
||||
const { year, month, day, hour, minute, second } = this.nativeDateToCronosDate(date);
|
||||
const asUTC = Date.UTC(year, month - 1, day, hour, minute, second), asTS = ts - (ts % 1000);
|
||||
return (asUTC - asTS) / 60000;
|
||||
}
|
||||
nativeDateToCronosDate(date) {
|
||||
if (!this.dateTimeFormat) {
|
||||
return CronosDate['fromUTCTimestamp'](date.getTime() + (this.fixedOffset || 0) * 60000);
|
||||
}
|
||||
return this.dateTimeFormat['formatToParts']
|
||||
? partsOffset(this.dateTimeFormat, date)
|
||||
: hackyOffset(this.dateTimeFormat, date);
|
||||
}
|
||||
cronosDateToNativeDate(date) {
|
||||
if (!this.dateTimeFormat) {
|
||||
return new Date(date['toUTCTimestamp']() - (this.fixedOffset || 0) * 60000);
|
||||
}
|
||||
const provisionalOffset = ((date.month > 3 || date.month < 11) ? this.summerOffset : this.winterOffset) || 0;
|
||||
const UTCTimestamp = date['toUTCTimestamp']();
|
||||
// Find the right offset a given local time.
|
||||
// Our UTC time is just a guess because our offset is just a guess
|
||||
let utcGuess = UTCTimestamp - provisionalOffset * 60000;
|
||||
// Test whether the zone matches the offset for this ts
|
||||
const o2 = this.offset(utcGuess);
|
||||
// If so, offset didn't change and we're done
|
||||
if (provisionalOffset === o2)
|
||||
return new Date(utcGuess);
|
||||
// If not, change the ts by the difference in the offset
|
||||
utcGuess -= (o2 - provisionalOffset) * 60000;
|
||||
// If that gives us the local time we want, we're done
|
||||
const o3 = this.offset(utcGuess);
|
||||
if (o2 === o3)
|
||||
return new Date(utcGuess);
|
||||
// If it's different, we're in a hole time. The offset has changed, but the we don't adjust the time
|
||||
return new Date(UTCTimestamp - Math.min(o2, o3) * 60000);
|
||||
}
|
||||
}
|
||||
function hackyOffset(dtf, date) {
|
||||
const formatted = dtf.format(date).replace(/\u200E/g, ""), parsed = formatted.match(/(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/), [, month, day, year, hour, minute, second] = (parsed !== null && parsed !== void 0 ? parsed : []).map(n => parseInt(n, 10));
|
||||
return new CronosDate(year, month, day, hour % 24, minute, second);
|
||||
}
|
||||
function partsOffset(dtf, date) {
|
||||
const formatted = dtf.formatToParts(date);
|
||||
return new CronosDate(parseInt(formatted[4].value, 10), parseInt(formatted[0].value, 10), parseInt(formatted[2].value, 10), parseInt(formatted[6].value, 10) % 24, parseInt(formatted[8].value, 10), parseInt(formatted[10].value, 10));
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
import { _parse } from './parser';
|
||||
import { CronosDate, CronosTimezone } from './date';
|
||||
import { flatMap } from './utils';
|
||||
const hourinms = 60 * 60 * 1000;
|
||||
const findFirstFrom = (from, list) => list.findIndex(n => n >= from);
|
||||
export class CronosExpression {
|
||||
constructor(cronString, seconds, minutes, hours, days, months, years) {
|
||||
this.cronString = cronString;
|
||||
this.seconds = seconds;
|
||||
this.minutes = minutes;
|
||||
this.hours = hours;
|
||||
this.days = days;
|
||||
this.months = months;
|
||||
this.years = years;
|
||||
this.skipRepeatedHour = true;
|
||||
this.missingHour = 'insert';
|
||||
this._warnings = null;
|
||||
}
|
||||
static parse(cronstring, options = {}) {
|
||||
var _a;
|
||||
const parsedFields = _parse(cronstring);
|
||||
if (options.strict) {
|
||||
let warnings = flatMap(parsedFields, field => field.warnings);
|
||||
if (typeof options.strict === 'object') {
|
||||
warnings = warnings
|
||||
.filter(warning => !!options.strict[warning.type]);
|
||||
}
|
||||
if (warnings.length > 0) {
|
||||
throw new Error(`Strict mode: Parsing failed with ${warnings.length} warnings`);
|
||||
}
|
||||
}
|
||||
const expr = new CronosExpression(cronstring, parsedFields[0].values, parsedFields[1].values, parsedFields[2].values, parsedFields[3].values, parsedFields[4].values, parsedFields[5]);
|
||||
expr.timezone = options.timezone instanceof CronosTimezone ? options.timezone :
|
||||
(options.timezone !== undefined ? new CronosTimezone(options.timezone) : undefined);
|
||||
expr.skipRepeatedHour = options.skipRepeatedHour !== undefined ? options.skipRepeatedHour : expr.skipRepeatedHour;
|
||||
expr.missingHour = (_a = options.missingHour) !== null && _a !== void 0 ? _a : expr.missingHour;
|
||||
return expr;
|
||||
}
|
||||
get warnings() {
|
||||
if (!this._warnings) {
|
||||
const parsedFields = _parse(this.cronString);
|
||||
this._warnings = flatMap(parsedFields, field => field.warnings);
|
||||
}
|
||||
return this._warnings;
|
||||
}
|
||||
toString() {
|
||||
var _a, _b;
|
||||
const showTzOpts = !this.timezone || !!this.timezone.zoneName;
|
||||
const timezone = Object.entries({
|
||||
tz: (_b = (_a = this.timezone) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : 'Local',
|
||||
skipRepeatedHour: showTzOpts && this.skipRepeatedHour.toString(),
|
||||
missingHour: showTzOpts && this.missingHour,
|
||||
}).map(([key, val]) => val && key + ': ' + val).filter(s => s).join(', ');
|
||||
return `${this.cronString} (${timezone})`;
|
||||
}
|
||||
nextDate(afterDate = new Date()) {
|
||||
var _a;
|
||||
const fromCronosDate = CronosDate.fromDate(afterDate, this.timezone);
|
||||
if (((_a = this.timezone) === null || _a === void 0 ? void 0 : _a.fixedOffset) !== undefined) {
|
||||
return this._next(fromCronosDate).date;
|
||||
}
|
||||
const fromTimestamp = afterDate.getTime(), fromLocalTimestamp = fromCronosDate['toUTCTimestamp'](), prevHourLocalTimestamp = CronosDate.fromDate(new Date(fromTimestamp - hourinms), this.timezone)['toUTCTimestamp'](), nextHourLocalTimestamp = CronosDate.fromDate(new Date(fromTimestamp + hourinms), this.timezone)['toUTCTimestamp'](), nextHourRepeated = nextHourLocalTimestamp - fromLocalTimestamp === 0, thisHourRepeated = fromLocalTimestamp - prevHourLocalTimestamp === 0, thisHourMissing = fromLocalTimestamp - prevHourLocalTimestamp === hourinms * 2;
|
||||
if (this.skipRepeatedHour && thisHourRepeated) {
|
||||
return this._next(fromCronosDate.copyWith({ minute: 59, second: 60 }), false).date;
|
||||
}
|
||||
if (this.missingHour === 'offset' && thisHourMissing) {
|
||||
const nextDate = this._next(fromCronosDate.copyWith({ hour: fromCronosDate.hour - 1 })).date;
|
||||
if (!nextDate || nextDate.getTime() > fromTimestamp)
|
||||
return nextDate;
|
||||
}
|
||||
let { date: nextDate, cronosDate: nextCronosDate } = this._next(fromCronosDate);
|
||||
if (this.missingHour !== 'offset' && nextCronosDate && nextDate) {
|
||||
const nextDateNextHourTimestamp = nextCronosDate.copyWith({ hour: nextCronosDate.hour + 1 }).toDate(this.timezone).getTime();
|
||||
if (nextDateNextHourTimestamp === nextDate.getTime()) {
|
||||
if (this.missingHour === 'insert') {
|
||||
return nextCronosDate.copyWith({ minute: 0, second: 0 }).toDate(this.timezone);
|
||||
}
|
||||
// this.missingHour === 'skip'
|
||||
return this._next(nextCronosDate.copyWith({ minute: 59, second: 59 })).date;
|
||||
}
|
||||
}
|
||||
if (!this.skipRepeatedHour) {
|
||||
if (nextHourRepeated && (!nextDate || (nextDate.getTime() > fromTimestamp + hourinms))) {
|
||||
nextDate = this._next(fromCronosDate.copyWith({ minute: 0, second: 0 }), false).date;
|
||||
}
|
||||
if (nextDate && nextDate < afterDate) {
|
||||
nextDate = new Date(nextDate.getTime() + hourinms);
|
||||
}
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_next(date, after = true) {
|
||||
const nextDate = this._nextYear(after ? date.copyWith({ second: date.second + 1 }) : date);
|
||||
return {
|
||||
cronosDate: nextDate,
|
||||
date: nextDate ? nextDate.toDate(this.timezone) : null
|
||||
};
|
||||
}
|
||||
nextNDates(afterDate = new Date(), n = 5) {
|
||||
const dates = [];
|
||||
let lastDate = afterDate;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const date = this.nextDate(lastDate);
|
||||
if (!date)
|
||||
break;
|
||||
lastDate = date;
|
||||
dates.push(date);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
_nextYear(fromDate) {
|
||||
let year = fromDate.year;
|
||||
let nextDate = null;
|
||||
while (!nextDate) {
|
||||
year = this.years.nextYear(year);
|
||||
if (year === null)
|
||||
return null;
|
||||
nextDate = this._nextMonth((year === fromDate.year) ? fromDate : new CronosDate(year));
|
||||
year++;
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_nextMonth(fromDate) {
|
||||
let nextMonthIndex = findFirstFrom(fromDate.month, this.months);
|
||||
let nextDate = null;
|
||||
while (!nextDate) {
|
||||
const nextMonth = this.months[nextMonthIndex];
|
||||
if (nextMonth === undefined)
|
||||
return null;
|
||||
nextDate = this._nextDay((nextMonth === fromDate.month) ? fromDate : new CronosDate(fromDate.year, nextMonth));
|
||||
nextMonthIndex++;
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_nextDay(fromDate) {
|
||||
const days = this.days.getDays(fromDate.year, fromDate.month);
|
||||
let nextDayIndex = findFirstFrom(fromDate.day, days);
|
||||
let nextDate = null;
|
||||
while (!nextDate) {
|
||||
const nextDay = days[nextDayIndex];
|
||||
if (nextDay === undefined)
|
||||
return null;
|
||||
nextDate = this._nextHour((nextDay === fromDate.day) ? fromDate : new CronosDate(fromDate.year, fromDate.month, nextDay));
|
||||
nextDayIndex++;
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_nextHour(fromDate) {
|
||||
let nextHourIndex = findFirstFrom(fromDate.hour, this.hours);
|
||||
let nextDate = null;
|
||||
while (!nextDate) {
|
||||
const nextHour = this.hours[nextHourIndex];
|
||||
if (nextHour === undefined)
|
||||
return null;
|
||||
nextDate = this._nextMinute((nextHour === fromDate.hour) ? fromDate :
|
||||
new CronosDate(fromDate.year, fromDate.month, fromDate.day, nextHour));
|
||||
nextHourIndex++;
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_nextMinute(fromDate) {
|
||||
let nextMinuteIndex = findFirstFrom(fromDate.minute, this.minutes);
|
||||
let nextDate = null;
|
||||
while (!nextDate) {
|
||||
const nextMinute = this.minutes[nextMinuteIndex];
|
||||
if (nextMinute === undefined)
|
||||
return null;
|
||||
nextDate = this._nextSecond((nextMinute === fromDate.minute) ? fromDate :
|
||||
new CronosDate(fromDate.year, fromDate.month, fromDate.day, fromDate.hour, nextMinute));
|
||||
nextMinuteIndex++;
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_nextSecond(fromDate) {
|
||||
const nextSecondIndex = findFirstFrom(fromDate.second, this.seconds), nextSecond = this.seconds[nextSecondIndex];
|
||||
if (nextSecond === undefined)
|
||||
return null;
|
||||
return fromDate.copyWith({ second: nextSecond });
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { CronosExpression } from './expression';
|
||||
import { CronosTask, refreshSchedulerTimer } from './scheduler';
|
||||
import { CronosTimezone } from './date';
|
||||
export function scheduleTask(cronString, task, options) {
|
||||
const expression = CronosExpression.parse(cronString, options);
|
||||
return new CronosTask(expression)
|
||||
.on('run', task)
|
||||
.start();
|
||||
}
|
||||
export function validate(cronString, options) {
|
||||
try {
|
||||
CronosExpression.parse(cronString, options);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
export { CronosExpression, CronosTask, CronosTimezone, refreshSchedulerTimer };
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
import { sortAsc, flatMap } from './utils';
|
||||
const predefinedCronStrings = {
|
||||
'@yearly': '0 0 0 1 1 * *',
|
||||
'@annually': '0 0 0 1 1 * *',
|
||||
'@monthly': '0 0 0 1 * * *',
|
||||
'@weekly': '0 0 0 * * 0 *',
|
||||
'@daily': '0 0 0 * * * *',
|
||||
'@midnight': '0 0 0 * * * *',
|
||||
'@hourly': '0 0 * * * * *',
|
||||
};
|
||||
const monthReplacements = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
||||
const monthReplacementRegex = new RegExp(monthReplacements.join('|'), 'g');
|
||||
const dayOfWeekReplacements = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||
const dayOfWeekReplacementRegex = new RegExp(dayOfWeekReplacements.join('|'), 'g');
|
||||
/*
|
||||
"The actual range of times supported by ECMAScript Date objects is slightly smaller:
|
||||
exactly –100,000,000 days to 100,000,000 days measured relative to midnight at the
|
||||
beginning of 01 January, 1970 UTC. This gives a range of 8,640,000,000,000,000
|
||||
milliseconds to either side of 01 January, 1970 UTC."
|
||||
http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1
|
||||
|
||||
new Date(8640000000000000) => 00:00:00 13th Sep 275760
|
||||
Largest full year valid as JS date = 275759
|
||||
*/
|
||||
const maxValidYear = 275759;
|
||||
export var WarningType;
|
||||
(function (WarningType) {
|
||||
WarningType["IncrementLargerThanRange"] = "IncrementLargerThanRange";
|
||||
})(WarningType || (WarningType = {}));
|
||||
export function _parse(cronstring) {
|
||||
let expr = cronstring.trim().toLowerCase();
|
||||
if (predefinedCronStrings[expr]) {
|
||||
expr = predefinedCronStrings[expr];
|
||||
}
|
||||
const fields = expr.split(/\s+/g);
|
||||
if (fields.length < 5 || fields.length > 7) {
|
||||
throw new Error('Expression must have at least 5 fields, and no more than 7 fields');
|
||||
}
|
||||
switch (fields.length) {
|
||||
case 5:
|
||||
fields.unshift('0');
|
||||
case 6:
|
||||
fields.push('*');
|
||||
}
|
||||
return [
|
||||
new SecondsOrMinutesField(fields[0]),
|
||||
new SecondsOrMinutesField(fields[1]),
|
||||
new HoursField(fields[2]),
|
||||
new DaysField(fields[3], fields[5]),
|
||||
new MonthsField(fields[4]),
|
||||
new YearsField(fields[6])
|
||||
];
|
||||
}
|
||||
function getIncrementLargerThanRangeWarnings(items, first, last) {
|
||||
const warnings = [];
|
||||
for (let item of items) {
|
||||
let rangeLength;
|
||||
if (item.step > 1 &&
|
||||
item.step > (rangeLength = item.rangeLength(first, last))) {
|
||||
warnings.push({
|
||||
type: WarningType.IncrementLargerThanRange,
|
||||
message: `Increment (${item.step}) is larger than range (${rangeLength}) for expression '${item.itemString}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
class Field {
|
||||
constructor(field) {
|
||||
this.field = field;
|
||||
}
|
||||
parse() {
|
||||
return this.field.split(',')
|
||||
.map(item => FieldItem.parse(item, this.first, this.last, true));
|
||||
}
|
||||
get items() {
|
||||
if (!this._items)
|
||||
this._items = this.parse();
|
||||
return this._items;
|
||||
}
|
||||
get values() {
|
||||
return Field.getValues(this.items, this.first, this.last);
|
||||
}
|
||||
get warnings() {
|
||||
return getIncrementLargerThanRangeWarnings(this.items, this.first, this.last);
|
||||
}
|
||||
static getValues(items, first, last) {
|
||||
return Array.from(new Set(flatMap(items, item => item.values(first, last)))).sort(sortAsc);
|
||||
}
|
||||
}
|
||||
class FieldItem {
|
||||
constructor(itemString) {
|
||||
this.itemString = itemString;
|
||||
this.step = 1;
|
||||
}
|
||||
rangeLength(first, last) {
|
||||
var _a, _b, _c, _d;
|
||||
const start = (_b = (_a = this.range) === null || _a === void 0 ? void 0 : _a.from) !== null && _b !== void 0 ? _b : first, end = (_d = (_c = this.range) === null || _c === void 0 ? void 0 : _c.to) !== null && _d !== void 0 ? _d : last;
|
||||
return (end < start) ? ((last - start) + (end - first) + 1) : (end - start);
|
||||
}
|
||||
values(first, last) {
|
||||
const start = this.range ? this.range.from : first, rangeLength = this.rangeLength(first, last);
|
||||
return Array(Math.floor(rangeLength / this.step) + 1)
|
||||
.fill(0)
|
||||
.map((_, i) => first + ((start - first + (this.step * i)) % (last - first + 1)));
|
||||
}
|
||||
get any() {
|
||||
return this.range === undefined && this.step === 1;
|
||||
}
|
||||
get single() {
|
||||
return !!this.range && this.range.from === this.range.to;
|
||||
}
|
||||
static parse(item, first, last, allowCyclicRange = false, transformer) {
|
||||
var _a;
|
||||
const fieldItem = new FieldItem(item);
|
||||
const [match, all, startFrom, range, step] = ((_a = item.match(/^(?:(\*)|([0-9]+)|([0-9]+-[0-9]+))(?:\/([1-9][0-9]*))?$/)) !== null && _a !== void 0 ? _a : []);
|
||||
if (!match)
|
||||
throw new Error('Field item invalid format');
|
||||
if (step) {
|
||||
fieldItem.step = parseInt(step, 10);
|
||||
}
|
||||
if (startFrom) {
|
||||
let start = parseInt(startFrom, 10);
|
||||
start = transformer ? transformer(start) : start;
|
||||
if (start < first || start > last)
|
||||
throw new Error('Field item out of valid value range');
|
||||
fieldItem.range = {
|
||||
from: start,
|
||||
to: step ? undefined : start
|
||||
};
|
||||
}
|
||||
else if (range) {
|
||||
const [rangeStart, rangeEnd] = range.split('-').map(x => {
|
||||
const n = parseInt(x, 10);
|
||||
return transformer ? transformer(n) : n;
|
||||
});
|
||||
if (rangeStart < first || rangeStart > last || rangeEnd < first || rangeEnd > last ||
|
||||
(rangeEnd < rangeStart && !allowCyclicRange)) {
|
||||
throw new Error('Field item range invalid, either value out of valid range or start greater than end in non wraparound field');
|
||||
}
|
||||
fieldItem.range = {
|
||||
from: rangeStart,
|
||||
to: rangeEnd
|
||||
};
|
||||
}
|
||||
return fieldItem;
|
||||
}
|
||||
}
|
||||
FieldItem.asterisk = new FieldItem('*');
|
||||
export class SecondsOrMinutesField extends Field {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.first = 0;
|
||||
this.last = 59;
|
||||
}
|
||||
}
|
||||
export class HoursField extends Field {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.first = 0;
|
||||
this.last = 23;
|
||||
}
|
||||
}
|
||||
export class DaysField {
|
||||
constructor(daysOfMonthField, daysOfWeekField) {
|
||||
this.lastDay = false;
|
||||
this.lastWeekday = false;
|
||||
this.daysItems = [];
|
||||
this.nearestWeekdayItems = [];
|
||||
this.daysOfWeekItems = [];
|
||||
this.lastDaysOfWeekItems = [];
|
||||
this.nthDaysOfWeekItems = [];
|
||||
for (let item of daysOfMonthField.split(',').map(s => s === '?' ? '*' : s)) {
|
||||
if (item === 'l') {
|
||||
this.lastDay = true;
|
||||
}
|
||||
else if (item === 'lw') {
|
||||
this.lastWeekday = true;
|
||||
}
|
||||
else if (item.endsWith('w')) {
|
||||
this.nearestWeekdayItems.push(FieldItem.parse(item.slice(0, -1), 1, 31));
|
||||
}
|
||||
else {
|
||||
this.daysItems.push(FieldItem.parse(item, 1, 31));
|
||||
}
|
||||
}
|
||||
const normalisedDaysOfWeekField = daysOfWeekField.replace(dayOfWeekReplacementRegex, match => dayOfWeekReplacements.indexOf(match) + '');
|
||||
const parseDayOfWeek = (item) => FieldItem.parse(item, 0, 6, true, n => n === 7 ? 0 : n);
|
||||
for (let item of normalisedDaysOfWeekField.split(',').map(s => s === '?' ? '*' : s)) {
|
||||
const nthIndex = item.lastIndexOf('#');
|
||||
if (item.endsWith('l')) {
|
||||
this.lastDaysOfWeekItems.push(parseDayOfWeek(item.slice(0, -1)));
|
||||
}
|
||||
else if (nthIndex !== -1) {
|
||||
const nth = item.slice(nthIndex + 1);
|
||||
if (!/^[1-5]$/.test(nth))
|
||||
throw new Error('Field item nth of month (#) invalid');
|
||||
this.nthDaysOfWeekItems.push({
|
||||
item: parseDayOfWeek(item.slice(0, nthIndex)),
|
||||
nth: parseInt(nth, 10)
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.daysOfWeekItems.push(parseDayOfWeek(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
get values() {
|
||||
return DaysFieldValues.fromField(this);
|
||||
}
|
||||
get warnings() {
|
||||
const warnings = [], dayItems = [
|
||||
...this.daysItems,
|
||||
...this.nearestWeekdayItems,
|
||||
], weekItems = [
|
||||
...this.daysOfWeekItems,
|
||||
...this.lastDaysOfWeekItems,
|
||||
...this.nthDaysOfWeekItems.map(({ item }) => item),
|
||||
];
|
||||
warnings.push(...getIncrementLargerThanRangeWarnings(dayItems, 1, 31), ...getIncrementLargerThanRangeWarnings(weekItems, 0, 6));
|
||||
return warnings;
|
||||
}
|
||||
get allDays() {
|
||||
return (!this.lastDay &&
|
||||
!this.lastWeekday &&
|
||||
!this.nearestWeekdayItems.length &&
|
||||
!this.lastDaysOfWeekItems.length &&
|
||||
!this.nthDaysOfWeekItems.length &&
|
||||
this.daysItems.length === 1 && this.daysItems[0].any &&
|
||||
this.daysOfWeekItems.length === 1 && this.daysOfWeekItems[0].any);
|
||||
}
|
||||
}
|
||||
export class DaysFieldValues {
|
||||
constructor() {
|
||||
this.lastDay = false;
|
||||
this.lastWeekday = false;
|
||||
this.days = [];
|
||||
this.nearestWeekday = [];
|
||||
this.daysOfWeek = [];
|
||||
this.lastDaysOfWeek = [];
|
||||
this.nthDaysOfWeek = [];
|
||||
}
|
||||
static fromField(field) {
|
||||
const values = new DaysFieldValues();
|
||||
const filterAnyItems = (items) => items.filter(item => !item.any);
|
||||
values.lastDay = field.lastDay;
|
||||
values.lastWeekday = field.lastWeekday;
|
||||
values.days = Field.getValues(field.allDays ? [FieldItem.asterisk] : filterAnyItems(field.daysItems), 1, 31);
|
||||
values.nearestWeekday = Field.getValues(field.nearestWeekdayItems, 1, 31);
|
||||
values.daysOfWeek = Field.getValues(filterAnyItems(field.daysOfWeekItems), 0, 6);
|
||||
values.lastDaysOfWeek = Field.getValues(field.lastDaysOfWeekItems, 0, 6);
|
||||
const nthDaysHashes = new Set();
|
||||
for (let item of field.nthDaysOfWeekItems) {
|
||||
for (let n of item.item.values(0, 6)) {
|
||||
let hash = n * 10 + item.nth;
|
||||
if (!nthDaysHashes.has(hash)) {
|
||||
nthDaysHashes.add(hash);
|
||||
values.nthDaysOfWeek.push([n, item.nth]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
getDays(year, month) {
|
||||
const days = new Set(this.days);
|
||||
const lastDateOfMonth = new Date(year, month, 0).getDate();
|
||||
const firstDayOfWeek = new Date(year, month - 1, 1).getDay();
|
||||
const getNearestWeekday = (day) => {
|
||||
if (day > lastDateOfMonth)
|
||||
day = lastDateOfMonth;
|
||||
const dayOfWeek = (day + firstDayOfWeek - 1) % 7;
|
||||
let weekday = day + (dayOfWeek === 0 ? 1 : (dayOfWeek === 6 ? -1 : 0));
|
||||
return weekday + (weekday < 1 ? 3 : (weekday > lastDateOfMonth ? -3 : 0));
|
||||
};
|
||||
if (this.lastDay) {
|
||||
days.add(lastDateOfMonth);
|
||||
}
|
||||
if (this.lastWeekday) {
|
||||
days.add(getNearestWeekday(lastDateOfMonth));
|
||||
}
|
||||
for (const day of this.nearestWeekday) {
|
||||
days.add(getNearestWeekday(day));
|
||||
}
|
||||
if (this.daysOfWeek.length ||
|
||||
this.lastDaysOfWeek.length ||
|
||||
this.nthDaysOfWeek.length) {
|
||||
const daysOfWeek = Array(7).fill(0).map(() => ([]));
|
||||
for (let day = 1; day < 36; day++) {
|
||||
daysOfWeek[(day + firstDayOfWeek - 1) % 7].push(day);
|
||||
}
|
||||
for (const dayOfWeek of this.daysOfWeek) {
|
||||
for (const day of daysOfWeek[dayOfWeek]) {
|
||||
days.add(day);
|
||||
}
|
||||
}
|
||||
for (const dayOfWeek of this.lastDaysOfWeek) {
|
||||
for (let i = daysOfWeek[dayOfWeek].length - 1; i >= 0; i--) {
|
||||
if (daysOfWeek[dayOfWeek][i] <= lastDateOfMonth) {
|
||||
days.add(daysOfWeek[dayOfWeek][i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [dayOfWeek, nthOfMonth] of this.nthDaysOfWeek) {
|
||||
days.add(daysOfWeek[dayOfWeek][nthOfMonth - 1]);
|
||||
}
|
||||
}
|
||||
return Array.from(days).filter(day => day <= lastDateOfMonth).sort(sortAsc);
|
||||
}
|
||||
}
|
||||
export class MonthsField extends Field {
|
||||
constructor(field) {
|
||||
super(field.replace(monthReplacementRegex, match => {
|
||||
return monthReplacements.indexOf(match) + 1 + '';
|
||||
}));
|
||||
this.first = 1;
|
||||
this.last = 12;
|
||||
}
|
||||
}
|
||||
export class YearsField extends Field {
|
||||
constructor(field) {
|
||||
super(field);
|
||||
this.first = 1970;
|
||||
this.last = 2099;
|
||||
this.items;
|
||||
}
|
||||
parse() {
|
||||
return this.field.split(',')
|
||||
.map(item => FieldItem.parse(item, 0, maxValidYear));
|
||||
}
|
||||
get warnings() {
|
||||
return getIncrementLargerThanRangeWarnings(this.items, this.first, maxValidYear);
|
||||
}
|
||||
nextYear(fromYear) {
|
||||
var _a;
|
||||
return (_a = this.items.reduce((years, item) => {
|
||||
var _a, _b, _c, _d;
|
||||
if (item.any)
|
||||
years.push(fromYear);
|
||||
else if (item.single) {
|
||||
const year = item.range.from;
|
||||
if (year >= fromYear)
|
||||
years.push(year);
|
||||
}
|
||||
else {
|
||||
const start = (_b = (_a = item.range) === null || _a === void 0 ? void 0 : _a.from) !== null && _b !== void 0 ? _b : this.first;
|
||||
if (start > fromYear)
|
||||
years.push(start);
|
||||
else {
|
||||
const nextYear = start + Math.ceil((fromYear - start) / item.step) * item.step;
|
||||
if (nextYear <= ((_d = (_c = item.range) === null || _c === void 0 ? void 0 : _c.to) !== null && _d !== void 0 ? _d : maxValidYear))
|
||||
years.push(nextYear);
|
||||
}
|
||||
}
|
||||
return years;
|
||||
}, []).sort(sortAsc)[0]) !== null && _a !== void 0 ? _a : null;
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
const maxTimeout = Math.pow(2, 31) - 1;
|
||||
const scheduledTasks = [];
|
||||
let runningTimer = null;
|
||||
function addTask(task) {
|
||||
if (task['_timestamp'] !== undefined) {
|
||||
const insertIndex = scheduledTasks.findIndex(t => t['_timestamp'] < task['_timestamp']);
|
||||
if (insertIndex >= 0)
|
||||
scheduledTasks.splice(insertIndex, 0, task);
|
||||
else
|
||||
scheduledTasks.push(task);
|
||||
}
|
||||
}
|
||||
function removeTask(task) {
|
||||
const removeIndex = scheduledTasks.indexOf(task);
|
||||
if (removeIndex >= 0)
|
||||
scheduledTasks.splice(removeIndex, 1);
|
||||
if (scheduledTasks.length === 0 && runningTimer) {
|
||||
clearTimeout(runningTimer);
|
||||
runningTimer = null;
|
||||
}
|
||||
}
|
||||
function runScheduledTasks(skipRun = false) {
|
||||
if (runningTimer)
|
||||
clearTimeout(runningTimer);
|
||||
const now = Date.now();
|
||||
const removeIndex = scheduledTasks.findIndex(task => task['_timestamp'] <= now);
|
||||
const tasksToRun = removeIndex >= 0 ? scheduledTasks.splice(removeIndex) : [];
|
||||
for (let task of tasksToRun) {
|
||||
if (!skipRun)
|
||||
task['_runTask']();
|
||||
if (task.isRunning) {
|
||||
task['_updateTimestamp']();
|
||||
addTask(task);
|
||||
}
|
||||
}
|
||||
const nextTask = scheduledTasks[scheduledTasks.length - 1];
|
||||
if (nextTask) {
|
||||
runningTimer = setTimeout(runScheduledTasks, Math.min(nextTask['_timestamp'] - Date.now(), maxTimeout));
|
||||
}
|
||||
else
|
||||
runningTimer = null;
|
||||
}
|
||||
export function refreshSchedulerTimer() {
|
||||
for (const task of scheduledTasks) {
|
||||
task['_updateTimestamp']();
|
||||
if (!task.isRunning)
|
||||
removeTask(task);
|
||||
}
|
||||
scheduledTasks.sort((a, b) => b['_timestamp'] - a['_timestamp']);
|
||||
runScheduledTasks(true);
|
||||
}
|
||||
class DateArraySequence {
|
||||
constructor(dateLikes) {
|
||||
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) {
|
||||
const nextIndex = this._dates.findIndex(d => d > afterDate);
|
||||
return nextIndex === -1 ? null : this._dates[nextIndex];
|
||||
}
|
||||
}
|
||||
export class CronosTask {
|
||||
constructor(sequenceOrDates) {
|
||||
this._listeners = {
|
||||
'started': new Set(),
|
||||
'stopped': new Set(),
|
||||
'run': new Set(),
|
||||
'ended': new Set(),
|
||||
};
|
||||
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() {
|
||||
if (!this.isRunning) {
|
||||
this._updateTimestamp();
|
||||
addTask(this);
|
||||
runScheduledTasks();
|
||||
if (this.isRunning)
|
||||
this._emit('started');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
stop() {
|
||||
if (this.isRunning) {
|
||||
this._timestamp = undefined;
|
||||
removeTask(this);
|
||||
this._emit('stopped');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
get nextRun() {
|
||||
return this.isRunning ? new Date(this._timestamp) : undefined;
|
||||
}
|
||||
get isRunning() {
|
||||
return this._timestamp !== undefined;
|
||||
}
|
||||
_runTask() {
|
||||
this._emit('run', this._timestamp);
|
||||
}
|
||||
_updateTimestamp() {
|
||||
const nextDate = this._sequence.nextDate(new Date());
|
||||
this._timestamp = nextDate ? nextDate.getTime() : undefined;
|
||||
if (!this.isRunning)
|
||||
this._emit('ended');
|
||||
}
|
||||
on(event, listener) {
|
||||
this._listeners[event].add(listener);
|
||||
return this;
|
||||
}
|
||||
off(event, listener) {
|
||||
this._listeners[event].delete(listener);
|
||||
return this;
|
||||
}
|
||||
_emit(event, ...args) {
|
||||
this._listeners[event].forEach((listener) => {
|
||||
listener.call(this, ...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export const sortAsc = (a, b) => a - b;
|
||||
export function flatMap(arr, mapper) {
|
||||
return arr.reduce((acc, val, i) => {
|
||||
acc.push(...mapper(val, i, arr));
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
export declare class CronosDate {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
hour: number;
|
||||
minute: number;
|
||||
second: number;
|
||||
constructor(year: number, month?: number, day?: number, hour?: number, minute?: number, second?: number);
|
||||
static fromDate(date: Date, timezone?: CronosTimezone): CronosDate;
|
||||
toDate(timezone?: CronosTimezone): Date;
|
||||
private static fromUTCTimestamp;
|
||||
private toUTCTimestamp;
|
||||
copyWith({ year, month, day, hour, minute, second }?: {
|
||||
year?: number | undefined;
|
||||
month?: number | undefined;
|
||||
day?: number | undefined;
|
||||
hour?: number | undefined;
|
||||
minute?: number | undefined;
|
||||
second?: number | undefined;
|
||||
}): CronosDate;
|
||||
}
|
||||
export declare class CronosTimezone {
|
||||
zoneName?: string;
|
||||
fixedOffset?: number;
|
||||
private dateTimeFormat?;
|
||||
private winterOffset?;
|
||||
private summerOffset?;
|
||||
constructor(IANANameOrOffset: string | number);
|
||||
toString(): string | undefined;
|
||||
private offset;
|
||||
private nativeDateToCronosDate;
|
||||
private cronosDateToNativeDate;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { WarningType, Warning } from './parser';
|
||||
import { CronosTimezone } from './date';
|
||||
import { DateSequence } from './scheduler';
|
||||
export declare class CronosExpression implements DateSequence {
|
||||
readonly cronString: string;
|
||||
private readonly seconds;
|
||||
private readonly minutes;
|
||||
private readonly hours;
|
||||
private readonly days;
|
||||
private readonly months;
|
||||
private readonly years;
|
||||
private timezone?;
|
||||
private skipRepeatedHour;
|
||||
private missingHour;
|
||||
private _warnings;
|
||||
private constructor();
|
||||
static parse(cronstring: string, options?: {
|
||||
timezone?: string | number | CronosTimezone;
|
||||
skipRepeatedHour?: boolean;
|
||||
missingHour?: CronosExpression['missingHour'];
|
||||
strict?: boolean | {
|
||||
[key in WarningType]?: boolean;
|
||||
};
|
||||
}): CronosExpression;
|
||||
get warnings(): Warning[];
|
||||
toString(): string;
|
||||
nextDate(afterDate?: Date): Date | null;
|
||||
private _next;
|
||||
nextNDates(afterDate?: Date, n?: number): Date[];
|
||||
private _nextYear;
|
||||
private _nextMonth;
|
||||
private _nextDay;
|
||||
private _nextHour;
|
||||
private _nextMinute;
|
||||
private _nextSecond;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { CronosExpression } from './expression';
|
||||
import { CronosTask, refreshSchedulerTimer } from './scheduler';
|
||||
import { CronosTimezone } from './date';
|
||||
export declare function scheduleTask(cronString: string, task: (timestamp: number) => void, options: Parameters<typeof CronosExpression.parse>[1]): CronosTask;
|
||||
export declare function validate(cronString: string, options?: {
|
||||
strict: NonNullable<Parameters<typeof CronosExpression.parse>[1]>['strict'];
|
||||
}): boolean;
|
||||
export { CronosExpression, CronosTask, CronosTimezone, refreshSchedulerTimer };
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
export declare enum WarningType {
|
||||
IncrementLargerThanRange = "IncrementLargerThanRange"
|
||||
}
|
||||
export interface Warning {
|
||||
type: WarningType;
|
||||
message: string;
|
||||
}
|
||||
export declare function _parse(cronstring: string): [
|
||||
SecondsOrMinutesField,
|
||||
SecondsOrMinutesField,
|
||||
HoursField,
|
||||
DaysField,
|
||||
MonthsField,
|
||||
YearsField
|
||||
];
|
||||
declare abstract class Field {
|
||||
protected field: string;
|
||||
abstract first: number;
|
||||
abstract last: number;
|
||||
constructor(field: string);
|
||||
protected parse(): FieldItem[];
|
||||
private _items?;
|
||||
protected get items(): FieldItem[];
|
||||
get values(): number[];
|
||||
get warnings(): Warning[];
|
||||
static getValues(items: FieldItem[], first: number, last: number): number[];
|
||||
}
|
||||
declare class FieldItem {
|
||||
itemString: string;
|
||||
range?: {
|
||||
from: number;
|
||||
to?: number;
|
||||
};
|
||||
step: number;
|
||||
private constructor();
|
||||
rangeLength(first: number, last: number): number;
|
||||
values(first: number, last: number): number[];
|
||||
get any(): boolean;
|
||||
get single(): boolean;
|
||||
static parse(item: string, first: number, last: number, allowCyclicRange?: boolean, transformer?: (n: number) => number): FieldItem;
|
||||
static asterisk: FieldItem;
|
||||
}
|
||||
export declare class SecondsOrMinutesField extends Field {
|
||||
readonly first = 0;
|
||||
readonly last = 59;
|
||||
}
|
||||
export declare class HoursField extends Field {
|
||||
readonly first = 0;
|
||||
readonly last = 23;
|
||||
}
|
||||
export declare class DaysField {
|
||||
lastDay: boolean;
|
||||
lastWeekday: boolean;
|
||||
daysItems: FieldItem[];
|
||||
nearestWeekdayItems: FieldItem[];
|
||||
daysOfWeekItems: FieldItem[];
|
||||
lastDaysOfWeekItems: FieldItem[];
|
||||
nthDaysOfWeekItems: {
|
||||
item: FieldItem;
|
||||
nth: number;
|
||||
}[];
|
||||
constructor(daysOfMonthField: string, daysOfWeekField: string);
|
||||
get values(): DaysFieldValues;
|
||||
get warnings(): Warning[];
|
||||
get allDays(): boolean;
|
||||
}
|
||||
export declare class DaysFieldValues {
|
||||
lastDay: boolean;
|
||||
lastWeekday: boolean;
|
||||
days: number[];
|
||||
nearestWeekday: number[];
|
||||
daysOfWeek: number[];
|
||||
lastDaysOfWeek: number[];
|
||||
nthDaysOfWeek: [number, number][];
|
||||
static fromField(field: DaysField): DaysFieldValues;
|
||||
getDays(year: number, month: number): number[];
|
||||
}
|
||||
export declare class MonthsField extends Field {
|
||||
readonly first = 1;
|
||||
readonly last = 12;
|
||||
constructor(field: string);
|
||||
}
|
||||
export declare class YearsField extends Field {
|
||||
readonly first = 1970;
|
||||
readonly last = 2099;
|
||||
constructor(field: string);
|
||||
protected parse(): FieldItem[];
|
||||
get warnings(): Warning[];
|
||||
nextYear(fromYear: number): number;
|
||||
}
|
||||
export {};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
export declare function refreshSchedulerTimer(): void;
|
||||
export interface DateSequence {
|
||||
nextDate: (afterDate: Date) => Date | null;
|
||||
}
|
||||
declare type CronosTaskListeners = {
|
||||
'started': () => void;
|
||||
'stopped': () => void;
|
||||
'run': (timestamp: number) => void;
|
||||
'ended': () => void;
|
||||
};
|
||||
declare type DateLike = Date | string | number;
|
||||
export declare class CronosTask {
|
||||
private _listeners;
|
||||
private _timestamp?;
|
||||
private _sequence;
|
||||
constructor(sequence: DateSequence);
|
||||
constructor(dates: DateLike[]);
|
||||
constructor(date: DateLike);
|
||||
start(): this;
|
||||
stop(): this;
|
||||
get nextRun(): Date | undefined;
|
||||
get isRunning(): boolean;
|
||||
private _runTask;
|
||||
private _updateTimestamp;
|
||||
on<K extends keyof CronosTaskListeners>(event: K, listener: CronosTaskListeners[K]): this;
|
||||
off<K extends keyof CronosTaskListeners>(event: K, listener: CronosTaskListeners[K]): this;
|
||||
private _emit;
|
||||
}
|
||||
export {};
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare const sortAsc: (a: number, b: number) => number;
|
||||
export declare function flatMap<T, U>(arr: T[], mapper: (value: T, index: number, array: T[]) => U[]): U[];
|
||||
+823
@@ -0,0 +1,823 @@
|
||||
const sortAsc = (a, b) => a - b;
|
||||
function flatMap(arr, mapper) {
|
||||
return arr.reduce((acc, val, i) => {
|
||||
acc.push(...mapper(val, i, arr));
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
const predefinedCronStrings = {
|
||||
'@yearly': '0 0 0 1 1 * *',
|
||||
'@annually': '0 0 0 1 1 * *',
|
||||
'@monthly': '0 0 0 1 * * *',
|
||||
'@weekly': '0 0 0 * * 0 *',
|
||||
'@daily': '0 0 0 * * * *',
|
||||
'@midnight': '0 0 0 * * * *',
|
||||
'@hourly': '0 0 * * * * *',
|
||||
};
|
||||
const monthReplacements = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
||||
const monthReplacementRegex = new RegExp(monthReplacements.join('|'), 'g');
|
||||
const dayOfWeekReplacements = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||
const dayOfWeekReplacementRegex = new RegExp(dayOfWeekReplacements.join('|'), 'g');
|
||||
/*
|
||||
"The actual range of times supported by ECMAScript Date objects is slightly smaller:
|
||||
exactly –100,000,000 days to 100,000,000 days measured relative to midnight at the
|
||||
beginning of 01 January, 1970 UTC. This gives a range of 8,640,000,000,000,000
|
||||
milliseconds to either side of 01 January, 1970 UTC."
|
||||
http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1
|
||||
|
||||
new Date(8640000000000000) => 00:00:00 13th Sep 275760
|
||||
Largest full year valid as JS date = 275759
|
||||
*/
|
||||
const maxValidYear = 275759;
|
||||
var WarningType;
|
||||
(function (WarningType) {
|
||||
WarningType["IncrementLargerThanRange"] = "IncrementLargerThanRange";
|
||||
})(WarningType || (WarningType = {}));
|
||||
function _parse(cronstring) {
|
||||
let expr = cronstring.trim().toLowerCase();
|
||||
if (predefinedCronStrings[expr]) {
|
||||
expr = predefinedCronStrings[expr];
|
||||
}
|
||||
const fields = expr.split(/\s+/g);
|
||||
if (fields.length < 5 || fields.length > 7) {
|
||||
throw new Error('Expression must have at least 5 fields, and no more than 7 fields');
|
||||
}
|
||||
switch (fields.length) {
|
||||
case 5:
|
||||
fields.unshift('0');
|
||||
case 6:
|
||||
fields.push('*');
|
||||
}
|
||||
return [
|
||||
new SecondsOrMinutesField(fields[0]),
|
||||
new SecondsOrMinutesField(fields[1]),
|
||||
new HoursField(fields[2]),
|
||||
new DaysField(fields[3], fields[5]),
|
||||
new MonthsField(fields[4]),
|
||||
new YearsField(fields[6])
|
||||
];
|
||||
}
|
||||
function getIncrementLargerThanRangeWarnings(items, first, last) {
|
||||
const warnings = [];
|
||||
for (let item of items) {
|
||||
let rangeLength;
|
||||
if (item.step > 1 &&
|
||||
item.step > (rangeLength = item.rangeLength(first, last))) {
|
||||
warnings.push({
|
||||
type: WarningType.IncrementLargerThanRange,
|
||||
message: `Increment (${item.step}) is larger than range (${rangeLength}) for expression '${item.itemString}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
class Field {
|
||||
constructor(field) {
|
||||
this.field = field;
|
||||
}
|
||||
parse() {
|
||||
return this.field.split(',')
|
||||
.map(item => FieldItem.parse(item, this.first, this.last, true));
|
||||
}
|
||||
get items() {
|
||||
if (!this._items)
|
||||
this._items = this.parse();
|
||||
return this._items;
|
||||
}
|
||||
get values() {
|
||||
return Field.getValues(this.items, this.first, this.last);
|
||||
}
|
||||
get warnings() {
|
||||
return getIncrementLargerThanRangeWarnings(this.items, this.first, this.last);
|
||||
}
|
||||
static getValues(items, first, last) {
|
||||
return Array.from(new Set(flatMap(items, item => item.values(first, last)))).sort(sortAsc);
|
||||
}
|
||||
}
|
||||
class FieldItem {
|
||||
constructor(itemString) {
|
||||
this.itemString = itemString;
|
||||
this.step = 1;
|
||||
}
|
||||
rangeLength(first, last) {
|
||||
var _a, _b, _c, _d;
|
||||
const start = (_b = (_a = this.range) === null || _a === void 0 ? void 0 : _a.from) !== null && _b !== void 0 ? _b : first, end = (_d = (_c = this.range) === null || _c === void 0 ? void 0 : _c.to) !== null && _d !== void 0 ? _d : last;
|
||||
return (end < start) ? ((last - start) + (end - first) + 1) : (end - start);
|
||||
}
|
||||
values(first, last) {
|
||||
const start = this.range ? this.range.from : first, rangeLength = this.rangeLength(first, last);
|
||||
return Array(Math.floor(rangeLength / this.step) + 1)
|
||||
.fill(0)
|
||||
.map((_, i) => first + ((start - first + (this.step * i)) % (last - first + 1)));
|
||||
}
|
||||
get any() {
|
||||
return this.range === undefined && this.step === 1;
|
||||
}
|
||||
get single() {
|
||||
return !!this.range && this.range.from === this.range.to;
|
||||
}
|
||||
static parse(item, first, last, allowCyclicRange = false, transformer) {
|
||||
var _a;
|
||||
const fieldItem = new FieldItem(item);
|
||||
const [match, all, startFrom, range, step] = ((_a = item.match(/^(?:(\*)|([0-9]+)|([0-9]+-[0-9]+))(?:\/([1-9][0-9]*))?$/)) !== null && _a !== void 0 ? _a : []);
|
||||
if (!match)
|
||||
throw new Error('Field item invalid format');
|
||||
if (step) {
|
||||
fieldItem.step = parseInt(step, 10);
|
||||
}
|
||||
if (startFrom) {
|
||||
let start = parseInt(startFrom, 10);
|
||||
start = transformer ? transformer(start) : start;
|
||||
if (start < first || start > last)
|
||||
throw new Error('Field item out of valid value range');
|
||||
fieldItem.range = {
|
||||
from: start,
|
||||
to: step ? undefined : start
|
||||
};
|
||||
}
|
||||
else if (range) {
|
||||
const [rangeStart, rangeEnd] = range.split('-').map(x => {
|
||||
const n = parseInt(x, 10);
|
||||
return transformer ? transformer(n) : n;
|
||||
});
|
||||
if (rangeStart < first || rangeStart > last || rangeEnd < first || rangeEnd > last ||
|
||||
(rangeEnd < rangeStart && !allowCyclicRange)) {
|
||||
throw new Error('Field item range invalid, either value out of valid range or start greater than end in non wraparound field');
|
||||
}
|
||||
fieldItem.range = {
|
||||
from: rangeStart,
|
||||
to: rangeEnd
|
||||
};
|
||||
}
|
||||
return fieldItem;
|
||||
}
|
||||
}
|
||||
FieldItem.asterisk = new FieldItem('*');
|
||||
class SecondsOrMinutesField extends Field {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.first = 0;
|
||||
this.last = 59;
|
||||
}
|
||||
}
|
||||
class HoursField extends Field {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.first = 0;
|
||||
this.last = 23;
|
||||
}
|
||||
}
|
||||
class DaysField {
|
||||
constructor(daysOfMonthField, daysOfWeekField) {
|
||||
this.lastDay = false;
|
||||
this.lastWeekday = false;
|
||||
this.daysItems = [];
|
||||
this.nearestWeekdayItems = [];
|
||||
this.daysOfWeekItems = [];
|
||||
this.lastDaysOfWeekItems = [];
|
||||
this.nthDaysOfWeekItems = [];
|
||||
for (let item of daysOfMonthField.split(',').map(s => s === '?' ? '*' : s)) {
|
||||
if (item === 'l') {
|
||||
this.lastDay = true;
|
||||
}
|
||||
else if (item === 'lw') {
|
||||
this.lastWeekday = true;
|
||||
}
|
||||
else if (item.endsWith('w')) {
|
||||
this.nearestWeekdayItems.push(FieldItem.parse(item.slice(0, -1), 1, 31));
|
||||
}
|
||||
else {
|
||||
this.daysItems.push(FieldItem.parse(item, 1, 31));
|
||||
}
|
||||
}
|
||||
const normalisedDaysOfWeekField = daysOfWeekField.replace(dayOfWeekReplacementRegex, match => dayOfWeekReplacements.indexOf(match) + '');
|
||||
const parseDayOfWeek = (item) => FieldItem.parse(item, 0, 6, true, n => n === 7 ? 0 : n);
|
||||
for (let item of normalisedDaysOfWeekField.split(',').map(s => s === '?' ? '*' : s)) {
|
||||
const nthIndex = item.lastIndexOf('#');
|
||||
if (item.endsWith('l')) {
|
||||
this.lastDaysOfWeekItems.push(parseDayOfWeek(item.slice(0, -1)));
|
||||
}
|
||||
else if (nthIndex !== -1) {
|
||||
const nth = item.slice(nthIndex + 1);
|
||||
if (!/^[1-5]$/.test(nth))
|
||||
throw new Error('Field item nth of month (#) invalid');
|
||||
this.nthDaysOfWeekItems.push({
|
||||
item: parseDayOfWeek(item.slice(0, nthIndex)),
|
||||
nth: parseInt(nth, 10)
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.daysOfWeekItems.push(parseDayOfWeek(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
get values() {
|
||||
return DaysFieldValues.fromField(this);
|
||||
}
|
||||
get warnings() {
|
||||
const warnings = [], dayItems = [
|
||||
...this.daysItems,
|
||||
...this.nearestWeekdayItems,
|
||||
], weekItems = [
|
||||
...this.daysOfWeekItems,
|
||||
...this.lastDaysOfWeekItems,
|
||||
...this.nthDaysOfWeekItems.map(({ item }) => item),
|
||||
];
|
||||
warnings.push(...getIncrementLargerThanRangeWarnings(dayItems, 1, 31), ...getIncrementLargerThanRangeWarnings(weekItems, 0, 6));
|
||||
return warnings;
|
||||
}
|
||||
get allDays() {
|
||||
return (!this.lastDay &&
|
||||
!this.lastWeekday &&
|
||||
!this.nearestWeekdayItems.length &&
|
||||
!this.lastDaysOfWeekItems.length &&
|
||||
!this.nthDaysOfWeekItems.length &&
|
||||
this.daysItems.length === 1 && this.daysItems[0].any &&
|
||||
this.daysOfWeekItems.length === 1 && this.daysOfWeekItems[0].any);
|
||||
}
|
||||
}
|
||||
class DaysFieldValues {
|
||||
constructor() {
|
||||
this.lastDay = false;
|
||||
this.lastWeekday = false;
|
||||
this.days = [];
|
||||
this.nearestWeekday = [];
|
||||
this.daysOfWeek = [];
|
||||
this.lastDaysOfWeek = [];
|
||||
this.nthDaysOfWeek = [];
|
||||
}
|
||||
static fromField(field) {
|
||||
const values = new DaysFieldValues();
|
||||
const filterAnyItems = (items) => items.filter(item => !item.any);
|
||||
values.lastDay = field.lastDay;
|
||||
values.lastWeekday = field.lastWeekday;
|
||||
values.days = Field.getValues(field.allDays ? [FieldItem.asterisk] : filterAnyItems(field.daysItems), 1, 31);
|
||||
values.nearestWeekday = Field.getValues(field.nearestWeekdayItems, 1, 31);
|
||||
values.daysOfWeek = Field.getValues(filterAnyItems(field.daysOfWeekItems), 0, 6);
|
||||
values.lastDaysOfWeek = Field.getValues(field.lastDaysOfWeekItems, 0, 6);
|
||||
const nthDaysHashes = new Set();
|
||||
for (let item of field.nthDaysOfWeekItems) {
|
||||
for (let n of item.item.values(0, 6)) {
|
||||
let hash = n * 10 + item.nth;
|
||||
if (!nthDaysHashes.has(hash)) {
|
||||
nthDaysHashes.add(hash);
|
||||
values.nthDaysOfWeek.push([n, item.nth]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
getDays(year, month) {
|
||||
const days = new Set(this.days);
|
||||
const lastDateOfMonth = new Date(year, month, 0).getDate();
|
||||
const firstDayOfWeek = new Date(year, month - 1, 1).getDay();
|
||||
const getNearestWeekday = (day) => {
|
||||
if (day > lastDateOfMonth)
|
||||
day = lastDateOfMonth;
|
||||
const dayOfWeek = (day + firstDayOfWeek - 1) % 7;
|
||||
let weekday = day + (dayOfWeek === 0 ? 1 : (dayOfWeek === 6 ? -1 : 0));
|
||||
return weekday + (weekday < 1 ? 3 : (weekday > lastDateOfMonth ? -3 : 0));
|
||||
};
|
||||
if (this.lastDay) {
|
||||
days.add(lastDateOfMonth);
|
||||
}
|
||||
if (this.lastWeekday) {
|
||||
days.add(getNearestWeekday(lastDateOfMonth));
|
||||
}
|
||||
for (const day of this.nearestWeekday) {
|
||||
days.add(getNearestWeekday(day));
|
||||
}
|
||||
if (this.daysOfWeek.length ||
|
||||
this.lastDaysOfWeek.length ||
|
||||
this.nthDaysOfWeek.length) {
|
||||
const daysOfWeek = Array(7).fill(0).map(() => ([]));
|
||||
for (let day = 1; day < 36; day++) {
|
||||
daysOfWeek[(day + firstDayOfWeek - 1) % 7].push(day);
|
||||
}
|
||||
for (const dayOfWeek of this.daysOfWeek) {
|
||||
for (const day of daysOfWeek[dayOfWeek]) {
|
||||
days.add(day);
|
||||
}
|
||||
}
|
||||
for (const dayOfWeek of this.lastDaysOfWeek) {
|
||||
for (let i = daysOfWeek[dayOfWeek].length - 1; i >= 0; i--) {
|
||||
if (daysOfWeek[dayOfWeek][i] <= lastDateOfMonth) {
|
||||
days.add(daysOfWeek[dayOfWeek][i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [dayOfWeek, nthOfMonth] of this.nthDaysOfWeek) {
|
||||
days.add(daysOfWeek[dayOfWeek][nthOfMonth - 1]);
|
||||
}
|
||||
}
|
||||
return Array.from(days).filter(day => day <= lastDateOfMonth).sort(sortAsc);
|
||||
}
|
||||
}
|
||||
class MonthsField extends Field {
|
||||
constructor(field) {
|
||||
super(field.replace(monthReplacementRegex, match => {
|
||||
return monthReplacements.indexOf(match) + 1 + '';
|
||||
}));
|
||||
this.first = 1;
|
||||
this.last = 12;
|
||||
}
|
||||
}
|
||||
class YearsField extends Field {
|
||||
constructor(field) {
|
||||
super(field);
|
||||
this.first = 1970;
|
||||
this.last = 2099;
|
||||
this.items;
|
||||
}
|
||||
parse() {
|
||||
return this.field.split(',')
|
||||
.map(item => FieldItem.parse(item, 0, maxValidYear));
|
||||
}
|
||||
get warnings() {
|
||||
return getIncrementLargerThanRangeWarnings(this.items, this.first, maxValidYear);
|
||||
}
|
||||
nextYear(fromYear) {
|
||||
var _a;
|
||||
return (_a = this.items.reduce((years, item) => {
|
||||
var _a, _b, _c, _d;
|
||||
if (item.any)
|
||||
years.push(fromYear);
|
||||
else if (item.single) {
|
||||
const year = item.range.from;
|
||||
if (year >= fromYear)
|
||||
years.push(year);
|
||||
}
|
||||
else {
|
||||
const start = (_b = (_a = item.range) === null || _a === void 0 ? void 0 : _a.from) !== null && _b !== void 0 ? _b : this.first;
|
||||
if (start > fromYear)
|
||||
years.push(start);
|
||||
else {
|
||||
const nextYear = start + Math.ceil((fromYear - start) / item.step) * item.step;
|
||||
if (nextYear <= ((_d = (_c = item.range) === null || _c === void 0 ? void 0 : _c.to) !== null && _d !== void 0 ? _d : maxValidYear))
|
||||
years.push(nextYear);
|
||||
}
|
||||
}
|
||||
return years;
|
||||
}, []).sort(sortAsc)[0]) !== null && _a !== void 0 ? _a : null;
|
||||
}
|
||||
}
|
||||
|
||||
class CronosDate {
|
||||
constructor(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) {
|
||||
this.year = year;
|
||||
this.month = month;
|
||||
this.day = day;
|
||||
this.hour = hour;
|
||||
this.minute = minute;
|
||||
this.second = second;
|
||||
}
|
||||
static fromDate(date, timezone) {
|
||||
if (!timezone) {
|
||||
return new CronosDate(date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds());
|
||||
}
|
||||
return timezone['nativeDateToCronosDate'](date);
|
||||
}
|
||||
toDate(timezone) {
|
||||
if (!timezone) {
|
||||
return new Date(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
|
||||
}
|
||||
return timezone['cronosDateToNativeDate'](this);
|
||||
}
|
||||
static fromUTCTimestamp(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return new CronosDate(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
|
||||
}
|
||||
toUTCTimestamp() {
|
||||
return Date.UTC(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
|
||||
}
|
||||
copyWith({ year = this.year, month = this.month, day = this.day, hour = this.hour, minute = this.minute, second = this.second } = {}) {
|
||||
return new CronosDate(year, month, day, hour, minute, second);
|
||||
}
|
||||
}
|
||||
// Adapted from Intl.DateTimeFormat timezone handling in https://github.com/moment/luxon
|
||||
const ZoneCache = new Map();
|
||||
class CronosTimezone {
|
||||
constructor(IANANameOrOffset) {
|
||||
if (typeof IANANameOrOffset === 'number') {
|
||||
if (IANANameOrOffset > 840 || IANANameOrOffset < -840)
|
||||
throw new Error('Invalid offset');
|
||||
this.fixedOffset = IANANameOrOffset;
|
||||
return this;
|
||||
}
|
||||
const offsetMatch = IANANameOrOffset.match(/^([+-]?)(0[1-9]|1[0-4])(?::?([0-5][0-9]))?$/);
|
||||
if (offsetMatch) {
|
||||
this.fixedOffset = (offsetMatch[1] === '-' ? -1 : 1) * ((parseInt(offsetMatch[2], 10) * 60) + (parseInt(offsetMatch[3], 10) || 0));
|
||||
return this;
|
||||
}
|
||||
if (ZoneCache.has(IANANameOrOffset)) {
|
||||
return ZoneCache.get(IANANameOrOffset);
|
||||
}
|
||||
try {
|
||||
this.dateTimeFormat = new Intl.DateTimeFormat("en-US", {
|
||||
hour12: false,
|
||||
timeZone: IANANameOrOffset,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error('Invalid IANA name or offset');
|
||||
}
|
||||
this.zoneName = IANANameOrOffset;
|
||||
const currentYear = new Date().getUTCFullYear();
|
||||
this.winterOffset = this.offset(Date.UTC(currentYear, 0, 1));
|
||||
this.summerOffset = this.offset(Date.UTC(currentYear, 5, 1));
|
||||
ZoneCache.set(IANANameOrOffset, this);
|
||||
}
|
||||
toString() {
|
||||
if (this.fixedOffset) {
|
||||
const absOffset = Math.abs(this.fixedOffset);
|
||||
return [
|
||||
this.fixedOffset < 0 ? '-' : '+',
|
||||
Math.floor(absOffset / 60).toString().padStart(2, '0'),
|
||||
(absOffset % 60).toString().padStart(2, '0')
|
||||
].join('');
|
||||
}
|
||||
return this.zoneName;
|
||||
}
|
||||
offset(ts) {
|
||||
if (!this.dateTimeFormat)
|
||||
return this.fixedOffset || 0;
|
||||
const date = new Date(ts);
|
||||
const { year, month, day, hour, minute, second } = this.nativeDateToCronosDate(date);
|
||||
const asUTC = Date.UTC(year, month - 1, day, hour, minute, second), asTS = ts - (ts % 1000);
|
||||
return (asUTC - asTS) / 60000;
|
||||
}
|
||||
nativeDateToCronosDate(date) {
|
||||
if (!this.dateTimeFormat) {
|
||||
return CronosDate['fromUTCTimestamp'](date.getTime() + (this.fixedOffset || 0) * 60000);
|
||||
}
|
||||
return this.dateTimeFormat['formatToParts']
|
||||
? partsOffset(this.dateTimeFormat, date)
|
||||
: hackyOffset(this.dateTimeFormat, date);
|
||||
}
|
||||
cronosDateToNativeDate(date) {
|
||||
if (!this.dateTimeFormat) {
|
||||
return new Date(date['toUTCTimestamp']() - (this.fixedOffset || 0) * 60000);
|
||||
}
|
||||
const provisionalOffset = ((date.month > 3 || date.month < 11) ? this.summerOffset : this.winterOffset) || 0;
|
||||
const UTCTimestamp = date['toUTCTimestamp']();
|
||||
// Find the right offset a given local time.
|
||||
// Our UTC time is just a guess because our offset is just a guess
|
||||
let utcGuess = UTCTimestamp - provisionalOffset * 60000;
|
||||
// Test whether the zone matches the offset for this ts
|
||||
const o2 = this.offset(utcGuess);
|
||||
// If so, offset didn't change and we're done
|
||||
if (provisionalOffset === o2)
|
||||
return new Date(utcGuess);
|
||||
// If not, change the ts by the difference in the offset
|
||||
utcGuess -= (o2 - provisionalOffset) * 60000;
|
||||
// If that gives us the local time we want, we're done
|
||||
const o3 = this.offset(utcGuess);
|
||||
if (o2 === o3)
|
||||
return new Date(utcGuess);
|
||||
// If it's different, we're in a hole time. The offset has changed, but the we don't adjust the time
|
||||
return new Date(UTCTimestamp - Math.min(o2, o3) * 60000);
|
||||
}
|
||||
}
|
||||
function hackyOffset(dtf, date) {
|
||||
const formatted = dtf.format(date).replace(/\u200E/g, ""), parsed = formatted.match(/(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/), [, month, day, year, hour, minute, second] = (parsed !== null && parsed !== void 0 ? parsed : []).map(n => parseInt(n, 10));
|
||||
return new CronosDate(year, month, day, hour % 24, minute, second);
|
||||
}
|
||||
function partsOffset(dtf, date) {
|
||||
const formatted = dtf.formatToParts(date);
|
||||
return new CronosDate(parseInt(formatted[4].value, 10), parseInt(formatted[0].value, 10), parseInt(formatted[2].value, 10), parseInt(formatted[6].value, 10) % 24, parseInt(formatted[8].value, 10), parseInt(formatted[10].value, 10));
|
||||
}
|
||||
|
||||
const hourinms = 60 * 60 * 1000;
|
||||
const findFirstFrom = (from, list) => list.findIndex(n => n >= from);
|
||||
class CronosExpression {
|
||||
constructor(cronString, seconds, minutes, hours, days, months, years) {
|
||||
this.cronString = cronString;
|
||||
this.seconds = seconds;
|
||||
this.minutes = minutes;
|
||||
this.hours = hours;
|
||||
this.days = days;
|
||||
this.months = months;
|
||||
this.years = years;
|
||||
this.skipRepeatedHour = true;
|
||||
this.missingHour = 'insert';
|
||||
this._warnings = null;
|
||||
}
|
||||
static parse(cronstring, options = {}) {
|
||||
var _a;
|
||||
const parsedFields = _parse(cronstring);
|
||||
if (options.strict) {
|
||||
let warnings = flatMap(parsedFields, field => field.warnings);
|
||||
if (typeof options.strict === 'object') {
|
||||
warnings = warnings
|
||||
.filter(warning => !!options.strict[warning.type]);
|
||||
}
|
||||
if (warnings.length > 0) {
|
||||
throw new Error(`Strict mode: Parsing failed with ${warnings.length} warnings`);
|
||||
}
|
||||
}
|
||||
const expr = new CronosExpression(cronstring, parsedFields[0].values, parsedFields[1].values, parsedFields[2].values, parsedFields[3].values, parsedFields[4].values, parsedFields[5]);
|
||||
expr.timezone = options.timezone instanceof CronosTimezone ? options.timezone :
|
||||
(options.timezone !== undefined ? new CronosTimezone(options.timezone) : undefined);
|
||||
expr.skipRepeatedHour = options.skipRepeatedHour !== undefined ? options.skipRepeatedHour : expr.skipRepeatedHour;
|
||||
expr.missingHour = (_a = options.missingHour) !== null && _a !== void 0 ? _a : expr.missingHour;
|
||||
return expr;
|
||||
}
|
||||
get warnings() {
|
||||
if (!this._warnings) {
|
||||
const parsedFields = _parse(this.cronString);
|
||||
this._warnings = flatMap(parsedFields, field => field.warnings);
|
||||
}
|
||||
return this._warnings;
|
||||
}
|
||||
toString() {
|
||||
var _a, _b;
|
||||
const showTzOpts = !this.timezone || !!this.timezone.zoneName;
|
||||
const timezone = Object.entries({
|
||||
tz: (_b = (_a = this.timezone) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : 'Local',
|
||||
skipRepeatedHour: showTzOpts && this.skipRepeatedHour.toString(),
|
||||
missingHour: showTzOpts && this.missingHour,
|
||||
}).map(([key, val]) => val && key + ': ' + val).filter(s => s).join(', ');
|
||||
return `${this.cronString} (${timezone})`;
|
||||
}
|
||||
nextDate(afterDate = new Date()) {
|
||||
var _a;
|
||||
const fromCronosDate = CronosDate.fromDate(afterDate, this.timezone);
|
||||
if (((_a = this.timezone) === null || _a === void 0 ? void 0 : _a.fixedOffset) !== undefined) {
|
||||
return this._next(fromCronosDate).date;
|
||||
}
|
||||
const fromTimestamp = afterDate.getTime(), fromLocalTimestamp = fromCronosDate['toUTCTimestamp'](), prevHourLocalTimestamp = CronosDate.fromDate(new Date(fromTimestamp - hourinms), this.timezone)['toUTCTimestamp'](), nextHourLocalTimestamp = CronosDate.fromDate(new Date(fromTimestamp + hourinms), this.timezone)['toUTCTimestamp'](), nextHourRepeated = nextHourLocalTimestamp - fromLocalTimestamp === 0, thisHourRepeated = fromLocalTimestamp - prevHourLocalTimestamp === 0, thisHourMissing = fromLocalTimestamp - prevHourLocalTimestamp === hourinms * 2;
|
||||
if (this.skipRepeatedHour && thisHourRepeated) {
|
||||
return this._next(fromCronosDate.copyWith({ minute: 59, second: 60 }), false).date;
|
||||
}
|
||||
if (this.missingHour === 'offset' && thisHourMissing) {
|
||||
const nextDate = this._next(fromCronosDate.copyWith({ hour: fromCronosDate.hour - 1 })).date;
|
||||
if (!nextDate || nextDate.getTime() > fromTimestamp)
|
||||
return nextDate;
|
||||
}
|
||||
let { date: nextDate, cronosDate: nextCronosDate } = this._next(fromCronosDate);
|
||||
if (this.missingHour !== 'offset' && nextCronosDate && nextDate) {
|
||||
const nextDateNextHourTimestamp = nextCronosDate.copyWith({ hour: nextCronosDate.hour + 1 }).toDate(this.timezone).getTime();
|
||||
if (nextDateNextHourTimestamp === nextDate.getTime()) {
|
||||
if (this.missingHour === 'insert') {
|
||||
return nextCronosDate.copyWith({ minute: 0, second: 0 }).toDate(this.timezone);
|
||||
}
|
||||
// this.missingHour === 'skip'
|
||||
return this._next(nextCronosDate.copyWith({ minute: 59, second: 59 })).date;
|
||||
}
|
||||
}
|
||||
if (!this.skipRepeatedHour) {
|
||||
if (nextHourRepeated && (!nextDate || (nextDate.getTime() > fromTimestamp + hourinms))) {
|
||||
nextDate = this._next(fromCronosDate.copyWith({ minute: 0, second: 0 }), false).date;
|
||||
}
|
||||
if (nextDate && nextDate < afterDate) {
|
||||
nextDate = new Date(nextDate.getTime() + hourinms);
|
||||
}
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_next(date, after = true) {
|
||||
const nextDate = this._nextYear(after ? date.copyWith({ second: date.second + 1 }) : date);
|
||||
return {
|
||||
cronosDate: nextDate,
|
||||
date: nextDate ? nextDate.toDate(this.timezone) : null
|
||||
};
|
||||
}
|
||||
nextNDates(afterDate = new Date(), n = 5) {
|
||||
const dates = [];
|
||||
let lastDate = afterDate;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const date = this.nextDate(lastDate);
|
||||
if (!date)
|
||||
break;
|
||||
lastDate = date;
|
||||
dates.push(date);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
_nextYear(fromDate) {
|
||||
let year = fromDate.year;
|
||||
let nextDate = null;
|
||||
while (!nextDate) {
|
||||
year = this.years.nextYear(year);
|
||||
if (year === null)
|
||||
return null;
|
||||
nextDate = this._nextMonth((year === fromDate.year) ? fromDate : new CronosDate(year));
|
||||
year++;
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_nextMonth(fromDate) {
|
||||
let nextMonthIndex = findFirstFrom(fromDate.month, this.months);
|
||||
let nextDate = null;
|
||||
while (!nextDate) {
|
||||
const nextMonth = this.months[nextMonthIndex];
|
||||
if (nextMonth === undefined)
|
||||
return null;
|
||||
nextDate = this._nextDay((nextMonth === fromDate.month) ? fromDate : new CronosDate(fromDate.year, nextMonth));
|
||||
nextMonthIndex++;
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_nextDay(fromDate) {
|
||||
const days = this.days.getDays(fromDate.year, fromDate.month);
|
||||
let nextDayIndex = findFirstFrom(fromDate.day, days);
|
||||
let nextDate = null;
|
||||
while (!nextDate) {
|
||||
const nextDay = days[nextDayIndex];
|
||||
if (nextDay === undefined)
|
||||
return null;
|
||||
nextDate = this._nextHour((nextDay === fromDate.day) ? fromDate : new CronosDate(fromDate.year, fromDate.month, nextDay));
|
||||
nextDayIndex++;
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_nextHour(fromDate) {
|
||||
let nextHourIndex = findFirstFrom(fromDate.hour, this.hours);
|
||||
let nextDate = null;
|
||||
while (!nextDate) {
|
||||
const nextHour = this.hours[nextHourIndex];
|
||||
if (nextHour === undefined)
|
||||
return null;
|
||||
nextDate = this._nextMinute((nextHour === fromDate.hour) ? fromDate :
|
||||
new CronosDate(fromDate.year, fromDate.month, fromDate.day, nextHour));
|
||||
nextHourIndex++;
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_nextMinute(fromDate) {
|
||||
let nextMinuteIndex = findFirstFrom(fromDate.minute, this.minutes);
|
||||
let nextDate = null;
|
||||
while (!nextDate) {
|
||||
const nextMinute = this.minutes[nextMinuteIndex];
|
||||
if (nextMinute === undefined)
|
||||
return null;
|
||||
nextDate = this._nextSecond((nextMinute === fromDate.minute) ? fromDate :
|
||||
new CronosDate(fromDate.year, fromDate.month, fromDate.day, fromDate.hour, nextMinute));
|
||||
nextMinuteIndex++;
|
||||
}
|
||||
return nextDate;
|
||||
}
|
||||
_nextSecond(fromDate) {
|
||||
const nextSecondIndex = findFirstFrom(fromDate.second, this.seconds), nextSecond = this.seconds[nextSecondIndex];
|
||||
if (nextSecond === undefined)
|
||||
return null;
|
||||
return fromDate.copyWith({ second: nextSecond });
|
||||
}
|
||||
}
|
||||
|
||||
const maxTimeout = Math.pow(2, 31) - 1;
|
||||
const scheduledTasks = [];
|
||||
let runningTimer = null;
|
||||
function addTask(task) {
|
||||
if (task['_timestamp'] !== undefined) {
|
||||
const insertIndex = scheduledTasks.findIndex(t => t['_timestamp'] < task['_timestamp']);
|
||||
if (insertIndex >= 0)
|
||||
scheduledTasks.splice(insertIndex, 0, task);
|
||||
else
|
||||
scheduledTasks.push(task);
|
||||
}
|
||||
}
|
||||
function removeTask(task) {
|
||||
const removeIndex = scheduledTasks.indexOf(task);
|
||||
if (removeIndex >= 0)
|
||||
scheduledTasks.splice(removeIndex, 1);
|
||||
if (scheduledTasks.length === 0 && runningTimer) {
|
||||
clearTimeout(runningTimer);
|
||||
runningTimer = null;
|
||||
}
|
||||
}
|
||||
function runScheduledTasks(skipRun = false) {
|
||||
if (runningTimer)
|
||||
clearTimeout(runningTimer);
|
||||
const now = Date.now();
|
||||
const removeIndex = scheduledTasks.findIndex(task => task['_timestamp'] <= now);
|
||||
const tasksToRun = removeIndex >= 0 ? scheduledTasks.splice(removeIndex) : [];
|
||||
for (let task of tasksToRun) {
|
||||
if (!skipRun)
|
||||
task['_runTask']();
|
||||
if (task.isRunning) {
|
||||
task['_updateTimestamp']();
|
||||
addTask(task);
|
||||
}
|
||||
}
|
||||
const nextTask = scheduledTasks[scheduledTasks.length - 1];
|
||||
if (nextTask) {
|
||||
runningTimer = setTimeout(runScheduledTasks, Math.min(nextTask['_timestamp'] - Date.now(), maxTimeout));
|
||||
}
|
||||
else
|
||||
runningTimer = null;
|
||||
}
|
||||
function refreshSchedulerTimer() {
|
||||
for (const task of scheduledTasks) {
|
||||
task['_updateTimestamp']();
|
||||
if (!task.isRunning)
|
||||
removeTask(task);
|
||||
}
|
||||
scheduledTasks.sort((a, b) => b['_timestamp'] - a['_timestamp']);
|
||||
runScheduledTasks(true);
|
||||
}
|
||||
class DateArraySequence {
|
||||
constructor(dateLikes) {
|
||||
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) {
|
||||
const nextIndex = this._dates.findIndex(d => d > afterDate);
|
||||
return nextIndex === -1 ? null : this._dates[nextIndex];
|
||||
}
|
||||
}
|
||||
class CronosTask {
|
||||
constructor(sequenceOrDates) {
|
||||
this._listeners = {
|
||||
'started': new Set(),
|
||||
'stopped': new Set(),
|
||||
'run': new Set(),
|
||||
'ended': new Set(),
|
||||
};
|
||||
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() {
|
||||
if (!this.isRunning) {
|
||||
this._updateTimestamp();
|
||||
addTask(this);
|
||||
runScheduledTasks();
|
||||
if (this.isRunning)
|
||||
this._emit('started');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
stop() {
|
||||
if (this.isRunning) {
|
||||
this._timestamp = undefined;
|
||||
removeTask(this);
|
||||
this._emit('stopped');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
get nextRun() {
|
||||
return this.isRunning ? new Date(this._timestamp) : undefined;
|
||||
}
|
||||
get isRunning() {
|
||||
return this._timestamp !== undefined;
|
||||
}
|
||||
_runTask() {
|
||||
this._emit('run', this._timestamp);
|
||||
}
|
||||
_updateTimestamp() {
|
||||
const nextDate = this._sequence.nextDate(new Date());
|
||||
this._timestamp = nextDate ? nextDate.getTime() : undefined;
|
||||
if (!this.isRunning)
|
||||
this._emit('ended');
|
||||
}
|
||||
on(event, listener) {
|
||||
this._listeners[event].add(listener);
|
||||
return this;
|
||||
}
|
||||
off(event, listener) {
|
||||
this._listeners[event].delete(listener);
|
||||
return this;
|
||||
}
|
||||
_emit(event, ...args) {
|
||||
this._listeners[event].forEach((listener) => {
|
||||
listener.call(this, ...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleTask(cronString, task, options) {
|
||||
const expression = CronosExpression.parse(cronString, options);
|
||||
return new CronosTask(expression)
|
||||
.on('run', task)
|
||||
.start();
|
||||
}
|
||||
function validate(cronString, options) {
|
||||
try {
|
||||
CronosExpression.parse(cronString, options);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export { CronosExpression, CronosTask, CronosTimezone, refreshSchedulerTimer, scheduleTask, validate };
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "cronosjs",
|
||||
"description": "A cron based task scheduler for node and the browser, with extended syntax and timezone support.",
|
||||
"version": "1.7.1",
|
||||
"license": "ISC",
|
||||
"files": [
|
||||
"dist-*/",
|
||||
"bin/"
|
||||
],
|
||||
"pika": true,
|
||||
"sideEffects": false,
|
||||
"keywords": [
|
||||
"cron",
|
||||
"schedule",
|
||||
"scheduler",
|
||||
"timezone support"
|
||||
],
|
||||
"bugs": "https://github.com/jaclarke/cronosjs/issues",
|
||||
"repository": "github:jaclarke/cronosjs",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@pika/pack": "^0.5.0",
|
||||
"@pika/plugin-build-node": "^0.9.2",
|
||||
"@pika/plugin-build-web": "^0.9.2",
|
||||
"@pika/plugin-ts-standard-pkg": "^0.8.3",
|
||||
"coveralls": "^3.1.0",
|
||||
"jest": "^26.6.3",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"source": "dist-src/index.js",
|
||||
"types": "dist-types/index.d.ts",
|
||||
"main": "dist-node/index.js",
|
||||
"module": "dist-web/index.js"
|
||||
}
|
||||
Reference in New Issue
Block a user