/** * XDate v0.8 * Docs & Licensing: http://arshaw.com/xdate/ */ /* * Internal Architecture * --------------------- * An XDate wraps a native Date. The native Date is stored in the '0' property of the object. * UTC-mode is determined by whether the internal native Date's toString method is set to * Date.prototype.toUTCString (see getUTCMode). * */ var XDate = (function(Date, Math, Array, undefined) { /** @const */ var FULLYEAR = 0; /** @const */ var MONTH = 1; /** @const */ var DATE = 2; /** @const */ var HOURS = 3; /** @const */ var MINUTES = 4; /** @const */ var SECONDS = 5; /** @const */ var MILLISECONDS = 6; /** @const */ var DAY = 7; /** @const */ var YEAR = 8; /** @const */ var WEEK = 9; /** @const */ var DAY_MS = 86400000; var ISO_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ss(.fff)"; var ISO_FORMAT_STRING_TZ = ISO_FORMAT_STRING + "zzz"; var methodSubjects = [ 'FullYear', // 0 'Month', // 1 'Date', // 2 'Hours', // 3 'Minutes', // 4 'Seconds', // 5 'Milliseconds', // 6 'Day', // 7 'Year' // 8 ]; var subjectPlurals = [ 'Years', // 0 'Months', // 1 'Days' // 2 ]; var unitsWithin = [ 12, // months in year 31, // days in month (sort of) 24, // hours in day 60, // minutes in hour 60, // seconds in minute 1000, // milliseconds in second 1 // ]; var formatStringRE = new RegExp( "(([a-zA-Z])\\2*)|" + // 1, 2 "(\\(" + "(('.*?'|\\(.*?\\)|.)*?)" + "\\))|" + // 3, 4, 5 (allows for 1 level of inner quotes or parens) "('(.*?)')" // 6, 7 ); var UTC = Date.UTC; var toUTCString = Date.prototype.toUTCString; var proto = XDate.prototype; // This makes an XDate look pretty in Firebug and Web Inspector. // It makes an XDate seem array-like, and displays [ .toString() ] proto.length = 1; proto.splice = Array.prototype.splice; /* Constructor ---------------------------------------------------------------------------------*/ // TODO: in future, I'd change signature for the constructor regarding the `true` utc-mode param. ~ashaw // I'd move the boolean to be the *first* argument. Still optional. Seems cleaner. // I'd remove it from the `xdate`, `nativeDate`, and `milliseconds` constructors. // (because you can simply call .setUTCMode(true) after) // And I'd only leave it for the y/m/d/h/m/s/m and `dateString` constructors // (because those are the only constructors that need it for DST-gap data-loss reasons) // Should do this for 1.0 function XDate() { return init( (this instanceof XDate) ? this : new XDate(), arguments ); } function init(xdate, args) { var len = args.length; var utcMode; if (isBoolean(args[len-1])) { utcMode = args[--len]; args = slice(args, 0, len); } if (!len) { xdate[0] = new Date(); } else if (len == 1) { var arg = args[0]; if (arg instanceof Date || isNumber(arg)) { xdate[0] = new Date(+arg); } else if (arg instanceof XDate) { xdate[0] = _clone(arg); } else if (isString(arg)) { xdate[0] = new Date(0); xdate = parse(arg, utcMode || false, xdate); } } else { xdate[0] = new Date(UTC.apply(Date, args)); if (!utcMode) { xdate[0] = coerceToLocal(xdate[0]); } } if (isBoolean(utcMode)) { setUTCMode(xdate, utcMode); } return xdate; } /* UTC Mode Methods ---------------------------------------------------------------------------------*/ proto.getUTCMode = methodize(getUTCMode); function getUTCMode(xdate) { return xdate[0].toString === toUTCString; }; proto.setUTCMode = methodize(setUTCMode); function setUTCMode(xdate, utcMode, doCoercion) { if (utcMode) { if (!getUTCMode(xdate)) { if (doCoercion) { xdate[0] = coerceToUTC(xdate[0]); } xdate[0].toString = toUTCString; } }else{ if (getUTCMode(xdate)) { if (doCoercion) { xdate[0] = coerceToLocal(xdate[0]); }else{ xdate[0] = new Date(+xdate[0]); } // toString will have been cleared } } return xdate; // for chaining } proto.getTimezoneOffset = function() { if (getUTCMode(this)) { return 0; }else{ return this[0].getTimezoneOffset(); } }; /* get / set / add / diff Methods (except for week-related) ---------------------------------------------------------------------------------*/ each(methodSubjects, function(subject, fieldIndex) { proto['get' + subject] = function() { return _getField(this[0], getUTCMode(this), fieldIndex); }; if (fieldIndex != YEAR) { // because there is no getUTCYear proto['getUTC' + subject] = function() { return _getField(this[0], true, fieldIndex); }; } if (fieldIndex != DAY) { // because there is no setDay or setUTCDay // and the add* and diff* methods use DATE instead proto['set' + subject] = function(value) { _set(this, fieldIndex, value, arguments, getUTCMode(this)); return this; // for chaining }; if (fieldIndex != YEAR) { // because there is no setUTCYear // and the add* and diff* methods use FULLYEAR instead proto['setUTC' + subject] = function(value) { _set(this, fieldIndex, value, arguments, true); return this; // for chaining }; proto['add' + (subjectPlurals[fieldIndex] || subject)] = function(delta, preventOverflow) { _add(this, fieldIndex, delta, preventOverflow); return this; // for chaining }; proto['diff' + (subjectPlurals[fieldIndex] || subject)] = function(otherDate) { return _diff(this, otherDate, fieldIndex); }; } } }); function _set(xdate, fieldIndex, value, args, useUTC) { var getField = curry(_getField, xdate[0], useUTC); var setField = curry(_setField, xdate[0], useUTC); var expectedMonth; var preventOverflow = false; if (args.length == 2 && isBoolean(args[1])) { preventOverflow = args[1]; args = [ value ]; } if (fieldIndex == MONTH) { expectedMonth = (value % 12 + 12) % 12; }else{ expectedMonth = getField(MONTH); } setField(fieldIndex, args); if (preventOverflow && getField(MONTH) != expectedMonth) { setField(MONTH, [ getField(MONTH) - 1 ]); setField(DATE, [ getDaysInMonth(getField(FULLYEAR), getField(MONTH)) ]); } } function _add(xdate, fieldIndex, delta, preventOverflow) { delta = Number(delta); var intDelta = Math.floor(delta); xdate['set' + methodSubjects[fieldIndex]]( xdate['get' + methodSubjects[fieldIndex]]() + intDelta, preventOverflow || false ); if (intDelta != delta && fieldIndex < MILLISECONDS) { _add(xdate, fieldIndex+1, (delta-intDelta)*unitsWithin[fieldIndex], preventOverflow); } } function _diff(xdate1, xdate2, fieldIndex) { // fieldIndex=FULLYEAR is for years, fieldIndex=DATE is for days xdate1 = xdate1.clone().setUTCMode(true, true); xdate2 = XDate(xdate2).setUTCMode(true, true); var v = 0; if (fieldIndex == FULLYEAR || fieldIndex == MONTH) { for (var i=MILLISECONDS, methodName; i>=fieldIndex; i--) { v /= unitsWithin[i]; v += _getField(xdate2, false, i) - _getField(xdate1, false, i); } if (fieldIndex == MONTH) { v += (xdate2.getFullYear() - xdate1.getFullYear()) * 12; } } else if (fieldIndex == DATE) { var clear1 = xdate1.toDate().setUTCHours(0, 0, 0, 0); // returns an ms value var clear2 = xdate2.toDate().setUTCHours(0, 0, 0, 0); // returns an ms value v = Math.round((clear2 - clear1) / DAY_MS) + ((xdate2 - clear2) - (xdate1 - clear1)) / DAY_MS; } else { v = (xdate2 - xdate1) / [ 3600000, // milliseconds in hour 60000, // milliseconds in minute 1000, // milliseconds in second 1 // ][fieldIndex - 3]; } return v; } /* Week Methods ---------------------------------------------------------------------------------*/ proto.getWeek = function() { return _getWeek(curry(_getField, this, false)); }; proto.getUTCWeek = function() { return _getWeek(curry(_getField, this, true)); }; proto.setWeek = function(n, year) { _setWeek(this, n, year, false); return this; // for chaining }; proto.setUTCWeek = function(n, year) { _setWeek(this, n, year, true); return this; // for chaining }; proto.addWeeks = function(delta) { return this.addDays(Number(delta) * 7); }; proto.diffWeeks = function(otherDate) { return _diff(this, otherDate, DATE) / 7; }; function _getWeek(getField) { return getWeek(getField(FULLYEAR), getField(MONTH), getField(DATE)); } function getWeek(year, month, date) { var d = new Date(UTC(year, month, date)); var week1 = getWeek1( getWeekYear(year, month, date) ); return Math.floor(Math.round((d - week1) / DAY_MS) / 7) + 1; } function getWeekYear(year, month, date) { // get the year that the date's week # belongs to var d = new Date(UTC(year, month, date)); if (d < getWeek1(year)) { return year - 1; } else if (d >= getWeek1(year + 1)) { return year + 1; } return year; } function getWeek1(year) { // returns Date of first week of year, in UTC var d = new Date(UTC(year, 0, 4)); d.setUTCDate(d.getUTCDate() - (d.getUTCDay() + 6) % 7); // make it Monday of the week return d; } function _setWeek(xdate, n, year, useUTC) { var getField = curry(_getField, xdate, useUTC); var setField = curry(_setField, xdate, useUTC); if (year === undefined) { year = getWeekYear( getField(FULLYEAR), getField(MONTH), getField(DATE) ); } var week1 = getWeek1(year); if (!useUTC) { week1 = coerceToLocal(week1); } xdate.setTime(+week1); setField(DATE, [ getField(DATE) + (n-1) * 7 ]); // would have used xdate.addUTCWeeks :( // n-1 because n is 1-based } /* Parsing ---------------------------------------------------------------------------------*/ XDate.parsers = [ parseISO ]; XDate.parse = function(str) { return +XDate(''+str); }; function parse(str, utcMode, xdate) { var parsers = XDate.parsers; var i = 0; var res; for (; i=0; i--) { uniqueness.push(getField(i)); } } return getField(fieldIndex); } return _format(xdate, formatString, getFieldAndTrace, getSetting, useUTC); } function _format(xdate, formatString, getField, getSetting, useUTC) { var m; var subout; var out = ''; while (m = formatString.match(formatStringRE)) { out += formatString.substr(0, m.index); if (m[1]) { // consecutive alphabetic characters out += processTokenString(xdate, m[1], getField, getSetting, useUTC); } else if (m[3]) { // parenthesis subout = _format(xdate, m[4], getField, getSetting, useUTC); if (parseInt(subout.replace(/\D/g, ''), 10)) { // if any of the numbers are non-zero. or no numbers at all out += subout; } } else { // else if (m[6]) { // single quotes out += m[7] || "'"; // if inner is blank, meaning 2 consecutive quotes = literal single quote } formatString = formatString.substr(m.index + m[0].length); } return out + formatString; } function processTokenString(xdate, tokenString, getField, getSetting, useUTC) { var end = tokenString.length; var replacement; var out = ''; while (end > 0) { replacement = getTokenReplacement(xdate, tokenString.substr(0, end), getField, getSetting, useUTC); if (replacement !== undefined) { out += replacement; tokenString = tokenString.substr(end); end = tokenString.length; }else{ end--; } } return out + tokenString; } function getTokenReplacement(xdate, token, getField, getSetting, useUTC) { var formatter = XDate.formatters[token]; if (isString(formatter)) { return _format(xdate, formatter, getField, getSetting, useUTC); } else if (isFunction(formatter)) { return formatter(xdate, useUTC || false, getSetting); } switch (token) { case 'fff' : return zeroPad(getField(MILLISECONDS), 3); case 's' : return getField(SECONDS); case 'ss' : return zeroPad(getField(SECONDS)); case 'm' : return getField(MINUTES); case 'mm' : return zeroPad(getField(MINUTES)); case 'h' : return getField(HOURS) % 12 || 12; case 'hh' : return zeroPad(getField(HOURS) % 12 || 12); case 'H' : return getField(HOURS); case 'HH' : return zeroPad(getField(HOURS)); case 'd' : return getField(DATE); case 'dd' : return zeroPad(getField(DATE)); case 'ddd' : return getSetting('dayNamesShort')[getField(DAY)] || ''; case 'dddd' : return getSetting('dayNames')[getField(DAY)] || ''; case 'M' : return getField(MONTH) + 1; case 'MM' : return zeroPad(getField(MONTH) + 1); case 'MMM' : return getSetting('monthNamesShort')[getField(MONTH)] || ''; case 'MMMM' : return getSetting('monthNames')[getField(MONTH)] || ''; case 'yy' : return (getField(FULLYEAR)+'').substring(2); case 'yyyy' : return getField(FULLYEAR); case 't' : return _getDesignator(getField, getSetting).substr(0, 1).toLowerCase(); case 'tt' : return _getDesignator(getField, getSetting).toLowerCase(); case 'T' : return _getDesignator(getField, getSetting).substr(0, 1); case 'TT' : return _getDesignator(getField, getSetting); case 'z' : case 'zz' : case 'zzz' : return useUTC ? 'Z' : _getTZString(xdate, token); case 'w' : return _getWeek(getField); case 'ww' : return zeroPad(_getWeek(getField)); case 'S' : var d = getField(DATE); if (d > 10 && d < 20) return 'th'; return ['st', 'nd', 'rd'][d % 10 - 1] || 'th'; } } function _getTZString(xdate, token) { var tzo = xdate.getTimezoneOffset(); var sign = tzo < 0 ? '+' : '-'; var hours = Math.floor(Math.abs(tzo) / 60); var minutes = Math.abs(tzo) % 60; var out = hours; if (token == 'zz') { out = zeroPad(hours); } else if (token == 'zzz') { out = zeroPad(hours) + ':' + zeroPad(minutes); } return sign + out; } function _getDesignator(getField, getSetting) { return getField(HOURS) < 12 ? getSetting('amDesignator') : getSetting('pmDesignator'); } /* Misc Methods ---------------------------------------------------------------------------------*/ each( [ // other getters 'getTime', 'valueOf', 'toDateString', 'toTimeString', 'toLocaleString', 'toLocaleDateString', 'toLocaleTimeString', 'toJSON' ], function(methodName) { proto[methodName] = function() { return this[0][methodName](); }; } ); proto.setTime = function(t) { this[0].setTime(t); return this; // for chaining }; proto.valid = methodize(valid); function valid(xdate) { return !isNaN(+xdate[0]); } proto.clone = function() { return new XDate(this); }; proto.clearTime = function() { return this.setHours(0, 0, 0, 0); // will return an XDate for chaining }; proto.toDate = function() { return new Date(+this[0]); }; /* Misc Class Methods ---------------------------------------------------------------------------------*/ XDate.now = function() { return +new Date(); }; XDate.today = function() { return new XDate().clearTime(); }; XDate.UTC = UTC; XDate.getDaysInMonth = getDaysInMonth; /* Internal Utilities ---------------------------------------------------------------------------------*/ function _clone(xdate) { // returns the internal Date object that should be used var d = new Date(+xdate[0]); if (getUTCMode(xdate)) { d.toString = toUTCString; } return d; } function _getField(d, useUTC, fieldIndex) { return d['get' + (useUTC ? 'UTC' : '') + methodSubjects[fieldIndex]](); } function _setField(d, useUTC, fieldIndex, args) { d['set' + (useUTC ? 'UTC' : '') + methodSubjects[fieldIndex]].apply(d, args); } /* Date Math Utilities ---------------------------------------------------------------------------------*/ function coerceToUTC(date) { return new Date(UTC( date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds() )); } function coerceToLocal(date) { return new Date( date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds() ); } function getDaysInMonth(year, month) { return 32 - new Date(UTC(year, month, 32)).getUTCDate(); } /* General Utilities ---------------------------------------------------------------------------------*/ function methodize(f) { return function() { return f.apply(undefined, [this].concat(slice(arguments))); }; } function curry(f) { var firstArgs = slice(arguments, 1); return function() { return f.apply(undefined, firstArgs.concat(slice(arguments))); }; } function slice(a, start, end) { return Array.prototype.slice.call( a, start || 0, // start and end cannot be undefined for IE end===undefined ? a.length : end ); } function each(a, f) { for (var i=0; i