src/helpers/date-parser.js
'use strict';
angular.module('mgcrea.ngStrap.helpers.dateParser', [])
.provider('$dateParser', function ($localeProvider) {
// define a custom ParseDate object to use instead of native Date
// to avoid date values wrapping when setting date component values
function ParseDate () {
this.year = 1970;
this.month = 0;
this.day = 1;
this.hours = 0;
this.minutes = 0;
this.seconds = 0;
this.milliseconds = 0;
}
ParseDate.prototype.setMilliseconds = function (value) { this.milliseconds = value; };
ParseDate.prototype.setSeconds = function (value) { this.seconds = value; };
ParseDate.prototype.setMinutes = function (value) { this.minutes = value; };
ParseDate.prototype.setHours = function (value) { this.hours = value; };
ParseDate.prototype.getHours = function () { return this.hours; };
ParseDate.prototype.setDate = function (value) { this.day = value; };
ParseDate.prototype.setMonth = function (value) { this.month = value; };
ParseDate.prototype.setFullYear = function (value) { this.year = value; };
ParseDate.prototype.fromDate = function (value) {
this.year = value.getFullYear();
this.month = value.getMonth();
this.day = value.getDate();
this.hours = value.getHours();
this.minutes = value.getMinutes();
this.seconds = value.getSeconds();
this.milliseconds = value.getMilliseconds();
return this;
};
ParseDate.prototype.toDate = function () {
return new Date(this.year, this.month, this.day, this.hours, this.minutes, this.seconds, this.milliseconds);
};
var proto = ParseDate.prototype;
function noop () {
}
function isNumeric (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
function indexOfCaseInsensitive (array, value) {
var len = array.length;
var str = value.toString().toLowerCase();
for (var i = 0; i < len; i++) {
if (array[i].toLowerCase() === str) { return i; }
}
return -1; // Return -1 per the "Array.indexOf()" method.
}
var defaults = this.defaults = {
format: 'shortDate',
strict: false
};
this.$get = function ($locale, dateFilter) {
var DateParserFactory = function (config) {
var options = angular.extend({}, defaults, config);
var $dateParser = {};
/* eslint-disable key-spacing, quote-props */
var regExpMap = {
'sss' : '[0-9]{3}',
'ss' : '[0-5][0-9]',
's' : options.strict ? '[1-5]?[0-9]' : '[0-9]|[0-5][0-9]',
'mm' : '[0-5][0-9]',
'm' : options.strict ? '[1-5]?[0-9]' : '[0-9]|[0-5][0-9]',
'HH' : '[01][0-9]|2[0-3]',
'H' : options.strict ? '1?[0-9]|2[0-3]' : '[01]?[0-9]|2[0-3]',
'hh' : '[0][1-9]|[1][012]',
'h' : options.strict ? '[1-9]|1[012]' : '0?[1-9]|1[012]',
'a' : 'AM|PM',
'EEEE' : $locale.DATETIME_FORMATS.DAY.join('|'),
'EEE' : $locale.DATETIME_FORMATS.SHORTDAY.join('|'),
'dd' : '0[1-9]|[12][0-9]|3[01]',
'd' : options.strict ? '[1-9]|[1-2][0-9]|3[01]' : '0?[1-9]|[1-2][0-9]|3[01]',
'MMMM' : $locale.DATETIME_FORMATS.MONTH.join('|'),
'MMM' : $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
'MM' : '0[1-9]|1[012]',
'M' : options.strict ? '[1-9]|1[012]' : '0?[1-9]|1[012]',
'yyyy' : '[1]{1}[0-9]{3}|[2]{1}[0-9]{3}',
'yy' : '[0-9]{2}',
'y' : options.strict ? '-?(0|[1-9][0-9]{0,3})' : '-?0*[0-9]{1,4}'
};
var setFnMap = {
'sss' : proto.setMilliseconds,
'ss' : proto.setSeconds,
's' : proto.setSeconds,
'mm' : proto.setMinutes,
'm' : proto.setMinutes,
'HH' : proto.setHours,
'H' : proto.setHours,
'hh' : proto.setHours,
'h' : proto.setHours,
'EEEE' : noop,
'EEE' : noop,
'dd' : proto.setDate,
'd' : proto.setDate,
'a' : function (value) { var hours = this.getHours() % 12; return this.setHours(value.match(/pm/i) ? hours + 12 : hours); },
'MMMM' : function (value) { return this.setMonth(indexOfCaseInsensitive($locale.DATETIME_FORMATS.MONTH, value)); },
'MMM' : function (value) { return this.setMonth(indexOfCaseInsensitive($locale.DATETIME_FORMATS.SHORTMONTH, value)); },
'MM' : function (value) { return this.setMonth(1 * value - 1); },
'M' : function (value) { return this.setMonth(1 * value - 1); },
'yyyy' : proto.setFullYear,
'yy' : function (value) { return this.setFullYear(2000 + 1 * value); },
'y' : function (value) { return (1 * value <= 50 && value.length === 2) ? this.setFullYear(2000 + 1 * value) : this.setFullYear(1 * value); }
};
/* eslint-enable key-spacing, quote-props */
var regex;
var setMap;
$dateParser.init = function () {
$dateParser.$format = $locale.DATETIME_FORMATS[options.format] || options.format;
regex = regExpForFormat($dateParser.$format);
setMap = setMapForFormat($dateParser.$format);
};
$dateParser.isValid = function (date) {
if (angular.isDate(date)) return !isNaN(date.getTime());
return regex.test(date);
};
$dateParser.parse = function (value, baseDate, format, timezone) {
// check for date format special names
if (format) format = $locale.DATETIME_FORMATS[format] || format;
if (angular.isDate(value)) value = dateFilter(value, format || $dateParser.$format, timezone);
var formatRegex = format ? regExpForFormat(format) : regex;
var formatSetMap = format ? setMapForFormat(format) : setMap;
var matches = formatRegex.exec(value);
if (!matches) return false;
// use custom ParseDate object to set parsed values
var date = baseDate && !isNaN(baseDate.getTime()) ? new ParseDate().fromDate(baseDate) : new ParseDate().fromDate(new Date(1970, 0, 1, 0));
for (var i = 0; i < matches.length - 1; i++) {
if (formatSetMap[i]) formatSetMap[i].call(date, matches[i + 1]);
}
// convert back to native Date object
var newDate = date.toDate();
// check new native Date object for day values overflow
if (parseInt(date.day, 10) !== newDate.getDate()) {
return false;
}
return newDate;
};
$dateParser.getDateForAttribute = function (key, value) {
var date;
if (value === 'today') {
var today = new Date();
date = new Date(today.getFullYear(), today.getMonth(), today.getDate() + (key === 'maxDate' ? 1 : 0), 0, 0, 0, (key === 'minDate' ? 0 : -1));
} else if (angular.isString(value) && value.match(/^".+"$/)) { // Support {{ dateObj }}
if (value.match(/Z/)) {
date = new Date(value.substr(1, value.length - 3));
} else {
date = new Date(value.substr(1, value.length - 2));
}
} else if (isNumeric(value)) {
date = new Date(parseInt(value, 10));
} else if (angular.isString(value) && value.length === 0) { // Reset date
date = key === 'minDate' ? -Infinity : +Infinity;
} else {
date = new Date(value);
}
return date;
};
$dateParser.getTimeForAttribute = function (key, value) {
var time;
if (value === 'now') {
time = new Date().setFullYear(1970, 0, 1);
} else if (angular.isString(value) && value.match(/^".+"$/)) {
time = new Date(value.substr(1, value.length - 2)).setFullYear(1970, 0, 1);
} else if (isNumeric(value)) {
time = new Date(parseInt(value, 10)).setFullYear(1970, 0, 1);
} else if (angular.isString(value) && value.length === 0) { // Reset time
time = key === 'minTime' ? -Infinity : +Infinity;
} else {
time = $dateParser.parse(value, new Date(1970, 0, 1, 0));
}
return time;
};
/* Handle switch to/from daylight saving.
* Hours may be non-zero on daylight saving cut-over:
* > 12 when midnight changeover, but then cannot generate
* midnight datetime, so jump to 1AM, otherwise reset.
* @param date (Date) the date to check
* @return (Date) the corrected date
*
* __ copied from jquery ui datepicker __
*/
$dateParser.daylightSavingAdjust = function (date) {
if (!date) {
return null;
}
date.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0);
return date;
};
/* Correct the date for timezone offset.
* @param date (Date) the date to adjust
* @param timezone (string) the timezone to adjust for
* @param undo (boolean) to add or subtract timezone offset
* @return (Date) the corrected date
*/
$dateParser.timezoneOffsetAdjust = function (date, timezone, undo) {
if (!date) {
return null;
}
// Right now, only 'UTC' is supported.
if (timezone && timezone === 'UTC') {
date = new Date(date.getTime());
date.setMinutes(date.getMinutes() + (undo ? -1 : 1) * date.getTimezoneOffset());
}
return date;
};
// Private functions
function regExpForFormat (format) {
// `format` string can contain literal values.
// These need to be escaped by surrounding with
// single quotes (e.g. `"h 'in the morning'"`).
// In order to output a single quote, escape it - i.e.,
// two single quotes in a sequence (e.g. `"h 'o''clock'"`).
var re = buildDateAbstractRegex(format);
return buildDateParseRegex(re);
}
function buildDateAbstractRegex (format) {
var escapedFormat = escapeReservedSymbols(format);
var escapedLiteralFormat = escapedFormat.replace(/''/g, '\\\'');
var literalRegex = /('(?:\\'|.)*?')/;
var formatParts = escapedLiteralFormat.split(literalRegex);
var dateElements = Object.keys(regExpMap);
var dateRegexParts = [];
angular.forEach(formatParts, function (part) {
if (isFormatStringLiteral(part)) {
part = trimLiteralEscapeChars(part);
} else {
// Abstract replaces to avoid collisions
for (var i = 0; i < dateElements.length; i++) {
part = part.split(dateElements[i]).join('${' + i + '}');
}
}
dateRegexParts.push(part);
});
return dateRegexParts.join('');
}
function escapeReservedSymbols (text) {
return text.replace(/\\/g, '[\\\\]')
.replace(/-/g, '[-]')
.replace(/\./g, '[.]')
.replace(/\*/g, '[*]')
.replace(/\+/g, '[+]')
.replace(/\?/g, '[?]')
.replace(/\$/g, '[$]')
.replace(/\^/g, '[^]')
.replace(/\//g, '[/]')
.replace(/\\s/g, '[\\s]');
}
function isFormatStringLiteral (text) {
return /^'.*'$/.test(text);
}
function trimLiteralEscapeChars (text) {
return text.replace(/^'(.*)'$/, '$1');
}
function buildDateParseRegex (abstractRegex) {
var dateElements = Object.keys(regExpMap);
var re = abstractRegex;
// Replace abstracted values
for (var i = 0; i < dateElements.length; i++) {
re = re.split('${' + i + '}').join('(' + regExpMap[dateElements[i]] + ')');
}
return new RegExp('^' + re + '$', ['i']);
}
function setMapForFormat (format) {
var re = buildDateAbstractRegex(format);
return buildDateParseValuesMap(re);
}
function buildDateParseValuesMap (abstractRegex) {
var dateElements = Object.keys(regExpMap);
var valuesRegex = new RegExp('\\${(\\d+)}', 'g');
var valuesMatch;
var keyIndex;
var valueKey;
var valueFunction;
var valuesFunctionMap = [];
/* eslint-disable no-cond-assign */
while ((valuesMatch = valuesRegex.exec(abstractRegex)) !== null) {
keyIndex = valuesMatch[1];
valueKey = dateElements[keyIndex];
valueFunction = setFnMap[valueKey];
valuesFunctionMap.push(valueFunction);
}
return valuesFunctionMap;
}
$dateParser.init();
return $dateParser;
};
return DateParserFactory;
};
});