sent again friend request

master
Bhaskara Kishore 3 years ago
parent af472bb9f6
commit 3960daade2

42
node_modules/.package-lock.json generated vendored

@ -4393,6 +4393,17 @@
"node": ">=10"
}
},
"node_modules/cron-parser": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.8.1.tgz",
"integrity": "sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==",
"dependencies": {
"luxon": "^3.2.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/cryptiles": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
@ -6618,6 +6629,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/long-timeout": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz",
"integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w=="
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -6637,6 +6653,14 @@
"yallist": "^3.0.2"
}
},
"node_modules/luxon": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz",
"integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==",
"engines": {
"node": ">=12"
}
},
"node_modules/magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
@ -7194,6 +7218,19 @@
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
},
"node_modules/node-schedule": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz",
"integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==",
"dependencies": {
"cron-parser": "^4.2.0",
"long-timeout": "0.1.1",
"sorted-array-functions": "^1.3.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/node-uuid": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
@ -8981,6 +9018,11 @@
"flatstr": "^1.0.12"
}
},
"node_modules/sorted-array-functions": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz",
"integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA=="
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",

@ -0,0 +1,15 @@
{
"rules": {
"eol-last": ["error", "always"],
"quotes": ["error", "single"],
"semi": ["error", "always"]
},
"overrides": [
{
"files": ["test/**.js"],
"parserOptions": {
"ecmaVersion": 6
}
}
]
}

@ -0,0 +1,25 @@
name: Build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 15.x, 16.x, 17.x, 18.x, 19.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test

21
node_modules/cron-parser/LICENSE generated vendored

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2016 Harri Siirak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

175
node_modules/cron-parser/README.md generated vendored

@ -0,0 +1,175 @@
cron-parser
================
[![Build Status](https://github.com/harrisiirak/cron-parser/actions/workflows/push.yml/badge.svg?branch=master)](https://github.com/harrisiirak/cron-parser/actions/workflows/push.yml)
[![NPM version](https://badge.fury.io/js/cron-parser.png)](http://badge.fury.io/js/cron-parser)
Node.js library for parsing and manipulating crontab instructions. It includes support for timezones and DST transitions.
__Compatibility__
Node >= 12.0.0
TypeScript >= 4.2
Setup
========
```bash
npm install cron-parser
```
Supported format
========
```
* * * * * *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ |
│ │ │ │ │ └ day of week (0 - 7, 1L - 7L) (0 or 7 is Sun)
│ │ │ │ └───── month (1 - 12)
│ │ │ └────────── day of month (1 - 31, L)
│ │ └─────────────── hour (0 - 23)
│ └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, optional)
```
Supports mixed use of ranges and range increments (W character not supported currently). See tests for examples.
Usage
========
Simple expression.
```javascript
var parser = require('cron-parser');
try {
var interval = parser.parseExpression('*/2 * * * *');
console.log('Date: ', interval.next().toString()); // Sat Dec 29 2012 00:42:00 GMT+0200 (EET)
console.log('Date: ', interval.next().toString()); // Sat Dec 29 2012 00:44:00 GMT+0200 (EET)
console.log('Date: ', interval.prev().toString()); // Sat Dec 29 2012 00:42:00 GMT+0200 (EET)
console.log('Date: ', interval.prev().toString()); // Sat Dec 29 2012 00:40:00 GMT+0200 (EET)
} catch (err) {
console.log('Error: ' + err.message);
}
```
Iteration with limited timespan. Also returns ES6 compatible iterator (when iterator flag is set to true).
```javascript
var parser = require('cron-parser');
var options = {
currentDate: new Date('Wed, 26 Dec 2012 12:38:53 UTC'),
endDate: new Date('Wed, 26 Dec 2012 14:40:00 UTC'),
iterator: true
};
try {
var interval = parser.parseExpression('*/22 * * * *', options);
while (true) {
try {
var obj = interval.next();
console.log('value:', obj.value.toString(), 'done:', obj.done);
} catch (e) {
break;
}
}
// value: Wed Dec 26 2012 14:44:00 GMT+0200 (EET) done: false
// value: Wed Dec 26 2012 15:00:00 GMT+0200 (EET) done: false
// value: Wed Dec 26 2012 15:22:00 GMT+0200 (EET) done: false
// value: Wed Dec 26 2012 15:44:00 GMT+0200 (EET) done: false
// value: Wed Dec 26 2012 16:00:00 GMT+0200 (EET) done: false
// value: Wed Dec 26 2012 16:22:00 GMT+0200 (EET) done: true
} catch (err) {
console.log('Error: ' + err.message);
}
```
Timezone support
```javascript
var parser = require('cron-parser');
var options = {
currentDate: '2016-03-27 00:00:01',
tz: 'Europe/Athens'
};
try {
var interval = parser.parseExpression('0 * * * *', options);
console.log('Date: ', interval.next().toString()); // Date: Sun Mar 27 2016 01:00:00 GMT+0200
console.log('Date: ', interval.next().toString()); // Date: Sun Mar 27 2016 02:00:00 GMT+0200
console.log('Date: ', interval.next().toString()); // Date: Sun Mar 27 2016 04:00:00 GMT+0300 (Notice DST transition)
} catch (err) {
console.log('Error: ' + err.message);
}
```
Manipulation
```javascript
var parser = require('cron-parser');
var interval = parser.parseExpression('0 7 * * 0-4');
var fields = JSON.parse(JSON.stringify(interval.fields)); // Fields is immutable
fields.hour = [8];
fields.minute = [29];
fields.dayOfWeek = [1,3,4,5,6,7];
var modifiedInterval = parser.fieldsToExpression(fields);
var cronString = modifiedInterval.stringify();
console.log(cronString); // "29 8 * * 1,3-7"
```
Options
========
* *currentDate* - Start date of the iteration
* *endDate* - End date of the iteration
`currentDate` and `endDate` accept `string`, `integer` and `Date` as input.
In case of using `string` as input, not every string format accepted
by the `Date` constructor will work correctly.
The supported formats are:
- [`ISO8601`](https://moment.github.io/luxon/#/parsing?id=iso-8601)
- [`HTTP and RFC2822`](https://moment.github.io/luxon/#/parsing?id=http-and-rfc2822)
- [`SQL`](https://moment.github.io/luxon/#/parsing?id=sql)
The reason being that those are the formats accepted by the
[`luxon`](https://moment.github.io/luxon/) library which is being used to handle dates.
Using `Date` as an input can be problematic specially when using the `tz` option. The issue being that, when creating a new `Date` object without
any timezone information, it will be created in the timezone of the system that is running the code. This (most of times) won't be what the user
will be expecting. Using one of the supported `string` formats will solve the issue(see timezone example).
* *iterator* - Return ES6 compatible iterator object
* *utc* - Enable UTC
* *tz* - Timezone string. It won't be used in case `utc` is enabled
Last weekday of the month
=========================
This library supports parsing the range `0L - 7L` in the `weekday` position of
the cron expression, where the `L` means "last occurrence of this weekday for
the month in progress".
For example, the following expression will run on the last monday of the month
at midnight:
```
0 0 0 * * 1L
```
The library also supports combining `L` expressions with other weekday
expressions. For example, the following cron will run every Monday as well
as the last Wednesday of the month:
```
0 0 0 * * 1,3L
```

@ -0,0 +1,21 @@
{
"name": "cron-parser",
"repo": "harrisiirak/cron-parser",
"description": "Node.js library for parsing crontab instructions",
"version": "4.8.1",
"keywords": [
"cron",
"crontab",
"parser"
],
"dependencies": {},
"development": {},
"main": "lib/parser.js",
"scripts": [
"lib/parser.js",
"lib/expression.js",
"lib/date.js",
"lib/field_compactor.js",
"lib/field_stringify.js"
]
}

@ -0,0 +1 @@
export * from './types'

@ -0,0 +1,252 @@
'use strict';
var luxon = require('luxon');
CronDate.prototype.addYear = function() {
this._date = this._date.plus({ years: 1 });
};
CronDate.prototype.addMonth = function() {
this._date = this._date.plus({ months: 1 }).startOf('month');
};
CronDate.prototype.addDay = function() {
this._date = this._date.plus({ days: 1 }).startOf('day');
};
CronDate.prototype.addHour = function() {
var prev = this._date;
this._date = this._date.plus({ hours: 1 }).startOf('hour');
if (this._date <= prev) {
this._date = this._date.plus({ hours: 1 });
}
};
CronDate.prototype.addMinute = function() {
var prev = this._date;
this._date = this._date.plus({ minutes: 1 }).startOf('minute');
if (this._date < prev) {
this._date = this._date.plus({ hours: 1 });
}
};
CronDate.prototype.addSecond = function() {
var prev = this._date;
this._date = this._date.plus({ seconds: 1 }).startOf('second');
if (this._date < prev) {
this._date = this._date.plus({ hours: 1 });
}
};
CronDate.prototype.subtractYear = function() {
this._date = this._date.minus({ years: 1 });
};
CronDate.prototype.subtractMonth = function() {
this._date = this._date
.minus({ months: 1 })
.endOf('month')
.startOf('second');
};
CronDate.prototype.subtractDay = function() {
this._date = this._date
.minus({ days: 1 })
.endOf('day')
.startOf('second');
};
CronDate.prototype.subtractHour = function() {
var prev = this._date;
this._date = this._date
.minus({ hours: 1 })
.endOf('hour')
.startOf('second');
if (this._date >= prev) {
this._date = this._date.minus({ hours: 1 });
}
};
CronDate.prototype.subtractMinute = function() {
var prev = this._date;
this._date = this._date.minus({ minutes: 1 })
.endOf('minute')
.startOf('second');
if (this._date > prev) {
this._date = this._date.minus({ hours: 1 });
}
};
CronDate.prototype.subtractSecond = function() {
var prev = this._date;
this._date = this._date
.minus({ seconds: 1 })
.startOf('second');
if (this._date > prev) {
this._date = this._date.minus({ hours: 1 });
}
};
CronDate.prototype.getDate = function() {
return this._date.day;
};
CronDate.prototype.getFullYear = function() {
return this._date.year;
};
CronDate.prototype.getDay = function() {
var weekday = this._date.weekday;
return weekday == 7 ? 0 : weekday;
};
CronDate.prototype.getMonth = function() {
return this._date.month - 1;
};
CronDate.prototype.getHours = function() {
return this._date.hour;
};
CronDate.prototype.getMinutes = function() {
return this._date.minute;
};
CronDate.prototype.getSeconds = function() {
return this._date.second;
};
CronDate.prototype.getMilliseconds = function() {
return this._date.millisecond;
};
CronDate.prototype.getTime = function() {
return this._date.valueOf();
};
CronDate.prototype.getUTCDate = function() {
return this._getUTC().day;
};
CronDate.prototype.getUTCFullYear = function() {
return this._getUTC().year;
};
CronDate.prototype.getUTCDay = function() {
var weekday = this._getUTC().weekday;
return weekday == 7 ? 0 : weekday;
};
CronDate.prototype.getUTCMonth = function() {
return this._getUTC().month - 1;
};
CronDate.prototype.getUTCHours = function() {
return this._getUTC().hour;
};
CronDate.prototype.getUTCMinutes = function() {
return this._getUTC().minute;
};
CronDate.prototype.getUTCSeconds = function() {
return this._getUTC().second;
};
CronDate.prototype.toISOString = function() {
return this._date.toUTC().toISO();
};
CronDate.prototype.toJSON = function() {
return this._date.toJSON();
};
CronDate.prototype.setDate = function(d) {
this._date = this._date.set({ day: d });
};
CronDate.prototype.setFullYear = function(y) {
this._date = this._date.set({ year: y });
};
CronDate.prototype.setDay = function(d) {
this._date = this._date.set({ weekday: d });
};
CronDate.prototype.setMonth = function(m) {
this._date = this._date.set({ month: m + 1 });
};
CronDate.prototype.setHours = function(h) {
this._date = this._date.set({ hour: h });
};
CronDate.prototype.setMinutes = function(m) {
this._date = this._date.set({ minute: m });
};
CronDate.prototype.setSeconds = function(s) {
this._date = this._date.set({ second: s });
};
CronDate.prototype.setMilliseconds = function(s) {
this._date = this._date.set({ millisecond: s });
};
CronDate.prototype._getUTC = function() {
return this._date.toUTC();
};
CronDate.prototype.toString = function() {
return this.toDate().toString();
};
CronDate.prototype.toDate = function() {
return this._date.toJSDate();
};
CronDate.prototype.isLastDayOfMonth = function() {
//next day
var newDate = this._date.plus({ days: 1 }).startOf('day');
return this._date.month !== newDate.month;
};
/**
* Returns true when the current weekday is the last occurrence of this weekday
* for the present month.
*/
CronDate.prototype.isLastWeekdayOfMonth = function() {
// Check this by adding 7 days to the current date and seeing if it's
// a different month
var newDate = this._date.plus({ days: 7 }).startOf('day');
return this._date.month !== newDate.month;
};
function CronDate (timestamp, tz) {
var dateOpts = { zone: tz };
if (!timestamp) {
this._date = luxon.DateTime.local();
} else if (timestamp instanceof CronDate) {
this._date = timestamp._date;
} else if (timestamp instanceof Date) {
this._date = luxon.DateTime.fromJSDate(timestamp, dateOpts);
} else if (typeof timestamp === 'number') {
this._date = luxon.DateTime.fromMillis(timestamp, dateOpts);
} else if (typeof timestamp === 'string') {
this._date = luxon.DateTime.fromISO(timestamp, dateOpts);
this._date.isValid || (this._date = luxon.DateTime.fromRFC2822(timestamp, dateOpts));
this._date.isValid || (this._date = luxon.DateTime.fromSQL(timestamp, dateOpts));
// RFC2822-like format without the required timezone offset (used in tests)
this._date.isValid || (this._date = luxon.DateTime.fromFormat(timestamp, 'EEE, d MMM yyyy HH:mm:ss', dateOpts));
}
if (!this._date || !this._date.isValid) {
throw new Error('CronDate: unhandled timestamp: ' + JSON.stringify(timestamp));
}
if (tz && tz !== this._date.zoneName) {
this._date = this._date.setZone(tz);
}
}
module.exports = CronDate;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,70 @@
'use strict';
function buildRange(item) {
return {
start: item,
count: 1
};
}
function completeRangeWithItem(range, item) {
range.end = item;
range.step = item - range.start;
range.count = 2;
}
function finalizeCurrentRange(results, currentRange, currentItemRange) {
if (currentRange) {
// Two elements do not form a range so split them into 2 single elements
if (currentRange.count === 2) {
results.push(buildRange(currentRange.start));
results.push(buildRange(currentRange.end));
} else {
results.push(currentRange);
}
}
if (currentItemRange) {
results.push(currentItemRange);
}
}
function compactField(arr) {
var results = [];
var currentRange = undefined;
for (var i = 0; i < arr.length; i++) {
var currentItem = arr[i];
if (typeof currentItem !== 'number') {
// String elements can't form a range
finalizeCurrentRange(results, currentRange, buildRange(currentItem));
currentRange = undefined;
} else if (!currentRange) {
// Start a new range
currentRange = buildRange(currentItem);
} else if (currentRange.count === 1) {
// Guess that the current item starts a range
completeRangeWithItem(currentRange, currentItem);
} else {
if (currentRange.step === currentItem - currentRange.end) {
// We found another item that matches the current range
currentRange.count++;
currentRange.end = currentItem;
} else if (currentRange.count === 2) { // The current range can't be continued
// Break the first item of the current range into a single element, and try to start a new range with the second item
results.push(buildRange(currentRange.start));
currentRange = buildRange(currentRange.end);
completeRangeWithItem(currentRange, currentItem);
} else {
// Persist the current range and start a new one with current item
finalizeCurrentRange(results, currentRange);
currentRange = buildRange(currentItem);
}
}
}
finalizeCurrentRange(results, currentRange);
return results;
}
module.exports = compactField;

@ -0,0 +1,58 @@
'use strict';
var compactField = require('./field_compactor');
function stringifyField(arr, min, max) {
var ranges = compactField(arr);
if (ranges.length === 1) {
var singleRange = ranges[0];
var step = singleRange.step;
if (step === 1 && singleRange.start === min && singleRange.end === max) {
return '*';
}
if (step !== 1 && singleRange.start === min && singleRange.end === max - step + 1) {
return '*/' + step;
}
}
var result = [];
for (var i = 0, l = ranges.length; i < l; ++i) {
var range = ranges[i];
if (range.count === 1) {
result.push(range.start);
continue;
}
var step = range.step;
if (range.step === 1) {
result.push(range.start + '-' + range.end);
continue;
}
var multiplier = range.start == 0 ? range.count - 1 : range.count;
if (range.step * multiplier > range.end) {
result = result.concat(
Array
.from({ length: range.end - range.start + 1 })
.map(function (_, index) {
var value = range.start + index;
if ((value - range.start) % range.step === 0) {
return value;
}
return null;
})
.filter(function (value) {
return value != null;
})
);
} else if (range.end === max - range.step + 1) {
result.push(range.start + '/' + range.step);
} else {
result.push(range.start + '-' + range.end + '/' + range.step);
}
}
return result.join(',');
}
module.exports = stringifyField;

@ -0,0 +1,116 @@
'use strict';
var CronExpression = require('./expression');
function CronParser() {}
/**
* Parse crontab entry
*
* @private
* @param {String} entry Crontab file entry/line
*/
CronParser._parseEntry = function _parseEntry (entry) {
var atoms = entry.split(' ');
if (atoms.length === 6) {
return {
interval: CronExpression.parse(entry)
};
} else if (atoms.length > 6) {
return {
interval: CronExpression.parse(
atoms.slice(0, 6).join(' ')
),
command: atoms.slice(6, atoms.length)
};
} else {
throw new Error('Invalid entry: ' + entry);
}
};
/**
* Wrapper for CronExpression.parser method
*
* @public
* @param {String} expression Input expression
* @param {Object} [options] Parsing options
* @return {Object}
*/
CronParser.parseExpression = function parseExpression (expression, options) {
return CronExpression.parse(expression, options);
};
/**
* Wrapper for CronExpression.fieldsToExpression method
*
* @public
* @param {Object} fields Input fields
* @param {Object} [options] Parsing options
* @return {Object}
*/
CronParser.fieldsToExpression = function fieldsToExpression (fields, options) {
return CronExpression.fieldsToExpression(fields, options);
};
/**
* Parse content string
*
* @public
* @param {String} data Crontab content
* @return {Object}
*/
CronParser.parseString = function parseString (data) {
var blocks = data.split('\n');
var response = {
variables: {},
expressions: [],
errors: {}
};
for (var i = 0, c = blocks.length; i < c; i++) {
var block = blocks[i];
var matches = null;
var entry = block.trim(); // Remove surrounding spaces
if (entry.length > 0) {
if (entry.match(/^#/)) { // Comment
continue;
} else if ((matches = entry.match(/^(.*)=(.*)$/))) { // Variable
response.variables[matches[1]] = matches[2];
} else { // Expression?
var result = null;
try {
result = CronParser._parseEntry('0 ' + entry);
response.expressions.push(result.interval);
} catch (err) {
response.errors[entry] = err;
}
}
}
}
return response;
};
/**
* Parse crontab file
*
* @public
* @param {String} filePath Path to file
* @param {Function} callback
*/
CronParser.parseFile = function parseFile (filePath, callback) {
require('fs').readFile(filePath, function(err, data) {
if (err) {
callback(err);
return;
}
return callback(null, CronParser.parseString(data.toString()));
});
};
module.exports = CronParser;

@ -0,0 +1,85 @@
{
"name": "cron-parser",
"version": "4.8.1",
"description": "Node.js library for parsing crontab instructions",
"main": "lib/parser.js",
"types": "index.d.ts",
"typesVersions": {
"<4.1": {
"*": [
"types/ts3/*"
]
}
},
"directories": {
"test": "test"
},
"scripts": {
"test:tsd": "tsd",
"test:unit": "TZ=UTC tap ./test/*.js",
"test:cover": "TZ=UTC tap --coverage-report=html ./test/*.js",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "npm run lint && npm run test:unit && npm run test:tsd"
},
"repository": {
"type": "git",
"url": "https://github.com/harrisiirak/cron-parser.git"
},
"keywords": [
"cron",
"crontab",
"parser"
],
"author": "Harri Siirak",
"contributors": [
"Nicholas Clawson",
"Daniel Prentis <daniel@salsitasoft.com>",
"Renault John Lecoultre",
"Richard Astbury <richard.astbury@gmail.com>",
"Meaglin Wasabi <Meaglin.wasabi@gmail.com>",
"Mike Kusold <hello@mikekusold.com>",
"Alex Kit <alex.kit@atmajs.com>",
"Santiago Gimeno <santiago.gimeno@gmail.com>",
"Daniel <darc.tec@gmail.com>",
"Christian Steininger <christian.steininger.cs@gmail.com>",
"Mykola Piskovyi <m.piskovyi@gmail.com>",
"Brian Vaughn <brian.david.vaughn@gmail.com>",
"Nicholas Clawson <nickclaw@gmail.com>",
"Yasuhiroki <yasuhiroki.duck@gmail.com>",
"Nicholas Clawson <nickclaw@gmail.com>",
"Brendan Warkentin <faazshift@gmail.com>",
"Charlie Fish <fishcharlie.code@gmail.com>",
"Ian Graves <ian+diskimage@iangrav.es>",
"Andy Thompson <me@andytson.com>",
"Regev Brody <regevbr@gmail.com>"
],
"license": "MIT",
"dependencies": {
"luxon": "^3.2.1"
},
"devDependencies": {
"eslint": "^8.27.0",
"sinon": "^15.0.1",
"tap": "^16.3.3",
"tsd": "^0.26.0"
},
"engines": {
"node": ">=12.0.0"
},
"browser": {
"fs": false
},
"tap": {
"check-coverage": false
},
"tsd": {
"directory": "test",
"compilerOptions": {
"lib": [
"es2017",
"dom"
]
}
}
}

@ -0,0 +1,17 @@
var util = require('util');
var test = require('tap').test;
var expression = require('../lib/expression');
test('expression 31 of month', function(t) {
try {
var interval = expression.parse('0 0 31 * *');
var i;
var d;
for (i = 0; i < 20; ++i) {
d = interval.next();
}
t.end();
} catch (err) {
t.error(err, 'Interval parse error');
}
});

@ -0,0 +1,29 @@
var util = require('util');
var test = require('tap').test;
var CronExpression = require('../lib/expression');
var CronDate = require('../lib/date');
test('parse expression as UTC', function(t) {
try {
var options = {
utc: true
};
var interval = CronExpression.parse('0 0 10 * * *', options);
var date = interval.next();
t.equal(date.getUTCHours(), 10, 'Correct UTC hour value');
interval = CronExpression.parse('0 */5 * * * *', options);
var date = interval.next(), now = new Date();
now.setMinutes(now.getMinutes() + 5 - (now.getMinutes() % 5));
t.equal(date.getHours(), now.getUTCHours(), 'Correct local time for 5 minute interval');
} catch (err) {
t.error(err, 'UTC parse error');
}
t.end();
});

@ -0,0 +1,16 @@
// empty around comma
var test = require('tap').test;
var CronDate = require('../lib/date');
test('is the last weekday of the month', function (t) {
// Last monday of septhember
var date = new CronDate(new Date(2021, 8, 27));
t.equal(date.isLastWeekdayOfMonth(), true);
// Second-to-last monday of septhember
var date = new CronDate(new Date(2021, 8, 20));
t.equal(date.isLastWeekdayOfMonth(), false);
t.end();
});

@ -0,0 +1,7 @@
# Comment line (ignore)
ENV1="test1"
ENV2="test2"
*/10 * * * * /path/to/exe
*/10 * * * * /path/to/exe
0 09-18 * * 1-5 /path/to/exe

@ -0,0 +1,22 @@
// empty around comma
var test = require('tap').test;
var CronExpression = require('../lib/expression');
const options = {
utc: true
};
test('both empty around comma', function (t) {
t.throws(function () {
CronExpression.parse('*/10 * * * * ,', options);
}, new Error('Invalid list value format'));
t.end();
});
test('one side empty around comma', function (t) {
t.throws(function () {
CronExpression.parse('*/10 * * * * ,2', options);
}, new Error('Invalid list value format'));
t.end();
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,250 @@
'use strict';
var test = require('tap').test;
var compactField = require('../lib/field_compactor');
test('compact field - empty array', function(t) {
try {
var result = compactField([]);
t.same(result, []);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});
test('compact field - single element array', function(t) {
try {
var result = compactField([1]);
t.same(result, [{
start: 1,
count: 1
}]);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});
test('compact field - 2 elements array', function(t) {
try {
var result = compactField([1, 2]);
t.same(result, [
{
start: 1,
count: 1
},
{
start: 2,
count: 1
}
]);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});
test('compact field - 2 elements array big step', function(t) {
try {
var result = compactField([1, 5]);
t.same(result, [
{
start: 1,
count: 1
},
{
start: 5,
count: 1
}
]);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});
test('compact field - 3 elements array 1 step', function(t) {
try {
var result = compactField([1, 2, 3]);
t.same(result, [
{
start: 1,
end: 3,
count: 3,
step: 1
}
]);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});
test('compact field - 3 elements array 1 step, dangling extra at end', function(t) {
try {
var result = compactField([1, 2, 3, 5]);
t.same(result, [
{
start: 1,
end: 3,
count: 3,
step: 1
},
{
start: 5,
count: 1
}
]);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});
test('compact field - 3 elements array 1 step, dangling extra at end and beginning', function(t) {
try {
var result = compactField([1, 4, 5, 6, 9]);
t.same(result, [
{
start: 1,
count: 1
},
{
start: 4,
end: 6,
count: 3,
step: 1
},
{
start: 9,
count: 1
}
]);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});
test('compact field - 2 ranges with dangling in the middle', function(t) {
try {
var result = compactField([1, 2, 3, 6, 9, 11, 13]);
t.same(result, [
{
start: 1,
end: 3,
count: 3,
step: 1
},
{
start: 6,
count: 1
},
{
start: 9,
end: 13,
count: 3,
step: 2
}
]);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});
test('compact field - with chars', function(t) {
try {
var result = compactField(['L', 'W']);
t.same(result, [
{
start: 'L',
count: 1
},
{
start: 'W',
count: 1
}
]);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});
test('compact field - with chars and range', function(t) {
try {
var result = compactField([1, 'L', 'W']);
t.same(result, [
{
start: 1,
count: 1,
},
{
start: 'L',
count: 1
},
{
start: 'W',
count: 1
}
]);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});
test('compact field - with chars and range (v2)', function(t) {
try {
var result = compactField([1, 2, 'L', 'W']);
t.same(result, [
{
start: 1,
count: 1,
},
{
start: 2,
count: 1,
},
{
start: 'L',
count: 1
},
{
start: 'W',
count: 1
}
]);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});
test('compact field - with chars and range (v3)', function(t) {
try {
var result = compactField([1, 2, 3, 'L', 'W']);
t.same(result, [
{
start: 1,
end: 3,
count: 3,
step: 1
},
{
start: 'L',
count: 1
},
{
start: 'W',
count: 1
}
]);
} catch (err) {
t.error(err, 'compact field error');
}
t.end();
});

@ -0,0 +1,94 @@
'use strict';
var test = require('tap').test;
var stringifyField = require('../lib/field_stringify');
test('stringify astrix', function (t) {
try {
var str = stringifyField([1, 2, 3, 4], 1, 4);
t.equal(str, '*');
} catch (err) {
t.error(err, 'stringify field error');
}
t.end();
});
test('stringify astrix step', function (t) {
try {
var str = stringifyField([0, 2, 4, 6], 0, 7);
t.equal(str, '*/2');
} catch (err) {
t.error(err, 'stringify field error');
}
t.end();
});
test('stringify single value', function (t) {
try {
var str = stringifyField([2], 0, 7);
t.equal(str, '2');
} catch (err) {
t.error(err, 'stringify field error');
}
t.end();
});
test('stringify multiple single values', function (t) {
try {
var str = stringifyField([2, 5, 9], 0, 9);
t.equal(str, '2,5,9');
} catch (err) {
t.error(err, 'stringify field error');
}
t.end();
});
test('stringify multiple ranged values', function (t) {
try {
var str = stringifyField([1, 3, 5, 6], 0, 9);
t.equal(str, '1,3,5,6');
} catch (err) {
t.error(err, 'stringify field error');
}
t.end();
});
test('stringify range', function (t) {
try {
var str = stringifyField([2, 3, 4], 0, 7);
t.equal(str, '2-4');
} catch (err) {
t.error(err, 'stringify field error');
}
t.end();
});
test('stringify range step', function (t) {
try {
var str = stringifyField([2, 4, 6], 0, 8);
t.equal(str, '2-6/2');
} catch (err) {
t.error(err, 'stringify field error');
}
t.end();
});
test('stringify semi range step', function (t) {
try {
var str = stringifyField([4, 6, 8], 0, 9);
t.equal(str, '4/2');
} catch (err) {
t.error(err, 'stringify field error');
}
t.end();
});
test('stringify multi types', function (t) {
try {
var str = stringifyField([1, 2, 4, 5, 6, 7, 8, 9, 10, 20, 25, 30, 35, 57], 0, 59);
t.equal(str, '1,2,4-10,20-35/5,57');
} catch (err) {
t.error(err, 'stringify field error');
}
t.end();
});

@ -0,0 +1,32 @@
var test = require('tap').test;
var CronExpression = require('../lib/expression');
test('Fields are exposed', function(t){
try {
var interval = CronExpression.parse('0 1 2 3 * 1-3,5');
t.ok(interval, 'Interval parsed');
CronExpression.map.forEach(function(field) {
interval.fields[field] = [];
t.throws(function() {
interval.fields[field].push(-1);
}, /Cannot add property .*?, object is not extensible/, field + ' is frozen');
delete interval.fields[field];
});
interval.fields.dummy = [];
t.same(interval.fields.dummy, undefined, 'Fields is frozen');
t.same(interval.fields.second, [0], 'Second matches');
t.same(interval.fields.minute, [1], 'Minute matches');
t.same(interval.fields.hour, [2], 'Hour matches');
t.same(interval.fields.dayOfMonth, [3], 'Day of month matches');
t.same(interval.fields.month, [1,2,3,4,5,6,7,8,9,10,11,12], 'Month matches');
t.same(interval.fields.dayOfWeek, [1,2,3,5], 'Day of week matches');
} catch (err) {
t.error(err, 'Interval parse error');
}
t.end();
});

@ -0,0 +1,22 @@
var util = require('util');
var sinon = require('sinon');
var test = require('tap').test;
var CronExpression = require('../lib/expression');
test('increment_on_first_itereation', function(t) {
try {
var clock = sinon.useFakeTimers();
var fake_now = new Date('Tue Feb 21 2017 16:45:00');
clock.tick(fake_now.getTime());
var interval = CronExpression.parse('* * * * *');
t.ok(interval, 'Interval parsed');
var next = interval.next();
t.ok(next, 'Found next scheduled interval');
// Make sure next has incremented in 1 minute
t.equal(fake_now.getTime() + 60000, next.getTime());
clock.restore();
t.end();
} catch (err) {
t.error(err, 'Interval parse error');
}
});

@ -0,0 +1,133 @@
import {expectAssignable, expectError, expectNotAssignable, expectType} from 'tsd';
import {
CronDate,
CronExpression,
CronFields, DateType,
parseExpression,
parseFile, ParserOptions,
parseString,
fieldsToExpression,
StringResult
} from '../types/ts3';
const interval = parseExpression('0 1 2 3 * 1-3,5');
const intervalIterator = parseExpression('0 1 2 3 * 1-3,5', {iterator: true});
expectType<readonly number[]>(interval.fields.second);
expectType<readonly number[]>(interval.fields.minute);
expectType<readonly number[]>(interval.fields.hour);
expectType<readonly (number | 'L')[]>(interval.fields.dayOfMonth);
expectType<readonly number[]>(interval.fields.month);
expectType<readonly number[]>(interval.fields.dayOfWeek);
expectError(interval.fields = interval.fields);
expectError(interval.fields.second = []);
expectError(interval.fields.second.push(1));
expectError(interval.fields.minute = []);
expectError(interval.fields.minute.push(1));
expectError(interval.fields.hour = []);
expectError(interval.fields.hour.push(1));
expectError(interval.fields.dayOfMonth = []);
expectError(interval.fields.dayOfMonth.push(1));
expectError(interval.fields.month = []);
expectError(interval.fields.month.push(1));
expectError(interval.fields.dayOfWeek = []);
expectError(interval.fields.dayOfWeek.push(1));
expectAssignable<typeof interval.fields.second[0]>(0);
expectAssignable<typeof interval.fields.second[0]>(59);
expectAssignable<typeof interval.fields.minute[0]>(0);
expectAssignable<typeof interval.fields.minute[0]>(59);
expectAssignable<typeof interval.fields.hour[0]>(0);
expectAssignable<typeof interval.fields.hour[0]>(23);
expectAssignable<typeof interval.fields.dayOfMonth[0]>(1);
expectAssignable<typeof interval.fields.dayOfMonth[0]>(31);
expectAssignable<typeof interval.fields.dayOfMonth[0]>('L');
expectAssignable<typeof interval.fields.month[0]>(1);
expectAssignable<typeof interval.fields.month[0]>(12);
expectAssignable<typeof interval.fields.dayOfWeek[0]>(0);
expectAssignable<typeof interval.fields.dayOfWeek[0]>(7);
const parseOptions: ParserOptions<true> = {
currentDate: 'f',
startDate: 4,
endDate: new Date(),
iterator: true,
utc: true,
tz: 'f',
nthDayOfWeek: 5,
}
expectAssignable<{
currentDate?: string | number | Date
startDate?: string | number | Date
endDate?: string | number | Date
iterator?: boolean
utc?: boolean
tz?: string
nthDayOfWeek?: number
}>(parseOptions)
expectType<CronExpression>(parseExpression('0 1 2 3 * 1-3,5'))
expectType<CronExpression<true>>(parseExpression('0 1 2 3 * 1-3,5', parseOptions))
const fields: CronFields = {
second: [1, 1],
minute: [1],
hour: [1],
dayOfMonth: [1],
month: [1],
dayOfWeek: [1],
}
expectType<CronExpression>(fieldsToExpression(fields))
expectType<CronExpression<true>>(fieldsToExpression(fields, parseOptions))
expectType<string>(fieldsToExpression(fields).stringify())
expectType<string>(fieldsToExpression(fields, parseOptions).stringify())
expectType<string>(fieldsToExpression(fields, parseOptions).stringify(true))
expectType<void>(parseFile('path', (err: any, data: StringResult) => console.log(data)))
expectType<StringResult>(parseString('path'))
const stringResult = parseString('path');
expectType<{
variables: Record<string, string>,
expressions: CronExpression[],
errors: Record<string, any>,
}>(stringResult)
expectType<CronFields>(interval.fields)
expectType<CronDate>(interval.next())
expectType<CronDate>(interval.prev())
expectType<boolean>(interval.hasNext())
expectType<boolean>(interval.hasPrev())
expectType<string>(interval.stringify())
expectType<string>(interval.stringify(true))
expectType<void>(interval.reset())
expectType<void>(interval.reset("Sdf"))
expectType<void>(interval.reset(5))
expectType<void>(interval.reset(new Date()))
expectType<CronDate[]>(interval.iterate(5))
expectType<CronDate[]>(interval.iterate(5, (item: CronDate, i: number) => {}))
expectAssignable<DateType>(new Date())
expectAssignable<DateType>(5)
expectAssignable<DateType>("SDf")
expectType<IteratorResult<CronDate, CronDate>>(intervalIterator.next())
expectType<IteratorResult<CronDate, CronDate>>(intervalIterator.prev())
expectType<IteratorResult<CronDate, CronDate>[]>(intervalIterator.iterate(5))
expectType<IteratorResult<CronDate, CronDate>[]>(intervalIterator.iterate(5, (item: IteratorResult<CronDate, CronDate>, i: number) => {}))

@ -0,0 +1,138 @@
import {expectAssignable, expectError, expectNotAssignable, expectType} from 'tsd';
import {
CronDate,
CronExpression,
CronFields, DateType,
parseExpression,
parseFile, ParserOptions,
parseString,
fieldsToExpression,
StringResult
} from '../index';
const interval = parseExpression('0 1 2 3 * 1-3,5');
const intervalIterator = parseExpression('0 1 2 3 * 1-3,5', {iterator: true});
expectError(interval.fields = interval.fields);
expectError(interval.fields.second = []);
expectError(interval.fields.second.push(1));
expectError(interval.fields.minute = []);
expectError(interval.fields.minute.push(1));
expectError(interval.fields.hour = []);
expectError(interval.fields.hour.push(1));
expectError(interval.fields.dayOfMonth = []);
expectError(interval.fields.dayOfMonth.push(1));
expectError(interval.fields.month = []);
expectError(interval.fields.month.push(1));
expectError(interval.fields.dayOfWeek = []);
expectError(interval.fields.dayOfWeek.push(1));
expectAssignable<typeof interval.fields.second[0]>(0);
expectAssignable<typeof interval.fields.second[0]>(59);
expectNotAssignable<typeof interval.fields.second[0]>(-1);
expectNotAssignable<typeof interval.fields.second[0]>(60);
expectAssignable<typeof interval.fields.minute[0]>(0);
expectAssignable<typeof interval.fields.minute[0]>(59);
expectNotAssignable<typeof interval.fields.minute[0]>(-1);
expectNotAssignable<typeof interval.fields.minute[0]>(60);
expectAssignable<typeof interval.fields.hour[0]>(0);
expectAssignable<typeof interval.fields.hour[0]>(23);
expectNotAssignable<typeof interval.fields.hour[0]>(-1);
expectNotAssignable<typeof interval.fields.hour[0]>(24);
expectAssignable<typeof interval.fields.dayOfMonth[0]>(1);
expectAssignable<typeof interval.fields.dayOfMonth[0]>(31);
expectAssignable<typeof interval.fields.dayOfMonth[0]>('L');
expectNotAssignable<typeof interval.fields.dayOfMonth[0]>(0);
expectNotAssignable<typeof interval.fields.dayOfMonth[0]>(32);
expectAssignable<typeof interval.fields.month[0]>(1);
expectAssignable<typeof interval.fields.month[0]>(12);
expectNotAssignable<typeof interval.fields.month[0]>(0);
expectNotAssignable<typeof interval.fields.month[0]>(13);
expectAssignable<typeof interval.fields.dayOfWeek[0]>(0);
expectAssignable<typeof interval.fields.dayOfWeek[0]>(7);
expectNotAssignable<typeof interval.fields.dayOfWeek[0]>(-1);
expectNotAssignable<typeof interval.fields.dayOfWeek[0]>(8);
const parseOptions: ParserOptions<true> = {
currentDate: 'f',
startDate: 4,
endDate: new Date(),
iterator: true,
utc: true,
tz: 'f',
nthDayOfWeek: 5,
}
expectAssignable<{
currentDate?: string | number | Date
startDate?: string | number | Date
endDate?: string | number | Date
iterator?: boolean
utc?: boolean
tz?: string
nthDayOfWeek?: number
}>(parseOptions)
expectType<CronExpression>(parseExpression('0 1 2 3 * 1-3,5'))
expectType<CronExpression<true>>(parseExpression('0 1 2 3 * 1-3,5', parseOptions))
const fields: CronFields = {
second: [1, 1],
minute: [1],
hour: [1],
dayOfMonth: [1],
month: [1],
dayOfWeek: [1],
}
expectType<CronExpression>(fieldsToExpression(fields))
expectType<CronExpression<true>>(fieldsToExpression(fields, parseOptions))
expectType<string>(fieldsToExpression(fields).stringify())
expectType<string>(fieldsToExpression(fields, parseOptions).stringify())
expectType<string>(fieldsToExpression(fields, parseOptions).stringify(true))
expectType<void>(parseFile('path', (err: any, data: StringResult) => console.log(data)))
expectType<StringResult>(parseString('path'))
const stringResult = parseString('path');
expectType<{
variables: Record<string, string>,
expressions: CronExpression[],
errors: Record<string, any>,
}>(stringResult)
expectType<CronFields>(interval.fields)
expectType<CronDate>(interval.next())
expectType<CronDate>(interval.prev())
expectType<boolean>(interval.hasNext())
expectType<boolean>(interval.hasPrev())
expectType<string>(interval.stringify())
expectType<string>(interval.stringify(true))
expectType<void>(interval.reset())
expectType<void>(interval.reset("Sdf"))
expectType<void>(interval.reset(5))
expectType<void>(interval.reset(new Date()))
expectType<CronDate[]>(interval.iterate(5))
expectType<CronDate[]>(interval.iterate(5, (item: CronDate, i: number) => {}))
expectAssignable<DateType>(new Date())
expectAssignable<DateType>(5)
expectAssignable<DateType>("SDf")
expectType<IteratorResult<CronDate, CronDate>>(intervalIterator.next())
expectType<IteratorResult<CronDate, CronDate>>(intervalIterator.prev())
expectType<IteratorResult<CronDate, CronDate>[]>(intervalIterator.iterate(5))
expectType<IteratorResult<CronDate, CronDate>[]>(intervalIterator.iterate(5, (item: IteratorResult<CronDate, CronDate>, i: number) => {}))

@ -0,0 +1,17 @@
var util = require('util');
var test = require('tap').test;
var expression = require('../lib/expression');
test('leap year', function(t) {
try {
var interval = expression.parse('0 0 29 2 *');
var i;
var d;
for (i = 0; i < 20; ++i) {
d = interval.next();
}
t.end();
} catch (err) {
t.error(err, 'Interval parse error');
}
});

@ -0,0 +1,46 @@
var test = require('tap').test;
var CronParser = require('../lib/parser');
// Globals
test('load crontab file', function(t) {
CronParser.parseFile(__dirname + '/crontab.example', function(err, result) {
t.error(err, 'File read error');
t.ok(result, 'Crontab parsed parsed');
t.equal(Object.keys(result.variables).length, 2, 'variables length matches');
t.equal(Object.keys(result.errors).length, 0, 'errors length matches');
t.equal(result.expressions.length, 3, 'expressions length matches');
// Parse expressions
var next = null;
t.equal(result.expressions[0].hasNext(), true);
next = result.expressions[0].next();
t.ok(next, 'first date');
next = result.expressions[1].next();
t.ok(next, 'second date');
next = result.expressions[2].next();
t.ok(next, 'third date');
t.end();
});
});
test('no next date', function(t) {
var options = {
currentDate: new Date(2014, 0, 1),
endDate: new Date(2014, 0, 1)
};
try {
var interval = CronParser.parseExpression('* * 2 * *', options);
t.equal(interval.hasNext(), false);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});

@ -0,0 +1,145 @@
var luxon = require('luxon');
var test = require('tap').test;
var CronDate = require('../lib/date');
test('parse cron date formats with local timezone', (t) => {
// Some tests need the local offset to be compatible without invoking timezone management.
// Local offset is dependent on what the system being tested on is
var offset = new Date().getTimezoneOffset();
var offsetHours = Math.abs(Math.floor(offset/60));
var offsetMinutes = offset % 60;
var offsetSign = offset < 0 ? '-' : '+';
var expectedTime = new Date(2021, 0, 4, 10, 0, 0).toString();
test('undefined date', (t) => {
const realDate = new Date();
var d = new CronDate();
t.equal(d.toDate().toString(), realDate.toString());
t.end();
});
test('JS Date', (t) => {
var d = new CronDate(new Date(2021, 0, 4, 10, 0, 0));
t.equal(d.toDate().toString(), expectedTime);
t.end();
});
test('ISO 8601', (t) => {
var d = new CronDate('2021-01-04T10:00:00');
t.equal(d.toDate().toString(), expectedTime);
t.end();
});
test('ISO 8601 date', (t) => {
var d = new CronDate('2021-01-04');
var expectedTime = new Date(2021, 0, 4, 0, 0, 0).toString();
t.equal(d.toDate().toString(), expectedTime);
t.end();
});
test('RFC2822', (t) => {
var offsetString = offsetSign + String(offsetHours).padStart(2, 0) + String(offsetMinutes).padStart(2, 0);
var d = new CronDate('Mon, 4 Jan 2021 10:00:00 ' + offsetString);
t.equal(d.toDate().toString(), expectedTime);
t.end();
});
test('RFC2822-like without timezone offset', (t) => {
var d = new CronDate('Mon, 4 Jan 2021 10:00:00');
t.equal(d.toDate().toString(), expectedTime);
t.end();
});
test('SQL', (t) => {
var d = new CronDate('2021-01-04 10:00:00');
t.equal(d.toDate().toString(), expectedTime);
t.end();
});
test('milliseconds', (t) => {
var d = new CronDate(new Date('2021-01-04 10:00:00').valueOf());
t.equal(d.toDate().toString(), expectedTime);
t.end();
});
test('CronDate', (t) => {
var date = new CronDate('Mon, 4 Jan 2021 10:00:00');
var d = new CronDate(date);
t.equal(d.toDate().toString(), expectedTime);
t.end();
});
test('invalid', (t) => {
t.throws(() => {
var d = new CronDate('2021-01-4 10:00:00');
});
t.end();
});
t.end();
});
test('parse cron date formats with another timezone', (t) => {
test('ISO 8601 without offset', (t) => {
// implies format already in timezone
var d = new CronDate('2021-01-04T10:00:00', 'Europe/Athens');
t.equal(d.toISOString(), '2021-01-04T08:00:00.000Z');
t.end();
});
test('ISO 8601 with non-local offset', (t) => {
var d = new CronDate('2021-01-04T10:00:00+01:00', 'Europe/Athens');
t.equal(d.toISOString(), '2021-01-04T09:00:00.000Z');
t.end();
});
test('RFC2822 with non-local offset', (t) => {
var d = new CronDate('Mon, 4 Jan 2021 10:00:00 +0100', 'Europe/Athens');
t.equal(d.toISOString(), '2021-01-04T09:00:00.000Z');
t.end();
});
test('milliseconds', (t) => {
var date = luxon.DateTime.fromISO('2021-01-04T11:00:00.000+02:00').valueOf();
var d = new CronDate(date, 'Europe/Athens');
t.equal(d.toISOString(), '2021-01-04T09:00:00.000Z');
t.end();
});
test('CronDate with same timezone', (t) => {
var date = new CronDate('Mon, 4 Jan 2021 10:00:00', 'Europe/Athens');
var d = new CronDate(date);
t.equal(d.toISOString(), '2021-01-04T08:00:00.000Z');
t.end();
});
test('CronDate with different timezone', (t) => {
var date = new CronDate('Mon, 4 Jan 2021 10:00:00', 'America/New_York');
var d = new CronDate(date, 'Europe/Athens');
t.equal(d.toISOString(), '2021-01-04T15:00:00.000Z');
t.end();
});
t.end('crondate input should');
});

@ -0,0 +1,169 @@
var test = require('tap').test;
var CronParser = require('../lib/parser');
test('parse cron with last day in a month', function(t) {
var options = {
currentDate: new Date(2014, 0, 1),
endDate: new Date(2014, 10, 1)
};
try {
var interval = CronParser.parseExpression('0 0 L * *', options);
t.equal(interval.hasNext(), true);
for (i = 0; i < 10; ++i) {
var next = interval.next();
t.ok(next, 'has a date');
}
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('parse cron with last day in feb', function(t) {
var options = {
currentDate: new Date(2016, 0, 1),
endDate: new Date(2016, 10, 1)
};
try {
var interval = CronParser.parseExpression('0 0 6-20/2,L 2 *', options);
t.equal(interval.hasNext(), true);
var next = null;
var items = 9;
var i = 0;
while(interval.hasNext()) {
next = interval.next();
i += 1;
t.ok(next, 'has a date');
}
//leap year
t.equal(next.getDate(), 29);
t.equal(i, items);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('parse cron with last day in feb', function(t) {
var options = {
currentDate: new Date(2014, 0, 1),
endDate: new Date(2014, 10, 1)
};
try {
var interval = CronParser.parseExpression('0 0 1,3,6-10,L 2 *', options);
t.equal(interval.hasNext(), true);
var next = null;
while(interval.hasNext()) {
next = interval.next();
t.ok(next, 'has a date');
}
//common year
t.equal(next.getDate(), 28);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('parse cron with last weekday of the month', function(t) {
var options = {
currentDate: new Date(2021, 8, 1),
endDate: new Date(2021, 11, 1)
};
var testCases = [
{ expression: '0 0 0 * * 1L', expectedDate: 27 },
{ expression: '0 0 0 * * 2L', expectedDate: 28 },
{ expression: '0 0 0 * * 3L', expectedDate: 29 },
{ expression: '0 0 0 * * 4L', expectedDate: 30 },
{ expression: '0 0 0 * * 5L', expectedDate: 24 },
{ expression: '0 0 0 * * 6L', expectedDate: 25 },
{ expression: '0 0 0 * * 0L', expectedDate: 26 },
{ expression: '0 0 0 * * 7L', expectedDate: 26 }
];
testCases.forEach(function({ expression, expectedDate }) {
t.test(expression, function(t) {
try {
var interval = CronParser.parseExpression(expression, options);
t.equal(interval.hasNext(), true);
var next = interval.next();
t.equal(next.getDate(), expectedDate);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
});
t.end();
});
test('parses expression that runs on both last monday and friday of the month', function(t) {
var options = {
currentDate: new Date(2021, 8, 1),
endDate: new Date(2021, 11, 1)
};
try {
var interval = CronParser.parseExpression('0 0 0 * * 1L,5L', options);
t.equal(interval.next().getDate(), 24);
t.equal(interval.next().getDate(), 27);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('parses expression that runs on both every monday and last friday of mont', function(t) {
var options = {
currentDate: new Date(2021, 8, 1),
endDate: new Date(2021, 8, 30)
};
try {
var interval = CronParser.parseExpression('0 0 0 * * 1,5L', options);
var dates = [];
while(true) {
try {
dates.push(interval.next().getDate());
} catch (e) {
if (e.message !== 'Out of the timespan range') {
throw e;
}
break;
}
}
t.same(dates, [6, 13, 20, 24, 27]);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('fails to parse for invalid last weekday of month expression', function(t) {
t.throws(function() {
var interval = CronParser.parseExpression('0 0 0 * * L');
interval.next();
});
t.end();
});

@ -0,0 +1,32 @@
var test = require('tap').test;
var CronExpression = require('../lib/expression');
test('prev should match correctly (issue #98) when milliseconds are greater than 0', function(t) {
var options = {
currentDate: new Date('2017-06-13T18:21:25.002Z')
};
var interval = CronExpression.parse('*/5 * * * * *', options);
var prev = interval.prev();
t.equal(prev.getSeconds(), 25);
t.end();
});
test('prev should match correctly (issue #98) when milliseconds are equal to 0', function(t) {
var interval = CronExpression.parse('59 59 23 * * *',{
currentDate : new Date('2012-12-26 14:38:53')
});
[25, 24, 23, 22].forEach(function(date) {
var prev = interval.prev();
t.equal(prev.getFullYear(), 2012);
t.equal(prev.getMonth(), 11);
t.equal(prev.getDate(), date);
t.equal(prev.getHours(), 23);
t.equal(prev.getMinutes(), 59);
t.equal(prev.getSeconds(), 59);
});
t.end();
});

@ -0,0 +1,488 @@
'use strict';
var test = require('tap').test;
var CronParser = require('../lib/parser');
test('stringify cron expression all stars no seconds', function (t) {
try {
var expected = '0 * * * * *';
var interval = CronParser.parseExpression('* * * * *', {});
var str = interval.stringify(true);
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression all stars no seconds (discard seconds)', function (t) {
try {
var expected = '* * * * *';
var interval = CronParser.parseExpression('* * * * *', {});
var str = interval.stringify();
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify();
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression all stars with seconds', function (t) {
try {
var expected = '* * * * * *';
var interval = CronParser.parseExpression('* * * * * *', {});
var str = interval.stringify(true);
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression all stars with seconds (discard seconds)', function (t) {
try {
var expected = '* * * * *';
var interval = CronParser.parseExpression('* * * * * *', {});
var str = interval.stringify();
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify();
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression', function (t) {
try {
var expected = '0 1,2,4-10,20-35/5,57 * * * *';
var interval = CronParser.parseExpression('1,2,4-10,20-35/5,57 * * * *', {});
var str = interval.stringify(true);
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression (discard seconds)', function (t) {
try {
var expected = '1,2,4-10,20-35/5,57 * * * *';
var interval = CronParser.parseExpression('1,2,4-10,20-35/5,57 * * * *', {});
var str = interval.stringify();
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify();
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with star range step', function (t) {
try {
var expected = '0 */5 */2 * * *';
var interval = CronParser.parseExpression('*/5 */2 */1 * *', {});
var str = interval.stringify(true);
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with multiple values and retain original value', function (t) {
try {
var expected = '0 * * * * 1,3,5';
var interval = CronParser.parseExpression('* * * * 1,3,5', {});
var str = interval.stringify(true);
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with multiple values and convert value to range step', function (t) {
try {
var expected = '0 * * * * 0-6/2';
var interval = CronParser.parseExpression('* * * * 0,2,4,6', {});
var str = interval.stringify(true);
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with star range step (discard seconds)', function (t) {
try {
var expected = '*/5 */2 * * *';
var interval = CronParser.parseExpression('*/5 */2 */1 * *', {});
var str = interval.stringify();
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify();
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with semi range step', function (t) {
try {
var expected = '0 5/5 * * * *';
var interval = CronParser.parseExpression('5/5 * * * *', {});
var str = interval.stringify(true);
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with semi range step (discard seconds)', function (t) {
try {
var expected = '5/5 * * * *';
var interval = CronParser.parseExpression('5/5 * * * *', {});
var str = interval.stringify();
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify();
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with L', function (t) {
try {
var expected = '0 * * 1,4-10,L * *';
var interval = CronParser.parseExpression('* * 1,4-10,L * *', {});
var str = interval.stringify(true);
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with L (discard seconds)', function (t) {
try {
var expected = '* * 1,4-10,L * *';
var interval = CronParser.parseExpression('* * 1,4-10,L * *', {});
var str = interval.stringify();
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify();
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with weekday L', function (t) {
try {
var expected = '0 0 0 * * 1L';
var interval = CronParser.parseExpression(expected, {});
var str = interval.stringify(true);
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with multiple weekday, one of them with an L', function (t) {
try {
var expected = '0 0 0 * * 4,6L';
var interval = CronParser.parseExpression(expected, {});
var str = interval.stringify(true);
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with multiple weekday, two of them with an L', function (t) {
try {
var expected = '0 0 0 * * 1L,5L';
var interval = CronParser.parseExpression(expected, {});
var str = interval.stringify(true);
t.equal(str, expected);
str = CronParser.fieldsToExpression(interval.fields).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with wildcard day of month and single month value', function (t) {
try {
var expected = '* * * 4 *';
var interval = CronParser.parseExpression(expected, {});
var str = interval.stringify();
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with wildcard day of month and month range', function (t) {
try {
var expected = '* * * 4-6 *';
var interval = CronParser.parseExpression(expected, {});
var str = interval.stringify();
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with day of month range and single month value', function (t) {
try {
var expected = '* * 1-25 4 *';
var interval = CronParser.parseExpression(expected, {});
var str = interval.stringify();
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify from fields out of order', function (t) {
try {
var expected = '1-5 1 1 1 1 1';
var str = CronParser.fieldsToExpression({
second: [5,2,1,4,3],
minute: [1],
hour: [1],
month: [1],
dayOfMonth: [1],
dayOfWeek: [1],
}).stringify(true);
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify from fields out of order (discard seconds)', function (t) {
try {
var expected = '1 1 1 1 1';
var str = CronParser.fieldsToExpression({
second: [5,2,1,4,3],
minute: [1],
hour: [1],
month: [1],
dayOfMonth: [1],
dayOfWeek: [1],
}).stringify();
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('stringify cron expression with extended day of week range (0,7)', function (t) {
try {
var expected = '* * * * *';
var interval = CronParser.parseExpression('* * * * *');
var str = CronParser.fieldsToExpression({
second: interval.fields.second,
minute: interval.fields.minute,
hour: interval.fields.hour,
month: interval.fields.month,
dayOfMonth: interval.fields.dayOfMonth,
dayOfWeek: [0, 1, 2, 3, 4, 5, 6],
}).stringify();
t.equal(str, expected);
str = CronParser.fieldsToExpression({
second: interval.fields.second,
minute: interval.fields.minute,
hour: interval.fields.hour,
month: interval.fields.month,
dayOfMonth: interval.fields.dayOfMonth,
dayOfWeek: [0, 1, 2, 3, 4, 5, 6, 7],
}).stringify();
t.equal(str, expected);
} catch (err) {
t.error(err, 'Parse read error');
}
t.end();
});
test('validation error - missing seconds', function (t) {
t.throws(function () {
CronParser.fieldsToExpression({
minute: [1],
hour: [1],
dayOfMonth: [1],
month: [1],
dayOfWeek: [1],
});
}, new Error('Validation error, Field second is missing'));
t.end();
});
test('validation error - empty seconds', function (t) {
t.throws(function () {
CronParser.fieldsToExpression({
second: [],
minute: [1],
hour: [1],
dayOfMonth: [1],
month: [1],
dayOfWeek: [1],
});
}, new Error('Validation error, Field second contains no values'));
t.end();
});
test('validation error - missing values - empty array', function (t) {
t.throws(function () {
CronParser.fieldsToExpression({
second: [1],
minute: [],
hour: [1],
dayOfMonth: [1],
month: [1],
dayOfWeek: [1],
});
}, new Error('Validation error, Field minute contains no values'));
t.end();
});
test('validation error - missing values', function (t) {
t.throws(function () {
CronParser.fieldsToExpression({
second: [1],
hour: [1],
dayOfMonth: [1],
month: [1],
dayOfWeek: [1],
});
}, new Error('Validation error, Field minute is missing'));
t.end();
});
test('validation error - range error', function (t) {
t.throws(function () {
CronParser.fieldsToExpression({
second: [-1, 1, 0],
minute: [1],
hour: [1],
dayOfMonth: [1],
month: [1],
dayOfWeek: [1],
});
}, new Error('Constraint error, got value -1 expected range 0-59'));
t.end();
});
test('validation error - bad chars error', function (t) {
t.throws(function () {
CronParser.fieldsToExpression({
second: [0, 'R'],
minute: [1],
hour: [1],
dayOfMonth: [1],
month: [1],
dayOfWeek: [1],
});
}, new Error('Constraint error, got value R expected range 0-59'));
t.end();
});
test('validation error - duplicates', function (t) {
t.throws(function () {
CronParser.fieldsToExpression({
second: [1, 1],
minute: [1],
hour: [1],
dayOfMonth: [1],
month: [1],
dayOfWeek: [1],
});
}, new Error('Validation error, Field second contains duplicate values'));
t.end();
});

@ -0,0 +1,422 @@
var test = require('tap').test;
var CronExpression = require('../lib/expression');
test('It works on DST start', function(t) {
try {
var options = {
currentDate: '2016-03-27 02:00:01',
tz: 'Europe/Athens'
};
var interval, date;
interval = CronExpression.parse('0 * * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 4, 'Due to DST start in Athens, 3 is skipped');
t.equal(date.getDate(), 27, 'on the 27th');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 5, '5 AM');
t.equal(date.getDate(), 27, 'on the 27th');
interval = CronExpression.parse('30 2 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getMinutes(), 30, '30 Minutes');
t.equal(date.getHours(), 2, '2 AM');
t.equal(date.getDate(), 27, 'on the 27th');
date = interval.next();
t.equal(date.getMinutes(), 30, '30 Minutes');
t.equal(date.getHours(), 2, '2 AM');
t.equal(date.getDate(), 28, 'on the 28th');
interval = CronExpression.parse('0 3 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 4, 'Due to DST start in Athens, 3 is skipped');
t.equal(date.getDate(), 27, 'on the 27th');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 3, '3 on the 28th');
t.equal(date.getDate(), 28, 'on the 28th');
interval = CronExpression.parse('*/20 3 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 4, 'Due to DST start in Athens, 3 is skipped');
t.equal(date.getDate(), 27, 'on the 27th');
date = interval.next();
t.equal(date.getMinutes(), 20, '20 Minutes');
t.equal(date.getHours(), 4, 'Due to DST start in Athens, 3 is skipped');
t.equal(date.getDate(), 27, 'on the 27th');
date = interval.next();
t.equal(date.getMinutes(), 40, '20 Minutes');
t.equal(date.getHours(), 4, 'Due to DST start in Athens, 3 is skipped');
t.equal(date.getDate(), 27, 'on the 27th');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 28, 'on the 28th');
options.currentDate = '2016-03-27 00:00:01';
interval = CronExpression.parse('0 * 27 * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 1, '1 AM');
t.equal(date.getDate(), 27, 'on the 27th');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 2, '2 AM');
t.equal(date.getDate(), 27, 'on the 27th');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 4, '4 AM');
t.equal(date.getDate(), 27, 'on the 27th');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 5, '5 AM');
t.equal(date.getDate(), 27, 'on the 27th');
options.currentDate = '2016-03-27 00:00:01';
options.endDate = '2016-03-27 03:00:01';
interval = CronExpression.parse('0 * * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 1, '1 AM');
t.equal(date.getDate(), 27, 'on the 27th');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 2, '2 AM');
t.equal(date.getDate(), 27, 'on the 27th');
date = interval.next();
t.equal(date.getMinutes(), 0, '0 Minutes');
t.equal(date.getHours(), 4, '4 AM');
t.equal(date.getDate(), 27, 'on the 27th');
// Out of the timespan range
t.throws(function() {
date = interval.next();
});
} catch (err) {
t.error(err, 'Interval parse error');
}
t.end();
});
test('It works on DST end', function(t) {
try {
var options = {
currentDate: '2016-10-30 02:00:01',
tz: 'Europe/Athens'
};
var interval, date;
interval = CronExpression.parse('0 * * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 3, 'Due to DST end in Athens (4-->3)');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 4, '4 AM');
t.equal(date.getDate(), 30, '30th');
interval = CronExpression.parse('0 3 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 31, '31st');
interval = CronExpression.parse('*/20 3 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getMinutes(), 0, '0');
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getMinutes(), 20, '20');
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getMinutes(), 40, '40');
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getMinutes(), 0, '0');
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 31, '31st');
options.currentDate = '2016-10-30 00:00:01';
interval = CronExpression.parse('0 * 30 * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 1, '1 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 2, '2 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 4, '4 AM');
t.equal(date.getDate(), 30, '30th');
options.currentDate = '2016-10-30 00:00:01';
// specify the DST offset via ISO 8601 format, as 3am is repeated
options.endDate = '2016-10-30T03:00:01+03';
interval = CronExpression.parse('0 * * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 1, '1 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 2, '2 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 30, '30th');
// Out of the timespan range
t.throws(function() {
date = interval.next();
});
options.endDate = '2016-10-30 04:00:01';
interval = CronExpression.parse('0 * * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 1, '1 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 2, '2 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 3, '3 AM');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 4, '4 AM');
t.equal(date.getDate(), 30, '30th');
// Out of the timespan range
t.throws(function() {
date = interval.next();
});
options = {
currentDate : new Date('Sun Oct 29 2016 01:00:00 GMT+0200')
};
interval = CronExpression.parse('0 12 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 29, '29th');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 31, '31st');
options = {
currentDate : new Date('Sun Oct 29 2016 02:59:00 GMT+0200')
};
interval = CronExpression.parse('0 12 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 29, '29th');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 31, '31st');
options = {
currentDate : new Date('Sun Oct 29 2016 02:59:59 GMT+0200')
};
interval = CronExpression.parse('0 12 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 29, '29th');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 31, '31st');
options = {
currentDate : new Date('Sun Oct 30 2016 01:00:00 GMT+0200')
};
interval = CronExpression.parse('0 12 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 31, '31st');
options = {
currentDate : new Date('Sun Oct 30 2016 01:59:00 GMT+0200')
};
interval = CronExpression.parse('0 12 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 31, '31st');
options = {
currentDate : new Date('Sun Oct 30 2016 01:59:59 GMT+0200')
};
interval = CronExpression.parse('0 12 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 31, '31st');
options = {
currentDate : new Date('Sun Oct 30 2016 02:59:00 GMT+0200')
};
interval = CronExpression.parse('0 12 * * *', options);
t.ok(interval, 'Interval parsed');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 30, '30th');
date = interval.next();
t.equal(date.getHours(), 12, '12');
t.equal(date.getDate(), 31, '31st');
} catch (err) {
t.error(err, 'Interval parse error');
}
t.end();
});
test('it will work with #131 issue case', function(t) {
var options = {
tz: 'America/Sao_Paulo',
currentDate : new Date('Sun Oct 30 2018 02:59:00 GMT+0200')
};
var interval = CronExpression.parse('0 9 1 1 *', options);
var date = interval.next();
t.equal(date.getFullYear(), 2019);
t.equal(date.getDate(), 1);
t.equal(date.getMonth(), 0);
date = interval.prev();
t.equal(date.getFullYear(), 2018);
t.equal(date.getDate(), 1);
t.equal(date.getMonth(), 0);
date = interval.prev();
t.equal(date.getFullYear(), 2017);
t.equal(date.getDate(), 1);
t.equal(date.getMonth(), 0);
t.end();
});
test('it will work with #137 issue case', function(t) {
var options = {
tz: 'America/New_York',
currentDate : new Date('10/28/2018')
};
var interval = CronExpression.parse('0 12 * * 3', options);
var date = interval.next();
t.equal(date.getFullYear(), 2018);
t.equal(date.getDate(), 31);
t.equal(date.getMonth(), 9);
t.end();
});

@ -0,0 +1,131 @@
export type DateType = Date | number | string
export interface CronDate {
addYear(): void
addMonth(): void
addDay(): void
addHour(): void
addMinute(): void
addSecond(): void
subtractYear(): void
subtractMonth(): void
subtractDay(): void
subtractHour(): void
subtractMinute(): void
subtractSecond(): void
getDate(): number
getFullYear(): number
getDay(): number
getMonth(): number
getHours(): number
getMinutes(): number
getSeconds(): number
getMilliseconds(): number
getTime(): number
getUTCDate(): number
getUTCFullYear(): number
getUTCDay(): number
getUTCMonth(): number
getUTCHours(): number
getUTCMinutes(): number
getUTCSeconds(): number
toISOString(): string
toJSON(): string
setDate(d: number): void
setFullYear(y: number): void
setDay(d: number): void
setMonth(m: number): void
setHours(h: number): void
setMinutes(m: number): void
setSeconds(s: number): void
setMilliseconds(s: number): void
getTime(): number
toString(): string
toDate(): Date
isLastDayOfMonth(): boolean
}
export interface ParserOptions<IsIterable extends boolean = false> {
currentDate?: DateType
startDate?: DateType
endDate?: DateType
utc?: boolean
tz?: string
nthDayOfWeek?: number
iterator?: IsIterable
}
type IteratorResultOrCronDate<IsIterable extends boolean> = IsIterable extends true
? IteratorResult<CronDate, CronDate>
: CronDate;
export interface ICronExpression<CronFields, IsIterable extends boolean> {
readonly fields: CronFields;
/** Find next suitable date */
next(): IteratorResultOrCronDate<IsIterable>
/** Find previous suitable date */
prev(): IteratorResultOrCronDate<IsIterable>
/** Check if next suitable date exists */
hasNext(): boolean
/** Check if previous suitable date exists */
hasPrev(): boolean
/** Iterate over expression iterator */
iterate(steps: number, callback?: (item: IteratorResultOrCronDate<IsIterable>, i: number) => void): IteratorResultOrCronDate<IsIterable>[]
/** Reset expression iterator state */
reset(resetDate?: string | number | Date): void
stringify(includeSeconds?: boolean): string
}
export interface IStringResult<CronFields> {
variables: Record<string, string>,
expressions: ICronExpression<CronFields, false>[],
errors: Record<string, any>,
}

@ -0,0 +1,45 @@
import {
CronDate,
DateType,
ICronExpression,
IStringResult,
ParserOptions,
} from './common';
type BuildRangeTuple<Current extends [...number[]], Count extends number> =
Current["length"] extends Count
? Current
: BuildRangeTuple<[number, ...Current], Count>
type RangeTuple<Count extends number> = BuildRangeTuple<[], Count>
type BuildRange<Current extends number, End extends number, Accu extends [...number[]]> =
Accu["length"] extends End
? Current
: BuildRange<Current | Accu["length"], End, [number, ...Accu]>
type Range<StartInclusive extends number, EndExclusive extends number> = BuildRange<StartInclusive, EndExclusive, RangeTuple<StartInclusive>>
export type SixtyRange = Range<0, 30> | Range<30, 60>; // Typescript restriction on recursion depth
export type HourRange = Range<0, 24>;
export type DayOfTheMonthRange = Range<1, 32> | 'L';
export type MonthRange = Range<1, 13>;
export type DayOfTheWeekRange = Range<0, 8>;
export type CronFields = {
readonly second: readonly SixtyRange[];
readonly minute: readonly SixtyRange[];
readonly hour: readonly HourRange[];
readonly dayOfMonth: readonly DayOfTheMonthRange[];
readonly month: readonly MonthRange[];
readonly dayOfWeek: readonly DayOfTheWeekRange[];
}
export {ParserOptions, CronDate, DateType}
export type CronExpression<IsIterable extends boolean = false> = ICronExpression<CronFields, IsIterable>
export type StringResult = IStringResult<CronFields>
export function parseExpression<IsIterable extends boolean = false>(expression: string, options?: ParserOptions<IsIterable>): CronExpression<IsIterable>;
export function fieldsToExpression<IsIterable extends boolean = false>(fields: CronFields, options?: ParserOptions<IsIterable>): CronExpression<IsIterable>;
export function parseFile(filePath: string, callback: (err: any, data: StringResult) => any): void;
export function parseString(data: string): StringResult;

@ -0,0 +1,28 @@
import {
CronDate,
DateType,
ICronExpression,
IStringResult,
ParserOptions,
} from '../common';
export type CronFields = {
readonly second: readonly number[];
readonly minute: readonly number[];
readonly hour: readonly number[];
readonly dayOfMonth: readonly (number | 'L')[];
readonly month: readonly number[];
readonly dayOfWeek: readonly number[];
}
export {ParserOptions, CronDate, DateType}
export type CronExpression<IsIterable extends boolean = false> = ICronExpression<CronFields, IsIterable>
export type StringResult = IStringResult<CronFields>
export function parseExpression<IsIterable extends boolean = false>(expression: string, options?: ParserOptions<IsIterable>): CronExpression<IsIterable>;
export function fieldsToExpression<IsIterable extends boolean = false>(fields: CronFields, options?: ParserOptions<IsIterable>): CronExpression<IsIterable>;
export function parseFile(filePath: string, callback: (err: any, data: StringResult) => any): void;
export function parseString(data: string): StringResult;

@ -0,0 +1,32 @@
# Long timeouts
Long timeout makes it possible to have a timeout or interval that is longer than 24.8 days (2^31-1 milliseconds).
## Usage
```js
var lt = require('long-timeout')
var timeout = lt.setTimeout(function() {
console.log('in 30 days')
}, 1000 * 60 * 60 * 24 * 30)
var interval = lt.setInterval(function() {
console.log('every 30 days')
}, 1000 * 60 * 60 * 24 * 30)
// Clear them
lt.clearTimeout(timeout)
lt.clearInterval(interval)
```
## Install
npm install long-timeout
## Licence
MIT

@ -0,0 +1,23 @@
var lt = require('./')
/*
Timeouts
*/
lt.setTimeout(function() {
console.log('in a long time')
}, Number.MAX_VALUE)
lt.setTimeout(function() {
console.log('2 seconds')
}, 2000)
/*
Intervals
*/
lt.setInterval(function() {
console.log('long interval')
}, Number.MAX_VALUE)
lt.setInterval(function() {
console.log("2 second interval")
}, 2000)

101
node_modules/long-timeout/index.js generated vendored

@ -0,0 +1,101 @@
var TIMEOUT_MAX = 2147483647; // 2^31-1
exports.setTimeout = function(listener, after) {
return new Timeout(listener, after)
}
exports.setInterval = function(listener, after) {
return new Interval(listener, after)
}
exports.clearTimeout = function(timer) {
if (timer) timer.close()
}
exports.clearInterval = exports.clearTimeout
exports.Timeout = Timeout
exports.Interval = Interval
function Timeout(listener, after) {
this.listener = listener
this.after = after
this.unreffed = false
this.start()
}
Timeout.prototype.unref = function() {
if (!this.unreffed) {
this.unreffed = true
this.timeout.unref()
}
}
Timeout.prototype.ref = function() {
if (this.unreffed) {
this.unreffed = false
this.timeout.ref()
}
}
Timeout.prototype.start = function() {
if (this.after <= TIMEOUT_MAX) {
this.timeout = setTimeout(this.listener, this.after)
} else {
var self = this
this.timeout = setTimeout(function() {
self.after -= TIMEOUT_MAX
self.start()
}, TIMEOUT_MAX)
}
if (this.unreffed) {
this.timeout.unref()
}
}
Timeout.prototype.close = function() {
clearTimeout(this.timeout)
}
function Interval(listener, after) {
this.listener = listener
this.after = this.timeLeft = after
this.unreffed = false
this.start()
}
Interval.prototype.unref = function() {
if (!this.unreffed) {
this.unreffed = true
this.timeout.unref()
}
}
Interval.prototype.ref = function() {
if (this.unreffed) {
this.unreffed = false
this.timeout.ref()
}
}
Interval.prototype.start = function() {
var self = this
if (this.timeLeft <= TIMEOUT_MAX) {
this.timeout = setTimeout(function() {
self.listener()
self.timeLeft = self.after
self.start()
}, this.timeLeft)
} else {
this.timeout = setTimeout(function() {
self.timeLeft -= TIMEOUT_MAX
self.start()
}, TIMEOUT_MAX)
}
if (this.unreffed) {
this.timeout.unref()
}
}
Interval.prototype.close = function() {
Timeout.prototype.close.apply(this, arguments)
}

@ -0,0 +1,22 @@
{
"name": "long-timeout",
"version": "0.1.1",
"description": "Long timeout makes it possible to have a timeout or interval that is longer than 24.8 days (2^31-1 milliseconds).",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git://github.com/tellnes/long-timeout.git"
},
"author": "Christian Tellnes <christian@tellnes.no> (http://christian.tellnes.com/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/tellnes/long-timeout/issues"
},
"homepage": "https://github.com/tellnes/long-timeout",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
}
}

7
node_modules/luxon/LICENSE.md generated vendored

@ -0,0 +1,7 @@
Copyright 2019 JS Foundation and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

55
node_modules/luxon/README.md generated vendored

@ -0,0 +1,55 @@
# Luxon
[![MIT License][license-image]][license] [![Build Status][github-action-image]][github-action-url] [![NPM version][npm-version-image]][npm-url] [![Coverage Status][test-coverage-image]][test-coverage-url] [![PRs welcome][contributing-image]][contributing-url]
Luxon is a library for working with dates and times in JavaScript.
```js
DateTime.now().setZone("America/New_York").minus({ weeks: 1 }).endOf("day").toISO();
```
## Upgrading to 3.0
[Guide](https://moment.github.io/luxon/#upgrading)
## Features
* DateTime, Duration, and Interval types.
* Immutable, chainable, unambiguous API.
* Parsing and formatting for common and custom formats.
* Native time zone and Intl support (no locale or tz files).
## Download/install
[Download/install instructions](https://moment.github.io/luxon/#/install)
## Documentation
* [General documentation](https://moment.github.io/luxon/#/?id=luxon)
* [API docs](https://moment.github.io/luxon/api-docs/index.html)
* [Quick tour](https://moment.github.io/luxon/#/tour)
* [For Moment users](https://moment.github.io/luxon/#/moment)
* [Why does Luxon exist?](https://moment.github.io/luxon/#/why)
* [A quick demo](https://moment.github.io/luxon/demo/global.html)
## Development
See [contributing](CONTRIBUTING.md).
![Phasers to stun][phasers-image]
[license-image]: http://img.shields.io/badge/license-MIT-blue.svg
[license]: LICENSE.md
[github-action-image]: https://github.com/moment/luxon/actions/workflows/test.yml/badge.svg
[github-action-url]: https://github.com/moment/luxon/actions/workflows/test.yml
[npm-url]: https://npmjs.org/package/luxon
[npm-version-image]: https://badge.fury.io/js/luxon.svg
[test-coverage-url]: https://codecov.io/gh/moment/luxon
[test-coverage-image]: https://codecov.io/gh/moment/luxon/branch/master/graph/badge.svg
[contributing-url]: https://github.com/moment/luxon/blob/master/CONTRIBUTING.md
[contributing-image]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
[phasers-image]: https://img.shields.io/badge/phasers-stun-brightgreen.svg

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
This contains Luxon's tiny website. Use `npm run site` command to copy it to the build directory.

85
node_modules/luxon/package.json generated vendored

@ -0,0 +1,85 @@
{
"name": "luxon",
"version": "3.3.0",
"description": "Immutable date wrapper",
"author": "Isaac Cambron",
"keywords": [
"date",
"immutable"
],
"repository": "https://github.com/moment/luxon",
"exports": {
".": {
"import": "./src/luxon.js",
"require": "./build/node/luxon.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "babel-node tasks/buildAll.js",
"build-node": "babel-node tasks/buildNode.js",
"build-global": "babel-node tasks/buildGlobal.js",
"jest": "jest",
"test": "jest --coverage",
"api-docs": "mkdir -p build && documentation build src/luxon.js -f html -o build/api-docs && sed -i.bak 's/<\\/body>/<script src=\"\\..\\/global\\/luxon.js\"><\\/script><script>console.log(\"You can try Luxon right here using the `luxon` global, like `luxon.DateTime.now()`\");<\\/script><\\/body>/g' build/api-docs/index.html && rm build/api-docs/index.html.bak",
"copy-site": "mkdir -p build && rsync -a docs/ build/docs && rsync -a site/ build",
"site": "npm run api-docs && npm run copy-site",
"format": "prettier --write 'src/**/*.js' 'test/**/*.js' 'benchmarks/*.js'",
"format-check": "prettier --check 'src/**/*.js' 'test/**/*.js' 'benchmarks/*.js'",
"benchmark": "babel-node benchmarks/index.js",
"codecov": "codecov",
"prepack": "babel-node tasks/buildAll.js",
"prepare": "husky install"
},
"lint-staged": {
"*.{js,json}": [
"prettier --write"
]
},
"devDependencies": {
"@babel/core": "^7.18.6",
"@babel/node": "^7.18.6",
"@babel/plugin-external-helpers": "^7.18.6",
"@babel/preset-env": "^7.18.6",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^19.0.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"babel-jest": "^28.1.2",
"benchmark": "latest",
"codecov": "latest",
"documentation": "latest",
"fs-extra": "^6.0.1",
"husky": "^7.0.0",
"jest": "^29.4.3",
"lint-staged": "^11.0.0",
"prettier": "latest",
"rollup": "^2.52.7",
"rollup-plugin-terser": "^7.0.2",
"uglify-js": "^3.13.10"
},
"main": "build/node/luxon.js",
"module": "src/luxon.js",
"browser": "build/cjs-browser/luxon.js",
"jsdelivr": "build/global/luxon.min.js",
"unpkg": "build/global/luxon.min.js",
"engines": {
"node": ">=12"
},
"files": [
"build/node/luxon.js",
"build/node/luxon.js.map",
"build/cjs-browser/luxon.js",
"build/cjs-browser/luxon.js.map",
"build/amd/luxon.js",
"build/amd/luxon.js.map",
"build/global/luxon.js",
"build/global/luxon.js.map",
"build/global/luxon.min.js",
"build/global/luxon.min.js.map",
"build/es6/luxon.js",
"build/es6/luxon.js.map",
"src"
],
"license": "MIT",
"sideEffects": false
}

2248
node_modules/luxon/src/datetime.js generated vendored

File diff suppressed because it is too large Load Diff

948
node_modules/luxon/src/duration.js generated vendored

@ -0,0 +1,948 @@
import { InvalidArgumentError, InvalidDurationError, InvalidUnitError } from "./errors.js";
import Formatter from "./impl/formatter.js";
import Invalid from "./impl/invalid.js";
import Locale from "./impl/locale.js";
import { parseISODuration, parseISOTimeOnly } from "./impl/regexParser.js";
import {
asNumber,
hasOwnProperty,
isNumber,
isUndefined,
normalizeObject,
roundTo,
} from "./impl/util.js";
import Settings from "./settings.js";
const INVALID = "Invalid Duration";
// unit conversion constants
export const lowOrderMatrix = {
weeks: {
days: 7,
hours: 7 * 24,
minutes: 7 * 24 * 60,
seconds: 7 * 24 * 60 * 60,
milliseconds: 7 * 24 * 60 * 60 * 1000,
},
days: {
hours: 24,
minutes: 24 * 60,
seconds: 24 * 60 * 60,
milliseconds: 24 * 60 * 60 * 1000,
},
hours: { minutes: 60, seconds: 60 * 60, milliseconds: 60 * 60 * 1000 },
minutes: { seconds: 60, milliseconds: 60 * 1000 },
seconds: { milliseconds: 1000 },
},
casualMatrix = {
years: {
quarters: 4,
months: 12,
weeks: 52,
days: 365,
hours: 365 * 24,
minutes: 365 * 24 * 60,
seconds: 365 * 24 * 60 * 60,
milliseconds: 365 * 24 * 60 * 60 * 1000,
},
quarters: {
months: 3,
weeks: 13,
days: 91,
hours: 91 * 24,
minutes: 91 * 24 * 60,
seconds: 91 * 24 * 60 * 60,
milliseconds: 91 * 24 * 60 * 60 * 1000,
},
months: {
weeks: 4,
days: 30,
hours: 30 * 24,
minutes: 30 * 24 * 60,
seconds: 30 * 24 * 60 * 60,
milliseconds: 30 * 24 * 60 * 60 * 1000,
},
...lowOrderMatrix,
},
daysInYearAccurate = 146097.0 / 400,
daysInMonthAccurate = 146097.0 / 4800,
accurateMatrix = {
years: {
quarters: 4,
months: 12,
weeks: daysInYearAccurate / 7,
days: daysInYearAccurate,
hours: daysInYearAccurate * 24,
minutes: daysInYearAccurate * 24 * 60,
seconds: daysInYearAccurate * 24 * 60 * 60,
milliseconds: daysInYearAccurate * 24 * 60 * 60 * 1000,
},
quarters: {
months: 3,
weeks: daysInYearAccurate / 28,
days: daysInYearAccurate / 4,
hours: (daysInYearAccurate * 24) / 4,
minutes: (daysInYearAccurate * 24 * 60) / 4,
seconds: (daysInYearAccurate * 24 * 60 * 60) / 4,
milliseconds: (daysInYearAccurate * 24 * 60 * 60 * 1000) / 4,
},
months: {
weeks: daysInMonthAccurate / 7,
days: daysInMonthAccurate,
hours: daysInMonthAccurate * 24,
minutes: daysInMonthAccurate * 24 * 60,
seconds: daysInMonthAccurate * 24 * 60 * 60,
milliseconds: daysInMonthAccurate * 24 * 60 * 60 * 1000,
},
...lowOrderMatrix,
};
// units ordered by size
const orderedUnits = [
"years",
"quarters",
"months",
"weeks",
"days",
"hours",
"minutes",
"seconds",
"milliseconds",
];
const reverseUnits = orderedUnits.slice(0).reverse();
// clone really means "create another instance just like this one, but with these changes"
function clone(dur, alts, clear = false) {
// deep merge for vals
const conf = {
values: clear ? alts.values : { ...dur.values, ...(alts.values || {}) },
loc: dur.loc.clone(alts.loc),
conversionAccuracy: alts.conversionAccuracy || dur.conversionAccuracy,
matrix: alts.matrix || dur.matrix,
};
return new Duration(conf);
}
function antiTrunc(n) {
return n < 0 ? Math.floor(n) : Math.ceil(n);
}
// NB: mutates parameters
function convert(matrix, fromMap, fromUnit, toMap, toUnit) {
const conv = matrix[toUnit][fromUnit],
raw = fromMap[fromUnit] / conv,
sameSign = Math.sign(raw) === Math.sign(toMap[toUnit]),
// ok, so this is wild, but see the matrix in the tests
added =
!sameSign && toMap[toUnit] !== 0 && Math.abs(raw) <= 1 ? antiTrunc(raw) : Math.trunc(raw);
toMap[toUnit] += added;
fromMap[fromUnit] -= added * conv;
}
// NB: mutates parameters
function normalizeValues(matrix, vals) {
reverseUnits.reduce((previous, current) => {
if (!isUndefined(vals[current])) {
if (previous) {
convert(matrix, vals, previous, vals, current);
}
return current;
} else {
return previous;
}
}, null);
}
// Remove all properties with a value of 0 from an object
function removeZeroes(vals) {
const newVals = {};
for (const [key, value] of Object.entries(vals)) {
if (value !== 0) {
newVals[key] = value;
}
}
return newVals;
}
/**
* A Duration object represents a period of time, like "2 months" or "1 day, 1 hour". Conceptually, it's just a map of units to their quantities, accompanied by some additional configuration and methods for creating, parsing, interrogating, transforming, and formatting them. They can be used on their own or in conjunction with other Luxon types; for example, you can use {@link DateTime#plus} to add a Duration object to a DateTime, producing another DateTime.
*
* Here is a brief overview of commonly used methods and getters in Duration:
*
* * **Creation** To create a Duration, use {@link Duration.fromMillis}, {@link Duration.fromObject}, or {@link Duration.fromISO}.
* * **Unit values** See the {@link Duration#years}, {@link Duration#months}, {@link Duration#weeks}, {@link Duration#days}, {@link Duration#hours}, {@link Duration#minutes}, {@link Duration#seconds}, {@link Duration#milliseconds} accessors.
* * **Configuration** See {@link Duration#locale} and {@link Duration#numberingSystem} accessors.
* * **Transformation** To create new Durations out of old ones use {@link Duration#plus}, {@link Duration#minus}, {@link Duration#normalize}, {@link Duration#set}, {@link Duration#reconfigure}, {@link Duration#shiftTo}, and {@link Duration#negate}.
* * **Output** To convert the Duration into other representations, see {@link Duration#as}, {@link Duration#toISO}, {@link Duration#toFormat}, and {@link Duration#toJSON}
*
* There's are more methods documented below. In addition, for more information on subtler topics like internationalization and validity, see the external documentation.
*/
export default class Duration {
/**
* @private
*/
constructor(config) {
const accurate = config.conversionAccuracy === "longterm" || false;
let matrix = accurate ? accurateMatrix : casualMatrix;
if (config.matrix) {
matrix = config.matrix;
}
/**
* @access private
*/
this.values = config.values;
/**
* @access private
*/
this.loc = config.loc || Locale.create();
/**
* @access private
*/
this.conversionAccuracy = accurate ? "longterm" : "casual";
/**
* @access private
*/
this.invalid = config.invalid || null;
/**
* @access private
*/
this.matrix = matrix;
/**
* @access private
*/
this.isLuxonDuration = true;
}
/**
* Create Duration from a number of milliseconds.
* @param {number} count of milliseconds
* @param {Object} opts - options for parsing
* @param {string} [opts.locale='en-US'] - the locale to use
* @param {string} opts.numberingSystem - the numbering system to use
* @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
* @return {Duration}
*/
static fromMillis(count, opts) {
return Duration.fromObject({ milliseconds: count }, opts);
}
/**
* Create a Duration from a JavaScript object with keys like 'years' and 'hours'.
* If this object is empty then a zero milliseconds duration is returned.
* @param {Object} obj - the object to create the DateTime from
* @param {number} obj.years
* @param {number} obj.quarters
* @param {number} obj.months
* @param {number} obj.weeks
* @param {number} obj.days
* @param {number} obj.hours
* @param {number} obj.minutes
* @param {number} obj.seconds
* @param {number} obj.milliseconds
* @param {Object} [opts=[]] - options for creating this Duration
* @param {string} [opts.locale='en-US'] - the locale to use
* @param {string} opts.numberingSystem - the numbering system to use
* @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use
* @param {string} [opts.matrix=Object] - the custom conversion system to use
* @return {Duration}
*/
static fromObject(obj, opts = {}) {
if (obj == null || typeof obj !== "object") {
throw new InvalidArgumentError(
`Duration.fromObject: argument expected to be an object, got ${
obj === null ? "null" : typeof obj
}`
);
}
return new Duration({
values: normalizeObject(obj, Duration.normalizeUnit),
loc: Locale.fromObject(opts),
conversionAccuracy: opts.conversionAccuracy,
matrix: opts.matrix,
});
}
/**
* Create a Duration from DurationLike.
*
* @param {Object | number | Duration} durationLike
* One of:
* - object with keys like 'years' and 'hours'.
* - number representing milliseconds
* - Duration instance
* @return {Duration}
*/
static fromDurationLike(durationLike) {
if (isNumber(durationLike)) {
return Duration.fromMillis(durationLike);
} else if (Duration.isDuration(durationLike)) {
return durationLike;
} else if (typeof durationLike === "object") {
return Duration.fromObject(durationLike);
} else {
throw new InvalidArgumentError(
`Unknown duration argument ${durationLike} of type ${typeof durationLike}`
);
}
}
/**
* Create a Duration from an ISO 8601 duration string.
* @param {string} text - text to parse
* @param {Object} opts - options for parsing
* @param {string} [opts.locale='en-US'] - the locale to use
* @param {string} opts.numberingSystem - the numbering system to use
* @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use
* @param {string} [opts.matrix=Object] - the preset conversion system to use
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations
* @example Duration.fromISO('P3Y6M1W4DT12H30M5S').toObject() //=> { years: 3, months: 6, weeks: 1, days: 4, hours: 12, minutes: 30, seconds: 5 }
* @example Duration.fromISO('PT23H').toObject() //=> { hours: 23 }
* @example Duration.fromISO('P5Y3M').toObject() //=> { years: 5, months: 3 }
* @return {Duration}
*/
static fromISO(text, opts) {
const [parsed] = parseISODuration(text);
if (parsed) {
return Duration.fromObject(parsed, opts);
} else {
return Duration.invalid("unparsable", `the input "${text}" can't be parsed as ISO 8601`);
}
}
/**
* Create a Duration from an ISO 8601 time string.
* @param {string} text - text to parse
* @param {Object} opts - options for parsing
* @param {string} [opts.locale='en-US'] - the locale to use
* @param {string} opts.numberingSystem - the numbering system to use
* @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use
* @param {string} [opts.matrix=Object] - the conversion system to use
* @see https://en.wikipedia.org/wiki/ISO_8601#Times
* @example Duration.fromISOTime('11:22:33.444').toObject() //=> { hours: 11, minutes: 22, seconds: 33, milliseconds: 444 }
* @example Duration.fromISOTime('11:00').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }
* @example Duration.fromISOTime('T11:00').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }
* @example Duration.fromISOTime('1100').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }
* @example Duration.fromISOTime('T1100').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }
* @return {Duration}
*/
static fromISOTime(text, opts) {
const [parsed] = parseISOTimeOnly(text);
if (parsed) {
return Duration.fromObject(parsed, opts);
} else {
return Duration.invalid("unparsable", `the input "${text}" can't be parsed as ISO 8601`);
}
}
/**
* Create an invalid Duration.
* @param {string} reason - simple string of why this datetime is invalid. Should not contain parameters or anything else data-dependent
* @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information
* @return {Duration}
*/
static invalid(reason, explanation = null) {
if (!reason) {
throw new InvalidArgumentError("need to specify a reason the Duration is invalid");
}
const invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation);
if (Settings.throwOnInvalid) {
throw new InvalidDurationError(invalid);
} else {
return new Duration({ invalid });
}
}
/**
* @private
*/
static normalizeUnit(unit) {
const normalized = {
year: "years",
years: "years",
quarter: "quarters",
quarters: "quarters",
month: "months",
months: "months",
week: "weeks",
weeks: "weeks",
day: "days",
days: "days",
hour: "hours",
hours: "hours",
minute: "minutes",
minutes: "minutes",
second: "seconds",
seconds: "seconds",
millisecond: "milliseconds",
milliseconds: "milliseconds",
}[unit ? unit.toLowerCase() : unit];
if (!normalized) throw new InvalidUnitError(unit);
return normalized;
}
/**
* Check if an object is a Duration. Works across context boundaries
* @param {object} o
* @return {boolean}
*/
static isDuration(o) {
return (o && o.isLuxonDuration) || false;
}
/**
* Get the locale of a Duration, such 'en-GB'
* @type {string}
*/
get locale() {
return this.isValid ? this.loc.locale : null;
}
/**
* Get the numbering system of a Duration, such 'beng'. The numbering system is used when formatting the Duration
*
* @type {string}
*/
get numberingSystem() {
return this.isValid ? this.loc.numberingSystem : null;
}
/**
* Returns a string representation of this Duration formatted according to the specified format string. You may use these tokens:
* * `S` for milliseconds
* * `s` for seconds
* * `m` for minutes
* * `h` for hours
* * `d` for days
* * `w` for weeks
* * `M` for months
* * `y` for years
* Notes:
* * Add padding by repeating the token, e.g. "yy" pads the years to two digits, "hhhh" pads the hours out to four digits
* * Tokens can be escaped by wrapping with single quotes.
* * The duration will be converted to the set of units in the format string using {@link Duration#shiftTo} and the Durations's conversion accuracy setting.
* @param {string} fmt - the format string
* @param {Object} opts - options
* @param {boolean} [opts.floor=true] - floor numerical values
* @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("y d s") //=> "1 6 2"
* @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("yy dd sss") //=> "01 06 002"
* @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("M S") //=> "12 518402000"
* @return {string}
*/
toFormat(fmt, opts = {}) {
// reverse-compat since 1.2; we always round down now, never up, and we do it by default
const fmtOpts = {
...opts,
floor: opts.round !== false && opts.floor !== false,
};
return this.isValid
? Formatter.create(this.loc, fmtOpts).formatDurationFromString(this, fmt)
: INVALID;
}
/**
* Returns a string representation of a Duration with all units included.
* To modify its behavior use the `listStyle` and any Intl.NumberFormat option, though `unitDisplay` is especially relevant.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
* @param opts - On option object to override the formatting. Accepts the same keys as the options parameter of the native `Int.NumberFormat` constructor, as well as `listStyle`.
* @example
* ```js
* var dur = Duration.fromObject({ days: 1, hours: 5, minutes: 6 })
* dur.toHuman() //=> '1 day, 5 hours, 6 minutes'
* dur.toHuman({ listStyle: "long" }) //=> '1 day, 5 hours, and 6 minutes'
* dur.toHuman({ unitDisplay: "short" }) //=> '1 day, 5 hr, 6 min'
* ```
*/
toHuman(opts = {}) {
const l = orderedUnits
.map((unit) => {
const val = this.values[unit];
if (isUndefined(val)) {
return null;
}
return this.loc
.numberFormatter({ style: "unit", unitDisplay: "long", ...opts, unit: unit.slice(0, -1) })
.format(val);
})
.filter((n) => n);
return this.loc
.listFormatter({ type: "conjunction", style: opts.listStyle || "narrow", ...opts })
.format(l);
}
/**
* Returns a JavaScript object with this Duration's values.
* @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toObject() //=> { years: 1, days: 6, seconds: 2 }
* @return {Object}
*/
toObject() {
if (!this.isValid) return {};
return { ...this.values };
}
/**
* Returns an ISO 8601-compliant string representation of this Duration.
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations
* @example Duration.fromObject({ years: 3, seconds: 45 }).toISO() //=> 'P3YT45S'
* @example Duration.fromObject({ months: 4, seconds: 45 }).toISO() //=> 'P4MT45S'
* @example Duration.fromObject({ months: 5 }).toISO() //=> 'P5M'
* @example Duration.fromObject({ minutes: 5 }).toISO() //=> 'PT5M'
* @example Duration.fromObject({ milliseconds: 6 }).toISO() //=> 'PT0.006S'
* @return {string}
*/
toISO() {
// we could use the formatter, but this is an easier way to get the minimum string
if (!this.isValid) return null;
let s = "P";
if (this.years !== 0) s += this.years + "Y";
if (this.months !== 0 || this.quarters !== 0) s += this.months + this.quarters * 3 + "M";
if (this.weeks !== 0) s += this.weeks + "W";
if (this.days !== 0) s += this.days + "D";
if (this.hours !== 0 || this.minutes !== 0 || this.seconds !== 0 || this.milliseconds !== 0)
s += "T";
if (this.hours !== 0) s += this.hours + "H";
if (this.minutes !== 0) s += this.minutes + "M";
if (this.seconds !== 0 || this.milliseconds !== 0)
// this will handle "floating point madness" by removing extra decimal places
// https://stackoverflow.com/questions/588004/is-floating-point-math-broken
s += roundTo(this.seconds + this.milliseconds / 1000, 3) + "S";
if (s === "P") s += "T0S";
return s;
}
/**
* Returns an ISO 8601-compliant string representation of this Duration, formatted as a time of day.
* Note that this will return null if the duration is invalid, negative, or equal to or greater than 24 hours.
* @see https://en.wikipedia.org/wiki/ISO_8601#Times
* @param {Object} opts - options
* @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0
* @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0
* @param {boolean} [opts.includePrefix=false] - include the `T` prefix
* @param {string} [opts.format='extended'] - choose between the basic and extended format
* @example Duration.fromObject({ hours: 11 }).toISOTime() //=> '11:00:00.000'
* @example Duration.fromObject({ hours: 11 }).toISOTime({ suppressMilliseconds: true }) //=> '11:00:00'
* @example Duration.fromObject({ hours: 11 }).toISOTime({ suppressSeconds: true }) //=> '11:00'
* @example Duration.fromObject({ hours: 11 }).toISOTime({ includePrefix: true }) //=> 'T11:00:00.000'
* @example Duration.fromObject({ hours: 11 }).toISOTime({ format: 'basic' }) //=> '110000.000'
* @return {string}
*/
toISOTime(opts = {}) {
if (!this.isValid) return null;
const millis = this.toMillis();
if (millis < 0 || millis >= 86400000) return null;
opts = {
suppressMilliseconds: false,
suppressSeconds: false,
includePrefix: false,
format: "extended",
...opts,
};
const value = this.shiftTo("hours", "minutes", "seconds", "milliseconds");
let fmt = opts.format === "basic" ? "hhmm" : "hh:mm";
if (!opts.suppressSeconds || value.seconds !== 0 || value.milliseconds !== 0) {
fmt += opts.format === "basic" ? "ss" : ":ss";
if (!opts.suppressMilliseconds || value.milliseconds !== 0) {
fmt += ".SSS";
}
}
let str = value.toFormat(fmt);
if (opts.includePrefix) {
str = "T" + str;
}
return str;
}
/**
* Returns an ISO 8601 representation of this Duration appropriate for use in JSON.
* @return {string}
*/
toJSON() {
return this.toISO();
}
/**
* Returns an ISO 8601 representation of this Duration appropriate for use in debugging.
* @return {string}
*/
toString() {
return this.toISO();
}
/**
* Returns an milliseconds value of this Duration.
* @return {number}
*/
toMillis() {
return this.as("milliseconds");
}
/**
* Returns an milliseconds value of this Duration. Alias of {@link toMillis}
* @return {number}
*/
valueOf() {
return this.toMillis();
}
/**
* Make this Duration longer by the specified amount. Return a newly-constructed Duration.
* @param {Duration|Object|number} duration - The amount to add. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()
* @return {Duration}
*/
plus(duration) {
if (!this.isValid) return this;
const dur = Duration.fromDurationLike(duration),
result = {};
for (const k of orderedUnits) {
if (hasOwnProperty(dur.values, k) || hasOwnProperty(this.values, k)) {
result[k] = dur.get(k) + this.get(k);
}
}
return clone(this, { values: result }, true);
}
/**
* Make this Duration shorter by the specified amount. Return a newly-constructed Duration.
* @param {Duration|Object|number} duration - The amount to subtract. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()
* @return {Duration}
*/
minus(duration) {
if (!this.isValid) return this;
const dur = Duration.fromDurationLike(duration);
return this.plus(dur.negate());
}
/**
* Scale this Duration by the specified amount. Return a newly-constructed Duration.
* @param {function} fn - The function to apply to each unit. Arity is 1 or 2: the value of the unit and, optionally, the unit name. Must return a number.
* @example Duration.fromObject({ hours: 1, minutes: 30 }).mapUnits(x => x * 2) //=> { hours: 2, minutes: 60 }
* @example Duration.fromObject({ hours: 1, minutes: 30 }).mapUnits((x, u) => u === "hours" ? x * 2 : x) //=> { hours: 2, minutes: 30 }
* @return {Duration}
*/
mapUnits(fn) {
if (!this.isValid) return this;
const result = {};
for (const k of Object.keys(this.values)) {
result[k] = asNumber(fn(this.values[k], k));
}
return clone(this, { values: result }, true);
}
/**
* Get the value of unit.
* @param {string} unit - a unit such as 'minute' or 'day'
* @example Duration.fromObject({years: 2, days: 3}).get('years') //=> 2
* @example Duration.fromObject({years: 2, days: 3}).get('months') //=> 0
* @example Duration.fromObject({years: 2, days: 3}).get('days') //=> 3
* @return {number}
*/
get(unit) {
return this[Duration.normalizeUnit(unit)];
}
/**
* "Set" the values of specified units. Return a newly-constructed Duration.
* @param {Object} values - a mapping of units to numbers
* @example dur.set({ years: 2017 })
* @example dur.set({ hours: 8, minutes: 30 })
* @return {Duration}
*/
set(values) {
if (!this.isValid) return this;
const mixed = { ...this.values, ...normalizeObject(values, Duration.normalizeUnit) };
return clone(this, { values: mixed });
}
/**
* "Set" the locale and/or numberingSystem. Returns a newly-constructed Duration.
* @example dur.reconfigure({ locale: 'en-GB' })
* @return {Duration}
*/
reconfigure({ locale, numberingSystem, conversionAccuracy, matrix } = {}) {
const loc = this.loc.clone({ locale, numberingSystem });
const opts = { loc, matrix, conversionAccuracy };
return clone(this, opts);
}
/**
* Return the length of the duration in the specified unit.
* @param {string} unit - a unit such as 'minutes' or 'days'
* @example Duration.fromObject({years: 1}).as('days') //=> 365
* @example Duration.fromObject({years: 1}).as('months') //=> 12
* @example Duration.fromObject({hours: 60}).as('days') //=> 2.5
* @return {number}
*/
as(unit) {
return this.isValid ? this.shiftTo(unit).get(unit) : NaN;
}
/**
* Reduce this Duration to its canonical representation in its current units.
* @example Duration.fromObject({ years: 2, days: 5000 }).normalize().toObject() //=> { years: 15, days: 255 }
* @example Duration.fromObject({ hours: 12, minutes: -45 }).normalize().toObject() //=> { hours: 11, minutes: 15 }
* @return {Duration}
*/
normalize() {
if (!this.isValid) return this;
const vals = this.toObject();
normalizeValues(this.matrix, vals);
return clone(this, { values: vals }, true);
}
/**
* Rescale units to its largest representation
* @example Duration.fromObject({ milliseconds: 90000 }).rescale().toObject() //=> { minutes: 1, seconds: 30 }
* @return {Duration}
*/
rescale() {
if (!this.isValid) return this;
const vals = removeZeroes(this.normalize().shiftToAll().toObject());
return clone(this, { values: vals }, true);
}
/**
* Convert this Duration into its representation in a different set of units.
* @example Duration.fromObject({ hours: 1, seconds: 30 }).shiftTo('minutes', 'milliseconds').toObject() //=> { minutes: 60, milliseconds: 30000 }
* @return {Duration}
*/
shiftTo(...units) {
if (!this.isValid) return this;
if (units.length === 0) {
return this;
}
units = units.map((u) => Duration.normalizeUnit(u));
const built = {},
accumulated = {},
vals = this.toObject();
let lastUnit;
for (const k of orderedUnits) {
if (units.indexOf(k) >= 0) {
lastUnit = k;
let own = 0;
// anything we haven't boiled down yet should get boiled to this unit
for (const ak in accumulated) {
own += this.matrix[ak][k] * accumulated[ak];
accumulated[ak] = 0;
}
// plus anything that's already in this unit
if (isNumber(vals[k])) {
own += vals[k];
}
const i = Math.trunc(own);
built[k] = i;
accumulated[k] = (own * 1000 - i * 1000) / 1000;
// plus anything further down the chain that should be rolled up in to this
for (const down in vals) {
if (orderedUnits.indexOf(down) > orderedUnits.indexOf(k)) {
convert(this.matrix, vals, down, built, k);
}
}
// otherwise, keep it in the wings to boil it later
} else if (isNumber(vals[k])) {
accumulated[k] = vals[k];
}
}
// anything leftover becomes the decimal for the last unit
// lastUnit must be defined since units is not empty
for (const key in accumulated) {
if (accumulated[key] !== 0) {
built[lastUnit] +=
key === lastUnit ? accumulated[key] : accumulated[key] / this.matrix[lastUnit][key];
}
}
return clone(this, { values: built }, true).normalize();
}
/**
* Shift this Duration to all available units.
* Same as shiftTo("years", "months", "weeks", "days", "hours", "minutes", "seconds", "milliseconds")
* @return {Duration}
*/
shiftToAll() {
if (!this.isValid) return this;
return this.shiftTo(
"years",
"months",
"weeks",
"days",
"hours",
"minutes",
"seconds",
"milliseconds"
);
}
/**
* Return the negative of this Duration.
* @example Duration.fromObject({ hours: 1, seconds: 30 }).negate().toObject() //=> { hours: -1, seconds: -30 }
* @return {Duration}
*/
negate() {
if (!this.isValid) return this;
const negated = {};
for (const k of Object.keys(this.values)) {
negated[k] = this.values[k] === 0 ? 0 : -this.values[k];
}
return clone(this, { values: negated }, true);
}
/**
* Get the years.
* @type {number}
*/
get years() {
return this.isValid ? this.values.years || 0 : NaN;
}
/**
* Get the quarters.
* @type {number}
*/
get quarters() {
return this.isValid ? this.values.quarters || 0 : NaN;
}
/**
* Get the months.
* @type {number}
*/
get months() {
return this.isValid ? this.values.months || 0 : NaN;
}
/**
* Get the weeks
* @type {number}
*/
get weeks() {
return this.isValid ? this.values.weeks || 0 : NaN;
}
/**
* Get the days.
* @type {number}
*/
get days() {
return this.isValid ? this.values.days || 0 : NaN;
}
/**
* Get the hours.
* @type {number}
*/
get hours() {
return this.isValid ? this.values.hours || 0 : NaN;
}
/**
* Get the minutes.
* @type {number}
*/
get minutes() {
return this.isValid ? this.values.minutes || 0 : NaN;
}
/**
* Get the seconds.
* @return {number}
*/
get seconds() {
return this.isValid ? this.values.seconds || 0 : NaN;
}
/**
* Get the milliseconds.
* @return {number}
*/
get milliseconds() {
return this.isValid ? this.values.milliseconds || 0 : NaN;
}
/**
* Returns whether the Duration is invalid. Invalid durations are returned by diff operations
* on invalid DateTimes or Intervals.
* @return {boolean}
*/
get isValid() {
return this.invalid === null;
}
/**
* Returns an error code if this Duration became invalid, or null if the Duration is valid
* @return {string}
*/
get invalidReason() {
return this.invalid ? this.invalid.reason : null;
}
/**
* Returns an explanation of why this Duration became invalid, or null if the Duration is valid
* @type {string}
*/
get invalidExplanation() {
return this.invalid ? this.invalid.explanation : null;
}
/**
* Equality check
* Two Durations are equal iff they have the same units and the same values for each unit.
* @param {Duration} other
* @return {boolean}
*/
equals(other) {
if (!this.isValid || !other.isValid) {
return false;
}
if (!this.loc.equals(other.loc)) {
return false;
}
function eq(v1, v2) {
// Consider 0 and undefined as equal
if (v1 === undefined || v1 === 0) return v2 === undefined || v2 === 0;
return v1 === v2;
}
for (const u of orderedUnits) {
if (!eq(this.values[u], other.values[u])) {
return false;
}
}
return true;
}
}

61
node_modules/luxon/src/errors.js generated vendored

@ -0,0 +1,61 @@
// these aren't really private, but nor are they really useful to document
/**
* @private
*/
class LuxonError extends Error {}
/**
* @private
*/
export class InvalidDateTimeError extends LuxonError {
constructor(reason) {
super(`Invalid DateTime: ${reason.toMessage()}`);
}
}
/**
* @private
*/
export class InvalidIntervalError extends LuxonError {
constructor(reason) {
super(`Invalid Interval: ${reason.toMessage()}`);
}
}
/**
* @private
*/
export class InvalidDurationError extends LuxonError {
constructor(reason) {
super(`Invalid Duration: ${reason.toMessage()}`);
}
}
/**
* @private
*/
export class ConflictingSpecificationError extends LuxonError {}
/**
* @private
*/
export class InvalidUnitError extends LuxonError {
constructor(unit) {
super(`Invalid unit ${unit}`);
}
}
/**
* @private
*/
export class InvalidArgumentError extends LuxonError {}
/**
* @private
*/
export class ZoneIsAbstractError extends LuxonError {
constructor() {
super("Zone is an abstract class");
}
}

@ -0,0 +1,161 @@
import {
integerBetween,
isLeapYear,
timeObject,
daysInYear,
daysInMonth,
weeksInWeekYear,
isInteger,
} from "./util.js";
import Invalid from "./invalid.js";
const nonLeapLadder = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334],
leapLadder = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];
function unitOutOfRange(unit, value) {
return new Invalid(
"unit out of range",
`you specified ${value} (of type ${typeof value}) as a ${unit}, which is invalid`
);
}
function dayOfWeek(year, month, day) {
const d = new Date(Date.UTC(year, month - 1, day));
if (year < 100 && year >= 0) {
d.setUTCFullYear(d.getUTCFullYear() - 1900);
}
const js = d.getUTCDay();
return js === 0 ? 7 : js;
}
function computeOrdinal(year, month, day) {
return day + (isLeapYear(year) ? leapLadder : nonLeapLadder)[month - 1];
}
function uncomputeOrdinal(year, ordinal) {
const table = isLeapYear(year) ? leapLadder : nonLeapLadder,
month0 = table.findIndex((i) => i < ordinal),
day = ordinal - table[month0];
return { month: month0 + 1, day };
}
/**
* @private
*/
export function gregorianToWeek(gregObj) {
const { year, month, day } = gregObj,
ordinal = computeOrdinal(year, month, day),
weekday = dayOfWeek(year, month, day);
let weekNumber = Math.floor((ordinal - weekday + 10) / 7),
weekYear;
if (weekNumber < 1) {
weekYear = year - 1;
weekNumber = weeksInWeekYear(weekYear);
} else if (weekNumber > weeksInWeekYear(year)) {
weekYear = year + 1;
weekNumber = 1;
} else {
weekYear = year;
}
return { weekYear, weekNumber, weekday, ...timeObject(gregObj) };
}
export function weekToGregorian(weekData) {
const { weekYear, weekNumber, weekday } = weekData,
weekdayOfJan4 = dayOfWeek(weekYear, 1, 4),
yearInDays = daysInYear(weekYear);
let ordinal = weekNumber * 7 + weekday - weekdayOfJan4 - 3,
year;
if (ordinal < 1) {
year = weekYear - 1;
ordinal += daysInYear(year);
} else if (ordinal > yearInDays) {
year = weekYear + 1;
ordinal -= daysInYear(weekYear);
} else {
year = weekYear;
}
const { month, day } = uncomputeOrdinal(year, ordinal);
return { year, month, day, ...timeObject(weekData) };
}
export function gregorianToOrdinal(gregData) {
const { year, month, day } = gregData;
const ordinal = computeOrdinal(year, month, day);
return { year, ordinal, ...timeObject(gregData) };
}
export function ordinalToGregorian(ordinalData) {
const { year, ordinal } = ordinalData;
const { month, day } = uncomputeOrdinal(year, ordinal);
return { year, month, day, ...timeObject(ordinalData) };
}
export function hasInvalidWeekData(obj) {
const validYear = isInteger(obj.weekYear),
validWeek = integerBetween(obj.weekNumber, 1, weeksInWeekYear(obj.weekYear)),
validWeekday = integerBetween(obj.weekday, 1, 7);
if (!validYear) {
return unitOutOfRange("weekYear", obj.weekYear);
} else if (!validWeek) {
return unitOutOfRange("week", obj.week);
} else if (!validWeekday) {
return unitOutOfRange("weekday", obj.weekday);
} else return false;
}
export function hasInvalidOrdinalData(obj) {
const validYear = isInteger(obj.year),
validOrdinal = integerBetween(obj.ordinal, 1, daysInYear(obj.year));
if (!validYear) {
return unitOutOfRange("year", obj.year);
} else if (!validOrdinal) {
return unitOutOfRange("ordinal", obj.ordinal);
} else return false;
}
export function hasInvalidGregorianData(obj) {
const validYear = isInteger(obj.year),
validMonth = integerBetween(obj.month, 1, 12),
validDay = integerBetween(obj.day, 1, daysInMonth(obj.year, obj.month));
if (!validYear) {
return unitOutOfRange("year", obj.year);
} else if (!validMonth) {
return unitOutOfRange("month", obj.month);
} else if (!validDay) {
return unitOutOfRange("day", obj.day);
} else return false;
}
export function hasInvalidTimeData(obj) {
const { hour, minute, second, millisecond } = obj;
const validHour =
integerBetween(hour, 0, 23) ||
(hour === 24 && minute === 0 && second === 0 && millisecond === 0),
validMinute = integerBetween(minute, 0, 59),
validSecond = integerBetween(second, 0, 59),
validMillisecond = integerBetween(millisecond, 0, 999);
if (!validHour) {
return unitOutOfRange("hour", hour);
} else if (!validMinute) {
return unitOutOfRange("minute", minute);
} else if (!validSecond) {
return unitOutOfRange("second", second);
} else if (!validMillisecond) {
return unitOutOfRange("millisecond", millisecond);
} else return false;
}

@ -0,0 +1,75 @@
import Duration from "../duration.js";
function dayDiff(earlier, later) {
const utcDayStart = (dt) => dt.toUTC(0, { keepLocalTime: true }).startOf("day").valueOf(),
ms = utcDayStart(later) - utcDayStart(earlier);
return Math.floor(Duration.fromMillis(ms).as("days"));
}
function highOrderDiffs(cursor, later, units) {
const differs = [
["years", (a, b) => b.year - a.year],
["quarters", (a, b) => b.quarter - a.quarter + (b.year - a.year) * 4],
["months", (a, b) => b.month - a.month + (b.year - a.year) * 12],
[
"weeks",
(a, b) => {
const days = dayDiff(a, b);
return (days - (days % 7)) / 7;
},
],
["days", dayDiff],
];
const results = {};
const earlier = cursor;
let lowestOrder, highWater;
for (const [unit, differ] of differs) {
if (units.indexOf(unit) >= 0) {
lowestOrder = unit;
results[unit] = differ(cursor, later);
highWater = earlier.plus(results);
if (highWater > later) {
results[unit]--;
cursor = earlier.plus(results);
} else {
cursor = highWater;
}
}
}
return [cursor, results, highWater, lowestOrder];
}
export default function (earlier, later, units, opts) {
let [cursor, results, highWater, lowestOrder] = highOrderDiffs(earlier, later, units);
const remainingMillis = later - cursor;
const lowerOrderUnits = units.filter(
(u) => ["hours", "minutes", "seconds", "milliseconds"].indexOf(u) >= 0
);
if (lowerOrderUnits.length === 0) {
if (highWater < later) {
highWater = cursor.plus({ [lowestOrder]: 1 });
}
if (highWater !== cursor) {
results[lowestOrder] = (results[lowestOrder] || 0) + remainingMillis / (highWater - cursor);
}
}
const duration = Duration.fromObject(results, opts);
if (lowerOrderUnits.length > 0) {
return Duration.fromMillis(remainingMillis, opts)
.shiftTo(...lowerOrderUnits)
.plus(duration);
} else {
return duration;
}
}

@ -0,0 +1,75 @@
const numberingSystems = {
arab: "[\u0660-\u0669]",
arabext: "[\u06F0-\u06F9]",
bali: "[\u1B50-\u1B59]",
beng: "[\u09E6-\u09EF]",
deva: "[\u0966-\u096F]",
fullwide: "[\uFF10-\uFF19]",
gujr: "[\u0AE6-\u0AEF]",
hanidec: "[|一|二|三|四|五|六|七|八|九]",
khmr: "[\u17E0-\u17E9]",
knda: "[\u0CE6-\u0CEF]",
laoo: "[\u0ED0-\u0ED9]",
limb: "[\u1946-\u194F]",
mlym: "[\u0D66-\u0D6F]",
mong: "[\u1810-\u1819]",
mymr: "[\u1040-\u1049]",
orya: "[\u0B66-\u0B6F]",
tamldec: "[\u0BE6-\u0BEF]",
telu: "[\u0C66-\u0C6F]",
thai: "[\u0E50-\u0E59]",
tibt: "[\u0F20-\u0F29]",
latn: "\\d",
};
const numberingSystemsUTF16 = {
arab: [1632, 1641],
arabext: [1776, 1785],
bali: [6992, 7001],
beng: [2534, 2543],
deva: [2406, 2415],
fullwide: [65296, 65303],
gujr: [2790, 2799],
khmr: [6112, 6121],
knda: [3302, 3311],
laoo: [3792, 3801],
limb: [6470, 6479],
mlym: [3430, 3439],
mong: [6160, 6169],
mymr: [4160, 4169],
orya: [2918, 2927],
tamldec: [3046, 3055],
telu: [3174, 3183],
thai: [3664, 3673],
tibt: [3872, 3881],
};
const hanidecChars = numberingSystems.hanidec.replace(/[\[|\]]/g, "").split("");
export function parseDigits(str) {
let value = parseInt(str, 10);
if (isNaN(value)) {
value = "";
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
if (str[i].search(numberingSystems.hanidec) !== -1) {
value += hanidecChars.indexOf(str[i]);
} else {
for (const key in numberingSystemsUTF16) {
const [min, max] = numberingSystemsUTF16[key];
if (code >= min && code <= max) {
value += code - min;
}
}
}
}
return parseInt(value, 10);
} else {
return value;
}
}
export function digitRegex({ numberingSystem }, append = "") {
return new RegExp(`${numberingSystems[numberingSystem || "latn"]}${append}`);
}

@ -0,0 +1,233 @@
import * as Formats from "./formats.js";
import { pick } from "./util.js";
function stringify(obj) {
return JSON.stringify(obj, Object.keys(obj).sort());
}
/**
* @private
*/
export const monthsLong = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
export const monthsShort = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
export const monthsNarrow = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"];
export function months(length) {
switch (length) {
case "narrow":
return [...monthsNarrow];
case "short":
return [...monthsShort];
case "long":
return [...monthsLong];
case "numeric":
return ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"];
case "2-digit":
return ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"];
default:
return null;
}
}
export const weekdaysLong = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];
export const weekdaysShort = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
export const weekdaysNarrow = ["M", "T", "W", "T", "F", "S", "S"];
export function weekdays(length) {
switch (length) {
case "narrow":
return [...weekdaysNarrow];
case "short":
return [...weekdaysShort];
case "long":
return [...weekdaysLong];
case "numeric":
return ["1", "2", "3", "4", "5", "6", "7"];
default:
return null;
}
}
export const meridiems = ["AM", "PM"];
export const erasLong = ["Before Christ", "Anno Domini"];
export const erasShort = ["BC", "AD"];
export const erasNarrow = ["B", "A"];
export function eras(length) {
switch (length) {
case "narrow":
return [...erasNarrow];
case "short":
return [...erasShort];
case "long":
return [...erasLong];
default:
return null;
}
}
export function meridiemForDateTime(dt) {
return meridiems[dt.hour < 12 ? 0 : 1];
}
export function weekdayForDateTime(dt, length) {
return weekdays(length)[dt.weekday - 1];
}
export function monthForDateTime(dt, length) {
return months(length)[dt.month - 1];
}
export function eraForDateTime(dt, length) {
return eras(length)[dt.year < 0 ? 0 : 1];
}
export function formatRelativeTime(unit, count, numeric = "always", narrow = false) {
const units = {
years: ["year", "yr."],
quarters: ["quarter", "qtr."],
months: ["month", "mo."],
weeks: ["week", "wk."],
days: ["day", "day", "days"],
hours: ["hour", "hr."],
minutes: ["minute", "min."],
seconds: ["second", "sec."],
};
const lastable = ["hours", "minutes", "seconds"].indexOf(unit) === -1;
if (numeric === "auto" && lastable) {
const isDay = unit === "days";
switch (count) {
case 1:
return isDay ? "tomorrow" : `next ${units[unit][0]}`;
case -1:
return isDay ? "yesterday" : `last ${units[unit][0]}`;
case 0:
return isDay ? "today" : `this ${units[unit][0]}`;
default: // fall through
}
}
const isInPast = Object.is(count, -0) || count < 0,
fmtValue = Math.abs(count),
singular = fmtValue === 1,
lilUnits = units[unit],
fmtUnit = narrow
? singular
? lilUnits[1]
: lilUnits[2] || lilUnits[1]
: singular
? units[unit][0]
: unit;
return isInPast ? `${fmtValue} ${fmtUnit} ago` : `in ${fmtValue} ${fmtUnit}`;
}
export function formatString(knownFormat) {
// these all have the offsets removed because we don't have access to them
// without all the intl stuff this is backfilling
const filtered = pick(knownFormat, [
"weekday",
"era",
"year",
"month",
"day",
"hour",
"minute",
"second",
"timeZoneName",
"hourCycle",
]),
key = stringify(filtered),
dateTimeHuge = "EEEE, LLLL d, yyyy, h:mm a";
switch (key) {
case stringify(Formats.DATE_SHORT):
return "M/d/yyyy";
case stringify(Formats.DATE_MED):
return "LLL d, yyyy";
case stringify(Formats.DATE_MED_WITH_WEEKDAY):
return "EEE, LLL d, yyyy";
case stringify(Formats.DATE_FULL):
return "LLLL d, yyyy";
case stringify(Formats.DATE_HUGE):
return "EEEE, LLLL d, yyyy";
case stringify(Formats.TIME_SIMPLE):
return "h:mm a";
case stringify(Formats.TIME_WITH_SECONDS):
return "h:mm:ss a";
case stringify(Formats.TIME_WITH_SHORT_OFFSET):
return "h:mm a";
case stringify(Formats.TIME_WITH_LONG_OFFSET):
return "h:mm a";
case stringify(Formats.TIME_24_SIMPLE):
return "HH:mm";
case stringify(Formats.TIME_24_WITH_SECONDS):
return "HH:mm:ss";
case stringify(Formats.TIME_24_WITH_SHORT_OFFSET):
return "HH:mm";
case stringify(Formats.TIME_24_WITH_LONG_OFFSET):
return "HH:mm";
case stringify(Formats.DATETIME_SHORT):
return "M/d/yyyy, h:mm a";
case stringify(Formats.DATETIME_MED):
return "LLL d, yyyy, h:mm a";
case stringify(Formats.DATETIME_FULL):
return "LLLL d, yyyy, h:mm a";
case stringify(Formats.DATETIME_HUGE):
return dateTimeHuge;
case stringify(Formats.DATETIME_SHORT_WITH_SECONDS):
return "M/d/yyyy, h:mm:ss a";
case stringify(Formats.DATETIME_MED_WITH_SECONDS):
return "LLL d, yyyy, h:mm:ss a";
case stringify(Formats.DATETIME_MED_WITH_WEEKDAY):
return "EEE, d LLL yyyy, h:mm a";
case stringify(Formats.DATETIME_FULL_WITH_SECONDS):
return "LLLL d, yyyy, h:mm:ss a";
case stringify(Formats.DATETIME_HUGE_WITH_SECONDS):
return "EEEE, LLLL d, yyyy, h:mm:ss a";
default:
return dateTimeHuge;
}
}

@ -0,0 +1,176 @@
/**
* @private
*/
const n = "numeric",
s = "short",
l = "long";
export const DATE_SHORT = {
year: n,
month: n,
day: n,
};
export const DATE_MED = {
year: n,
month: s,
day: n,
};
export const DATE_MED_WITH_WEEKDAY = {
year: n,
month: s,
day: n,
weekday: s,
};
export const DATE_FULL = {
year: n,
month: l,
day: n,
};
export const DATE_HUGE = {
year: n,
month: l,
day: n,
weekday: l,
};
export const TIME_SIMPLE = {
hour: n,
minute: n,
};
export const TIME_WITH_SECONDS = {
hour: n,
minute: n,
second: n,
};
export const TIME_WITH_SHORT_OFFSET = {
hour: n,
minute: n,
second: n,
timeZoneName: s,
};
export const TIME_WITH_LONG_OFFSET = {
hour: n,
minute: n,
second: n,
timeZoneName: l,
};
export const TIME_24_SIMPLE = {
hour: n,
minute: n,
hourCycle: "h23",
};
export const TIME_24_WITH_SECONDS = {
hour: n,
minute: n,
second: n,
hourCycle: "h23",
};
export const TIME_24_WITH_SHORT_OFFSET = {
hour: n,
minute: n,
second: n,
hourCycle: "h23",
timeZoneName: s,
};
export const TIME_24_WITH_LONG_OFFSET = {
hour: n,
minute: n,
second: n,
hourCycle: "h23",
timeZoneName: l,
};
export const DATETIME_SHORT = {
year: n,
month: n,
day: n,
hour: n,
minute: n,
};
export const DATETIME_SHORT_WITH_SECONDS = {
year: n,
month: n,
day: n,
hour: n,
minute: n,
second: n,
};
export const DATETIME_MED = {
year: n,
month: s,
day: n,
hour: n,
minute: n,
};
export const DATETIME_MED_WITH_SECONDS = {
year: n,
month: s,
day: n,
hour: n,
minute: n,
second: n,
};
export const DATETIME_MED_WITH_WEEKDAY = {
year: n,
month: s,
day: n,
weekday: s,
hour: n,
minute: n,
};
export const DATETIME_FULL = {
year: n,
month: l,
day: n,
hour: n,
minute: n,
timeZoneName: s,
};
export const DATETIME_FULL_WITH_SECONDS = {
year: n,
month: l,
day: n,
hour: n,
minute: n,
second: n,
timeZoneName: s,
};
export const DATETIME_HUGE = {
year: n,
month: l,
day: n,
weekday: l,
hour: n,
minute: n,
timeZoneName: l,
};
export const DATETIME_HUGE_WITH_SECONDS = {
year: n,
month: l,
day: n,
weekday: l,
hour: n,
minute: n,
second: n,
timeZoneName: l,
};

@ -0,0 +1,400 @@
import * as English from "./english.js";
import * as Formats from "./formats.js";
import { padStart } from "./util.js";
function stringifyTokens(splits, tokenToString) {
let s = "";
for (const token of splits) {
if (token.literal) {
s += token.val;
} else {
s += tokenToString(token.val);
}
}
return s;
}
const macroTokenToFormatOpts = {
D: Formats.DATE_SHORT,
DD: Formats.DATE_MED,
DDD: Formats.DATE_FULL,
DDDD: Formats.DATE_HUGE,
t: Formats.TIME_SIMPLE,
tt: Formats.TIME_WITH_SECONDS,
ttt: Formats.TIME_WITH_SHORT_OFFSET,
tttt: Formats.TIME_WITH_LONG_OFFSET,
T: Formats.TIME_24_SIMPLE,
TT: Formats.TIME_24_WITH_SECONDS,
TTT: Formats.TIME_24_WITH_SHORT_OFFSET,
TTTT: Formats.TIME_24_WITH_LONG_OFFSET,
f: Formats.DATETIME_SHORT,
ff: Formats.DATETIME_MED,
fff: Formats.DATETIME_FULL,
ffff: Formats.DATETIME_HUGE,
F: Formats.DATETIME_SHORT_WITH_SECONDS,
FF: Formats.DATETIME_MED_WITH_SECONDS,
FFF: Formats.DATETIME_FULL_WITH_SECONDS,
FFFF: Formats.DATETIME_HUGE_WITH_SECONDS,
};
/**
* @private
*/
export default class Formatter {
static create(locale, opts = {}) {
return new Formatter(locale, opts);
}
static parseFormat(fmt) {
// white-space is always considered a literal in user-provided formats
// the " " token has a special meaning (see unitForToken)
let current = null,
currentFull = "",
bracketed = false;
const splits = [];
for (let i = 0; i < fmt.length; i++) {
const c = fmt.charAt(i);
if (c === "'") {
if (currentFull.length > 0) {
splits.push({ literal: bracketed || /^\s+$/.test(currentFull), val: currentFull });
}
current = null;
currentFull = "";
bracketed = !bracketed;
} else if (bracketed) {
currentFull += c;
} else if (c === current) {
currentFull += c;
} else {
if (currentFull.length > 0) {
splits.push({ literal: /^\s+$/.test(currentFull), val: currentFull });
}
currentFull = c;
current = c;
}
}
if (currentFull.length > 0) {
splits.push({ literal: bracketed || /^\s+$/.test(currentFull), val: currentFull });
}
return splits;
}
static macroTokenToFormatOpts(token) {
return macroTokenToFormatOpts[token];
}
constructor(locale, formatOpts) {
this.opts = formatOpts;
this.loc = locale;
this.systemLoc = null;
}
formatWithSystemDefault(dt, opts) {
if (this.systemLoc === null) {
this.systemLoc = this.loc.redefaultToSystem();
}
const df = this.systemLoc.dtFormatter(dt, { ...this.opts, ...opts });
return df.format();
}
formatDateTime(dt, opts = {}) {
const df = this.loc.dtFormatter(dt, { ...this.opts, ...opts });
return df.format();
}
formatDateTimeParts(dt, opts = {}) {
const df = this.loc.dtFormatter(dt, { ...this.opts, ...opts });
return df.formatToParts();
}
formatInterval(interval, opts = {}) {
const df = this.loc.dtFormatter(interval.start, { ...this.opts, ...opts });
return df.dtf.formatRange(interval.start.toJSDate(), interval.end.toJSDate());
}
resolvedOptions(dt, opts = {}) {
const df = this.loc.dtFormatter(dt, { ...this.opts, ...opts });
return df.resolvedOptions();
}
num(n, p = 0) {
// we get some perf out of doing this here, annoyingly
if (this.opts.forceSimple) {
return padStart(n, p);
}
const opts = { ...this.opts };
if (p > 0) {
opts.padTo = p;
}
return this.loc.numberFormatter(opts).format(n);
}
formatDateTimeFromString(dt, fmt) {
const knownEnglish = this.loc.listingMode() === "en",
useDateTimeFormatter = this.loc.outputCalendar && this.loc.outputCalendar !== "gregory",
string = (opts, extract) => this.loc.extract(dt, opts, extract),
formatOffset = (opts) => {
if (dt.isOffsetFixed && dt.offset === 0 && opts.allowZ) {
return "Z";
}
return dt.isValid ? dt.zone.formatOffset(dt.ts, opts.format) : "";
},
meridiem = () =>
knownEnglish
? English.meridiemForDateTime(dt)
: string({ hour: "numeric", hourCycle: "h12" }, "dayperiod"),
month = (length, standalone) =>
knownEnglish
? English.monthForDateTime(dt, length)
: string(standalone ? { month: length } : { month: length, day: "numeric" }, "month"),
weekday = (length, standalone) =>
knownEnglish
? English.weekdayForDateTime(dt, length)
: string(
standalone ? { weekday: length } : { weekday: length, month: "long", day: "numeric" },
"weekday"
),
maybeMacro = (token) => {
const formatOpts = Formatter.macroTokenToFormatOpts(token);
if (formatOpts) {
return this.formatWithSystemDefault(dt, formatOpts);
} else {
return token;
}
},
era = (length) =>
knownEnglish ? English.eraForDateTime(dt, length) : string({ era: length }, "era"),
tokenToString = (token) => {
// Where possible: http://cldr.unicode.org/translation/date-time-1/date-time#TOC-Standalone-vs.-Format-Styles
switch (token) {
// ms
case "S":
return this.num(dt.millisecond);
case "u":
// falls through
case "SSS":
return this.num(dt.millisecond, 3);
// seconds
case "s":
return this.num(dt.second);
case "ss":
return this.num(dt.second, 2);
// fractional seconds
case "uu":
return this.num(Math.floor(dt.millisecond / 10), 2);
case "uuu":
return this.num(Math.floor(dt.millisecond / 100));
// minutes
case "m":
return this.num(dt.minute);
case "mm":
return this.num(dt.minute, 2);
// hours
case "h":
return this.num(dt.hour % 12 === 0 ? 12 : dt.hour % 12);
case "hh":
return this.num(dt.hour % 12 === 0 ? 12 : dt.hour % 12, 2);
case "H":
return this.num(dt.hour);
case "HH":
return this.num(dt.hour, 2);
// offset
case "Z":
// like +6
return formatOffset({ format: "narrow", allowZ: this.opts.allowZ });
case "ZZ":
// like +06:00
return formatOffset({ format: "short", allowZ: this.opts.allowZ });
case "ZZZ":
// like +0600
return formatOffset({ format: "techie", allowZ: this.opts.allowZ });
case "ZZZZ":
// like EST
return dt.zone.offsetName(dt.ts, { format: "short", locale: this.loc.locale });
case "ZZZZZ":
// like Eastern Standard Time
return dt.zone.offsetName(dt.ts, { format: "long", locale: this.loc.locale });
// zone
case "z":
// like America/New_York
return dt.zoneName;
// meridiems
case "a":
return meridiem();
// dates
case "d":
return useDateTimeFormatter ? string({ day: "numeric" }, "day") : this.num(dt.day);
case "dd":
return useDateTimeFormatter ? string({ day: "2-digit" }, "day") : this.num(dt.day, 2);
// weekdays - standalone
case "c":
// like 1
return this.num(dt.weekday);
case "ccc":
// like 'Tues'
return weekday("short", true);
case "cccc":
// like 'Tuesday'
return weekday("long", true);
case "ccccc":
// like 'T'
return weekday("narrow", true);
// weekdays - format
case "E":
// like 1
return this.num(dt.weekday);
case "EEE":
// like 'Tues'
return weekday("short", false);
case "EEEE":
// like 'Tuesday'
return weekday("long", false);
case "EEEEE":
// like 'T'
return weekday("narrow", false);
// months - standalone
case "L":
// like 1
return useDateTimeFormatter
? string({ month: "numeric", day: "numeric" }, "month")
: this.num(dt.month);
case "LL":
// like 01, doesn't seem to work
return useDateTimeFormatter
? string({ month: "2-digit", day: "numeric" }, "month")
: this.num(dt.month, 2);
case "LLL":
// like Jan
return month("short", true);
case "LLLL":
// like January
return month("long", true);
case "LLLLL":
// like J
return month("narrow", true);
// months - format
case "M":
// like 1
return useDateTimeFormatter
? string({ month: "numeric" }, "month")
: this.num(dt.month);
case "MM":
// like 01
return useDateTimeFormatter
? string({ month: "2-digit" }, "month")
: this.num(dt.month, 2);
case "MMM":
// like Jan
return month("short", false);
case "MMMM":
// like January
return month("long", false);
case "MMMMM":
// like J
return month("narrow", false);
// years
case "y":
// like 2014
return useDateTimeFormatter ? string({ year: "numeric" }, "year") : this.num(dt.year);
case "yy":
// like 14
return useDateTimeFormatter
? string({ year: "2-digit" }, "year")
: this.num(dt.year.toString().slice(-2), 2);
case "yyyy":
// like 0012
return useDateTimeFormatter
? string({ year: "numeric" }, "year")
: this.num(dt.year, 4);
case "yyyyyy":
// like 000012
return useDateTimeFormatter
? string({ year: "numeric" }, "year")
: this.num(dt.year, 6);
// eras
case "G":
// like AD
return era("short");
case "GG":
// like Anno Domini
return era("long");
case "GGGGG":
return era("narrow");
case "kk":
return this.num(dt.weekYear.toString().slice(-2), 2);
case "kkkk":
return this.num(dt.weekYear, 4);
case "W":
return this.num(dt.weekNumber);
case "WW":
return this.num(dt.weekNumber, 2);
case "o":
return this.num(dt.ordinal);
case "ooo":
return this.num(dt.ordinal, 3);
case "q":
// like 1
return this.num(dt.quarter);
case "qq":
// like 01
return this.num(dt.quarter, 2);
case "X":
return this.num(Math.floor(dt.ts / 1000));
case "x":
return this.num(dt.ts);
default:
return maybeMacro(token);
}
};
return stringifyTokens(Formatter.parseFormat(fmt), tokenToString);
}
formatDurationFromString(dur, fmt) {
const tokenToField = (token) => {
switch (token[0]) {
case "S":
return "millisecond";
case "s":
return "second";
case "m":
return "minute";
case "h":
return "hour";
case "d":
return "day";
case "w":
return "week";
case "M":
return "month";
case "y":
return "year";
default:
return null;
}
},
tokenToString = (lildur) => (token) => {
const mapped = tokenToField(token);
if (mapped) {
return this.num(lildur.get(mapped), token.length);
} else {
return token;
}
},
tokens = Formatter.parseFormat(fmt),
realTokens = tokens.reduce(
(found, { literal, val }) => (literal ? found : found.concat(val)),
[]
),
collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter((t) => t));
return stringifyTokens(tokens, tokenToString(collapsed));
}
}

@ -0,0 +1,14 @@
export default class Invalid {
constructor(reason, explanation) {
this.reason = reason;
this.explanation = explanation;
}
toMessage() {
if (this.explanation) {
return `${this.reason}: ${this.explanation}`;
} else {
return this.reason;
}
}
}

@ -0,0 +1,494 @@
import { padStart, roundTo, hasRelative, formatOffset } from "./util.js";
import * as English from "./english.js";
import Settings from "../settings.js";
import DateTime from "../datetime.js";
import IANAZone from "../zones/IANAZone.js";
// todo - remap caching
let intlLFCache = {};
function getCachedLF(locString, opts = {}) {
const key = JSON.stringify([locString, opts]);
let dtf = intlLFCache[key];
if (!dtf) {
dtf = new Intl.ListFormat(locString, opts);
intlLFCache[key] = dtf;
}
return dtf;
}
let intlDTCache = {};
function getCachedDTF(locString, opts = {}) {
const key = JSON.stringify([locString, opts]);
let dtf = intlDTCache[key];
if (!dtf) {
dtf = new Intl.DateTimeFormat(locString, opts);
intlDTCache[key] = dtf;
}
return dtf;
}
let intlNumCache = {};
function getCachedINF(locString, opts = {}) {
const key = JSON.stringify([locString, opts]);
let inf = intlNumCache[key];
if (!inf) {
inf = new Intl.NumberFormat(locString, opts);
intlNumCache[key] = inf;
}
return inf;
}
let intlRelCache = {};
function getCachedRTF(locString, opts = {}) {
const { base, ...cacheKeyOpts } = opts; // exclude `base` from the options
const key = JSON.stringify([locString, cacheKeyOpts]);
let inf = intlRelCache[key];
if (!inf) {
inf = new Intl.RelativeTimeFormat(locString, opts);
intlRelCache[key] = inf;
}
return inf;
}
let sysLocaleCache = null;
function systemLocale() {
if (sysLocaleCache) {
return sysLocaleCache;
} else {
sysLocaleCache = new Intl.DateTimeFormat().resolvedOptions().locale;
return sysLocaleCache;
}
}
function parseLocaleString(localeStr) {
// I really want to avoid writing a BCP 47 parser
// see, e.g. https://github.com/wooorm/bcp-47
// Instead, we'll do this:
// a) if the string has no -u extensions, just leave it alone
// b) if it does, use Intl to resolve everything
// c) if Intl fails, try again without the -u
// private subtags and unicode subtags have ordering requirements,
// and we're not properly parsing this, so just strip out the
// private ones if they exist.
const xIndex = localeStr.indexOf("-x-");
if (xIndex !== -1) {
localeStr = localeStr.substring(0, xIndex);
}
const uIndex = localeStr.indexOf("-u-");
if (uIndex === -1) {
return [localeStr];
} else {
let options;
let selectedStr;
try {
options = getCachedDTF(localeStr).resolvedOptions();
selectedStr = localeStr;
} catch (e) {
const smaller = localeStr.substring(0, uIndex);
options = getCachedDTF(smaller).resolvedOptions();
selectedStr = smaller;
}
const { numberingSystem, calendar } = options;
return [selectedStr, numberingSystem, calendar];
}
}
function intlConfigString(localeStr, numberingSystem, outputCalendar) {
if (outputCalendar || numberingSystem) {
if (!localeStr.includes("-u-")) {
localeStr += "-u";
}
if (outputCalendar) {
localeStr += `-ca-${outputCalendar}`;
}
if (numberingSystem) {
localeStr += `-nu-${numberingSystem}`;
}
return localeStr;
} else {
return localeStr;
}
}
function mapMonths(f) {
const ms = [];
for (let i = 1; i <= 12; i++) {
const dt = DateTime.utc(2016, i, 1);
ms.push(f(dt));
}
return ms;
}
function mapWeekdays(f) {
const ms = [];
for (let i = 1; i <= 7; i++) {
const dt = DateTime.utc(2016, 11, 13 + i);
ms.push(f(dt));
}
return ms;
}
function listStuff(loc, length, defaultOK, englishFn, intlFn) {
const mode = loc.listingMode(defaultOK);
if (mode === "error") {
return null;
} else if (mode === "en") {
return englishFn(length);
} else {
return intlFn(length);
}
}
function supportsFastNumbers(loc) {
if (loc.numberingSystem && loc.numberingSystem !== "latn") {
return false;
} else {
return (
loc.numberingSystem === "latn" ||
!loc.locale ||
loc.locale.startsWith("en") ||
new Intl.DateTimeFormat(loc.intl).resolvedOptions().numberingSystem === "latn"
);
}
}
/**
* @private
*/
class PolyNumberFormatter {
constructor(intl, forceSimple, opts) {
this.padTo = opts.padTo || 0;
this.floor = opts.floor || false;
const { padTo, floor, ...otherOpts } = opts;
if (!forceSimple || Object.keys(otherOpts).length > 0) {
const intlOpts = { useGrouping: false, ...opts };
if (opts.padTo > 0) intlOpts.minimumIntegerDigits = opts.padTo;
this.inf = getCachedINF(intl, intlOpts);
}
}
format(i) {
if (this.inf) {
const fixed = this.floor ? Math.floor(i) : i;
return this.inf.format(fixed);
} else {
// to match the browser's numberformatter defaults
const fixed = this.floor ? Math.floor(i) : roundTo(i, 3);
return padStart(fixed, this.padTo);
}
}
}
/**
* @private
*/
class PolyDateFormatter {
constructor(dt, intl, opts) {
this.opts = opts;
this.originalZone = undefined;
let z = undefined;
if (this.opts.timeZone) {
// Don't apply any workarounds if a timeZone is explicitly provided in opts
this.dt = dt;
} else if (dt.zone.type === "fixed") {
// UTC-8 or Etc/UTC-8 are not part of tzdata, only Etc/GMT+8 and the like.
// That is why fixed-offset TZ is set to that unless it is:
// 1. Representing offset 0 when UTC is used to maintain previous behavior and does not become GMT.
// 2. Unsupported by the browser:
// - some do not support Etc/
// - < Etc/GMT-14, > Etc/GMT+12, and 30-minute or 45-minute offsets are not part of tzdata
const gmtOffset = -1 * (dt.offset / 60);
const offsetZ = gmtOffset >= 0 ? `Etc/GMT+${gmtOffset}` : `Etc/GMT${gmtOffset}`;
if (dt.offset !== 0 && IANAZone.create(offsetZ).valid) {
z = offsetZ;
this.dt = dt;
} else {
// Not all fixed-offset zones like Etc/+4:30 are present in tzdata so
// we manually apply the offset and substitute the zone as needed.
z = "UTC";
this.dt = dt.offset === 0 ? dt : dt.setZone("UTC").plus({ minutes: dt.offset });
this.originalZone = dt.zone;
}
} else if (dt.zone.type === "system") {
this.dt = dt;
} else if (dt.zone.type === "iana") {
this.dt = dt;
z = dt.zone.name;
} else {
// Custom zones can have any offset / offsetName so we just manually
// apply the offset and substitute the zone as needed.
z = "UTC";
this.dt = dt.setZone("UTC").plus({ minutes: dt.offset });
this.originalZone = dt.zone;
}
const intlOpts = { ...this.opts };
intlOpts.timeZone = intlOpts.timeZone || z;
this.dtf = getCachedDTF(intl, intlOpts);
}
format() {
if (this.originalZone) {
// If we have to substitute in the actual zone name, we have to use
// formatToParts so that the timezone can be replaced.
return this.formatToParts()
.map(({ value }) => value)
.join("");
}
return this.dtf.format(this.dt.toJSDate());
}
formatToParts() {
const parts = this.dtf.formatToParts(this.dt.toJSDate());
if (this.originalZone) {
return parts.map((part) => {
if (part.type === "timeZoneName") {
const offsetName = this.originalZone.offsetName(this.dt.ts, {
locale: this.dt.locale,
format: this.opts.timeZoneName,
});
return {
...part,
value: offsetName,
};
} else {
return part;
}
});
}
return parts;
}
resolvedOptions() {
return this.dtf.resolvedOptions();
}
}
/**
* @private
*/
class PolyRelFormatter {
constructor(intl, isEnglish, opts) {
this.opts = { style: "long", ...opts };
if (!isEnglish && hasRelative()) {
this.rtf = getCachedRTF(intl, opts);
}
}
format(count, unit) {
if (this.rtf) {
return this.rtf.format(count, unit);
} else {
return English.formatRelativeTime(unit, count, this.opts.numeric, this.opts.style !== "long");
}
}
formatToParts(count, unit) {
if (this.rtf) {
return this.rtf.formatToParts(count, unit);
} else {
return [];
}
}
}
/**
* @private
*/
export default class Locale {
static fromOpts(opts) {
return Locale.create(opts.locale, opts.numberingSystem, opts.outputCalendar, opts.defaultToEN);
}
static create(locale, numberingSystem, outputCalendar, defaultToEN = false) {
const specifiedLocale = locale || Settings.defaultLocale;
// the system locale is useful for human readable strings but annoying for parsing/formatting known formats
const localeR = specifiedLocale || (defaultToEN ? "en-US" : systemLocale());
const numberingSystemR = numberingSystem || Settings.defaultNumberingSystem;
const outputCalendarR = outputCalendar || Settings.defaultOutputCalendar;
return new Locale(localeR, numberingSystemR, outputCalendarR, specifiedLocale);
}
static resetCache() {
sysLocaleCache = null;
intlDTCache = {};
intlNumCache = {};
intlRelCache = {};
}
static fromObject({ locale, numberingSystem, outputCalendar } = {}) {
return Locale.create(locale, numberingSystem, outputCalendar);
}
constructor(locale, numbering, outputCalendar, specifiedLocale) {
const [parsedLocale, parsedNumberingSystem, parsedOutputCalendar] = parseLocaleString(locale);
this.locale = parsedLocale;
this.numberingSystem = numbering || parsedNumberingSystem || null;
this.outputCalendar = outputCalendar || parsedOutputCalendar || null;
this.intl = intlConfigString(this.locale, this.numberingSystem, this.outputCalendar);
this.weekdaysCache = { format: {}, standalone: {} };
this.monthsCache = { format: {}, standalone: {} };
this.meridiemCache = null;
this.eraCache = {};
this.specifiedLocale = specifiedLocale;
this.fastNumbersCached = null;
}
get fastNumbers() {
if (this.fastNumbersCached == null) {
this.fastNumbersCached = supportsFastNumbers(this);
}
return this.fastNumbersCached;
}
listingMode() {
const isActuallyEn = this.isEnglish();
const hasNoWeirdness =
(this.numberingSystem === null || this.numberingSystem === "latn") &&
(this.outputCalendar === null || this.outputCalendar === "gregory");
return isActuallyEn && hasNoWeirdness ? "en" : "intl";
}
clone(alts) {
if (!alts || Object.getOwnPropertyNames(alts).length === 0) {
return this;
} else {
return Locale.create(
alts.locale || this.specifiedLocale,
alts.numberingSystem || this.numberingSystem,
alts.outputCalendar || this.outputCalendar,
alts.defaultToEN || false
);
}
}
redefaultToEN(alts = {}) {
return this.clone({ ...alts, defaultToEN: true });
}
redefaultToSystem(alts = {}) {
return this.clone({ ...alts, defaultToEN: false });
}
months(length, format = false, defaultOK = true) {
return listStuff(this, length, defaultOK, English.months, () => {
const intl = format ? { month: length, day: "numeric" } : { month: length },
formatStr = format ? "format" : "standalone";
if (!this.monthsCache[formatStr][length]) {
this.monthsCache[formatStr][length] = mapMonths((dt) => this.extract(dt, intl, "month"));
}
return this.monthsCache[formatStr][length];
});
}
weekdays(length, format = false, defaultOK = true) {
return listStuff(this, length, defaultOK, English.weekdays, () => {
const intl = format
? { weekday: length, year: "numeric", month: "long", day: "numeric" }
: { weekday: length },
formatStr = format ? "format" : "standalone";
if (!this.weekdaysCache[formatStr][length]) {
this.weekdaysCache[formatStr][length] = mapWeekdays((dt) =>
this.extract(dt, intl, "weekday")
);
}
return this.weekdaysCache[formatStr][length];
});
}
meridiems(defaultOK = true) {
return listStuff(
this,
undefined,
defaultOK,
() => English.meridiems,
() => {
// In theory there could be aribitrary day periods. We're gonna assume there are exactly two
// for AM and PM. This is probably wrong, but it's makes parsing way easier.
if (!this.meridiemCache) {
const intl = { hour: "numeric", hourCycle: "h12" };
this.meridiemCache = [DateTime.utc(2016, 11, 13, 9), DateTime.utc(2016, 11, 13, 19)].map(
(dt) => this.extract(dt, intl, "dayperiod")
);
}
return this.meridiemCache;
}
);
}
eras(length, defaultOK = true) {
return listStuff(this, length, defaultOK, English.eras, () => {
const intl = { era: length };
// This is problematic. Different calendars are going to define eras totally differently. What I need is the minimum set of dates
// to definitely enumerate them.
if (!this.eraCache[length]) {
this.eraCache[length] = [DateTime.utc(-40, 1, 1), DateTime.utc(2017, 1, 1)].map((dt) =>
this.extract(dt, intl, "era")
);
}
return this.eraCache[length];
});
}
extract(dt, intlOpts, field) {
const df = this.dtFormatter(dt, intlOpts),
results = df.formatToParts(),
matching = results.find((m) => m.type.toLowerCase() === field);
return matching ? matching.value : null;
}
numberFormatter(opts = {}) {
// this forcesimple option is never used (the only caller short-circuits on it, but it seems safer to leave)
// (in contrast, the rest of the condition is used heavily)
return new PolyNumberFormatter(this.intl, opts.forceSimple || this.fastNumbers, opts);
}
dtFormatter(dt, intlOpts = {}) {
return new PolyDateFormatter(dt, this.intl, intlOpts);
}
relFormatter(opts = {}) {
return new PolyRelFormatter(this.intl, this.isEnglish(), opts);
}
listFormatter(opts = {}) {
return getCachedLF(this.intl, opts);
}
isEnglish() {
return (
this.locale === "en" ||
this.locale.toLowerCase() === "en-us" ||
new Intl.DateTimeFormat(this.intl).resolvedOptions().locale.startsWith("en-us")
);
}
equals(other) {
return (
this.locale === other.locale &&
this.numberingSystem === other.numberingSystem &&
this.outputCalendar === other.outputCalendar
);
}
}

@ -0,0 +1,335 @@
import {
untruncateYear,
signedOffset,
parseInteger,
parseMillis,
isUndefined,
parseFloating,
} from "./util.js";
import * as English from "./english.js";
import FixedOffsetZone from "../zones/fixedOffsetZone.js";
import IANAZone from "../zones/IANAZone.js";
/*
* This file handles parsing for well-specified formats. Here's how it works:
* Two things go into parsing: a regex to match with and an extractor to take apart the groups in the match.
* An extractor is just a function that takes a regex match array and returns a { year: ..., month: ... } object
* parse() does the work of executing the regex and applying the extractor. It takes multiple regex/extractor pairs to try in sequence.
* Extractors can take a "cursor" representing the offset in the match to look at. This makes it easy to combine extractors.
* combineExtractors() does the work of combining them, keeping track of the cursor through multiple extractions.
* Some extractions are super dumb and simpleParse and fromStrings help DRY them.
*/
const ianaRegex = /[A-Za-z_+-]{1,256}(?::?\/[A-Za-z0-9_+-]{1,256}(?:\/[A-Za-z0-9_+-]{1,256})?)?/;
function combineRegexes(...regexes) {
const full = regexes.reduce((f, r) => f + r.source, "");
return RegExp(`^${full}$`);
}
function combineExtractors(...extractors) {
return (m) =>
extractors
.reduce(
([mergedVals, mergedZone, cursor], ex) => {
const [val, zone, next] = ex(m, cursor);
return [{ ...mergedVals, ...val }, zone || mergedZone, next];
},
[{}, null, 1]
)
.slice(0, 2);
}
function parse(s, ...patterns) {
if (s == null) {
return [null, null];
}
for (const [regex, extractor] of patterns) {
const m = regex.exec(s);
if (m) {
return extractor(m);
}
}
return [null, null];
}
function simpleParse(...keys) {
return (match, cursor) => {
const ret = {};
let i;
for (i = 0; i < keys.length; i++) {
ret[keys[i]] = parseInteger(match[cursor + i]);
}
return [ret, null, cursor + i];
};
}
// ISO and SQL parsing
const offsetRegex = /(?:(Z)|([+-]\d\d)(?::?(\d\d))?)/;
const isoExtendedZone = `(?:${offsetRegex.source}?(?:\\[(${ianaRegex.source})\\])?)?`;
const isoTimeBaseRegex = /(\d\d)(?::?(\d\d)(?::?(\d\d)(?:[.,](\d{1,30}))?)?)?/;
const isoTimeRegex = RegExp(`${isoTimeBaseRegex.source}${isoExtendedZone}`);
const isoTimeExtensionRegex = RegExp(`(?:T${isoTimeRegex.source})?`);
const isoYmdRegex = /([+-]\d{6}|\d{4})(?:-?(\d\d)(?:-?(\d\d))?)?/;
const isoWeekRegex = /(\d{4})-?W(\d\d)(?:-?(\d))?/;
const isoOrdinalRegex = /(\d{4})-?(\d{3})/;
const extractISOWeekData = simpleParse("weekYear", "weekNumber", "weekDay");
const extractISOOrdinalData = simpleParse("year", "ordinal");
const sqlYmdRegex = /(\d{4})-(\d\d)-(\d\d)/; // dumbed-down version of the ISO one
const sqlTimeRegex = RegExp(
`${isoTimeBaseRegex.source} ?(?:${offsetRegex.source}|(${ianaRegex.source}))?`
);
const sqlTimeExtensionRegex = RegExp(`(?: ${sqlTimeRegex.source})?`);
function int(match, pos, fallback) {
const m = match[pos];
return isUndefined(m) ? fallback : parseInteger(m);
}
function extractISOYmd(match, cursor) {
const item = {
year: int(match, cursor),
month: int(match, cursor + 1, 1),
day: int(match, cursor + 2, 1),
};
return [item, null, cursor + 3];
}
function extractISOTime(match, cursor) {
const item = {
hours: int(match, cursor, 0),
minutes: int(match, cursor + 1, 0),
seconds: int(match, cursor + 2, 0),
milliseconds: parseMillis(match[cursor + 3]),
};
return [item, null, cursor + 4];
}
function extractISOOffset(match, cursor) {
const local = !match[cursor] && !match[cursor + 1],
fullOffset = signedOffset(match[cursor + 1], match[cursor + 2]),
zone = local ? null : FixedOffsetZone.instance(fullOffset);
return [{}, zone, cursor + 3];
}
function extractIANAZone(match, cursor) {
const zone = match[cursor] ? IANAZone.create(match[cursor]) : null;
return [{}, zone, cursor + 1];
}
// ISO time parsing
const isoTimeOnly = RegExp(`^T?${isoTimeBaseRegex.source}$`);
// ISO duration parsing
const isoDuration =
/^-?P(?:(?:(-?\d{1,20}(?:\.\d{1,20})?)Y)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20}(?:\.\d{1,20})?)W)?(?:(-?\d{1,20}(?:\.\d{1,20})?)D)?(?:T(?:(-?\d{1,20}(?:\.\d{1,20})?)H)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20})(?:[.,](-?\d{1,20}))?S)?)?)$/;
function extractISODuration(match) {
const [s, yearStr, monthStr, weekStr, dayStr, hourStr, minuteStr, secondStr, millisecondsStr] =
match;
const hasNegativePrefix = s[0] === "-";
const negativeSeconds = secondStr && secondStr[0] === "-";
const maybeNegate = (num, force = false) =>
num !== undefined && (force || (num && hasNegativePrefix)) ? -num : num;
return [
{
years: maybeNegate(parseFloating(yearStr)),
months: maybeNegate(parseFloating(monthStr)),
weeks: maybeNegate(parseFloating(weekStr)),
days: maybeNegate(parseFloating(dayStr)),
hours: maybeNegate(parseFloating(hourStr)),
minutes: maybeNegate(parseFloating(minuteStr)),
seconds: maybeNegate(parseFloating(secondStr), secondStr === "-0"),
milliseconds: maybeNegate(parseMillis(millisecondsStr), negativeSeconds),
},
];
}
// These are a little braindead. EDT *should* tell us that we're in, say, America/New_York
// and not just that we're in -240 *right now*. But since I don't think these are used that often
// I'm just going to ignore that
const obsOffsets = {
GMT: 0,
EDT: -4 * 60,
EST: -5 * 60,
CDT: -5 * 60,
CST: -6 * 60,
MDT: -6 * 60,
MST: -7 * 60,
PDT: -7 * 60,
PST: -8 * 60,
};
function fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr) {
const result = {
year: yearStr.length === 2 ? untruncateYear(parseInteger(yearStr)) : parseInteger(yearStr),
month: English.monthsShort.indexOf(monthStr) + 1,
day: parseInteger(dayStr),
hour: parseInteger(hourStr),
minute: parseInteger(minuteStr),
};
if (secondStr) result.second = parseInteger(secondStr);
if (weekdayStr) {
result.weekday =
weekdayStr.length > 3
? English.weekdaysLong.indexOf(weekdayStr) + 1
: English.weekdaysShort.indexOf(weekdayStr) + 1;
}
return result;
}
// RFC 2822/5322
const rfc2822 =
/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|(?:([+-]\d\d)(\d\d)))$/;
function extractRFC2822(match) {
const [
,
weekdayStr,
dayStr,
monthStr,
yearStr,
hourStr,
minuteStr,
secondStr,
obsOffset,
milOffset,
offHourStr,
offMinuteStr,
] = match,
result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr);
let offset;
if (obsOffset) {
offset = obsOffsets[obsOffset];
} else if (milOffset) {
offset = 0;
} else {
offset = signedOffset(offHourStr, offMinuteStr);
}
return [result, new FixedOffsetZone(offset)];
}
function preprocessRFC2822(s) {
// Remove comments and folding whitespace and replace multiple-spaces with a single space
return s
.replace(/\([^()]*\)|[\n\t]/g, " ")
.replace(/(\s\s+)/g, " ")
.trim();
}
// http date
const rfc1123 =
/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/,
rfc850 =
/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/,
ascii =
/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d\d) (\d\d):(\d\d):(\d\d) (\d{4})$/;
function extractRFC1123Or850(match) {
const [, weekdayStr, dayStr, monthStr, yearStr, hourStr, minuteStr, secondStr] = match,
result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr);
return [result, FixedOffsetZone.utcInstance];
}
function extractASCII(match) {
const [, weekdayStr, monthStr, dayStr, hourStr, minuteStr, secondStr, yearStr] = match,
result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr);
return [result, FixedOffsetZone.utcInstance];
}
const isoYmdWithTimeExtensionRegex = combineRegexes(isoYmdRegex, isoTimeExtensionRegex);
const isoWeekWithTimeExtensionRegex = combineRegexes(isoWeekRegex, isoTimeExtensionRegex);
const isoOrdinalWithTimeExtensionRegex = combineRegexes(isoOrdinalRegex, isoTimeExtensionRegex);
const isoTimeCombinedRegex = combineRegexes(isoTimeRegex);
const extractISOYmdTimeAndOffset = combineExtractors(
extractISOYmd,
extractISOTime,
extractISOOffset,
extractIANAZone
);
const extractISOWeekTimeAndOffset = combineExtractors(
extractISOWeekData,
extractISOTime,
extractISOOffset,
extractIANAZone
);
const extractISOOrdinalDateAndTime = combineExtractors(
extractISOOrdinalData,
extractISOTime,
extractISOOffset,
extractIANAZone
);
const extractISOTimeAndOffset = combineExtractors(
extractISOTime,
extractISOOffset,
extractIANAZone
);
/*
* @private
*/
export function parseISODate(s) {
return parse(
s,
[isoYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset],
[isoWeekWithTimeExtensionRegex, extractISOWeekTimeAndOffset],
[isoOrdinalWithTimeExtensionRegex, extractISOOrdinalDateAndTime],
[isoTimeCombinedRegex, extractISOTimeAndOffset]
);
}
export function parseRFC2822Date(s) {
return parse(preprocessRFC2822(s), [rfc2822, extractRFC2822]);
}
export function parseHTTPDate(s) {
return parse(
s,
[rfc1123, extractRFC1123Or850],
[rfc850, extractRFC1123Or850],
[ascii, extractASCII]
);
}
export function parseISODuration(s) {
return parse(s, [isoDuration, extractISODuration]);
}
const extractISOTimeOnly = combineExtractors(extractISOTime);
export function parseISOTimeOnly(s) {
return parse(s, [isoTimeOnly, extractISOTimeOnly]);
}
const sqlYmdWithTimeExtensionRegex = combineRegexes(sqlYmdRegex, sqlTimeExtensionRegex);
const sqlTimeCombinedRegex = combineRegexes(sqlTimeRegex);
const extractISOTimeOffsetAndIANAZone = combineExtractors(
extractISOTime,
extractISOOffset,
extractIANAZone
);
export function parseSQL(s) {
return parse(
s,
[sqlYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset],
[sqlTimeCombinedRegex, extractISOTimeOffsetAndIANAZone]
);
}

@ -0,0 +1,444 @@
import { parseMillis, isUndefined, untruncateYear, signedOffset, hasOwnProperty } from "./util.js";
import Formatter from "./formatter.js";
import FixedOffsetZone from "../zones/fixedOffsetZone.js";
import IANAZone from "../zones/IANAZone.js";
import DateTime from "../datetime.js";
import { digitRegex, parseDigits } from "./digits.js";
import { ConflictingSpecificationError } from "../errors.js";
const MISSING_FTP = "missing Intl.DateTimeFormat.formatToParts support";
function intUnit(regex, post = (i) => i) {
return { regex, deser: ([s]) => post(parseDigits(s)) };
}
const NBSP = String.fromCharCode(160);
const spaceOrNBSP = `[ ${NBSP}]`;
const spaceOrNBSPRegExp = new RegExp(spaceOrNBSP, "g");
function fixListRegex(s) {
// make dots optional and also make them literal
// make space and non breakable space characters interchangeable
return s.replace(/\./g, "\\.?").replace(spaceOrNBSPRegExp, spaceOrNBSP);
}
function stripInsensitivities(s) {
return s
.replace(/\./g, "") // ignore dots that were made optional
.replace(spaceOrNBSPRegExp, " ") // interchange space and nbsp
.toLowerCase();
}
function oneOf(strings, startIndex) {
if (strings === null) {
return null;
} else {
return {
regex: RegExp(strings.map(fixListRegex).join("|")),
deser: ([s]) =>
strings.findIndex((i) => stripInsensitivities(s) === stripInsensitivities(i)) + startIndex,
};
}
}
function offset(regex, groups) {
return { regex, deser: ([, h, m]) => signedOffset(h, m), groups };
}
function simple(regex) {
return { regex, deser: ([s]) => s };
}
function escapeToken(value) {
return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
}
function unitForToken(token, loc) {
const one = digitRegex(loc),
two = digitRegex(loc, "{2}"),
three = digitRegex(loc, "{3}"),
four = digitRegex(loc, "{4}"),
six = digitRegex(loc, "{6}"),
oneOrTwo = digitRegex(loc, "{1,2}"),
oneToThree = digitRegex(loc, "{1,3}"),
oneToSix = digitRegex(loc, "{1,6}"),
oneToNine = digitRegex(loc, "{1,9}"),
twoToFour = digitRegex(loc, "{2,4}"),
fourToSix = digitRegex(loc, "{4,6}"),
literal = (t) => ({ regex: RegExp(escapeToken(t.val)), deser: ([s]) => s, literal: true }),
unitate = (t) => {
if (token.literal) {
return literal(t);
}
switch (t.val) {
// era
case "G":
return oneOf(loc.eras("short", false), 0);
case "GG":
return oneOf(loc.eras("long", false), 0);
// years
case "y":
return intUnit(oneToSix);
case "yy":
return intUnit(twoToFour, untruncateYear);
case "yyyy":
return intUnit(four);
case "yyyyy":
return intUnit(fourToSix);
case "yyyyyy":
return intUnit(six);
// months
case "M":
return intUnit(oneOrTwo);
case "MM":
return intUnit(two);
case "MMM":
return oneOf(loc.months("short", true, false), 1);
case "MMMM":
return oneOf(loc.months("long", true, false), 1);
case "L":
return intUnit(oneOrTwo);
case "LL":
return intUnit(two);
case "LLL":
return oneOf(loc.months("short", false, false), 1);
case "LLLL":
return oneOf(loc.months("long", false, false), 1);
// dates
case "d":
return intUnit(oneOrTwo);
case "dd":
return intUnit(two);
// ordinals
case "o":
return intUnit(oneToThree);
case "ooo":
return intUnit(three);
// time
case "HH":
return intUnit(two);
case "H":
return intUnit(oneOrTwo);
case "hh":
return intUnit(two);
case "h":
return intUnit(oneOrTwo);
case "mm":
return intUnit(two);
case "m":
return intUnit(oneOrTwo);
case "q":
return intUnit(oneOrTwo);
case "qq":
return intUnit(two);
case "s":
return intUnit(oneOrTwo);
case "ss":
return intUnit(two);
case "S":
return intUnit(oneToThree);
case "SSS":
return intUnit(three);
case "u":
return simple(oneToNine);
case "uu":
return simple(oneOrTwo);
case "uuu":
return intUnit(one);
// meridiem
case "a":
return oneOf(loc.meridiems(), 0);
// weekYear (k)
case "kkkk":
return intUnit(four);
case "kk":
return intUnit(twoToFour, untruncateYear);
// weekNumber (W)
case "W":
return intUnit(oneOrTwo);
case "WW":
return intUnit(two);
// weekdays
case "E":
case "c":
return intUnit(one);
case "EEE":
return oneOf(loc.weekdays("short", false, false), 1);
case "EEEE":
return oneOf(loc.weekdays("long", false, false), 1);
case "ccc":
return oneOf(loc.weekdays("short", true, false), 1);
case "cccc":
return oneOf(loc.weekdays("long", true, false), 1);
// offset/zone
case "Z":
case "ZZ":
return offset(new RegExp(`([+-]${oneOrTwo.source})(?::(${two.source}))?`), 2);
case "ZZZ":
return offset(new RegExp(`([+-]${oneOrTwo.source})(${two.source})?`), 2);
// we don't support ZZZZ (PST) or ZZZZZ (Pacific Standard Time) in parsing
// because we don't have any way to figure out what they are
case "z":
return simple(/[a-z_+-/]{1,256}?/i);
// this special-case "token" represents a place where a macro-token expanded into a white-space literal
// in this case we accept any non-newline white-space
case " ":
return simple(/[^\S\n\r]/);
default:
return literal(t);
}
};
const unit = unitate(token) || {
invalidReason: MISSING_FTP,
};
unit.token = token;
return unit;
}
const partTypeStyleToTokenVal = {
year: {
"2-digit": "yy",
numeric: "yyyyy",
},
month: {
numeric: "M",
"2-digit": "MM",
short: "MMM",
long: "MMMM",
},
day: {
numeric: "d",
"2-digit": "dd",
},
weekday: {
short: "EEE",
long: "EEEE",
},
dayperiod: "a",
dayPeriod: "a",
hour: {
numeric: "h",
"2-digit": "hh",
},
minute: {
numeric: "m",
"2-digit": "mm",
},
second: {
numeric: "s",
"2-digit": "ss",
},
timeZoneName: {
long: "ZZZZZ",
short: "ZZZ",
},
};
function tokenForPart(part, formatOpts) {
const { type, value } = part;
if (type === "literal") {
const isSpace = /^\s+$/.test(value);
return {
literal: !isSpace,
val: isSpace ? " " : value,
};
}
const style = formatOpts[type];
let val = partTypeStyleToTokenVal[type];
if (typeof val === "object") {
val = val[style];
}
if (val) {
return {
literal: false,
val,
};
}
return undefined;
}
function buildRegex(units) {
const re = units.map((u) => u.regex).reduce((f, r) => `${f}(${r.source})`, "");
return [`^${re}$`, units];
}
function match(input, regex, handlers) {
const matches = input.match(regex);
if (matches) {
const all = {};
let matchIndex = 1;
for (const i in handlers) {
if (hasOwnProperty(handlers, i)) {
const h = handlers[i],
groups = h.groups ? h.groups + 1 : 1;
if (!h.literal && h.token) {
all[h.token.val[0]] = h.deser(matches.slice(matchIndex, matchIndex + groups));
}
matchIndex += groups;
}
}
return [matches, all];
} else {
return [matches, {}];
}
}
function dateTimeFromMatches(matches) {
const toField = (token) => {
switch (token) {
case "S":
return "millisecond";
case "s":
return "second";
case "m":
return "minute";
case "h":
case "H":
return "hour";
case "d":
return "day";
case "o":
return "ordinal";
case "L":
case "M":
return "month";
case "y":
return "year";
case "E":
case "c":
return "weekday";
case "W":
return "weekNumber";
case "k":
return "weekYear";
case "q":
return "quarter";
default:
return null;
}
};
let zone = null;
let specificOffset;
if (!isUndefined(matches.z)) {
zone = IANAZone.create(matches.z);
}
if (!isUndefined(matches.Z)) {
if (!zone) {
zone = new FixedOffsetZone(matches.Z);
}
specificOffset = matches.Z;
}
if (!isUndefined(matches.q)) {
matches.M = (matches.q - 1) * 3 + 1;
}
if (!isUndefined(matches.h)) {
if (matches.h < 12 && matches.a === 1) {
matches.h += 12;
} else if (matches.h === 12 && matches.a === 0) {
matches.h = 0;
}
}
if (matches.G === 0 && matches.y) {
matches.y = -matches.y;
}
if (!isUndefined(matches.u)) {
matches.S = parseMillis(matches.u);
}
const vals = Object.keys(matches).reduce((r, k) => {
const f = toField(k);
if (f) {
r[f] = matches[k];
}
return r;
}, {});
return [vals, zone, specificOffset];
}
let dummyDateTimeCache = null;
function getDummyDateTime() {
if (!dummyDateTimeCache) {
dummyDateTimeCache = DateTime.fromMillis(1555555555555);
}
return dummyDateTimeCache;
}
function maybeExpandMacroToken(token, locale) {
if (token.literal) {
return token;
}
const formatOpts = Formatter.macroTokenToFormatOpts(token.val);
const tokens = formatOptsToTokens(formatOpts, locale);
if (tokens == null || tokens.includes(undefined)) {
return token;
}
return tokens;
}
export function expandMacroTokens(tokens, locale) {
return Array.prototype.concat(...tokens.map((t) => maybeExpandMacroToken(t, locale)));
}
/**
* @private
*/
export function explainFromTokens(locale, input, format) {
const tokens = expandMacroTokens(Formatter.parseFormat(format), locale),
units = tokens.map((t) => unitForToken(t, locale)),
disqualifyingUnit = units.find((t) => t.invalidReason);
if (disqualifyingUnit) {
return { input, tokens, invalidReason: disqualifyingUnit.invalidReason };
} else {
const [regexString, handlers] = buildRegex(units),
regex = RegExp(regexString, "i"),
[rawMatches, matches] = match(input, regex, handlers),
[result, zone, specificOffset] = matches
? dateTimeFromMatches(matches)
: [null, null, undefined];
if (hasOwnProperty(matches, "a") && hasOwnProperty(matches, "H")) {
throw new ConflictingSpecificationError(
"Can't include meridiem when specifying 24-hour format"
);
}
return { input, tokens, regex, rawMatches, matches, result, zone, specificOffset };
}
}
export function parseFromTokens(locale, input, format) {
const { result, zone, specificOffset, invalidReason } = explainFromTokens(locale, input, format);
return [result, zone, specificOffset, invalidReason];
}
export function formatOptsToTokens(formatOpts, locale) {
if (!formatOpts) {
return null;
}
const formatter = Formatter.create(locale, formatOpts);
const parts = formatter.formatDateTimeParts(getDummyDateTime());
return parts.map((p) => tokenForPart(p, formatOpts));
}

274
node_modules/luxon/src/impl/util.js generated vendored

@ -0,0 +1,274 @@
/*
This is just a junk drawer, containing anything used across multiple classes.
Because Luxon is small(ish), this should stay small and we won't worry about splitting
it up into, say, parsingUtil.js and basicUtil.js and so on. But they are divided up by feature area.
*/
import { InvalidArgumentError } from "../errors.js";
import Settings from "../settings.js";
/**
* @private
*/
// TYPES
export function isUndefined(o) {
return typeof o === "undefined";
}
export function isNumber(o) {
return typeof o === "number";
}
export function isInteger(o) {
return typeof o === "number" && o % 1 === 0;
}
export function isString(o) {
return typeof o === "string";
}
export function isDate(o) {
return Object.prototype.toString.call(o) === "[object Date]";
}
// CAPABILITIES
export function hasRelative() {
try {
return typeof Intl !== "undefined" && !!Intl.RelativeTimeFormat;
} catch (e) {
return false;
}
}
// OBJECTS AND ARRAYS
export function maybeArray(thing) {
return Array.isArray(thing) ? thing : [thing];
}
export function bestBy(arr, by, compare) {
if (arr.length === 0) {
return undefined;
}
return arr.reduce((best, next) => {
const pair = [by(next), next];
if (!best) {
return pair;
} else if (compare(best[0], pair[0]) === best[0]) {
return best;
} else {
return pair;
}
}, null)[1];
}
export function pick(obj, keys) {
return keys.reduce((a, k) => {
a[k] = obj[k];
return a;
}, {});
}
export function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
// NUMBERS AND STRINGS
export function integerBetween(thing, bottom, top) {
return isInteger(thing) && thing >= bottom && thing <= top;
}
// x % n but takes the sign of n instead of x
export function floorMod(x, n) {
return x - n * Math.floor(x / n);
}
export function padStart(input, n = 2) {
const isNeg = input < 0;
let padded;
if (isNeg) {
padded = "-" + ("" + -input).padStart(n, "0");
} else {
padded = ("" + input).padStart(n, "0");
}
return padded;
}
export function parseInteger(string) {
if (isUndefined(string) || string === null || string === "") {
return undefined;
} else {
return parseInt(string, 10);
}
}
export function parseFloating(string) {
if (isUndefined(string) || string === null || string === "") {
return undefined;
} else {
return parseFloat(string);
}
}
export function parseMillis(fraction) {
// Return undefined (instead of 0) in these cases, where fraction is not set
if (isUndefined(fraction) || fraction === null || fraction === "") {
return undefined;
} else {
const f = parseFloat("0." + fraction) * 1000;
return Math.floor(f);
}
}
export function roundTo(number, digits, towardZero = false) {
const factor = 10 ** digits,
rounder = towardZero ? Math.trunc : Math.round;
return rounder(number * factor) / factor;
}
// DATE BASICS
export function isLeapYear(year) {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
}
export function daysInYear(year) {
return isLeapYear(year) ? 366 : 365;
}
export function daysInMonth(year, month) {
const modMonth = floorMod(month - 1, 12) + 1,
modYear = year + (month - modMonth) / 12;
if (modMonth === 2) {
return isLeapYear(modYear) ? 29 : 28;
} else {
return [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][modMonth - 1];
}
}
// covert a calendar object to a local timestamp (epoch, but with the offset baked in)
export function objToLocalTS(obj) {
let d = Date.UTC(
obj.year,
obj.month - 1,
obj.day,
obj.hour,
obj.minute,
obj.second,
obj.millisecond
);
// for legacy reasons, years between 0 and 99 are interpreted as 19XX; revert that
if (obj.year < 100 && obj.year >= 0) {
d = new Date(d);
// set the month and day again, this is necessary because year 2000 is a leap year, but year 100 is not
// so if obj.year is in 99, but obj.day makes it roll over into year 100,
// the calculations done by Date.UTC are using year 2000 - which is incorrect
d.setUTCFullYear(obj.year, obj.month - 1, obj.day);
}
return +d;
}
export function weeksInWeekYear(weekYear) {
const p1 =
(weekYear +
Math.floor(weekYear / 4) -
Math.floor(weekYear / 100) +
Math.floor(weekYear / 400)) %
7,
last = weekYear - 1,
p2 = (last + Math.floor(last / 4) - Math.floor(last / 100) + Math.floor(last / 400)) % 7;
return p1 === 4 || p2 === 3 ? 53 : 52;
}
export function untruncateYear(year) {
if (year > 99) {
return year;
} else return year > Settings.twoDigitCutoffYear ? 1900 + year : 2000 + year;
}
// PARSING
export function parseZoneInfo(ts, offsetFormat, locale, timeZone = null) {
const date = new Date(ts),
intlOpts = {
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
};
if (timeZone) {
intlOpts.timeZone = timeZone;
}
const modified = { timeZoneName: offsetFormat, ...intlOpts };
const parsed = new Intl.DateTimeFormat(locale, modified)
.formatToParts(date)
.find((m) => m.type.toLowerCase() === "timezonename");
return parsed ? parsed.value : null;
}
// signedOffset('-5', '30') -> -330
export function signedOffset(offHourStr, offMinuteStr) {
let offHour = parseInt(offHourStr, 10);
// don't || this because we want to preserve -0
if (Number.isNaN(offHour)) {
offHour = 0;
}
const offMin = parseInt(offMinuteStr, 10) || 0,
offMinSigned = offHour < 0 || Object.is(offHour, -0) ? -offMin : offMin;
return offHour * 60 + offMinSigned;
}
// COERCION
export function asNumber(value) {
const numericValue = Number(value);
if (typeof value === "boolean" || value === "" || Number.isNaN(numericValue))
throw new InvalidArgumentError(`Invalid unit value ${value}`);
return numericValue;
}
export function normalizeObject(obj, normalizer) {
const normalized = {};
for (const u in obj) {
if (hasOwnProperty(obj, u)) {
const v = obj[u];
if (v === undefined || v === null) continue;
normalized[normalizer(u)] = asNumber(v);
}
}
return normalized;
}
export function formatOffset(offset, format) {
const hours = Math.trunc(Math.abs(offset / 60)),
minutes = Math.trunc(Math.abs(offset % 60)),
sign = offset >= 0 ? "+" : "-";
switch (format) {
case "short":
return `${sign}${padStart(hours, 2)}:${padStart(minutes, 2)}`;
case "narrow":
return `${sign}${hours}${minutes > 0 ? `:${minutes}` : ""}`;
case "techie":
return `${sign}${padStart(hours, 2)}${padStart(minutes, 2)}`;
default:
throw new RangeError(`Value format ${format} is out of range for property format`);
}
}
export function timeObject(obj) {
return pick(obj, ["hour", "minute", "second", "millisecond"]);
}

@ -0,0 +1,34 @@
/**
* @private
*/
import Zone from "../zone.js";
import IANAZone from "../zones/IANAZone.js";
import FixedOffsetZone from "../zones/fixedOffsetZone.js";
import InvalidZone from "../zones/invalidZone.js";
import { isUndefined, isString, isNumber } from "./util.js";
import SystemZone from "../zones/systemZone.js";
export function normalizeZone(input, defaultZone) {
let offset;
if (isUndefined(input) || input === null) {
return defaultZone;
} else if (input instanceof Zone) {
return input;
} else if (isString(input)) {
const lowered = input.toLowerCase();
if (lowered === "default") return defaultZone;
else if (lowered === "local" || lowered === "system") return SystemZone.instance;
else if (lowered === "utc" || lowered === "gmt") return FixedOffsetZone.utcInstance;
else return FixedOffsetZone.parseSpecifier(lowered) || IANAZone.create(input);
} else if (isNumber(input)) {
return FixedOffsetZone.instance(input);
} else if (typeof input === "object" && input.offset && typeof input.offset === "number") {
// This is dumb, but the instanceof check above doesn't seem to really work
// so we're duck checking it
return input;
} else {
return new InvalidZone(input);
}
}

169
node_modules/luxon/src/info.js generated vendored

@ -0,0 +1,169 @@
import DateTime from "./datetime.js";
import Settings from "./settings.js";
import Locale from "./impl/locale.js";
import IANAZone from "./zones/IANAZone.js";
import { normalizeZone } from "./impl/zoneUtil.js";
import { hasRelative } from "./impl/util.js";
/**
* The Info class contains static methods for retrieving general time and date related data. For example, it has methods for finding out if a time zone has a DST, for listing the months in any supported locale, and for discovering which of Luxon features are available in the current environment.
*/
export default class Info {
/**
* Return whether the specified zone contains a DST.
* @param {string|Zone} [zone='local'] - Zone to check. Defaults to the environment's local zone.
* @return {boolean}
*/
static hasDST(zone = Settings.defaultZone) {
const proto = DateTime.now().setZone(zone).set({ month: 12 });
return !zone.isUniversal && proto.offset !== proto.set({ month: 6 }).offset;
}
/**
* Return whether the specified zone is a valid IANA specifier.
* @param {string} zone - Zone to check
* @return {boolean}
*/
static isValidIANAZone(zone) {
return IANAZone.isValidZone(zone);
}
/**
* Converts the input into a {@link Zone} instance.
*
* * If `input` is already a Zone instance, it is returned unchanged.
* * If `input` is a string containing a valid time zone name, a Zone instance
* with that name is returned.
* * If `input` is a string that doesn't refer to a known time zone, a Zone
* instance with {@link Zone#isValid} == false is returned.
* * If `input is a number, a Zone instance with the specified fixed offset
* in minutes is returned.
* * If `input` is `null` or `undefined`, the default zone is returned.
* @param {string|Zone|number} [input] - the value to be converted
* @return {Zone}
*/
static normalizeZone(input) {
return normalizeZone(input, Settings.defaultZone);
}
/**
* Return an array of standalone month names.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
* @param {string} [length='long'] - the length of the month representation, such as "numeric", "2-digit", "narrow", "short", "long"
* @param {Object} opts - options
* @param {string} [opts.locale] - the locale code
* @param {string} [opts.numberingSystem=null] - the numbering system
* @param {string} [opts.locObj=null] - an existing locale object to use
* @param {string} [opts.outputCalendar='gregory'] - the calendar
* @example Info.months()[0] //=> 'January'
* @example Info.months('short')[0] //=> 'Jan'
* @example Info.months('numeric')[0] //=> '1'
* @example Info.months('short', { locale: 'fr-CA' } )[0] //=> 'janv.'
* @example Info.months('numeric', { locale: 'ar' })[0] //=> '١'
* @example Info.months('long', { outputCalendar: 'islamic' })[0] //=> 'Rabiʻ I'
* @return {Array}
*/
static months(
length = "long",
{ locale = null, numberingSystem = null, locObj = null, outputCalendar = "gregory" } = {}
) {
return (locObj || Locale.create(locale, numberingSystem, outputCalendar)).months(length);
}
/**
* Return an array of format month names.
* Format months differ from standalone months in that they're meant to appear next to the day of the month. In some languages, that
* changes the string.
* See {@link Info#months}
* @param {string} [length='long'] - the length of the month representation, such as "numeric", "2-digit", "narrow", "short", "long"
* @param {Object} opts - options
* @param {string} [opts.locale] - the locale code
* @param {string} [opts.numberingSystem=null] - the numbering system
* @param {string} [opts.locObj=null] - an existing locale object to use
* @param {string} [opts.outputCalendar='gregory'] - the calendar
* @return {Array}
*/
static monthsFormat(
length = "long",
{ locale = null, numberingSystem = null, locObj = null, outputCalendar = "gregory" } = {}
) {
return (locObj || Locale.create(locale, numberingSystem, outputCalendar)).months(length, true);
}
/**
* Return an array of standalone week names.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
* @param {string} [length='long'] - the length of the weekday representation, such as "narrow", "short", "long".
* @param {Object} opts - options
* @param {string} [opts.locale] - the locale code
* @param {string} [opts.numberingSystem=null] - the numbering system
* @param {string} [opts.locObj=null] - an existing locale object to use
* @example Info.weekdays()[0] //=> 'Monday'
* @example Info.weekdays('short')[0] //=> 'Mon'
* @example Info.weekdays('short', { locale: 'fr-CA' })[0] //=> 'lun.'
* @example Info.weekdays('short', { locale: 'ar' })[0] //=> 'الاثنين'
* @return {Array}
*/
static weekdays(length = "long", { locale = null, numberingSystem = null, locObj = null } = {}) {
return (locObj || Locale.create(locale, numberingSystem, null)).weekdays(length);
}
/**
* Return an array of format week names.
* Format weekdays differ from standalone weekdays in that they're meant to appear next to more date information. In some languages, that
* changes the string.
* See {@link Info#weekdays}
* @param {string} [length='long'] - the length of the month representation, such as "narrow", "short", "long".
* @param {Object} opts - options
* @param {string} [opts.locale=null] - the locale code
* @param {string} [opts.numberingSystem=null] - the numbering system
* @param {string} [opts.locObj=null] - an existing locale object to use
* @return {Array}
*/
static weekdaysFormat(
length = "long",
{ locale = null, numberingSystem = null, locObj = null } = {}
) {
return (locObj || Locale.create(locale, numberingSystem, null)).weekdays(length, true);
}
/**
* Return an array of meridiems.
* @param {Object} opts - options
* @param {string} [opts.locale] - the locale code
* @example Info.meridiems() //=> [ 'AM', 'PM' ]
* @example Info.meridiems({ locale: 'my' }) //=> [ 'နံနက်', 'ညနေ' ]
* @return {Array}
*/
static meridiems({ locale = null } = {}) {
return Locale.create(locale).meridiems();
}
/**
* Return an array of eras, such as ['BC', 'AD']. The locale can be specified, but the calendar system is always Gregorian.
* @param {string} [length='short'] - the length of the era representation, such as "short" or "long".
* @param {Object} opts - options
* @param {string} [opts.locale] - the locale code
* @example Info.eras() //=> [ 'BC', 'AD' ]
* @example Info.eras('long') //=> [ 'Before Christ', 'Anno Domini' ]
* @example Info.eras('long', { locale: 'fr' }) //=> [ 'avant Jésus-Christ', 'après Jésus-Christ' ]
* @return {Array}
*/
static eras(length = "short", { locale = null } = {}) {
return Locale.create(locale, null, "gregory").eras(length);
}
/**
* Return the set of available features in this environment.
* Some features of Luxon are not available in all environments. For example, on older browsers, relative time formatting support is not available. Use this function to figure out if that's the case.
* Keys:
* * `relative`: whether this environment supports relative time formatting
* @example Info.features() //=> { relative: false }
* @return {Object}
*/
static features() {
return { relative: hasRelative() };
}
}

637
node_modules/luxon/src/interval.js generated vendored

@ -0,0 +1,637 @@
import DateTime, { friendlyDateTime } from "./datetime.js";
import Duration from "./duration.js";
import Settings from "./settings.js";
import { InvalidArgumentError, InvalidIntervalError } from "./errors.js";
import Invalid from "./impl/invalid.js";
import Formatter from "./impl/formatter.js";
import * as Formats from "./impl/formats.js";
const INVALID = "Invalid Interval";
// checks if the start is equal to or before the end
function validateStartEnd(start, end) {
if (!start || !start.isValid) {
return Interval.invalid("missing or invalid start");
} else if (!end || !end.isValid) {
return Interval.invalid("missing or invalid end");
} else if (end < start) {
return Interval.invalid(
"end before start",
`The end of an interval must be after its start, but you had start=${start.toISO()} and end=${end.toISO()}`
);
} else {
return null;
}
}
/**
* An Interval object represents a half-open interval of time, where each endpoint is a {@link DateTime}. Conceptually, it's a container for those two endpoints, accompanied by methods for creating, parsing, interrogating, comparing, transforming, and formatting them.
*
* Here is a brief overview of the most commonly used methods and getters in Interval:
*
* * **Creation** To create an Interval, use {@link Interval.fromDateTimes}, {@link Interval.after}, {@link Interval.before}, or {@link Interval.fromISO}.
* * **Accessors** Use {@link Interval#start} and {@link Interval#end} to get the start and end.
* * **Interrogation** To analyze the Interval, use {@link Interval#count}, {@link Interval#length}, {@link Interval#hasSame}, {@link Interval#contains}, {@link Interval#isAfter}, or {@link Interval#isBefore}.
* * **Transformation** To create other Intervals out of this one, use {@link Interval#set}, {@link Interval#splitAt}, {@link Interval#splitBy}, {@link Interval#divideEqually}, {@link Interval.merge}, {@link Interval.xor}, {@link Interval#union}, {@link Interval#intersection}, or {@link Interval#difference}.
* * **Comparison** To compare this Interval to another one, use {@link Interval#equals}, {@link Interval#overlaps}, {@link Interval#abutsStart}, {@link Interval#abutsEnd}, {@link Interval#engulfs}
* * **Output** To convert the Interval into other representations, see {@link Interval#toString}, {@link Interval#toLocaleString}, {@link Interval#toISO}, {@link Interval#toISODate}, {@link Interval#toISOTime}, {@link Interval#toFormat}, and {@link Interval#toDuration}.
*/
export default class Interval {
/**
* @private
*/
constructor(config) {
/**
* @access private
*/
this.s = config.start;
/**
* @access private
*/
this.e = config.end;
/**
* @access private
*/
this.invalid = config.invalid || null;
/**
* @access private
*/
this.isLuxonInterval = true;
}
/**
* Create an invalid Interval.
* @param {string} reason - simple string of why this Interval is invalid. Should not contain parameters or anything else data-dependent
* @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information
* @return {Interval}
*/
static invalid(reason, explanation = null) {
if (!reason) {
throw new InvalidArgumentError("need to specify a reason the Interval is invalid");
}
const invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation);
if (Settings.throwOnInvalid) {
throw new InvalidIntervalError(invalid);
} else {
return new Interval({ invalid });
}
}
/**
* Create an Interval from a start DateTime and an end DateTime. Inclusive of the start but not the end.
* @param {DateTime|Date|Object} start
* @param {DateTime|Date|Object} end
* @return {Interval}
*/
static fromDateTimes(start, end) {
const builtStart = friendlyDateTime(start),
builtEnd = friendlyDateTime(end);
const validateError = validateStartEnd(builtStart, builtEnd);
if (validateError == null) {
return new Interval({
start: builtStart,
end: builtEnd,
});
} else {
return validateError;
}
}
/**
* Create an Interval from a start DateTime and a Duration to extend to.
* @param {DateTime|Date|Object} start
* @param {Duration|Object|number} duration - the length of the Interval.
* @return {Interval}
*/
static after(start, duration) {
const dur = Duration.fromDurationLike(duration),
dt = friendlyDateTime(start);
return Interval.fromDateTimes(dt, dt.plus(dur));
}
/**
* Create an Interval from an end DateTime and a Duration to extend backwards to.
* @param {DateTime|Date|Object} end
* @param {Duration|Object|number} duration - the length of the Interval.
* @return {Interval}
*/
static before(end, duration) {
const dur = Duration.fromDurationLike(duration),
dt = friendlyDateTime(end);
return Interval.fromDateTimes(dt.minus(dur), dt);
}
/**
* Create an Interval from an ISO 8601 string.
* Accepts `<start>/<end>`, `<start>/<duration>`, and `<duration>/<end>` formats.
* @param {string} text - the ISO string to parse
* @param {Object} [opts] - options to pass {@link DateTime#fromISO} and optionally {@link Duration#fromISO}
* @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
* @return {Interval}
*/
static fromISO(text, opts) {
const [s, e] = (text || "").split("/", 2);
if (s && e) {
let start, startIsValid;
try {
start = DateTime.fromISO(s, opts);
startIsValid = start.isValid;
} catch (e) {
startIsValid = false;
}
let end, endIsValid;
try {
end = DateTime.fromISO(e, opts);
endIsValid = end.isValid;
} catch (e) {
endIsValid = false;
}
if (startIsValid && endIsValid) {
return Interval.fromDateTimes(start, end);
}
if (startIsValid) {
const dur = Duration.fromISO(e, opts);
if (dur.isValid) {
return Interval.after(start, dur);
}
} else if (endIsValid) {
const dur = Duration.fromISO(s, opts);
if (dur.isValid) {
return Interval.before(end, dur);
}
}
}
return Interval.invalid("unparsable", `the input "${text}" can't be parsed as ISO 8601`);
}
/**
* Check if an object is an Interval. Works across context boundaries
* @param {object} o
* @return {boolean}
*/
static isInterval(o) {
return (o && o.isLuxonInterval) || false;
}
/**
* Returns the start of the Interval
* @type {DateTime}
*/
get start() {
return this.isValid ? this.s : null;
}
/**
* Returns the end of the Interval
* @type {DateTime}
*/
get end() {
return this.isValid ? this.e : null;
}
/**
* Returns whether this Interval's end is at least its start, meaning that the Interval isn't 'backwards'.
* @type {boolean}
*/
get isValid() {
return this.invalidReason === null;
}
/**
* Returns an error code if this Interval is invalid, or null if the Interval is valid
* @type {string}
*/
get invalidReason() {
return this.invalid ? this.invalid.reason : null;
}
/**
* Returns an explanation of why this Interval became invalid, or null if the Interval is valid
* @type {string}
*/
get invalidExplanation() {
return this.invalid ? this.invalid.explanation : null;
}
/**
* Returns the length of the Interval in the specified unit.
* @param {string} unit - the unit (such as 'hours' or 'days') to return the length in.
* @return {number}
*/
length(unit = "milliseconds") {
return this.isValid ? this.toDuration(...[unit]).get(unit) : NaN;
}
/**
* Returns the count of minutes, hours, days, months, or years included in the Interval, even in part.
* Unlike {@link Interval#length} this counts sections of the calendar, not periods of time, e.g. specifying 'day'
* asks 'what dates are included in this interval?', not 'how many days long is this interval?'
* @param {string} [unit='milliseconds'] - the unit of time to count.
* @return {number}
*/
count(unit = "milliseconds") {
if (!this.isValid) return NaN;
const start = this.start.startOf(unit),
end = this.end.startOf(unit);
return Math.floor(end.diff(start, unit).get(unit)) + (end.valueOf() !== this.end.valueOf());
}
/**
* Returns whether this Interval's start and end are both in the same unit of time
* @param {string} unit - the unit of time to check sameness on
* @return {boolean}
*/
hasSame(unit) {
return this.isValid ? this.isEmpty() || this.e.minus(1).hasSame(this.s, unit) : false;
}
/**
* Return whether this Interval has the same start and end DateTimes.
* @return {boolean}
*/
isEmpty() {
return this.s.valueOf() === this.e.valueOf();
}
/**
* Return whether this Interval's start is after the specified DateTime.
* @param {DateTime} dateTime
* @return {boolean}
*/
isAfter(dateTime) {
if (!this.isValid) return false;
return this.s > dateTime;
}
/**
* Return whether this Interval's end is before the specified DateTime.
* @param {DateTime} dateTime
* @return {boolean}
*/
isBefore(dateTime) {
if (!this.isValid) return false;
return this.e <= dateTime;
}
/**
* Return whether this Interval contains the specified DateTime.
* @param {DateTime} dateTime
* @return {boolean}
*/
contains(dateTime) {
if (!this.isValid) return false;
return this.s <= dateTime && this.e > dateTime;
}
/**
* "Sets" the start and/or end dates. Returns a newly-constructed Interval.
* @param {Object} values - the values to set
* @param {DateTime} values.start - the starting DateTime
* @param {DateTime} values.end - the ending DateTime
* @return {Interval}
*/
set({ start, end } = {}) {
if (!this.isValid) return this;
return Interval.fromDateTimes(start || this.s, end || this.e);
}
/**
* Split this Interval at each of the specified DateTimes
* @param {...DateTime} dateTimes - the unit of time to count.
* @return {Array}
*/
splitAt(...dateTimes) {
if (!this.isValid) return [];
const sorted = dateTimes
.map(friendlyDateTime)
.filter((d) => this.contains(d))
.sort(),
results = [];
let { s } = this,
i = 0;
while (s < this.e) {
const added = sorted[i] || this.e,
next = +added > +this.e ? this.e : added;
results.push(Interval.fromDateTimes(s, next));
s = next;
i += 1;
}
return results;
}
/**
* Split this Interval into smaller Intervals, each of the specified length.
* Left over time is grouped into a smaller interval
* @param {Duration|Object|number} duration - The length of each resulting interval.
* @return {Array}
*/
splitBy(duration) {
const dur = Duration.fromDurationLike(duration);
if (!this.isValid || !dur.isValid || dur.as("milliseconds") === 0) {
return [];
}
let { s } = this,
idx = 1,
next;
const results = [];
while (s < this.e) {
const added = this.start.plus(dur.mapUnits((x) => x * idx));
next = +added > +this.e ? this.e : added;
results.push(Interval.fromDateTimes(s, next));
s = next;
idx += 1;
}
return results;
}
/**
* Split this Interval into the specified number of smaller intervals.
* @param {number} numberOfParts - The number of Intervals to divide the Interval into.
* @return {Array}
*/
divideEqually(numberOfParts) {
if (!this.isValid) return [];
return this.splitBy(this.length() / numberOfParts).slice(0, numberOfParts);
}
/**
* Return whether this Interval overlaps with the specified Interval
* @param {Interval} other
* @return {boolean}
*/
overlaps(other) {
return this.e > other.s && this.s < other.e;
}
/**
* Return whether this Interval's end is adjacent to the specified Interval's start.
* @param {Interval} other
* @return {boolean}
*/
abutsStart(other) {
if (!this.isValid) return false;
return +this.e === +other.s;
}
/**
* Return whether this Interval's start is adjacent to the specified Interval's end.
* @param {Interval} other
* @return {boolean}
*/
abutsEnd(other) {
if (!this.isValid) return false;
return +other.e === +this.s;
}
/**
* Return whether this Interval engulfs the start and end of the specified Interval.
* @param {Interval} other
* @return {boolean}
*/
engulfs(other) {
if (!this.isValid) return false;
return this.s <= other.s && this.e >= other.e;
}
/**
* Return whether this Interval has the same start and end as the specified Interval.
* @param {Interval} other
* @return {boolean}
*/
equals(other) {
if (!this.isValid || !other.isValid) {
return false;
}
return this.s.equals(other.s) && this.e.equals(other.e);
}
/**
* Return an Interval representing the intersection of this Interval and the specified Interval.
* Specifically, the resulting Interval has the maximum start time and the minimum end time of the two Intervals.
* Returns null if the intersection is empty, meaning, the intervals don't intersect.
* @param {Interval} other
* @return {Interval}
*/
intersection(other) {
if (!this.isValid) return this;
const s = this.s > other.s ? this.s : other.s,
e = this.e < other.e ? this.e : other.e;
if (s >= e) {
return null;
} else {
return Interval.fromDateTimes(s, e);
}
}
/**
* Return an Interval representing the union of this Interval and the specified Interval.
* Specifically, the resulting Interval has the minimum start time and the maximum end time of the two Intervals.
* @param {Interval} other
* @return {Interval}
*/
union(other) {
if (!this.isValid) return this;
const s = this.s < other.s ? this.s : other.s,
e = this.e > other.e ? this.e : other.e;
return Interval.fromDateTimes(s, e);
}
/**
* Merge an array of Intervals into a equivalent minimal set of Intervals.
* Combines overlapping and adjacent Intervals.
* @param {Array} intervals
* @return {Array}
*/
static merge(intervals) {
const [found, final] = intervals
.sort((a, b) => a.s - b.s)
.reduce(
([sofar, current], item) => {
if (!current) {
return [sofar, item];
} else if (current.overlaps(item) || current.abutsStart(item)) {
return [sofar, current.union(item)];
} else {
return [sofar.concat([current]), item];
}
},
[[], null]
);
if (final) {
found.push(final);
}
return found;
}
/**
* Return an array of Intervals representing the spans of time that only appear in one of the specified Intervals.
* @param {Array} intervals
* @return {Array}
*/
static xor(intervals) {
let start = null,
currentCount = 0;
const results = [],
ends = intervals.map((i) => [
{ time: i.s, type: "s" },
{ time: i.e, type: "e" },
]),
flattened = Array.prototype.concat(...ends),
arr = flattened.sort((a, b) => a.time - b.time);
for (const i of arr) {
currentCount += i.type === "s" ? 1 : -1;
if (currentCount === 1) {
start = i.time;
} else {
if (start && +start !== +i.time) {
results.push(Interval.fromDateTimes(start, i.time));
}
start = null;
}
}
return Interval.merge(results);
}
/**
* Return an Interval representing the span of time in this Interval that doesn't overlap with any of the specified Intervals.
* @param {...Interval} intervals
* @return {Array}
*/
difference(...intervals) {
return Interval.xor([this].concat(intervals))
.map((i) => this.intersection(i))
.filter((i) => i && !i.isEmpty());
}
/**
* Returns a string representation of this Interval appropriate for debugging.
* @return {string}
*/
toString() {
if (!this.isValid) return INVALID;
return `[${this.s.toISO()} ${this.e.toISO()})`;
}
/**
* Returns a localized string representing this Interval. Accepts the same options as the
* Intl.DateTimeFormat constructor and any presets defined by Luxon, such as
* {@link DateTime.DATE_FULL} or {@link DateTime.TIME_SIMPLE}. The exact behavior of this method
* is browser-specific, but in general it will return an appropriate representation of the
* Interval in the assigned locale. Defaults to the system's locale if no locale has been
* specified.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
* @param {Object} [formatOpts=DateTime.DATE_SHORT] - Either a DateTime preset or
* Intl.DateTimeFormat constructor options.
* @param {Object} opts - Options to override the configuration of the start DateTime.
* @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(); //=> 11/7/2022 11/8/2022
* @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL); //=> November 7 8, 2022
* @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL, { locale: 'fr-FR' }); //=> 78 novembre 2022
* @example Interval.fromISO('2022-11-07T17:00Z/2022-11-07T19:00Z').toLocaleString(DateTime.TIME_SIMPLE); //=> 6:00 8:00 PM
* @example Interval.fromISO('2022-11-07T17:00Z/2022-11-07T19:00Z').toLocaleString({ weekday: 'short', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); //=> Mon, Nov 07, 6:00 8:00 p
* @return {string}
*/
toLocaleString(formatOpts = Formats.DATE_SHORT, opts = {}) {
return this.isValid
? Formatter.create(this.s.loc.clone(opts), formatOpts).formatInterval(this)
: INVALID;
}
/**
* Returns an ISO 8601-compliant string representation of this Interval.
* @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
* @param {Object} opts - The same options as {@link DateTime#toISO}
* @return {string}
*/
toISO(opts) {
if (!this.isValid) return INVALID;
return `${this.s.toISO(opts)}/${this.e.toISO(opts)}`;
}
/**
* Returns an ISO 8601-compliant string representation of date of this Interval.
* The time components are ignored.
* @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
* @return {string}
*/
toISODate() {
if (!this.isValid) return INVALID;
return `${this.s.toISODate()}/${this.e.toISODate()}`;
}
/**
* Returns an ISO 8601-compliant string representation of time of this Interval.
* The date components are ignored.
* @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
* @param {Object} opts - The same options as {@link DateTime#toISO}
* @return {string}
*/
toISOTime(opts) {
if (!this.isValid) return INVALID;
return `${this.s.toISOTime(opts)}/${this.e.toISOTime(opts)}`;
}
/**
* Returns a string representation of this Interval formatted according to the specified format
* string. **You may not want this.** See {@link Interval#toLocaleString} for a more flexible
* formatting tool.
* @param {string} dateFormat - The format string. This string formats the start and end time.
* See {@link DateTime#toFormat} for details.
* @param {Object} opts - Options.
* @param {string} [opts.separator = ' '] - A separator to place between the start and end
* representations.
* @return {string}
*/
toFormat(dateFormat, { separator = " " } = {}) {
if (!this.isValid) return INVALID;
return `${this.s.toFormat(dateFormat)}${separator}${this.e.toFormat(dateFormat)}`;
}
/**
* Return a Duration representing the time spanned by this interval.
* @param {string|string[]} [unit=['milliseconds']] - the unit or units (such as 'hours' or 'days') to include in the duration.
* @param {Object} opts - options that affect the creation of the Duration
* @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
* @example Interval.fromDateTimes(dt1, dt2).toDuration().toObject() //=> { milliseconds: 88489257 }
* @example Interval.fromDateTimes(dt1, dt2).toDuration('days').toObject() //=> { days: 1.0241812152777778 }
* @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes']).toObject() //=> { hours: 24, minutes: 34.82095 }
* @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes', 'seconds']).toObject() //=> { hours: 24, minutes: 34, seconds: 49.257 }
* @example Interval.fromDateTimes(dt1, dt2).toDuration('seconds').toObject() //=> { seconds: 88489.257 }
* @return {Duration}
*/
toDuration(unit, opts) {
if (!this.isValid) {
return Duration.invalid(this.invalidReason);
}
return this.e.diff(this.s, unit, opts);
}
/**
* Run mapFn on the interval start and end, returning a new Interval from the resulting DateTimes
* @param {function} mapFn
* @return {Interval}
* @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.toUTC())
* @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.plus({ hours: 2 }))
*/
mapEndpoints(mapFn) {
return Interval.fromDateTimes(mapFn(this.s), mapFn(this.e));
}
}

26
node_modules/luxon/src/luxon.js generated vendored

@ -0,0 +1,26 @@
import DateTime from "./datetime.js";
import Duration from "./duration.js";
import Interval from "./interval.js";
import Info from "./info.js";
import Zone from "./zone.js";
import FixedOffsetZone from "./zones/fixedOffsetZone.js";
import IANAZone from "./zones/IANAZone.js";
import InvalidZone from "./zones/invalidZone.js";
import SystemZone from "./zones/systemZone.js";
import Settings from "./settings.js";
const VERSION = "3.3.0";
export {
VERSION,
DateTime,
Duration,
Interval,
Info,
Zone,
FixedOffsetZone,
IANAZone,
InvalidZone,
SystemZone,
Settings,
};

@ -0,0 +1,4 @@
{
"type": "module",
"version": "3.3.0"
}

148
node_modules/luxon/src/settings.js generated vendored

@ -0,0 +1,148 @@
import SystemZone from "./zones/systemZone.js";
import IANAZone from "./zones/IANAZone.js";
import Locale from "./impl/locale.js";
import { normalizeZone } from "./impl/zoneUtil.js";
let now = () => Date.now(),
defaultZone = "system",
defaultLocale = null,
defaultNumberingSystem = null,
defaultOutputCalendar = null,
twoDigitCutoffYear = 60,
throwOnInvalid;
/**
* Settings contains static getters and setters that control Luxon's overall behavior. Luxon is a simple library with few options, but the ones it does have live here.
*/
export default class Settings {
/**
* Get the callback for returning the current timestamp.
* @type {function}
*/
static get now() {
return now;
}
/**
* Set the callback for returning the current timestamp.
* The function should return a number, which will be interpreted as an Epoch millisecond count
* @type {function}
* @example Settings.now = () => Date.now() + 3000 // pretend it is 3 seconds in the future
* @example Settings.now = () => 0 // always pretend it's Jan 1, 1970 at midnight in UTC time
*/
static set now(n) {
now = n;
}
/**
* Set the default time zone to create DateTimes in. Does not affect existing instances.
* Use the value "system" to reset this value to the system's time zone.
* @type {string}
*/
static set defaultZone(zone) {
defaultZone = zone;
}
/**
* Get the default time zone object currently used to create DateTimes. Does not affect existing instances.
* The default value is the system's time zone (the one set on the machine that runs this code).
* @type {Zone}
*/
static get defaultZone() {
return normalizeZone(defaultZone, SystemZone.instance);
}
/**
* Get the default locale to create DateTimes with. Does not affect existing instances.
* @type {string}
*/
static get defaultLocale() {
return defaultLocale;
}
/**
* Set the default locale to create DateTimes with. Does not affect existing instances.
* @type {string}
*/
static set defaultLocale(locale) {
defaultLocale = locale;
}
/**
* Get the default numbering system to create DateTimes with. Does not affect existing instances.
* @type {string}
*/
static get defaultNumberingSystem() {
return defaultNumberingSystem;
}
/**
* Set the default numbering system to create DateTimes with. Does not affect existing instances.
* @type {string}
*/
static set defaultNumberingSystem(numberingSystem) {
defaultNumberingSystem = numberingSystem;
}
/**
* Get the default output calendar to create DateTimes with. Does not affect existing instances.
* @type {string}
*/
static get defaultOutputCalendar() {
return defaultOutputCalendar;
}
/**
* Set the default output calendar to create DateTimes with. Does not affect existing instances.
* @type {string}
*/
static set defaultOutputCalendar(outputCalendar) {
defaultOutputCalendar = outputCalendar;
}
/**
* Get the cutoff year after which a string encoding a year as two digits is interpreted to occur in the current century.
* @type {number}
*/
static get twoDigitCutoffYear() {
return twoDigitCutoffYear;
}
/**
* Set the cutoff year after which a string encoding a year as two digits is interpreted to occur in the current century.
* @type {number}
* @example Settings.twoDigitCutoffYear = 0 // cut-off year is 0, so all 'yy' are interpretted as current century
* @example Settings.twoDigitCutoffYear = 50 // '49' -> 1949; '50' -> 2050
* @example Settings.twoDigitCutoffYear = 1950 // interpretted as 50
* @example Settings.twoDigitCutoffYear = 2050 // ALSO interpretted as 50
*/
static set twoDigitCutoffYear(cutoffYear) {
twoDigitCutoffYear = cutoffYear % 100;
}
/**
* Get whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals
* @type {boolean}
*/
static get throwOnInvalid() {
return throwOnInvalid;
}
/**
* Set whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals
* @type {boolean}
*/
static set throwOnInvalid(t) {
throwOnInvalid = t;
}
/**
* Reset Luxon's global caches. Should only be necessary in testing scenarios.
* @return {void}
*/
static resetCaches() {
Locale.resetCache();
IANAZone.resetCache();
}
}

91
node_modules/luxon/src/zone.js generated vendored

@ -0,0 +1,91 @@
import { ZoneIsAbstractError } from "./errors.js";
/**
* @interface
*/
export default class Zone {
/**
* The type of zone
* @abstract
* @type {string}
*/
get type() {
throw new ZoneIsAbstractError();
}
/**
* The name of this zone.
* @abstract
* @type {string}
*/
get name() {
throw new ZoneIsAbstractError();
}
get ianaName() {
return this.name;
}
/**
* Returns whether the offset is known to be fixed for the whole year.
* @abstract
* @type {boolean}
*/
get isUniversal() {
throw new ZoneIsAbstractError();
}
/**
* Returns the offset's common name (such as EST) at the specified timestamp
* @abstract
* @param {number} ts - Epoch milliseconds for which to get the name
* @param {Object} opts - Options to affect the format
* @param {string} opts.format - What style of offset to return. Accepts 'long' or 'short'.
* @param {string} opts.locale - What locale to return the offset name in.
* @return {string}
*/
offsetName(ts, opts) {
throw new ZoneIsAbstractError();
}
/**
* Returns the offset's value as a string
* @abstract
* @param {number} ts - Epoch milliseconds for which to get the offset
* @param {string} format - What style of offset to return.
* Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
* @return {string}
*/
formatOffset(ts, format) {
throw new ZoneIsAbstractError();
}
/**
* Return the offset in minutes for this zone at the specified timestamp.
* @abstract
* @param {number} ts - Epoch milliseconds for which to compute the offset
* @return {number}
*/
offset(ts) {
throw new ZoneIsAbstractError();
}
/**
* Return whether this Zone is equal to another zone
* @abstract
* @param {Zone} otherZone - the zone to compare
* @return {boolean}
*/
equals(otherZone) {
throw new ZoneIsAbstractError();
}
/**
* Return whether this Zone is valid.
* @abstract
* @type {boolean}
*/
get isValid() {
throw new ZoneIsAbstractError();
}
}

@ -0,0 +1,189 @@
import { formatOffset, parseZoneInfo, isUndefined, objToLocalTS } from "../impl/util.js";
import Zone from "../zone.js";
let dtfCache = {};
function makeDTF(zone) {
if (!dtfCache[zone]) {
dtfCache[zone] = new Intl.DateTimeFormat("en-US", {
hour12: false,
timeZone: zone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
era: "short",
});
}
return dtfCache[zone];
}
const typeToPos = {
year: 0,
month: 1,
day: 2,
era: 3,
hour: 4,
minute: 5,
second: 6,
};
function hackyOffset(dtf, date) {
const formatted = dtf.format(date).replace(/\u200E/g, ""),
parsed = /(\d+)\/(\d+)\/(\d+) (AD|BC),? (\d+):(\d+):(\d+)/.exec(formatted),
[, fMonth, fDay, fYear, fadOrBc, fHour, fMinute, fSecond] = parsed;
return [fYear, fMonth, fDay, fadOrBc, fHour, fMinute, fSecond];
}
function partsOffset(dtf, date) {
const formatted = dtf.formatToParts(date);
const filled = [];
for (let i = 0; i < formatted.length; i++) {
const { type, value } = formatted[i];
const pos = typeToPos[type];
if (type === "era") {
filled[pos] = value;
} else if (!isUndefined(pos)) {
filled[pos] = parseInt(value, 10);
}
}
return filled;
}
let ianaZoneCache = {};
/**
* A zone identified by an IANA identifier, like America/New_York
* @implements {Zone}
*/
export default class IANAZone extends Zone {
/**
* @param {string} name - Zone name
* @return {IANAZone}
*/
static create(name) {
if (!ianaZoneCache[name]) {
ianaZoneCache[name] = new IANAZone(name);
}
return ianaZoneCache[name];
}
/**
* Reset local caches. Should only be necessary in testing scenarios.
* @return {void}
*/
static resetCache() {
ianaZoneCache = {};
dtfCache = {};
}
/**
* Returns whether the provided string is a valid specifier. This only checks the string's format, not that the specifier identifies a known zone; see isValidZone for that.
* @param {string} s - The string to check validity on
* @example IANAZone.isValidSpecifier("America/New_York") //=> true
* @example IANAZone.isValidSpecifier("Sport~~blorp") //=> false
* @deprecated This method returns false for some valid IANA names. Use isValidZone instead.
* @return {boolean}
*/
static isValidSpecifier(s) {
return this.isValidZone(s);
}
/**
* Returns whether the provided string identifies a real zone
* @param {string} zone - The string to check
* @example IANAZone.isValidZone("America/New_York") //=> true
* @example IANAZone.isValidZone("Fantasia/Castle") //=> false
* @example IANAZone.isValidZone("Sport~~blorp") //=> false
* @return {boolean}
*/
static isValidZone(zone) {
if (!zone) {
return false;
}
try {
new Intl.DateTimeFormat("en-US", { timeZone: zone }).format();
return true;
} catch (e) {
return false;
}
}
constructor(name) {
super();
/** @private **/
this.zoneName = name;
/** @private **/
this.valid = IANAZone.isValidZone(name);
}
/** @override **/
get type() {
return "iana";
}
/** @override **/
get name() {
return this.zoneName;
}
/** @override **/
get isUniversal() {
return false;
}
/** @override **/
offsetName(ts, { format, locale }) {
return parseZoneInfo(ts, format, locale, this.name);
}
/** @override **/
formatOffset(ts, format) {
return formatOffset(this.offset(ts), format);
}
/** @override **/
offset(ts) {
const date = new Date(ts);
if (isNaN(date)) return NaN;
const dtf = makeDTF(this.name);
let [year, month, day, adOrBc, hour, minute, second] = dtf.formatToParts
? partsOffset(dtf, date)
: hackyOffset(dtf, date);
if (adOrBc === "BC") {
year = -Math.abs(year) + 1;
}
// because we're using hour12 and https://bugs.chromium.org/p/chromium/issues/detail?id=1025564&can=2&q=%2224%3A00%22%20datetimeformat
const adjustedHour = hour === 24 ? 0 : hour;
const asUTC = objToLocalTS({
year,
month,
day,
hour: adjustedHour,
minute,
second,
millisecond: 0,
});
let asTS = +date;
const over = asTS % 1000;
asTS -= over >= 0 ? over : 1000 + over;
return (asUTC - asTS) / (60 * 1000);
}
/** @override **/
equals(otherZone) {
return otherZone.type === "iana" && otherZone.name === this.name;
}
/** @override **/
get isValid() {
return this.valid;
}
}

@ -0,0 +1,102 @@
import { formatOffset, signedOffset } from "../impl/util.js";
import Zone from "../zone.js";
let singleton = null;
/**
* A zone with a fixed offset (meaning no DST)
* @implements {Zone}
*/
export default class FixedOffsetZone extends Zone {
/**
* Get a singleton instance of UTC
* @return {FixedOffsetZone}
*/
static get utcInstance() {
if (singleton === null) {
singleton = new FixedOffsetZone(0);
}
return singleton;
}
/**
* Get an instance with a specified offset
* @param {number} offset - The offset in minutes
* @return {FixedOffsetZone}
*/
static instance(offset) {
return offset === 0 ? FixedOffsetZone.utcInstance : new FixedOffsetZone(offset);
}
/**
* Get an instance of FixedOffsetZone from a UTC offset string, like "UTC+6"
* @param {string} s - The offset string to parse
* @example FixedOffsetZone.parseSpecifier("UTC+6")
* @example FixedOffsetZone.parseSpecifier("UTC+06")
* @example FixedOffsetZone.parseSpecifier("UTC-6:00")
* @return {FixedOffsetZone}
*/
static parseSpecifier(s) {
if (s) {
const r = s.match(/^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$/i);
if (r) {
return new FixedOffsetZone(signedOffset(r[1], r[2]));
}
}
return null;
}
constructor(offset) {
super();
/** @private **/
this.fixed = offset;
}
/** @override **/
get type() {
return "fixed";
}
/** @override **/
get name() {
return this.fixed === 0 ? "UTC" : `UTC${formatOffset(this.fixed, "narrow")}`;
}
get ianaName() {
if (this.fixed === 0) {
return "Etc/UTC";
} else {
return `Etc/GMT${formatOffset(-this.fixed, "narrow")}`;
}
}
/** @override **/
offsetName() {
return this.name;
}
/** @override **/
formatOffset(ts, format) {
return formatOffset(this.fixed, format);
}
/** @override **/
get isUniversal() {
return true;
}
/** @override **/
offset() {
return this.fixed;
}
/** @override **/
equals(otherZone) {
return otherZone.type === "fixed" && otherZone.fixed === this.fixed;
}
/** @override **/
get isValid() {
return true;
}
}

@ -0,0 +1,53 @@
import Zone from "../zone.js";
/**
* A zone that failed to parse. You should never need to instantiate this.
* @implements {Zone}
*/
export default class InvalidZone extends Zone {
constructor(zoneName) {
super();
/** @private */
this.zoneName = zoneName;
}
/** @override **/
get type() {
return "invalid";
}
/** @override **/
get name() {
return this.zoneName;
}
/** @override **/
get isUniversal() {
return false;
}
/** @override **/
offsetName() {
return null;
}
/** @override **/
formatOffset() {
return "";
}
/** @override **/
offset() {
return NaN;
}
/** @override **/
equals() {
return false;
}
/** @override **/
get isValid() {
return false;
}
}

@ -0,0 +1,61 @@
import { formatOffset, parseZoneInfo } from "../impl/util.js";
import Zone from "../zone.js";
let singleton = null;
/**
* Represents the local zone for this JavaScript environment.
* @implements {Zone}
*/
export default class SystemZone extends Zone {
/**
* Get a singleton instance of the local zone
* @return {SystemZone}
*/
static get instance() {
if (singleton === null) {
singleton = new SystemZone();
}
return singleton;
}
/** @override **/
get type() {
return "system";
}
/** @override **/
get name() {
return new Intl.DateTimeFormat().resolvedOptions().timeZone;
}
/** @override **/
get isUniversal() {
return false;
}
/** @override **/
offsetName(ts, { format, locale }) {
return parseZoneInfo(ts, format, locale);
}
/** @override **/
formatOffset(ts, format) {
return formatOffset(this.offset(ts), format);
}
/** @override **/
offset(ts) {
return -new Date(ts).getTimezoneOffset();
}
/** @override **/
equals(otherZone) {
return otherZone.type === "system";
}
/** @override **/
get isValid() {
return true;
}
}

@ -0,0 +1,40 @@
# Master (Unreleased)
# 2.1.1 - 11 January, 2023
### Bug fixes:
- Bump cron-parser from 3.5.0 to 4.2.0 - security fix #642
# 2.1.0 - 28 November, 2021
### New features:
- Implement support for graceful shutdown #583
- Emit a success event after a successful job invocation. #643
# 2.0.0 - 01 February, 2021
### Breaking changes:
- Drop support for Node < 6
- Remove support for job objects. See `UPGRADING.md` for more details. #557
### New features:
- Introduce compatibility with browsers and Electron #553
### Bug fixes:
- Avoid leaking memory on one-off jobs #581
- Address anonymous job id collision problem to ensure that node-schedule could run with the unlimited uptime #582
### Internal changes:
- Bump cron-parser from 2.18.0 to 3.1.0 #562
- Bump sinon from 1.17.7 to 9.2.4 #566
- Switch to nyc from istanbul #574
- Convert tests from nodeunit to Tape #575
- Replace var with let/const #577
- Execute airtap tests on a headless Chromium #578
- Make structure more modular #579

@ -0,0 +1,19 @@
Copyright (C) 2015 Matt Patenaude.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,304 @@
# Node Schedule
[![NPM version](http://img.shields.io/npm/v/node-schedule.svg)](https://www.npmjs.com/package/node-schedule)
[![Downloads](https://img.shields.io/npm/dm/node-schedule.svg)](https://www.npmjs.com/package/node-schedule)
[![Build Status](https://github.com/node-schedule/node-schedule/workflows/ci/badge.svg)](https://github.com/node-schedule/node-schedule/actions)
[![Coverage Status](https://coveralls.io/repos/node-schedule/node-schedule/badge.svg?branch=master)](https://coveralls.io/r/node-schedule/node-schedule?branch=master)
[![Join the chat at https://gitter.im/node-schedule/node-schedule](https://img.shields.io/badge/gitter-chat-green.svg)](https://gitter.im/node-schedule/node-schedule?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![NPM](https://nodei.co/npm/node-schedule.png?downloads=true)](https://nodei.co/npm/node-schedule/)
Node Schedule is a flexible cron-like and not-cron-like job scheduler for Node.js.
It allows you to schedule jobs (arbitrary functions) for execution at
specific dates, with optional recurrence rules. It only uses a single timer
at any given time (rather than reevaluating upcoming jobs every second/minute).
Node 6+ is supported.
## Overview
Node Schedule is for time-based scheduling, not interval-based scheduling.
While you can easily bend it to your will, if you only want to do something like
"run this function every 5 minutes", [toad-scheduler](https://github.com/kibertoad/toad-scheduler) would be a better choice. But if you want to, say, "run this function at the :20
and :50 of every hour on the third Tuesday of every month," you'll find that
Node Schedule suits your needs better. Additionally, Node Schedule has Windows
support, unlike true `cron`, since the node runtime is now fully supported.
Note that Node Schedule is designed for in-process scheduling, i.e. scheduled jobs
will only fire as long as your script is running, and the schedule will disappear
when execution completes. If you need to schedule jobs that will persist even when
your script *isn't* running, consider using actual [cron].
In case you need durable jobs that persist across restarts and lock system compatible with multi-node deployments,
try [agenda](https://github.com/agenda/agenda) or [bree](https://github.com/breejs/bree).
## Usage
### Installation
You can install using [npm](https://www.npmjs.com/package/node-schedule).
```
npm install node-schedule
```
### Jobs and Scheduling
Every scheduled job in Node Schedule is represented by a `Job` object. You can
create jobs manually, then execute the `schedule()` method to apply a schedule,
or use the convenience function `scheduleJob()` as demonstrated below.
`Job` objects are `EventEmitter`s, and emit the following events:
* A `run` event after each execution.
* A `scheduled` event each time they're scheduled to run.
* A `canceled` event when an invocation is canceled before it's executed.
Note that `canceled` is the single-L American spelling.
* An `error` event when a job invocation triggered by a schedule throws or returns
a rejected `Promise`.
* A `success` event when a job invocation triggered by a schedule returns successfully or
returns a resolved `Promise`. In any case, the `success` event receives the value returned
by the callback or in case of a promise, the resolved value.
(Both the `scheduled` and `canceled` events receive a JavaScript date object as
a parameter).
Note that jobs are scheduled the first time immediately, so if you create a job
using the `scheduleJob()` convenience method, you'll miss the first `scheduled`
event, but you can query the invocation manually (see below).
### Cron-style Scheduling
The cron format consists of:
```
* * * * * *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ │
│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
│ │ │ │ └───── month (1 - 12)
│ │ │ └────────── day of month (1 - 31)
│ │ └─────────────── hour (0 - 23)
│ └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, OPTIONAL)
```
Examples with the cron format:
```js
const schedule = require('node-schedule');
const job = schedule.scheduleJob('42 * * * *', function(){
console.log('The answer to life, the universe, and everything!');
});
```
Execute a cron job when the minute is 42 (e.g. 19:42, 20:42, etc.).
And:
```js
const job = schedule.scheduleJob('0 17 ? * 0,4-6', function(){
console.log('Today is recognized by Rebecca Black!');
});
```
Execute a cron job every 5 Minutes = */5 * * * *
You can also get when it is scheduled to run for every invocation of the job:
```js
const job = schedule.scheduleJob('0 1 * * *', function(fireDate){
console.log('This job was supposed to run at ' + fireDate + ', but actually ran at ' + new Date());
});
```
This is useful when you need to check if there is a delay of the job invocation when the system is busy, or save a record of all invocations of a job for audit purpose.
#### Unsupported Cron Features
Currently, `W` (nearest weekday) and `L` (last day of month/week) are not supported.
Most other features supported by popular cron implementations should work just fine,
including `#` (nth weekday of the month).
[cron-parser] is used to parse crontab instructions.
### Date-based Scheduling
Say you very specifically want a function to execute at 5:30am on December 21, 2012.
Remember - in JavaScript - 0 - January, 11 - December.
```js
const schedule = require('node-schedule');
const date = new Date(2012, 11, 21, 5, 30, 0);
const job = schedule.scheduleJob(date, function(){
console.log('The world is going to end today.');
});
```
To use current data in the future you can use binding:
```js
const schedule = require('node-schedule');
const date = new Date(2012, 11, 21, 5, 30, 0);
const x = 'Tada!';
const job = schedule.scheduleJob(date, function(y){
console.log(y);
}.bind(null,x));
x = 'Changing Data';
```
This will log 'Tada!' when the scheduled Job runs, rather than 'Changing Data',
which x changes to immediately after scheduling.
### Recurrence Rule Scheduling
You can build recurrence rules to specify when a job should recur. For instance,
consider this rule, which executes the function every hour at 42 minutes after the hour:
```js
const schedule = require('node-schedule');
const rule = new schedule.RecurrenceRule();
rule.minute = 42;
const job = schedule.scheduleJob(rule, function(){
console.log('The answer to life, the universe, and everything!');
});
```
You can also use arrays to specify a list of acceptable values, and the `Range`
object to specify a range of start and end values, with an optional step parameter.
For instance, this will print a message on Thursday, Friday, Saturday, and Sunday at 5pm:
```js
const rule = new schedule.RecurrenceRule();
rule.dayOfWeek = [0, new schedule.Range(4, 6)];
rule.hour = 17;
rule.minute = 0;
const job = schedule.scheduleJob(rule, function(){
console.log('Today is recognized by Rebecca Black!');
});
```
Timezones are also supported. Here is an example of executing at the start of every day in the UTC timezone.
```js
const rule = new schedule.RecurrenceRule();
rule.hour = 0;
rule.minute = 0;
rule.tz = 'Etc/UTC';
const job = schedule.scheduleJob(rule, function(){
console.log('A new day has begun in the UTC timezone!');
});
```
A list of acceptable tz (timezone) values can be found at <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>
#### RecurrenceRule properties
- `second (0-59)`
- `minute (0-59)`
- `hour (0-23)`
- `date (1-31)`
- `month (0-11)`
- `year`
- `dayOfWeek (0-6) Starting with Sunday`
- `tz`
> **Note**: It's worth noting that the default value of a component of a recurrence rule is
> `null` (except for second, which is 0 for familiarity with cron). *If we did not
> explicitly set `minute` to 0 above, the message would have instead been logged at
> 5:00pm, 5:01pm, 5:02pm, ..., 5:59pm.* Probably not what you want.
#### Object Literal Syntax
To make things a little easier, an object literal syntax is also supported, like
in this example which will log a message every Sunday at 2:30pm:
```js
const job = schedule.scheduleJob({hour: 14, minute: 30, dayOfWeek: 0}, function(){
console.log('Time for tea!');
});
```
#### Set StartTime and EndTime
It will run after 5 seconds and stop after 10 seconds in this example.
The ruledat supports the above.
```js
const startTime = new Date(Date.now() + 5000);
const endTime = new Date(startTime.getTime() + 5000);
const job = schedule.scheduleJob({ start: startTime, end: endTime, rule: '*/1 * * * * *' }, function(){
console.log('Time for tea!');
});
```
### Graceful Shutdown.
You can shutdown jobs gracefully.
`gracefulShutdown()` will cancel all jobs and return Promise.
It will wait until all jobs are terminated.
```js
schedule.gracefulShutdown();
```
You can also gracefully shutdown jobs when a system interrupt occurs.
```
process.on('SIGINT', function () {
schedule.gracefulShutdown()
.then(() => process.exit(0))
}
```
### Handle Jobs and Job Invocations
There are some function to get information for a Job and to handle the Job and
Invocations.
#### job.cancel(reschedule)
You can invalidate any job with the `cancel()` method:
```js
j.cancel();
```
All planned invocations will be canceled. When you set the parameter ***reschedule***
to true then the Job is newly scheduled afterwards.
#### job.cancelNext(reschedule)
This method invalidates the next planned invocation or the job.
When you set the parameter ***reschedule*** to true then the Job is newly scheduled
afterwards.
#### job.reschedule(spec)
This method cancels all pending invocation and registers the Job completely new again using the given specification.
Return true/false on success/failure.
#### job.nextInvocation()
This method returns a Date object for the planned next Invocation for this Job. If no invocation is planned the method returns null.
## Contributing
This module was originally developed by [Matt Patenaude] who eventually passed over maintainer's mantle over to [Tejas Manohar].
Currently it is being maintained by [Igor Savin] and [our amazing community].
We'd love to get your contributions. Individuals making significant and valuable
contributions are given commit-access to the project to contribute as they see fit.
Before jumping in, check out our [Contributing] page guide!
## Copyright and license
Copyright 2015 Matt Patenaude.
Licensed under the **[MIT License](https://github.com/node-schedule/node-schedule/blob/master/LICENSE)**.
[cron]: http://unixhelp.ed.ac.uk/CGI/man-cgi?crontab+5
[Contributing]: https://github.com/node-schedule/node-schedule/blob/master/CONTRIBUTING.md
[Matt Patenaude]: https://github.com/mattpat
[Tejas Manohar]: http://tejas.io
[Igor Savin]: https://twitter.com/kibertoad
[our amazing community]: https://github.com/node-schedule/node-schedule/graphs/contributors
[cron-parser]: https://github.com/harrisiirak/cron-parser

@ -0,0 +1,20 @@
## Upgrading to new node-schedule versions
### Upgrading to version 2.0.0+
* Node.js versions older than 6 are no longer supported, please update your environment before upgrading.
* In order to prevent memory leaks, one-off jobs (targeted to be executed at an exact date) cannot be rescheduled by name, as reference to them is no longer stored indefinitely. If you want to keep rescheduling them, make sure to store reference to the initial job.
* The `scheduleJob()` method no longer supports passing an object with the job method. If you were using an object, pass the job method directly.
E.g. code that previously looked like this:
```javascript
const obj = {
execute() {}
};
Scheduler.scheduleJob(obj);
```
should be changed to something like this:
```javascript
function execute() {}
Scheduler.scheduleJob(execute);
```

@ -0,0 +1,17 @@
'use strict';
const { cancelJob, rescheduleJob, scheduledJobs, scheduleJob, gracefulShutdown} = require('./lib/schedule')
const { Invocation, RecurrenceRule, Range} = require('./lib/Invocation')
const { Job } = require('./lib/Job')
module.exports = {
Job,
Invocation,
Range,
RecurrenceRule,
cancelJob,
rescheduleJob,
scheduledJobs,
scheduleJob,
gracefulShutdown,
}

@ -0,0 +1,348 @@
'use strict';
const lt = require('long-timeout')
const CronDate = require('cron-parser/lib/date')
const sorted = require('sorted-array-functions')
const invocations = [];
let currentInvocation = null;
/* DoesntRecur rule */
const DoesntRecur = new RecurrenceRule();
DoesntRecur.recurs = false;
/* Invocation object */
function Invocation(job, fireDate, recurrenceRule, endDate) {
this.job = job;
this.fireDate = fireDate;
this.endDate = endDate;
this.recurrenceRule = recurrenceRule || DoesntRecur;
this.timerID = null;
}
function sorter(a, b) {
return (a.fireDate.getTime() - b.fireDate.getTime());
}
/* Range object */
function Range(start, end, step) {
this.start = start || 0;
this.end = end || 60;
this.step = step || 1;
}
Range.prototype.contains = function(val) {
if (this.step === null || this.step === 1) {
return (val >= this.start && val <= this.end);
} else {
for (let i = this.start; i < this.end; i += this.step) {
if (i === val) {
return true;
}
}
return false;
}
};
/* RecurrenceRule object */
/*
Interpreting each property:
null - any value is valid
number - fixed value
Range - value must fall in range
array - value must validate against any item in list
NOTE: Cron months are 1-based, but RecurrenceRule months are 0-based.
*/
function RecurrenceRule(year, month, date, dayOfWeek, hour, minute, second) {
this.recurs = true;
this.year = (year == null) ? null : year;
this.month = (month == null) ? null : month;
this.date = (date == null) ? null : date;
this.dayOfWeek = (dayOfWeek == null) ? null : dayOfWeek;
this.hour = (hour == null) ? null : hour;
this.minute = (minute == null) ? null : minute;
this.second = (second == null) ? 0 : second;
}
RecurrenceRule.prototype.isValid = function() {
function isValidType(num) {
if (Array.isArray(num) || (num instanceof Array)) {
return num.every(function(e) {
return isValidType(e);
});
}
return !(Number.isNaN(Number(num)) && !(num instanceof Range));
}
if (this.month !== null && (this.month < 0 || this.month > 11 || !isValidType(this.month))) {
return false;
}
if (this.dayOfWeek !== null && (this.dayOfWeek < 0 || this.dayOfWeek > 6 || !isValidType(this.dayOfWeek))) {
return false;
}
if (this.hour !== null && (this.hour < 0 || this.hour > 23 || !isValidType(this.hour))) {
return false;
}
if (this.minute !== null && (this.minute < 0 || this.minute > 59 || !isValidType(this.minute))) {
return false;
}
if (this.second !== null && (this.second < 0 || this.second > 59 || !isValidType(this.second))) {
return false;
}
if (this.date !== null) {
if(!isValidType(this.date)) {
return false;
}
switch (this.month) {
case 3:
case 5:
case 8:
case 10:
if (this.date < 1 || this. date > 30) {
return false;
}
break;
case 1:
if (this.date < 1 || this. date > 29) {
return false;
}
break;
default:
if (this.date < 1 || this. date > 31) {
return false;
}
}
}
return true;
};
RecurrenceRule.prototype.nextInvocationDate = function(base) {
const next = this._nextInvocationDate(base);
return next ? next.toDate() : null;
};
RecurrenceRule.prototype._nextInvocationDate = function(base) {
base = ((base instanceof CronDate) || (base instanceof Date)) ? base : (new Date());
if (!this.recurs) {
return null;
}
if(!this.isValid()) {
return null;
}
const now = new CronDate(Date.now(), this.tz);
let fullYear = now.getFullYear();
if ((this.year !== null) &&
(typeof this.year == 'number') &&
(this.year < fullYear)) {
return null;
}
let next = new CronDate(base.getTime(), this.tz);
next.addSecond();
while (true) {
if (this.year !== null) {
fullYear = next.getFullYear();
if ((typeof this.year == 'number') && (this.year < fullYear)) {
next = null;
break;
}
if (!recurMatch(fullYear, this.year)) {
next.addYear();
next.setMonth(0);
next.setDate(1);
next.setHours(0);
next.setMinutes(0);
next.setSeconds(0);
continue;
}
}
if (this.month != null && !recurMatch(next.getMonth(), this.month)) {
next.addMonth();
continue;
}
if (this.date != null && !recurMatch(next.getDate(), this.date)) {
next.addDay();
continue;
}
if (this.dayOfWeek != null && !recurMatch(next.getDay(), this.dayOfWeek)) {
next.addDay();
continue;
}
if (this.hour != null && !recurMatch(next.getHours(), this.hour)) {
next.addHour();
continue;
}
if (this.minute != null && !recurMatch(next.getMinutes(), this.minute)) {
next.addMinute();
continue;
}
if (this.second != null && !recurMatch(next.getSeconds(), this.second)) {
next.addSecond();
continue;
}
break;
}
return next;
};
function recurMatch(val, matcher) {
if (matcher == null) {
return true;
}
if (typeof matcher === 'number') {
return (val === matcher);
} else if(typeof matcher === 'string') {
return (val === Number(matcher));
} else if (matcher instanceof Range) {
return matcher.contains(val);
} else if (Array.isArray(matcher) || (matcher instanceof Array)) {
for (let i = 0; i < matcher.length; i++) {
if (recurMatch(val, matcher[i])) {
return true;
}
}
}
return false;
}
/* Date-based scheduler */
function runOnDate(date, job) {
const now = Date.now();
const then = date.getTime();
return lt.setTimeout(function() {
if (then > Date.now())
runOnDate(date, job);
else
job();
}, (then < now ? 0 : then - now));
}
function scheduleInvocation(invocation) {
sorted.add(invocations, invocation, sorter);
prepareNextInvocation();
const date = invocation.fireDate instanceof CronDate ? invocation.fireDate.toDate() : invocation.fireDate;
invocation.job.emit('scheduled', date);
}
function prepareNextInvocation() {
if (invocations.length > 0 && currentInvocation !== invocations[0]) {
if (currentInvocation !== null) {
lt.clearTimeout(currentInvocation.timerID);
currentInvocation.timerID = null;
currentInvocation = null;
}
currentInvocation = invocations[0];
const job = currentInvocation.job;
const cinv = currentInvocation;
currentInvocation.timerID = runOnDate(currentInvocation.fireDate, function() {
currentInvocationFinished();
if (job.callback) {
job.callback();
}
if (cinv.recurrenceRule.recurs || cinv.recurrenceRule._endDate === null) {
const inv = scheduleNextRecurrence(cinv.recurrenceRule, cinv.job, cinv.fireDate, cinv.endDate);
if (inv !== null) {
inv.job.trackInvocation(inv);
}
}
job.stopTrackingInvocation(cinv);
try {
const result = job.invoke(cinv.fireDate instanceof CronDate ? cinv.fireDate.toDate() : cinv.fireDate);
job.emit('run');
job.running += 1;
if (result instanceof Promise) {
result.then(function (value) {
job.emit('success', value);
job.running -= 1;
}).catch(function (err) {
job.emit('error', err);
job.running -= 1;
});
} else {
job.emit('success', result);
job.running -= 1;
}
} catch (err) {
job.emit('error', err);
job.running -= 1;
}
if (job.isOneTimeJob) {
job.deleteFromSchedule();
}
});
}
}
function currentInvocationFinished() {
invocations.shift();
currentInvocation = null;
prepareNextInvocation();
}
function cancelInvocation(invocation) {
const idx = invocations.indexOf(invocation);
if (idx > -1) {
invocations.splice(idx, 1);
if (invocation.timerID !== null) {
lt.clearTimeout(invocation.timerID);
}
if (currentInvocation === invocation) {
currentInvocation = null;
}
invocation.job.emit('canceled', invocation.fireDate);
prepareNextInvocation();
}
}
/* Recurrence scheduler */
function scheduleNextRecurrence(rule, job, prevDate, endDate) {
prevDate = (prevDate instanceof CronDate) ? prevDate : new CronDate();
const date = (rule instanceof RecurrenceRule) ? rule._nextInvocationDate(prevDate) : rule.next();
if (date === null) {
return null;
}
if ((endDate instanceof CronDate) && date.getTime() > endDate.getTime()) {
return null;
}
const inv = new Invocation(job, date, rule, endDate);
scheduleInvocation(inv);
return inv;
}
module.exports = {
Range,
RecurrenceRule,
Invocation,
cancelInvocation,
scheduleInvocation,
scheduleNextRecurrence,
sorter,
_invocations: invocations
}

@ -0,0 +1,287 @@
'use strict';
const events = require('events')
const cronParser = require('cron-parser')
const CronDate = require('cron-parser/lib/date')
const sorted = require('sorted-array-functions')
const { scheduleNextRecurrence, scheduleInvocation, cancelInvocation, RecurrenceRule, sorter, Invocation } = require('./Invocation')
const { isValidDate } = require('./utils/dateUtils')
const scheduledJobs = {};
let anonJobCounter = 0;
function resolveAnonJobName() {
const now = new Date()
if (anonJobCounter === Number.MAX_SAFE_INTEGER) {
anonJobCounter = 0
}
anonJobCounter++
return `<Anonymous Job ${anonJobCounter} ${now.toISOString()}>`
}
function Job(name, job, callback) {
// setup a private pendingInvocations variable
this.pendingInvocations = [];
//setup a private number of invocations variable
let triggeredJobs = 0;
// Set scope vars
const jobName = name && typeof name === 'string' ? name : resolveAnonJobName();
this.job = name && typeof name === 'function' ? name : job;
// Make sure callback is actually a callback
if (this.job === name) {
// Name wasn't provided and maybe a callback is there
this.callback = typeof job === 'function' ? job : false;
} else {
// Name was provided, and maybe a callback is there
this.callback = typeof callback === 'function' ? callback : false;
}
// task count
this.running = 0;
// Check for generator
if (typeof this.job === 'function' &&
this.job.prototype &&
this.job.prototype.next) {
this.job = function() {
return this.next().value;
}.bind(this.job.call(this));
}
// define properties
Object.defineProperty(this, 'name', {
value: jobName,
writable: false,
enumerable: true
});
// method that require private access
this.trackInvocation = function(invocation) {
// add to our invocation list
sorted.add(this.pendingInvocations, invocation, sorter);
return true;
};
this.stopTrackingInvocation = function(invocation) {
const invIdx = this.pendingInvocations.indexOf(invocation);
if (invIdx > -1) {
this.pendingInvocations.splice(invIdx, 1);
return true;
}
return false;
};
this.triggeredJobs = function() {
return triggeredJobs;
};
this.setTriggeredJobs = function(triggeredJob) {
triggeredJobs = triggeredJob;
};
this.deleteFromSchedule = function() {
deleteScheduledJob(this.name)
};
this.cancel = function(reschedule) {
reschedule = (typeof reschedule == 'boolean') ? reschedule : false;
let inv, newInv;
const newInvs = [];
for (let j = 0; j < this.pendingInvocations.length; j++) {
inv = this.pendingInvocations[j];
cancelInvocation(inv);
if (reschedule && (inv.recurrenceRule.recurs || inv.recurrenceRule.next)) {
newInv = scheduleNextRecurrence(inv.recurrenceRule, this, inv.fireDate, inv.endDate);
if (newInv !== null) {
newInvs.push(newInv);
}
}
}
this.pendingInvocations = [];
for (let k = 0; k < newInvs.length; k++) {
this.trackInvocation(newInvs[k]);
}
// remove from scheduledJobs if reschedule === false
if (!reschedule) {
this.deleteFromSchedule()
}
return true;
};
this.cancelNext = function(reschedule) {
reschedule = (typeof reschedule == 'boolean') ? reschedule : true;
if (!this.pendingInvocations.length) {
return false;
}
let newInv;
const nextInv = this.pendingInvocations.shift();
cancelInvocation(nextInv);
if (reschedule && (nextInv.recurrenceRule.recurs || nextInv.recurrenceRule.next)) {
newInv = scheduleNextRecurrence(nextInv.recurrenceRule, this, nextInv.fireDate, nextInv.endDate);
if (newInv !== null) {
this.trackInvocation(newInv);
}
}
return true;
};
this.reschedule = function(spec) {
let inv;
const invocationsToCancel = this.pendingInvocations.slice();
for (let j = 0; j < invocationsToCancel.length; j++) {
inv = invocationsToCancel[j];
cancelInvocation(inv);
}
this.pendingInvocations = [];
if (this.schedule(spec)) {
this.setTriggeredJobs(0);
return true;
} else {
this.pendingInvocations = invocationsToCancel;
return false;
}
};
this.nextInvocation = function() {
if (!this.pendingInvocations.length) {
return null;
}
return this.pendingInvocations[0].fireDate;
};
}
Object.setPrototypeOf(Job.prototype, events.EventEmitter.prototype);
Job.prototype.invoke = function(fireDate) {
this.setTriggeredJobs(this.triggeredJobs() + 1);
return this.job(fireDate);
};
Job.prototype.runOnDate = function(date) {
return this.schedule(date);
};
Job.prototype.schedule = function(spec) {
const self = this;
let success = false;
let inv;
let start;
let end;
let tz;
// save passed-in value before 'spec' is replaced
if (typeof spec === 'object' && 'tz' in spec) {
tz = spec.tz;
}
if (typeof spec === 'object' && spec.rule) {
start = spec.start || undefined;
end = spec.end || undefined;
spec = spec.rule;
if (start) {
if (!(start instanceof Date)) {
start = new Date(start);
}
start = new CronDate(start, tz);
if (!isValidDate(start) || start.getTime() < Date.now()) {
start = undefined;
}
}
if (end && !(end instanceof Date) && !isValidDate(end = new Date(end))) {
end = undefined;
}
if (end) {
end = new CronDate(end, tz);
}
}
try {
const res = cronParser.parseExpression(spec, {currentDate: start, tz: tz});
inv = scheduleNextRecurrence(res, self, start, end);
if (inv !== null) {
success = self.trackInvocation(inv);
}
} catch (err) {
const type = typeof spec;
if ((type === 'string') || (type === 'number')) {
spec = new Date(spec);
}
if ((spec instanceof Date) && (isValidDate(spec))) {
spec = new CronDate(spec);
self.isOneTimeJob = true;
if (spec.getTime() >= Date.now()) {
inv = new Invocation(self, spec);
scheduleInvocation(inv);
success = self.trackInvocation(inv);
}
} else if (type === 'object') {
self.isOneTimeJob = false;
if (!(spec instanceof RecurrenceRule)) {
const r = new RecurrenceRule();
if ('year' in spec) {
r.year = spec.year;
}
if ('month' in spec) {
r.month = spec.month;
}
if ('date' in spec) {
r.date = spec.date;
}
if ('dayOfWeek' in spec) {
r.dayOfWeek = spec.dayOfWeek;
}
if ('hour' in spec) {
r.hour = spec.hour;
}
if ('minute' in spec) {
r.minute = spec.minute;
}
if ('second' in spec) {
r.second = spec.second;
}
spec = r;
}
spec.tz = tz;
inv = scheduleNextRecurrence(spec, self, start, end);
if (inv !== null) {
success = self.trackInvocation(inv);
}
}
}
scheduledJobs[this.name] = this;
return success;
};
function deleteScheduledJob(name) {
if (name) {
delete scheduledJobs[name];
}
}
module.exports = {
Job,
deleteScheduledJob,
scheduledJobs
}

@ -0,0 +1,114 @@
'use strict';
/*
node-schedule
A cron-like and not-cron-like job scheduler for Node.
*/
const { Job, scheduledJobs } = require('./Job')
/* API
invoke()
runOnDate(date)
schedule(date || recurrenceRule || cronstring)
cancel(reschedule = false)
cancelNext(reschedule = true)
Property constraints
name: readonly
job: readwrite
*/
/* Convenience methods */
function scheduleJob() {
if (arguments.length < 2) {
throw new RangeError('Invalid number of arguments');
}
const name = (arguments.length >= 3 && typeof arguments[0] === 'string') ? arguments[0] : null;
const spec = name ? arguments[1] : arguments[0];
const method = name ? arguments[2] : arguments[1];
const callback = name ? arguments[3] : arguments[2];
if (typeof method !== 'function') {
throw new RangeError('The job method must be a function.');
}
const job = new Job(name, method, callback);
if (job.schedule(spec)) {
return job;
}
return null;
}
function rescheduleJob(job, spec) {
if (job instanceof Job) {
if (job.reschedule(spec)) {
return job;
}
} else if (typeof job === 'string') {
if (Object.prototype.hasOwnProperty.call(scheduledJobs, job)) {
if (scheduledJobs[job].reschedule(spec)) {
return scheduledJobs[job];
}
} else {
throw new Error('Cannot reschedule one-off job by name, pass job reference instead')
}
}
return null;
}
function cancelJob(job) {
let success = false;
if (job instanceof Job) {
success = job.cancel();
} else if (typeof job == 'string' || job instanceof String) {
if (job in scheduledJobs && Object.prototype.hasOwnProperty.call(scheduledJobs, job)) {
success = scheduledJobs[job].cancel();
}
}
return success;
}
function gracefulShutdown() {
const jobs = Object.keys(scheduledJobs).map(key => scheduledJobs[key]);
jobs.forEach(function (job) {
job.cancel();
});
let running = false;
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].running > 0) {
running = true;
break;
}
}
return new Promise(function (resolve) {
if (running) {
setInterval(function () {
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].running > 0) {
return;
}
}
resolve();
}, 500);
} else {
resolve();
}
});
}
/* Public API */
module.exports = {
scheduleJob,
rescheduleJob,
scheduledJobs,
cancelJob,
gracefulShutdown,
}

@ -0,0 +1,11 @@
'use strict';
function isValidDate(date) {
// Taken from http://stackoverflow.com/a/12372720/1562178
// If getTime() returns NaN it'll return false anyway
return date.getTime() === date.getTime();
}
module.exports = {
isValidDate
}

@ -0,0 +1,60 @@
{
"name": "node-schedule",
"version": "2.1.1",
"description": "A cron-like and not-cron-like job scheduler for Node.",
"keywords": [
"schedule",
"task",
"job",
"cron",
"recurrent",
"in-memory"
],
"license": "MIT",
"main": "index.js",
"scripts": {
"test": "tape test/*.js",
"test:browser": "airtap test/cancel-long-running-jobs.js test/convenience-method-test.js test/date-convenience-methods-test.js test/range-test.js test/recurrence-rule-test.js test/schedule-cron-jobs.js test/start-end-test.js",
"coveralls": "nyc report --reporter=lcov",
"test:coverage": "nyc tape test/*.js",
"lint": "eslint lib test",
"lint:fix": "eslint --fix lib test"
},
"author": {
"name": "Matt Patenaude",
"email": "matt@mattpatenaude.com",
"url": "http://mattpatenaude.com"
},
"contributors": [
{
"name": "Igor Savin",
"email": "kibertoad@gmail.com"
}
],
"repository": {
"type": "git",
"url": "https://github.com/node-schedule/node-schedule.git"
},
"dependencies": {
"cron-parser": "^4.2.0",
"long-timeout": "0.1.1",
"sorted-array-functions": "^1.3.0"
},
"devDependencies": {
"airtap": "^4.0.4",
"eslint": "^8.3.0",
"nyc": "^15.1.0",
"sinon": "^9.2.4",
"tape": "^5.3.2"
},
"engines": {
"node": ">=6"
},
"files": [
"CHANGELOG.md",
"README.md",
"UPGRADING.md",
"LICENSE",
"lib/*"
]
}

@ -0,0 +1,6 @@
language: node_js
node_js:
- '0.10'
- '0.12'
- '4'
- '6'

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Mathias Buus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,75 @@
# sorted-array-functions
Maintain and search through a sorted array using some low level functions
```
npm install sorted-array-functions
```
[![build status](http://img.shields.io/travis/mafintosh/sorted-array-functions.svg?style=flat)](http://travis-ci.org/mafintosh/sorted-array-functions)
## Usage
``` js
var sorted = require('sorted-array-functions')
var list = []
sorted.add(list, 1)
sorted.add(list, 4)
sorted.add(list, 2)
console.log(list) // prints out [1, 2, 4]
console.log(sorted.has(list, 2)) // returns true
console.log(sorted.has(list, 3)) // returns false
console.log(sorted.eq(list, 2)) // returns 1 (the index)
console.log(sorted.gt(list, 2)) // returns 2
console.log(sorted.gt(list, 4)) // returns -1
```
## API
#### `sorted.add(list, value, [compare])`
Insert a new value into the list sorted.
Optionally you can use a custom compare function that returns, `compare(a, b)` that returns 1 if `a > b`, 0 if `a === b` and -1 if `a < b`.
#### `sorted.addFromFront(list, value, [compare])`
Inserts a new value (same result as `sorted.add()`) optimized for prepend.
#### `var bool = sorted.remove(list, value, [compare])`
Remove a value. Returns true if the value was in the list.
#### `var bool = sorted.has(list, value, [compare])`
Check if a value is in the list.
#### `var index = sorted.eq(list, value, [compare])`
Get the index of a value in the list (uses binary search).
If the value could not be found -1 is returned.
#### `var index = sorted.gte(list, value, [compare])`
Get the index of the first value that is `>=`.
If the value could not be found -1 is returned.
#### `var index = sorted.gt(list, value, [compare])`
Get the index of the first value that is `>`.
If the value could not be found -1 is returned.
#### `var index = sorted.lte(list, value, [compare])`
Get the index of the first value that is `<=`.
If the value could not be found -1 is returned.
#### `var index = sorted.lt(list, value, [compare])`
Get the index of the first value that is `<`.
If the value could not be found -1 is returned.
## License
MIT

@ -0,0 +1,13 @@
var sorted = require('./')
var list = []
sorted.add(list, 1)
sorted.add(list, 4)
sorted.add(list, 2)
console.log(list) // prints out [1, 2, 4]
console.log(sorted.has(list, 2)) // returns true
console.log(sorted.has(list, 3)) // returns false
console.log(sorted.eq(list, 2)) // returns 1 (the index)
console.log(sorted.gt(list, 2)) // returns 2
console.log(sorted.gt(list, 4)) // returns -1

@ -0,0 +1,173 @@
exports.add = add
exports.addFromFront = addFromFront
exports.remove = remove
exports.has = has
exports.eq = eq
exports.lte = lte
exports.lt = lt
exports.gte = gte
exports.gt = gt
exports.nearest = nearest
function defaultCmp (a, b) {
if (a === b) return 0
return a < b ? -1 : 1
}
function add (list, value, cmp) {
if (!cmp) cmp = defaultCmp
var top = list.push(value) - 1
while (top) {
if (cmp(list[top - 1], value) < 0) return
list[top] = list[top - 1]
list[top - 1] = value
top--
}
}
function addFromFront (list, value, cmp) {
if (!cmp) cmp = defaultCmp
var top = list.unshift(value) - 1
for (var i = 0; i < top; i++) {
if (cmp(value, list[i + 1]) < 0) return
list[i] = list[i + 1]
list[i + 1] = value
}
}
function lte (list, value, cmp) {
if (!cmp) cmp = defaultCmp
var i = indexOf(list, value, cmp)
if (i === -1) return -1
for (; i >= 0; i--) {
var c = cmp(list[i], value)
if (c <= 0) return i
}
return -1
}
function lt (list, value, cmp) {
if (!cmp) cmp = defaultCmp
var i = indexOf(list, value, cmp)
if (i === -1) return -1
for (; i >= 0; i--) {
var c = cmp(list[i], value)
if (c < 0) return i
}
return -1
}
function gte (list, value, cmp) {
if (!cmp) cmp = defaultCmp
var i = indexOf(list, value, cmp)
if (i === -1) return -1
for (; i < list.length; i++) {
var c = cmp(list[i], value)
if (c >= 0) return i
}
return -1
}
function gt (list, value, cmp) {
if (!cmp) cmp = defaultCmp
var i = indexOf(list, value, cmp)
if (i === -1) return -1
for (; i < list.length; i++) {
var c = cmp(list[i], value)
if (c > 0) return i
}
return -1
}
function eq (list, value, cmp) {
if (!cmp) cmp = defaultCmp
var i = indexOf(list, value, cmp)
if (i === -1) return -1
return cmp(list[i], value) === 0 ? i : -1
}
function nearest (list, value, cmp) {
if (!cmp) cmp = defaultCmp
var len = list.length
var top = len - 1
var btm = 0
var mid = -1
var trending = 1 // 0 = down, 2 = up
while (top >= btm && btm >= 0 && top < len) {
mid = Math.floor((top + btm) / 2)
var c = cmp(list[mid], value)
if (c === 0) return mid
if (c >= 0) {
if (trending === 1) trending = 0
else if (trending === 2) {
if (Math.abs(list[mid] - value) > Math.abs(list[mid - 1] - value)) return mid - 1
return mid
}
top = mid - 1
} else {
if (trending === 1) trending = 2
else if (trending === 0) return mid
btm = mid + 1
}
}
return mid
}
function indexOf (list, value, cmp) {
if (!cmp) cmp = defaultCmp
var len = list.length
var top = len - 1
var btm = 0
var mid = -1
while (top >= btm && btm >= 0 && top < len) {
mid = Math.floor((top + btm) / 2)
var c = cmp(list[mid], value)
if (c === 0) return mid
if (c >= 0) {
top = mid - 1
} else {
btm = mid + 1
}
}
return mid
}
function has (list, value, cmp) {
return eq(list, value, cmp) > -1
}
function remove (list, value, cmp) {
var i = eq(list, value, cmp)
if (i === -1) return false
list.splice(i, 1)
return true
}

@ -0,0 +1,24 @@
{
"name": "sorted-array-functions",
"version": "1.3.0",
"description": "Maintain and search through a sorted array using some low level functions",
"main": "index.js",
"dependencies": {},
"devDependencies": {
"standard": "^8.4.0",
"tape": "^4.6.2"
},
"scripts": {
"test": "standard && tape test.js"
},
"repository": {
"type": "git",
"url": "https://github.com/mafintosh/sorted-array-functions.git"
},
"author": "Mathias Buus (@mafintosh)",
"license": "MIT",
"bugs": {
"url": "https://github.com/mafintosh/sorted-array-functions/issues"
},
"homepage": "https://github.com/mafintosh/sorted-array-functions"
}

@ -0,0 +1,381 @@
var tape = require('tape')
var sorted = require('./')
tape('add', function (t) {
var list = []
sorted.add(list, 3)
sorted.add(list, 4)
sorted.add(list, 3)
sorted.add(list, 9)
sorted.add(list, 0)
sorted.add(list, 5)
sorted.add(list, 8)
t.same(list, [0, 3, 3, 4, 5, 8, 9])
t.end()
})
tape('addFromFront', function (t) {
var list = []
sorted.addFromFront(list, 3)
sorted.addFromFront(list, 4)
sorted.addFromFront(list, 3)
sorted.addFromFront(list, 9)
sorted.addFromFront(list, 0)
sorted.addFromFront(list, 5)
sorted.addFromFront(list, 8)
t.same(list, [0, 3, 3, 4, 5, 8, 9])
t.end()
})
tape('remove', function (t) {
var list = []
sorted.add(list, 3)
sorted.add(list, 4)
sorted.add(list, 3)
sorted.add(list, 9)
sorted.add(list, 0)
sorted.add(list, 5)
sorted.add(list, 8)
sorted.remove(list, 3)
sorted.remove(list, 5)
sorted.remove(list, 6)
t.same(list, [0, 3, 4, 8, 9])
t.end()
})
tape('has', function (t) {
var list = []
sorted.add(list, 3)
t.same(sorted.has(list, 3), true)
t.same(sorted.has(list, 2), false)
sorted.add(list, 5)
t.same(sorted.has(list, 5), true)
t.same(sorted.has(list, 3), true)
t.same(sorted.has(list, 2), false)
sorted.add(list, 1)
t.same(sorted.has(list, 1), true)
t.same(sorted.has(list, 5), true)
t.same(sorted.has(list, 3), true)
t.same(sorted.has(list, 2), false)
t.same(sorted.has(list, 8), false)
t.end()
})
tape('eq', function (t) {
var list = []
sorted.add(list, 3)
t.same(sorted.eq(list, 3), 0)
t.same(sorted.eq(list, 2), -1)
sorted.add(list, 5)
t.same(sorted.eq(list, 5), 1)
t.same(sorted.eq(list, 3), 0)
t.same(sorted.eq(list, 2), -1)
sorted.add(list, 1)
t.same(sorted.eq(list, 1), 0)
t.same(sorted.eq(list, 5), 2)
t.same(sorted.eq(list, 3), 1)
t.same(sorted.eq(list, 2), -1)
t.same(sorted.eq(list, 8), -1)
t.end()
})
tape('gte', function (t) {
var list = []
sorted.add(list, 3)
t.same(sorted.gte(list, 3), 0)
t.same(sorted.gte(list, 2), 0)
sorted.add(list, 5)
t.same(sorted.gte(list, 5), 1)
t.same(sorted.gte(list, 3), 0)
t.same(sorted.gte(list, 2), 0)
sorted.add(list, 1)
t.same(sorted.gte(list, 1), 0)
t.same(sorted.gte(list, 5), 2)
t.same(sorted.gte(list, 3), 1)
t.same(sorted.gte(list, 2), 1)
t.same(sorted.gte(list, 8), -1)
t.end()
})
tape('gt', function (t) {
var list = []
sorted.add(list, 3)
t.same(sorted.gt(list, 3), -1)
t.same(sorted.gt(list, 2), 0)
sorted.add(list, 5)
t.same(sorted.gt(list, 5), -1)
t.same(sorted.gt(list, 3), 1)
t.same(sorted.gt(list, 2), 0)
sorted.add(list, 1)
t.same(sorted.gt(list, 1), 1)
t.same(sorted.gt(list, 5), -1)
t.same(sorted.gt(list, 3), 2)
t.same(sorted.gt(list, 2), 1)
t.same(sorted.gt(list, 8), -1)
t.end()
})
tape('lte', function (t) {
var list = []
sorted.add(list, 3)
t.same(sorted.lte(list, 3), 0)
t.same(sorted.lte(list, 2), -1)
sorted.add(list, 5)
t.same(sorted.lte(list, 6), 1)
t.same(sorted.lte(list, 5), 1)
t.same(sorted.lte(list, 3), 0)
t.same(sorted.lte(list, 2), -1)
sorted.add(list, 1)
t.same(sorted.lte(list, 1), 0)
t.same(sorted.lte(list, 5), 2)
t.same(sorted.lte(list, 3), 1)
t.same(sorted.lte(list, 2), 0)
t.same(sorted.lte(list, 8), 2)
t.end()
})
tape('lt', function (t) {
var list = []
sorted.add(list, 3)
t.same(sorted.lt(list, 3), -1)
t.same(sorted.lt(list, 2), -1)
t.same(sorted.lt(list, 4), 0)
sorted.add(list, 5)
t.same(sorted.lt(list, 6), 1)
t.same(sorted.lt(list, 5), 0)
t.same(sorted.lt(list, 3), -1)
t.same(sorted.lt(list, 2), -1)
sorted.add(list, 1)
t.same(sorted.lt(list, 1), -1)
t.same(sorted.lt(list, 5), 1)
t.same(sorted.lt(list, 3), 0)
t.same(sorted.lt(list, 2), 0)
t.same(sorted.lt(list, 8), 2)
t.end()
})
tape('custom compare add', function (t) {
var list = []
sorted.add(list, {foo: 3}, cmp)
sorted.add(list, {foo: 4}, cmp)
sorted.add(list, {foo: 3}, cmp)
sorted.add(list, {foo: 9}, cmp)
sorted.add(list, {foo: 0}, cmp)
sorted.add(list, {foo: 5}, cmp)
sorted.add(list, {foo: 8}, cmp)
t.same(list, [{foo: 0}, {foo: 3}, {foo: 3}, {foo: 4}, {foo: 5}, {foo: 8}, {foo: 9}])
t.end()
})
tape('custom compare remove', function (t) {
var list = []
sorted.add(list, {foo: 3}, cmp)
sorted.add(list, {foo: 4}, cmp)
sorted.add(list, {foo: 3}, cmp)
sorted.add(list, {foo: 9}, cmp)
sorted.add(list, {foo: 0}, cmp)
sorted.add(list, {foo: 5}, cmp)
sorted.add(list, {foo: 8}, cmp)
sorted.remove(list, {foo: 3}, cmp)
sorted.remove(list, {foo: 5}, cmp)
sorted.remove(list, {foo: 6}, cmp)
t.same(list, [{foo: 0}, {foo: 3}, {foo: 4}, {foo: 8}, {foo: 9}])
t.end()
})
tape('custom compare has', function (t) {
var list = []
sorted.add(list, {foo: 3}, cmp)
t.same(sorted.has(list, {foo: 3}, cmp), true)
t.same(sorted.has(list, {foo: 2}, cmp), false)
sorted.add(list, {foo: 5}, cmp)
t.same(sorted.has(list, {foo: 5}, cmp), true)
t.same(sorted.has(list, {foo: 3}, cmp), true)
t.same(sorted.has(list, {foo: 2}, cmp), false)
sorted.add(list, {foo: 1}, cmp)
t.same(sorted.has(list, {foo: 1}, cmp), true)
t.same(sorted.has(list, {foo: 5}, cmp), true)
t.same(sorted.has(list, {foo: 3}, cmp), true)
t.same(sorted.has(list, {foo: 2}, cmp), false)
t.same(sorted.has(list, {foo: 8}, cmp), false)
t.end()
})
tape('custom compare eq', function (t) {
var list = []
sorted.add(list, {foo: 3}, cmp)
t.same(sorted.eq(list, {foo: 3}, cmp), 0)
t.same(sorted.eq(list, {foo: 2}, cmp), -1)
sorted.add(list, {foo: 5}, cmp)
t.same(sorted.eq(list, {foo: 5}, cmp), 1)
t.same(sorted.eq(list, {foo: 3}, cmp), 0)
t.same(sorted.eq(list, {foo: 2}, cmp), -1)
sorted.add(list, {foo: 1}, cmp)
t.same(sorted.eq(list, {foo: 1}, cmp), 0)
t.same(sorted.eq(list, {foo: 5}, cmp), 2)
t.same(sorted.eq(list, {foo: 3}, cmp), 1)
t.same(sorted.eq(list, {foo: 2}, cmp), -1)
t.same(sorted.eq(list, {foo: 8}, cmp), -1)
t.end()
})
tape('custom compare gte', function (t) {
var list = []
sorted.add(list, {foo: 3}, cmp)
t.same(sorted.gte(list, {foo: 3}, cmp), 0)
t.same(sorted.gte(list, {foo: 2}, cmp), 0)
sorted.add(list, {foo: 5}, cmp)
t.same(sorted.gte(list, {foo: 5}, cmp), 1)
t.same(sorted.gte(list, {foo: 3}, cmp), 0)
t.same(sorted.gte(list, {foo: 2}, cmp), 0)
sorted.add(list, {foo: 1}, cmp)
t.same(sorted.gte(list, {foo: 1}, cmp), 0)
t.same(sorted.gte(list, {foo: 5}, cmp), 2)
t.same(sorted.gte(list, {foo: 3}, cmp), 1)
t.same(sorted.gte(list, {foo: 2}, cmp), 1)
t.same(sorted.gte(list, {foo: 8}, cmp), -1)
t.end()
})
tape('custom compare gt', function (t) {
var list = []
sorted.add(list, {foo: 3}, cmp)
t.same(sorted.gt(list, {foo: 3}, cmp), -1)
t.same(sorted.gt(list, {foo: 2}, cmp), 0)
sorted.add(list, {foo: 5}, cmp)
t.same(sorted.gt(list, {foo: 5}, cmp), -1)
t.same(sorted.gt(list, {foo: 3}, cmp), 1)
t.same(sorted.gt(list, {foo: 2}, cmp), 0)
sorted.add(list, {foo: 1}, cmp)
t.same(sorted.gt(list, {foo: 1}, cmp), 1)
t.same(sorted.gt(list, {foo: 5}, cmp), -1)
t.same(sorted.gt(list, {foo: 3}, cmp), 2)
t.same(sorted.gt(list, {foo: 2}, cmp), 1)
t.same(sorted.gt(list, {foo: 8}, cmp), -1)
t.end()
})
tape('custom compare lte', function (t) {
var list = []
sorted.add(list, {foo: 3}, cmp)
t.same(sorted.lte(list, {foo: 3}, cmp), 0)
t.same(sorted.lte(list, {foo: 2}, cmp), -1)
sorted.add(list, {foo: 5}, cmp)
t.same(sorted.lte(list, {foo: 6}, cmp), 1)
t.same(sorted.lte(list, {foo: 5}, cmp), 1)
t.same(sorted.lte(list, {foo: 3}, cmp), 0)
t.same(sorted.lte(list, {foo: 2}, cmp), -1)
sorted.add(list, {foo: 1}, cmp)
t.same(sorted.lte(list, {foo: 1}, cmp), 0)
t.same(sorted.lte(list, {foo: 5}, cmp), 2)
t.same(sorted.lte(list, {foo: 3}, cmp), 1)
t.same(sorted.lte(list, {foo: 2}, cmp), 0)
t.same(sorted.lte(list, {foo: 8}, cmp), 2)
t.end()
})
tape('custom compare lt', function (t) {
var list = []
sorted.add(list, {foo: 3}, cmp)
t.same(sorted.lt(list, {foo: 3}, cmp), -1)
t.same(sorted.lt(list, {foo: 2}, cmp), -1)
t.same(sorted.lt(list, {foo: 4}, cmp), 0)
sorted.add(list, {foo: 5}, cmp)
t.same(sorted.lt(list, {foo: 6}, cmp), 1)
t.same(sorted.lt(list, {foo: 5}, cmp), 0)
t.same(sorted.lt(list, {foo: 3}, cmp), -1)
t.same(sorted.lt(list, {foo: 2}, cmp), -1)
sorted.add(list, {foo: 1}, cmp)
t.same(sorted.lt(list, {foo: 1}, cmp), -1)
t.same(sorted.lt(list, {foo: 5}, cmp), 1)
t.same(sorted.lt(list, {foo: 3}, cmp), 0)
t.same(sorted.lt(list, {foo: 2}, cmp), 0)
t.same(sorted.lt(list, {foo: 8}, cmp), 2)
t.end()
})
tape('find nearest value', function (t) {
var list = []
sorted.add(list, 0.001)
sorted.add(list, 10)
sorted.add(list, 20)
sorted.add(list, 30)
sorted.add(list, 40)
sorted.add(list, 50)
sorted.add(list, 70)
t.equal(sorted.nearest(list, 66), 6)
t.equal(sorted.nearest(list, 51), 5)
t.equal(sorted.nearest(list, 1), 0)
t.equal(sorted.nearest(list, 0), 0)
t.equal(sorted.nearest(list, 69.999), 6)
t.equal(sorted.nearest(list, 72), 6)
t.end()
})
function cmp (a, b) {
return a.foo - b.foo
}

76
package-lock.json generated

@ -42,6 +42,7 @@
"mongodb": "^5.0.1",
"mongoose": "^5.11.8",
"multer-gridfs-storage": "^5.0.2",
"node-schedule": "^2.1.1",
"nodemon": "^2.0.20",
"nunjucks": "^3.2.3",
"point-of-view": "^5.3.0",
@ -4441,6 +4442,17 @@
"node": ">=10"
}
},
"node_modules/cron-parser": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.8.1.tgz",
"integrity": "sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==",
"dependencies": {
"luxon": "^3.2.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/cryptiles": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
@ -6679,6 +6691,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/long-timeout": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz",
"integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w=="
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -6698,6 +6715,14 @@
"yallist": "^3.0.2"
}
},
"node_modules/luxon": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz",
"integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==",
"engines": {
"node": ">=12"
}
},
"node_modules/magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
@ -7255,6 +7280,19 @@
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
},
"node_modules/node-schedule": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz",
"integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==",
"dependencies": {
"cron-parser": "^4.2.0",
"long-timeout": "0.1.1",
"sorted-array-functions": "^1.3.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/node-uuid": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
@ -9042,6 +9080,11 @@
"flatstr": "^1.0.12"
}
},
"node_modules/sorted-array-functions": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz",
"integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA=="
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@ -13441,6 +13484,14 @@
"yaml": "^1.10.0"
}
},
"cron-parser": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.8.1.tgz",
"integrity": "sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==",
"requires": {
"luxon": "^3.2.1"
}
},
"cryptiles": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
@ -15230,6 +15281,11 @@
"is-unicode-supported": "^0.1.0"
}
},
"long-timeout": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz",
"integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w=="
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -15246,6 +15302,11 @@
"yallist": "^3.0.2"
}
},
"luxon": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz",
"integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg=="
},
"magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
@ -15664,6 +15725,16 @@
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
},
"node-schedule": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz",
"integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==",
"requires": {
"cron-parser": "^4.2.0",
"long-timeout": "0.1.1",
"sorted-array-functions": "^1.3.0"
}
},
"node-uuid": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
@ -17012,6 +17083,11 @@
"flatstr": "^1.0.12"
}
},
"sorted-array-functions": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz",
"integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA=="
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",

@ -43,6 +43,7 @@
"mongodb": "^5.0.1",
"mongoose": "^5.11.8",
"multer-gridfs-storage": "^5.0.2",
"node-schedule": "^2.1.1",
"nodemon": "^2.0.20",
"nunjucks": "^3.2.3",
"point-of-view": "^5.3.0",

@ -6,6 +6,7 @@ const fastify = require("fastify")({
});
const moment = require('moment');
const schedule = require('node-schedule');
@ -57,6 +58,54 @@ const moment = require('moment');
// }
// };
// exports.friendRequest = async (request, reply) => {
// try {
// const customerId = request.body.customerId;
// const supplierId = request.body.supplierId;
// const sender = await User.findOne({ customerId: customerId });
// const receiver = await Supplier.findOne({ supplierId: supplierId });
// if (!sender || !receiver) {
// throw new Error('Sender or receiver not found');
// }
// const existingRequest = await FriendRequest.findOne({ customerId: sender.customerId, supplierId: receiver.supplierId});
// if (existingRequest) {
// throw new Error('Friend request already sent');
// }
// const timestamp = moment().format('MM/DD/YYYY, hh:mm:ss A');
// const friendRequest = new FriendRequest({
// customerId: sender.customerId,
// supplierId: receiver.supplierId,
// timestamp: timestamp
// });
// await friendRequest.save();
// // Schedule the request to be sent again in 5 hours
// const rule = new schedule.RecurrenceRule();
// rule.hour = new schedule.Range(0, 23, 5); // Run every 5 hours
// schedule.scheduleJob(rule, async function() {
// const newTimestamp = moment().format('MM/DD/YYYY, hh:mm:ss A');
// const newFriendRequest = new FriendRequest({
// customerId: sender.customerId,
// supplierId: receiver.supplierId,
// timestamp: newTimestamp
// });
// await newFriendRequest.save();
// });
// reply.send({ timestamp: friendRequest.timestamp, message: 'Friend request sent' });
// } catch (err) {
// reply.status(400).send({ error: err.message });
// }
// };
exports.friendRequest = async (request, reply) => {
try {
const customerId = request.body.customerId;
@ -72,20 +121,35 @@ exports.friendRequest = async (request, reply) => {
const existingRequest = await FriendRequest.findOne({ customerId: sender.customerId, supplierId: receiver.supplierId});
if (existingRequest) {
throw new Error('Friend request already sent');
existingRequest.timestamp = moment().format('MM/DD/YYYY, hh:mm:ss A');
await existingRequest.save();
reply.send({ timestamp: existingRequest.timestamp, message: 'Friend request sent again' });
} else {
const timestamp = moment().format('MM/DD/YYYY, hh:mm:ss A');
const friendRequest = new FriendRequest({
customerId: sender.customerId,
supplierId: receiver.supplierId,
timestamp: timestamp
});
await friendRequest.save();
// Schedule the request to be sent again in 5 hours
const rule = new schedule.RecurrenceRule();
rule.hour = new schedule.Range(0, 23, 5); // Run every 5 hours
schedule.scheduleJob(rule, async function() {
const newTimestamp = moment().format('MM/DD/YYYY, hh:mm:ss A');
const newFriendRequest = new FriendRequest({
customerId: sender.customerId,
supplierId: receiver.supplierId,
timestamp: newTimestamp
});
await newFriendRequest.save();
});
reply.send({ timestamp: friendRequest.timestamp, message: 'Friend request sent' });
}
const timestamp = moment().format('MM/DD/YYYY, hh:mm:ss A');
const friendRequest = new FriendRequest({
customerId: sender.customerId,
supplierId: receiver.supplierId,
timestamp: timestamp
});
await friendRequest.save();
reply.send({ timestamp: friendRequest.timestamp, message: 'Friend request sent' });
} catch (err) {
reply.status(400).send({ error: err.message });
}

Loading…
Cancel
Save