qc-util
Version:
The utility module of the QC ecosystem.
1,486 lines (1,371 loc) • 54.7 kB
JavaScript
/* eslint no-magic-numbers: ["error", { ignore: [-1, 0, 1, 3, 10, 28, 30, 31, 100, 400], ignoreArrayIndexes: true }] */
/* eslint no-bitwise: "off" */
'use strict';
/**
* A set of date related methods.
*
* The {@link module:Date.convert convert} method has `format` options. The format consists of the symbols used by
* the [PHP date() function](http://www.php.net/date) and character literals. The current implementation does not
* support all the symbols but all symbols are reserved. Therefore, format symbols must be escaped with `\` to render
* them as character literals even if the symbol is not currently supported.
*
* The currently supported symbols are listed below:
* <pre>
* Symbol Description Example returned values
* ------ ----------------------------------------------------------- -----------------------
* d Day of the month, 2 digits with leading zeros 01 to 31
* j Day of the month without leading zeros 1 to 31
* m Numeric representation of a month, with leading zeros 01 to 12
* n Numeric representation of a month, without leading zeros 1 to 12
* Y A full numeric representation of a year, 4 digits Examples: 1999 or 2003
* a Lowercase Ante meridiem and Post meridiem am or pm
* A Uppercase Ante meridiem and Post meridiem AM or PM
* g 12-hour format of an hour without leading zeros 1 to 12
* G 24-hour format of an hour without leading zeros 0 to 23
* h 12-hour format of an hour with leading zeros 01 to 12
* H 24-hour format of an hour with leading zeros 00 to 23
* i Minutes, with leading zeros 00 to 59
* s Seconds, with leading zeros 00 to 59
* u Decimal fraction of a second Examples:
* (minimum 1 digit, arbitrary number of digits allowed) 001 (i.e. 0.001s) or
* 100 (i.e. 0.100s) or
* 999 (i.e. 0.999s) or
* 9876543210 (i.e. 0.9876543210s)
* U Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) 1193432466 or -2138434463
* O Difference to Greenwich time (GMT) in hours and minutes Example: +1030
* P Difference to Greenwich time (GMT) with colon between hours Example: -08:00
* and minutes
* c ISO 8601 date
* Notes: Examples:
* 1) If unspecified, the month / day defaults to the first 1991 or
* month / day, the time defaults to midnight, while the 1992-10 or
* timezone defaults to the browser's timezone. If a time 1993-09-20 or
* is specified, it must include both hours and minutes. 1994-08-19T16:20+01:00 or
* The "T" delimiter, seconds, milliseconds, and timezone 1995-07-18T17:21:28-02:00 or
* are optional.
* 2) The decimal fraction of a second, if specified, must 1996-06-17T18:22:29.98765+03:00 or
* contain at least 1 digit (there is no limit to the 1997-05-16T19:23:30,12345-0400 or
* maximum number of digits allowed), and may be delimited 1998-04-15T20:24:31.2468Z or
* by either a '.' or a ','.
* Refer to the examples on the right for the various levels 1999-03-14T20:24:32Z or
* of date-time granularity which are supported, or see 2000-02-13T21:25:33
* http://www.w3.org/TR/NOTE-datetime for more info. 2001-01-12 22:26:34
* </pre>
*
* The currently unsupported but reserved symbols are listed below:
* <pre>
* Symbol Description Example returned values
* ------ ----------------------------------------------------------- -----------------------
* D A short textual representation of the day of the week Mon to Sun
* l A full textual representation of the day of the week Sunday to Saturday
* N ISO-8601 numeric representation of the day of the week 1 (for Monday) through 7 (for Sunday)
* S English ordinal suffix for the day of the month, 2 st, nd, rd or th. Works well with j
* characters
* w Numeric representation of the day of the week 0 (for Sunday) to 6 (for Saturday)
* z The day of the year (starting from 0) 0 to 364 (365 in leap years)
* W ISO-8601 week number of year, weeks starting on Monday 01 to 53
* F A full textual representation of a month, such as January January to December
* or March
* M A short textual representation of a month Jan to Dec
* t Number of days in the given month 28 to 31
* L Whether it's a leap year 1 if it is a leap year, 0 otherwise.
* o ISO-8601 year number (identical to (Y), but if the ISO week Examples: 1998 or 2004
* number (W) belongs to the previous or next year, that year
* is used instead)
* y A two digit representation of a year Examples: 99 or 03
* B Swatch Internet time. 000 to 999
* I Whether or not the date is in daylight saving time. 1 is Daylight Saving Time,
* 0 otherwise.
* e Timezone identifier. Examples: UTC, GMT, Atlantic/Azores
* T Timezone abbreviation of the machine running the code Examples: EST, MDT, PDT ...
* Z Timezone offset in seconds (negative if west of UTC, -43200 to 50400
* positive if east)
* r RFC 2822 formatted date. Ex: Thu, 21 Dec 2000 16:01:07 +0200
* </pre>
* @module Date
*/
// NOTE: See http://dygraphs.com/date-formats.html for browser support of various date formats.
// NOTE: Check out https://jsperf.com/date-formatting/8 for performance of date formatting.
// NOTE: See https://docs.google.com/spreadsheets/d/1lPzBlmJVAkN6HUw28wBJmY1SdbSInRIBY0JCmBUDUmk/edit?hl=en_US#gid=0
// for a spreadsheet of various date formatting tokens.
let DTE;
const wrap = require('./Array').wrap;
const toInt = require('./Number').toInt;
const escapeRegX = require('./RegExp').escape;
const escapeStr = require('./String').escape;
const mergeStr = require('./String').merge;
const trim = require('./String').trim;
const typeOf = require('./TypeOf').typeOf;
const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const DAYS_IN_FEB_DURING_LEAP_YEAR = 29;
const LEAST_DAYS_IN_MONTH = 28;
const MILLISECONDS_IN_DAY = 86400000;
const MILLISECONDS_IN_HOUR = 3600000;
const MILLISECONDS_IN_MINUTE = 60000;
const MILLISECONDS_IN_SECOND = 1000;
const MINUTES_IN_HOUR = 60;
const MONTHS_IN_YEAR = 12;
const ISO8601_REGEXPS_LUT = [
// YYYY-MM-DDThh:mm:ss.sssTZD
{
regexp: /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})[.,](\d+)([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, m: 2, d: 3, h: 4, i: 5, s: 6, ms: 7, tzo: 8
},
// YYYY-MM-DDThh:mm:ss.sss
{
regexp: /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})[.,](\d+)$/i,
y: 1, m: 2, d: 3, h: 4, i: 5, s: 6, ms: 7
},
// YYYY-MM-DDThh:mm:ssTZD
{
regexp: /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, m: 2, d: 3, h: 4, i: 5, s: 6, tzo: 7
},
// YYYY-MM-DDThh:mm:ss
{
regexp: /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/i,
y: 1, m: 2, d: 3, h: 4, i: 5, s: 6
},
// YYYY-MM-DDThh:mmTZD
{
regexp: /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, m: 2, d: 3, h: 4, i: 5, tzo: 6
},
// YYYY-MM-DDThh:mm
{
regexp: /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/i,
y: 1, m: 2, d: 3, h: 4, i: 5
},
// YYYY-MM-DDThhTZD
{
regexp: /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2})([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, m: 2, d: 3, h: 4, tzo: 5
},
// YYYY-MM-DDThh
{
regexp: /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2})$/i,
y: 1, m: 2, d: 3, h: 4
},
// YYYY-MM-DDTTZD
{
regexp: /^(\d{4})-(\d{2})-(\d{2})[ T]([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, m: 2, d: 3, tzo: 4
},
// YYYY-MM-DD
{
regexp: /^(\d{4})-(\d{2})-(\d{2})T?$/i,
y: 1, m: 2, d: 3
},
// YYYY-MMThh:mm:ss.sssTZD
{
regexp: /^(\d{4})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})[.,](\d+)([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, m: 2, h: 3, i: 4, s: 5, ms: 6, tzo: 7
},
// YYYY-MMThh:mm:ss.sss
{
regexp: /^(\d{4})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})[.,](\d+)$/i,
y: 1, m: 2, h: 3, i: 4, s: 5, ms: 6
},
// YYYY-MMThh:mm:ssTZD
{
regexp: /^(\d{4})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, m: 2, h: 3, i: 4, s: 5, tzo: 6
},
// YYYY-MMThh:mm:ss
{
regexp: /^(\d{4})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/i,
y: 1, m: 2, h: 3, i: 4, s: 5
},
// YYYY-MMThh:mmTZD
{
regexp: /^(\d{4})-(\d{2})[ T](\d{2}):(\d{2})([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, m: 2, h: 3, i: 4, tzo: 5
},
// YYYY-MMThh:mm
{
regexp: /^(\d{4})-(\d{2})[ T](\d{2}):(\d{2})$/i,
y: 1, m: 2, h: 3, i: 4
},
// YYYY-MMThhTZD
{
regexp: /^(\d{4})-(\d{2})[ T](\d{2})([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, m: 2, h: 3, tzo: 4
},
// YYYY-MMThh
{
regexp: /^(\d{4})-(\d{2})[ T](\d{2})$/i,
y: 1, m: 2, h: 3
},
// YYYY-MMTTZD
{
regexp: /^(\d{4})-(\d{2})[ T]([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, m: 2, tzo: 3
},
// YYYY-MM
{
regexp: /^(\d{4})-(\d{2})T?$/i,
y: 1, m: 2
},
// YYYYThh:mm:ss.sssTZD
{
regexp: /^(\d{4})[ T](\d{2}):(\d{2}):(\d{2})[.,](\d+)([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, h: 2, i: 3, s: 4, ms: 5, tzo: 6
},
// YYYYThh:mm:ss.sss
{
regexp: /^(\d{4})[ T](\d{2}):(\d{2}):(\d{2})[.,](\d+)$/i,
y: 1, h: 2, i: 3, s: 4, ms: 5
},
// YYYYThh:mm:ssTZD
{
regexp: /^(\d{4})[ T](\d{2}):(\d{2}):(\d{2})([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, h: 2, i: 3, s: 4, tzo: 5
},
// YYYYThh:mm:ss
{
regexp: /^(\d{4})[ T](\d{2}):(\d{2}):(\d{2})$/i,
y: 1, h: 2, i: 3, s: 4
},
// YYYYThh:mmTZD
{
regexp: /^(\d{4})[ T](\d{2}):(\d{2})([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, h: 2, i: 3, tzo: 4
},
// YYYYThh:mm
{
regexp: /^(\d{4})[ T](\d{2}):(\d{2})$/i,
y: 1, h: 2, i: 3
},
// YYYYThhTZD
{
regexp: /^(\d{4})[ T](\d{2})([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, h: 2, tzo: 3
},
// YYYYThh
{
regexp: /^(\d{4})[ T](\d{2})$/i,
y: 1, h: 2
},
// YYYYTTZD
{
regexp: /^(\d{4})[ T]([+-]\d{2}:?\d{2}|Z|[+-]\d{2})$/i,
y: 1, tzo: 2
},
// YYYY
{
regexp: /^(\d{4})T?$/i,
y: 1
}
];
const TIMEZONE_OFFSET = /^([+-]\d{2}):?(\d{2})$/;
// ==========================================================================
/**
* The milliseconds unit constant used by the {@link module:Date.add add} and {@link module:Date.diff diff} functions.
*
* @private
* @const {string} module:Date.MILLISECONDS
*/
const MILLISECONDS = 'milliseconds';
/**
* The seconds unit constant used by the {@link module:Date.add add} and {@link module:Date.diff diff} functions.
*
* @private
* @const {string} module:Date.SECONDS
*/
const SECONDS = 'seconds';
/**
* The minutes unit constant used by the {@link module:Date.add add} and {@link module:Date.diff diff} functions.
*
* @private
* @const {string} module:Date.MINUTES
*/
const MINUTES = 'minutes';
/**
* The hours unit constant used by the {@link module:Date.add add} and {@link module:Date.diff diff} functions.
*
* @private
* @const {string} module:Date.HOURS
*/
const HOURS = 'hours';
/**
* The day unit constant used by the {@link module:Date.add add} and {@link module:Date.diff diff} functions.
*
* @private
* @const {string} module:Date.DAY
*/
const DAY = 'day';
/**
* The month unit constant used by the {@link module:Date.add add} and {@link module:Date.diff diff} functions.
*
* @private
* @const {string} module:Date.MONTH
*/
const MONTH = 'month';
/**
* The year unit constant used by the {@link module:Date.add add} and {@link module:Date.diff diff} functions.
*
* @private
* @const {string} module:Date.YEAR
*/
const YEAR = 'year';
// ==========================================================================
/**
* Adds/subtracts an interval from the specified date. The specified date is not modified. A new date is created
* representing the results of the calculation.
*
* ```js
* // Basic usage:
* var stPattysDay = add(new Date(1976, 2, 12), DAY, 5);
* var feb29th = add(new Date(2000, 0, 31), MONTH, 1);
*
* // Negative increment will be subtracted:
* var christmas = add(new Date(2000, 11, 30), DAY, -5);
* ```
*
* NOTE: Non-date input just falls through. It is not converted to a date before adding.
*
* @private
* @function module:Date.add
*
* @param {Date} date - The date to add/subtract the interval.
* @param {string} unit - A valid date unit enum value. Must be one of {@link module:Date.YEAR YEAR},
* {@link module:Date.MONTH MONTH}, {@link module:Date.DAY DAY}, {@link module:Date.HOURS HOURS},
* {@link module:Date.MINUTES MINUTES}, {@link module:Date.SECONDS SECONDS}, or
* {@link module:Date.MILLISECONDS MILLISECONDS}.
* @param {number} increment - The amount to add to the specified date. Negative values will substract from the date.
* Must be an integral number.
*
* @returns {Date} A new Date instance with the interval added/subtracted.
*/
function add(date, unit, increment) {
/*
* Dependencies:
* - Date.__add
* + Date.clone
* + Date.getFirstDateOfMonth
* + Date.toLastDateOfMonth
* - Date.getDaysInMonth
* + Date.isLeapYear
* + TypeOf.typeOf
*/
date = __add(date, unit, increment, true);
return date;
}
/**
* Adds/subtracts an interval from the specified date. The specified date is mutated.
*
* ```js
* // Basic usage:
* var stPattysDay = add(new Date(1976, 2, 12), DAY, 5);
* var feb29th = add(new Date(2000, 0, 31), MONTH, 1);
*
* // Negative increment will be subtracted:
* var christmas = add(new Date(2000, 11, 30), DAY, -5);
* ```
*
* NOTE: Non-date input just falls through. It is not converted to a date before adding.
*
* @private
* @function module:Date.addTo
*
* @param {Date} date - The date to add/subtract the interval.
* @param {string} unit - A valid date unit enum value. Must be one of {@link module:Date.YEAR YEAR},
* {@link module:Date.MONTH MONTH}, {@link module:Date.DAY DAY}, {@link module:Date.HOURS HOURS},
* {@link module:Date.MINUTES MINUTES}, {@link module:Date.SECONDS SECONDS}, or
* {@link module:Date.MILLISECONDS MILLISECONDS}.
* @param {number} increment - The amount to add to the specified date. Negative values will substract from the date.
* Must be an integral number.
*
* @returns {module:Date} A reference to the `Date` module.
*/
function addTo(date, unit, increment) {
/*
* Dependencies:
* - Date.__add
* + Date.clone
* + Date.getFirstDateOfMonth
* + Date.toLastDateOfMonth
* - Date.getDaysInMonth
* + Date.isLeapYear
* + TypeOf.typeOf
*/
__add(date, unit, increment, false);
return DTE;
}
/* eslint require-jsdoc: "off" */
function __add(date, unit, increment, makeClone) {
/*
* Dependencies:
* - Date.clone
* - Date.getFirstDateOfMonth
* - Date.toLastDateOfMonth
* + Date.getDaysInMonth
* - Date.isLeapYear
* - TypeOf.typeOf
*/
let day, firstDateOfMonth, firstDateOfNewMonth, lastDateOfNewMonth, month;
if (makeClone) {
date = clone(date);
}
if (date instanceof Date && typeOf(unit) == 'string' && increment !== 0) {
switch (unit.toLowerCase()) {
case MILLISECONDS:
date.setTime(date.getTime() + increment);
break;
case SECONDS:
date.setSeconds(date.getSeconds() + increment);
break;
case MINUTES:
date.setMinutes(date.getMinutes() + increment);
break;
case HOURS:
date.setHours(date.getHours() + increment);
break;
case DAY:
date.setDate(date.getDate() + increment);
break;
case MONTH:
day = date.getDate();
month = date.getMonth();
if (day > LEAST_DAYS_IN_MONTH) {
firstDateOfMonth = getFirstDateOfMonth(date);
firstDateOfNewMonth = __add(firstDateOfMonth, MONTH, increment, false);
lastDateOfNewMonth = toLastDateOfMonth(firstDateOfNewMonth);
day = Math.min(day, lastDateOfNewMonth.getDate());
}
date.setDate(day);
date.setMonth(month + increment);
break;
case YEAR:
date.setFullYear(date.getFullYear() + increment);
break;
}
}
return date;
}
// ==========================================================================
/**
* Clears all time information from the specified date. The specified date is mutated. A clone is not created first.
*
* ```js
* clearTime(new Date(2000, 4, 1, 13, 14, 7)); // Mon May 01 2000 00:00:00
* ```
*
* NOTE: Non-date input just falls through. It is not converted to a date before clearing.
*
* @private
* @function module:Date.clearTime
*
* @param {Date} date - The date to clear time information from.
*
* @returns {Date} The date with its time information set to midnight in local time.
*/
function clearTime(date) {
/*
* Dependencies:
* - Core JS API.
*/
if (date instanceof Date) {
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
}
return date;
}
// ==========================================================================
/**
* Returns a new `Date` instance with the exact same date and time as the specified date. Since JavaScript `Date`s are
* mutable, this is useful to use when passing dates around that may change its value unexpectedly.
*
* ```js
* var date2 = clone(date1);
* date1 === date2; // false
* ```
*
* NOTE: Non-date input just falls through. It is not converted to a date before cloning.
*
* @private
* @function module:Date.clone
*
* @param {Date} date - The date to clone.
*
* @returns {Date} The new `Date` instance.
*/
function clone(date) {
/*
* Dependencies:
* - Core JS API.
*/
if (date instanceof Date) {
date = new Date(date.getTime());
}
return date;
}
// ==========================================================================
/**
* Converts a date-like object to a date. Seven date-like objects are recognized.
*
* 1. A `Date` instance. It gets returned without modification.
* 2. A number. It will be interpreted as the number of milliseconds from the UNIX epoch.
* 3. An object with a `year`, `month` (1-based), `day` (1-based), `hours`, `minutes`, `seconds`, and `milliseconds`
* properties. The properties must all be number-like. All properties are optional. Missing year, month, or day
* information will default to their current value on the computer this function is being run. Missing hours,
* minutes, seconds, or milliseconds will default to `0`. The expected values are the same values as expected by the
* native `Date` constructor that accepts 2 to 7 arguments except for `month`. `month` is 1-based where the native
* `Date` constructor expects a 0-based month.
* 4. An array with year information at index 0, month information (1-based) at index 1, day information at index 2,
* hours information at index 3, minutes information at index 4, seconds information at index 5, and milliseconds
* information at index 6. All date part information is optional. Missing year, month, or day information will
* default to their current value on the computer this function is being run. Missing hours, minutes, seconds, or
* milliseconds will default to `0`. The expected values are the same values as expected by the native `Date`
* constructor that accepts 2 to 7 arguments except for `month`. `month` is 1-based where the native `Date`
* constructor expects a 0-based month.
* 5. A string in a parsible date format. The format consists of the symbols expected by PHP's date() function.
* The supported and reserved symbols are listed in the module's documentation. The format string may also be the
* pre-defined format strings. `'now'` will return a `Date` with the current date and time. `'today'` will return
* a `Date` with the current local date. These can be especially useful when multiple formats are declared and
* `'now'` or `'today'` is the last. This allows unresolvable input to default to a valid `Date`.
* 6. A string in one of the most common ISO 8601 date formats. The syntax of the supported formats are as follows:
* `YYYY-MM-DDThh:mm:ss.sssTZD` (same as the format used by the `Date#toISOString` method),
* `YYYY-MM-DDThh:mm:ss.sss`, `YYYY-MM-DDThh:mm:ssTZD`, `YYYY-MM-DDThh:mm:ss`, `YYYY-MM-DDThh:mmTZD`,
* `YYYY-MM-DDThh:mm`, `YYYY-MM-DDThhTZD`, `YYYY-MM-DDThh`, `YYYY-MM-DDTTZD`, `YYYY-MM-DD`,
* `YYYY-MMThh:mm:ss.sssTZD`, `YYYY-MMThh:mm:ss.sss`, `YYYY-MMThh:mm:ssTZD`, `YYYY-MMThh:mm:ss`, `YYYY-MMThh:mmTZD`,
* `YYYY-MMThh:mm`, `YYYY-MMThhTZD`, `YYYY-MMThh`, `YYYY-MMTTZD`, `YYYY-MM`, `YYYYThh:mm:ss.sssTZD`,
* `YYYYThh:mm:ss.sss`, `YYYYThh:mm:ssTZD`, `YYYYThh:mm:ss`, `YYYYThh:mmTZD`, `YYYYThh:mm`, `YYYYThhTZD`, `YYYYThh`,
* `YYYYTTZD`, and `YYYY`.
* 7. A Moment-like instance from the Moment.js library. A Moment-like object is an object which has a property named
* `toDate` that is a function which returns a `Date` instance when called with no arguments.
*
* Example Usage:
*
* ```js
* var date = convert('2001-08-11T07:42:00', { formats: 'Y-m-d\\TH:i:s' });
* date = convert('now'); // options don't matter.
* date = convert('today'); // options don't matter.
* date = convert(new Date()); // options don't matter.
* date = convert(1234567890); // options don't matter.
* date = convert({ year: 2000, month: 2, day: 29 });
* date = convert([2000, 2, 29]);
* date = convert('Possibly parsible string', { formats: ['Y-m-d\\TH:i:s', 'now'] });
* date = convert('Possibly parsible string', { formats: ['m/d/Y', 'today'] });
* ```
*
* See the Jasmine Specs for more example uses.
*
* NOTE: This function has many other features not found in some other third-party libraries. Notably: 1) Accepts more
* forms of date-like input. 2) Attempts parsing of several formats. 3) Can fallback to a declared default value.
*
* @function module:Date.convert
*
* @param {(Date|Object|number|number[]|string)} input - The value to convert to a `Date` instance.
* @param {Object} [options] - The options to use when parsing.
* @param {*} [options.def=null] - The default value to return if unable to convert. May also be `'now'` or
* `'today'`. `'now'` will return a `Date` with the current date and time. `'today'` will return a `Date` with the
* current local date.
* @param {(string|string[])} [options.formats] - The format(s) the string input may be in that should be tried when
* converting. Each is tried in order until one succeeds or all are attempted. May be or contain one or more of the
* pre-defined formats.
* @param {boolean} [options.strict=false] - `true` to assert resultant dates are valid. Invalid date strings will
* return `null`.
*
* @returns {(Date|*)} The input converted to a date or the default value if unable to convert.
*/
function convert(input, options) {
/*
* Dependencies:
* - Array.wrap
* - Date.__construct
* + Date.addTo
* - Date.__add
* + Date.clone
* + Date.getFirstDateOfMonth
* + Date.toLastDateOfMonth
* - Date.getDaysInMonth
* + Date.isLeapYear
* + TypeOf.typeOf
* + Date.isValid
* - Date.addTo
* + Date.__add
* - Date.clone
* - Date.getFirstDateOfMonth
* - Date.toLastDateOfMonth
* + Date.getDaysInMonth
* - Date.isLeapYear
* - TypeOf.typeOf
* + Number.toInt
* - Number.convert
* + Math.round
* - Math._decimalAdjust
* + TypeOf.typeOf
* + TypeOf.typeOf
* - Date.__createParser
* + Date.__construct
* - ...
* + Date.__parse
* - ...
* + String.escape
* + String.merge
* - Date.__parse
* + ...
* - Date.now
* - Date.today
* + Date.clearTime
* - Number.toInt
* + Number.convert
* - Math.round
* + Math._decimalAdjust
* - TypeOf.typeOf
* - TypeOf.typeOf
* - String.trim
* - TypeOf.typeOf
*/
let defValue, format, formats, output, parser, parserLut, scope, str, strict, typeOfInput;
options = options || {};
// If input is a moment instance or like a moment instance with a `toDate` function, then attempt to convert to a Date.
if (input && typeof input.toDate == 'function') {
input = input.toDate();
}
if (input instanceof Date) {
return input;
}
strict = options.strict === true;
typeOfInput = typeOf(input);
if (typeOfInput == 'number') {
input = toInt(input);
output = new Date(input);
}
else if (typeOfInput == 'string') {
str = trim(input).toLowerCase();
parserLut = __parse.parserLut;
if (str == 'now' || str == 'today') {
parser = parserLut[str];
output = parser(str, strict);
}
else {
formats = options.formats || [];
formats = wrap(formats);
formats = __injectMissingIso8601Format(formats);
scope = {
__construct,
__parse,
__parseISO8601,
};
for (let i = 0, len = formats.length; i < len; ++i) {
format = formats[i];
parser = parserLut[format];
if (!parser) {
parser = __createParser(format);
}
output = parser.call(scope, input, strict);
if (output instanceof Date) {
break;
}
}
}
}
else if (input && typeOfInput == 'array') {
parser = __parse.parserLut.array;
output = parser(input, strict);
}
else if (input && typeOfInput == 'object') {
parser = __parse.parserLut.struct;
output = parser(input, strict);
}
if (!(output instanceof Date)) {
defValue = options.hasOwnProperty('def') ? options.def : null;
if (defValue == 'now') {
defValue = now();
}
else if (defValue == 'today') {
defValue = today();
}
output = defValue;
}
return output;
}
// ==========================================================================
/**
* An alias for {@link module:Boolean.convert convert}. This is useful when importing this module, especially when
* importing a `convert` function from multiple modules.
*
* Use the following
* ```js
* import { toDate } from 'Date';
* import { toStr } from 'String';
* ```
* instead of
* ```js
* import { convert as toDate } from 'Date';
* import { convert as toStr } from 'String';
* ```
*
* @function module:Date.toDate
*/
const toDate = convert;
// ==========================================================================
/**
* Determines the difference between two dates.
*
* Example Usage:
*
* ```js
* var date1 = new Date(2010, 6, 4, 12, 0, 0);
* var date2 = new Date(2010, 6, 4, 12, 31, 59);
* diff(date1, date1, MINUTES); // 31 full minutes
* ```
*
* See the Jasmine Specs for more example uses.
*
* @private
* @function module:Date.diff
*
* @param {Date} date1 - The first date to check.
* @param {Date} date2 - The second date to check.
* @param {string} unit - A valid date unit enum value. Must be one of {@link module:Date.YEAR YEAR},
* {@link module:Date.MONTH MONTH}, {@link module:Date.DAY DAY}, {@link module:Date.HOURS HOURS},
* {@link module:Date.MINUTES MINUTES}, {@link module:Date.SECONDS SECONDS}, or
* {@link module:Date.MILLISECONDS MILLISECONDS}.
*
* @returns {number} The difference between the two dates as an integer. This value is not rounded.
*/
function diff(date1, date2, unit) {
/*
* Dependencies:
* - Date.add
* + Date.__add
* - Date.clone
* - Date.getFirstDateOfMonth
* - Date.toLastDateOfMonth
* + Date.getDaysInMonth
* - Date.isLeapYear
* - TypeOf.typeOf
* - TypeOf.typeOf
*/
let difference, msec1, msec2;
msec1 = date1.getTime();
msec2 = date2.getTime();
if (typeOf(date1) == 'date' && typeOf(date2) == 'date' && typeOf(unit) == 'string') {
switch (unit.toLowerCase()) {
case MILLISECONDS:
difference = msec2 - msec1;
break;
case SECONDS:
difference = (msec2 - msec1) / MILLISECONDS_IN_SECOND;
break;
case MINUTES:
difference = (msec2 - msec1) / MILLISECONDS_IN_MINUTE;
break;
case HOURS:
difference = (msec2 - msec1) / MILLISECONDS_IN_HOUR;
break;
case DAY:
difference = (msec2 - msec1) / MILLISECONDS_IN_DAY;
break;
case MONTH:
difference = ((date2.getFullYear() * MONTHS_IN_YEAR) + date2.getMonth()) - ((date1.getFullYear() * MONTHS_IN_YEAR) + date1.getMonth());
if (add(date1, unit, difference) > date2) {
difference -= 1;
}
break;
case YEAR:
difference = date2.getFullYear() - date1.getFullYear();
if (add(date1, unit, difference) > date2) {
difference -= 1;
}
break;
default:
throw new TypeError('`unit` must be a valid date/time unit.');
}
}
else {
throw new TypeError('`date1` and `date2` must be Date instances and `unit` must be a string of a valid date/time unit.');
}
if (difference < 0) {
difference = Math.ceil(difference);
}
else if (difference > 0) {
difference = Math.floor(difference);
}
return difference;
}
// ==========================================================================
/**
* Get the number of days in the specified month, adjusted for leap year.
*
* ```js
* getDaysInMonth(new Date(2000, 1)); // 29
* ```
*
* @private
* @function module:Date.getDaysInMonth
*
* @param {Date} date - The date to examine. The local, as opposed to UTC, month of the date is used to determine which
* month is being specified.
*
* @returns {(number|NaN)} The number of days in the month.
*/
function getDaysInMonth(date) {
/*
* Dependencies:
* - Date.isLeapYear
*/
let days = NaN,
month;
if (date instanceof Date) {
month = date.getMonth();
days = month === 1 && isLeapYear(date) ? DAYS_IN_FEB_DURING_LEAP_YEAR : DAYS_IN_MONTH[month];
}
return days;
}
// ==========================================================================
/**
* Get the date of the first day of the month in which the specified date resides.
*
* ```js
* getFirstDateOfMonth(new Date(2000, 1, 29)); // 2000-02-01
* ```
*
* NOTE: Any time information the specified date may have had will be cleared out.
*
* @private
* @function module:Date.getFirstDateOfMonth
*
* @param {Date} date - The date to examine.
*
* @returns {Date|null} A new date reset to the beginning of the month.
*/
function getFirstDateOfMonth(date) {
/*
* Dependencies:
* - Core JS API.
*/
let firstDateOfMonth = null;
if (date instanceof Date) {
firstDateOfMonth = new Date(date.getFullYear(), date.getMonth(), 1);
}
return firstDateOfMonth;
}
// ==========================================================================
/**
* Get the date of the last day of the month in which the specified date resides.
*
* ```js
* getLastDateOfMonth(new Date(2000, 1, 1)); // 2000-02-29
* ```
*
* NOTE: Any time information the specified date may have had will be cleared out.
*
* @private
* @function module:Date.getLastDateOfMonth
*
* @param {Date} date - The date to examine.
*
* @returns {Date|null} A new date reset to the end of the month.
*/
function getLastDateOfMonth(date) {
/*
* Dependencies:
* - Date.getDaysInMonth
* + Date.isLeapYear
*/
let lastDateOfMonth = null;
if (date instanceof Date) {
lastDateOfMonth = new Date(date.getFullYear(), date.getMonth(), getDaysInMonth(date));
}
return lastDateOfMonth;
}
function toLastDateOfMonth(date) {
/*
* Dependencies:
* - Date.getDaysInMonth
* + Date.isLeapYear
*/
if (date instanceof Date) {
date.setDate(getDaysInMonth(date));
clearTime(date);
}
return date;
}
// ==========================================================================
/**
* Determines if the specified date falls within a leap year. If `date` is a `Date` instance, then `true` or `false`
* is returned. Otherwise `null` is returned. Either way, a falsy value is returned if date is not determined to
* be a leap year.
*
* ```js
* isLeapYear(true); // null
* isLeapYear(new Date(2000, 0)); // true
* isLeapYear(new Date(2200, 0)); // false
* ```
*
* @private
* @function module:Date.isLeapYear
*
* @param {Date} date - The date to check.
*
* @returns {boolean|null} `true`, `false`, or `null` depending on whether date falls within leap year.
*/
function isLeapYear(date) {
/*
* Dependencies:
* - Core JS API.
*/
let answer = null,
year;
if (date instanceof Date) {
year = date.getFullYear();
answer = !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year)));
}
return answer;
}
// ==========================================================================
/**
* Determines if the specified date information will cause a JavaScript Date "rollover". The date information is
* invalid if a Date "rollover" occurs.
*
* Note: Invalid date information does not mean the date constructor will fail if this information is used to create a
* date instance. It does mean, however, that the input date part information will be different than some of the date
* part information returned by some of the getter methods (e.g., getFullYear, getMonth, getDate, getHours, etc).
*
* ```js
* // The following date information will cause a rollover into July.
* isValid(2001, 6, 31); // False. Only 30 days in June.
* ```
*
* @private
* @function module:Date.isValid
*
* @param {number} y - 4-digit year.
* @param {number} m - 1-based month.
* @param {number} [d=1] - Day of month.
* @param {number} [h=0] - Hours.
* @param {number} [i=0] - Minutes.
* @param {number} [s=0] - Seconds.
* @param {number} [ms=0] - Milliseconds.
*
* @returns {boolean} `true` if the specified date information does not cause a Date "rollover", `false` otherwise.
*/
function isValid(y, m, d, h, i, s, ms) {
/*
* Dependencies:
* - Date.addTo
* - Date.__add
* + Date.clone
* + Date.getFirstDateOfMonth
* + Date.toLastDateOfMonth
* - Date.getDaysInMonth
* + Date.isLeapYear
* + TypeOf.typeOf
*/
let dt, isDtValid;
m = (m || 1) - 1;
d = d || 1;
h = h || 0;
i = i || 0;
s = s || 0;
ms = ms || 0;
// Special handling for year < 100
const YR_THRESHOLD = 100;
dt = new Date(y < YR_THRESHOLD ? 400 : y, m, d, h, i, s, ms);
if (y < YR_THRESHOLD) {
addTo(dt, YEAR, y - 400);
}
isDtValid = (
y == dt.getFullYear() &&
m == dt.getMonth() &&
d == dt.getDate() &&
h == dt.getHours() &&
i == dt.getMinutes() &&
s == dt.getSeconds() &&
ms == dt.getMilliseconds()
);
return isDtValid;
}
// ==========================================================================
/**
* Returns a date representing the current date *and* time at the time of the function call.
*
* ```js
* now(); // e.g., 2010-09-08 07:06:54
* ```
*
* NOTE: This function returns a date object unlike the native `Date.now` function which returns a number.
*
* @private
* @function module:Date.now
*
* @returns {Date} The current date and time.
*/
function now() {
/*
* Dependencies:
* - Core JS API.
*/
return new Date();
}
// ==========================================================================
/**
* Returns a date representing the current local date at the time of the function call.
*
* ```js
* today(); // e.g., 2010-09-08 00:00:00
* ```
*
* @private
* @function module:Date.today
*
* @returns {Date} The current date with its time information set to midnight in local time.
*/
function today() {
/*
* Dependencies:
* - Date.clearTime
*/
let todayDt = clearTime(new Date());
return todayDt;
}
// ==========================================================================
function __construct(y, m, d, h, i, s, ms, tzo, strict) {
/*
* Dependencies:
* - Date.addTo
* + Date.__add
* - Date.clone
* - Date.getFirstDateOfMonth
* - Date.toLastDateOfMonth
* + Date.getDaysInMonth
* - Date.isLeapYear
* - TypeOf.typeOf
* - Date.isValid
* + Date.addTo
* - Date.__add
* + Date.clone
* + Date.getFirstDateOfMonth
* + Date.toLastDateOfMonth
* - Date.getDaysInMonth
* + Date.isLeapYear
* + TypeOf.typeOf
* - Number.toInt
* + Number.convert
* - Math.round
* + Math._decimalAdjust
* - TypeOf.typeOf
* - TypeOf.typeOf
*/
let commonOptions, dt;
dt = new Date();
y = toInt(y, { def: dt.getFullYear() });
m = toInt(m, { def: dt.getMonth() + 1 }) - 1;
d = toInt(d, { def: dt.getDate() });
commonOptions = { def: 0 };
h = toInt(h, commonOptions);
i = toInt(i, commonOptions);
s = toInt(s, commonOptions);
ms = toInt(ms, commonOptions);
tzo = __parseTimezoneOffset(tzo);
if (strict === true && !isValid(y, m + 1, d, h, i, s, ms)) { // Check for Date "rollover"
dt = void 0;
}
else {
const YR_THRESHOLD = 100;
// If year is < 100, then set year to 100 and then subtract y - 100 years from date.
if (typeof tzo == 'number') {
dt = new Date(Date.UTC(y < YR_THRESHOLD ? 400 : y, m, d, h, i, s, ms));
addTo(dt, MINUTES, -tzo);
}
else {
dt = new Date(y < YR_THRESHOLD ? 400 : y, m, d, h, i, s, ms);
}
if (y < YR_THRESHOLD) {
addTo(dt, YEAR, y - 400);
}
}
return dt;
}
// ==========================================================================
const NotImplementedButReserved = {
groupCount: 0,
pattern: '',
};
// ==========================================================================
const __parse = {
/*
* Parser Info Lookup Table:
*
* Keyed by parse symbol.
*
* - calculation: The calculation to perform when specified symbol is in format. This is a string template
* that MUST have a single placeholder which will be replaced with the current results index. See some of
* the calculation templates below to get an idea of what one should look like.
* - groupCount: The number of regular expression group references in the regular expression's pattern.
* - pattern: The regular expression pattern to use to extract out the information needed for the parser
* calculation.
*/
infoLut: {
/* Day: */
// Day of the month with leading zeroes (01 - 31).
d: {
calculation: 'd = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '(\\d{2})',
},
D: NotImplementedButReserved,
// Day of the month without leading zeroes (1 - 31).
j: {
calculation: 'd = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '(\\d{1,2})',
},
l: NotImplementedButReserved,
N: NotImplementedButReserved,
S: NotImplementedButReserved,
w: NotImplementedButReserved,
z: NotImplementedButReserved,
/* Week: */
W: NotImplementedButReserved,
/* Month: */
F: NotImplementedButReserved,
// Month number with leading zeros (01 - 12).
m: {
calculation: 'm = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '(\\d{2})',
},
M: NotImplementedButReserved,
// Month number without leading zeros (1 - 12).
n: {
calculation: 'm = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '(\\d{1,2})',
},
t: NotImplementedButReserved,
/* Year: */
L: NotImplementedButReserved,
o: NotImplementedButReserved,
// 4-digit year
Y: {
calculation: 'y = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '(\\d{4})',
},
y: NotImplementedButReserved,
/* Time: */
// Lowercase Ante meridiem and Post meridiem (am or pm).
a: {
/*
* NOTE: This calculation expects that the hours have already been determined. All known date formats
* using a meridiem symbol always come after the hours symbol.
*/
calculation: [
'if (/(am)/i.test(results[{0}])) {',
'if (!h || h == 12) { h = 0; }',
'}',
'else {',
'if (!h || h < 12) { h = (h || 0) + 12; }',
'}',
].join('\n'),
groupCount: 1,
pattern: '(am|pm)',
},
// Uppercase Ante meridiem and Post meridiem (AM or PM).
A: {
/*
* NOTE: This calculation expects that the hours have already been determined. All known date formats
* using a meridiem symbol always come after the hours symbol.
*/
calculation: [
'if (/(am)/i.test(results[{0}])) {',
'if (!h || h == 12) { h = 0; }',
'}',
'else {',
'if (!h || h < 12) { h = (h || 0) + 12; }',
'}',
].join('\n'),
groupCount: 1,
pattern: '(AM|PM)',
},
B: NotImplementedButReserved,
// 12-hr format of an hour without leading zeroes (1 - 12).
g: {
calculation: 'h = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '(\\d{1,2})',
},
// 24-hr format of an hour without leading zeroes (0 - 23).
G: {
calculation: 'h = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '(\\d{1,2})',
},
// 12-hr format of an hour with leading zeroes (01 - 12).
h: {
calculation: 'h = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '(\\d{2})',
},
// 24-hr format of an hour with leading zeroes (00 - 23).
H: {
calculation: 'h = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '(\\d{2})',
},
// Minutes with leading zeros (00 - 59).
i: {
calculation: 'i = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '(\\d{2})',
},
// Seconds with leading zeros (00 - 59).
s: {
calculation: 's = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '(\\d{2})',
},
// Decimal fraction of a second (minimum = 1 digit, maximum = unlimited)
u: {
calculation: 'ms = results[{0}]; ms = parseInt(ms, 10) / Math.pow(10, ms.length - 3);\n',
groupCount: 1,
pattern: '(\\d+)',
},
/* Timezone: */
e: NotImplementedButReserved,
I: NotImplementedButReserved,
// Difference to Greenwich time (GMT) in hours and minutes. Example: +1030
O: {
calculation: 'tzo = results[{0}];\n',
groupCount: 1,
pattern: '([-+]\\d{4}|[zZ]|[-+]\\d{2})',
},
// Difference to Greenwich time (GMT) with colon between hours and minutes. Example: -08:00
P: {
calculation: 'tzo = results[{0}];\n',
groupCount: 1,
pattern: '([-+]\\d{2}:\\d{2}|[zZ]|[-+]\\d{2})',
},
T: NotImplementedButReserved,
Z: NotImplementedButReserved,
/* Full Date/Time: */
c: {
calculation: 'type = "iso";\nc = this.__parseISO8601(results[{0}], strict);\n',
groupCount: 1,
pattern: '\\s*(.+)\\s*',
},
r: NotImplementedButReserved,
// Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT)
U: {
calculation: 'type = "sec";\nu = parseInt(results[{0}], 10);\n',
groupCount: 1,
pattern: '([-+]?\\d+)',
},
},
/*
* Cache of parser functions. It is keyed by the date format they handle.
*/
parserLut: {
array(input, strict) {
let dt, y, m, d, h, i, s, ms, tzo;
y = input[0];
m = input[1];
d = input[2];
h = input[3];
i = input[4];
s = input[5];
ms = input[6];
tzo = input[7];
dt = __construct(y, m, d, h, i, s, ms, tzo, strict);
return dt;
},
now(/* input, strict */) {
return now();
},
struct(input, strict) {
let dt, y, m, d, h, i, s, ms, tzo;
if (typeOf(input) == 'object') {
y = input.year;
m = input.month;
d = input.day;
h = input.hours;
i = input.minutes;
s = input.seconds;
ms = input.milliseconds;
tzo = input.tzo === 0 || input.tzo ? input.tzo : null;
dt = __construct(y, m, d, h, i, s, ms, tzo, strict);
}
return dt;
},
today(/* input, strict */) {
return today();
},
},
/*
* Cache of the regular expression used by the parser functions. It is keyed by the date format they handle.
*/
regExpLut: {},
};
const __createParser = (function () {
/*
* Dependencies:
* - Date.__construct
* + ...
* - Date.__parse
* + ...
* - String.escape
* - String.merge
*/
// Make a closure for efficiency. That is, don't redefine following code template each time function is called.
/*
* The parser function's code template.
*
* A parser function has 2 parameters.
*
* 1) input: The string input to parse.
* 2) string: A flag indicating whether date parsing is strict. That is, only valid dates are allowed and
* JavaScript Date "rollover" will not be allowed.
*
* The parser code template expects 2 values.
*
* 1) The date format the parser handles. (Used to lookup the regular expression used to extract date/time
* information.)
* 2) The JavaScript code that performs the calculations for the date/time information. These use the results from
* the regular expression match.
*/
/* eslint indent: "off" */
const codeTemplate = [
// function (input, strict) {
'var c = null,',
'u = null,',
'dt, y, m, d, h, i, s, ms, regExp, results, type, tzo;',
'regExp = this.__parse.regExpLut[\'{0}\'];',
'results = String(input).match(regExp);', // Either `null` or an array of matched strings
'if (results) {',
'{1}', // The set of calculations. These populate the c, u, or the y, m, d, h, i, s, ms, and tzo variables.
'switch (type) {',
'case "iso":',
'dt = c;',
'break;',
'case "sec":',
'if (u !== null) {',
'dt = new Date(u * 1000);',
'}',
'break;',
'default:',
'dt = this.__construct(y, m, d, h, i, s, ms, tzo, strict);',
'}',
'}',
'return dt;',
// };
].join('\n');
/*
* @param {string} format - The date format from which to create a parser function.
*
* @returns {Function} The parser function for the specified date format.
*/
return function (format) {
const BACK_SLASH = '\\';
let calcs = [],
literal = false,
regExp = [],
code, groupCount, parseInfo, parser, resultsIdx, symbol;
resultsIdx = 1; // The results of String#match(regExp) returns group references starting at index 1.
for (let i = 0, iLen = format.le