From 58dbb01e76f4412ee150e86587296de0bb83aba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 24 Jan 2014 16:54:18 +0100 Subject: [PATCH] Added new config setting timezoneOffset, usefull when your graphite server is on a different timezone thant your users browsers. This is not optimal (for example if your users have different timezones) can be improved by finding a way to query the timezone different between browser and graphite server. Or fix so that the &tz graphite parameter works for json, then all absolute filters can use UTC time. #31 --- package.json | 2 +- src/app/components/settings.js | 3 +- src/app/panels/graphite/styleEditor.html | 4 + src/app/services/graphite/graphiteSrv.js | 8 +- src/config.js | 6 + src/vendor/moment.js | 1194 +++++++++++++++++----- 6 files changed, 986 insertions(+), 231 deletions(-) diff --git a/package.json b/package.json index d91338a08ab..2f73392eb65 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "1.0.3", + "version": "1.0.4", "repository": { "type": "git", "url": "http://github.com/torkelo/grafana.git" diff --git a/src/app/components/settings.js b/src/app/components/settings.js index 13ed33468ea..3a1519db699 100644 --- a/src/app/components/settings.js +++ b/src/app/components/settings.js @@ -17,7 +17,8 @@ function (_, crypto) { graphiteUrl : "http://"+window.location.hostname+":8080", panel_names : [], default_route : '/dashboard/file/default.json', - grafana_index : 'grafana-dash' + grafana_index : 'grafana-dash', + timezoneOffset : null, }; // This initializes a new hash on purpose, to avoid adding parameters to diff --git a/src/app/panels/graphite/styleEditor.html b/src/app/panels/graphite/styleEditor.html index 9f8dc68b16a..127cfaa77bf 100644 --- a/src/app/panels/graphite/styleEditor.html +++ b/src/app/panels/graphite/styleEditor.html @@ -1,6 +1,10 @@
Chart Options
+
+ + +
diff --git a/src/app/services/graphite/graphiteSrv.js b/src/app/services/graphite/graphiteSrv.js index c11efaa1a72..65a9279de92 100644 --- a/src/app/services/graphite/graphiteSrv.js +++ b/src/app/services/graphite/graphiteSrv.js @@ -52,7 +52,13 @@ function (angular, _, $, config, kbn) { date = kbn.parseDate(date); } - return $.plot.formatDate(date, '%H%:%M_%Y%m%d'); + date = moment.utc(date).local(); + + if (config.timezoneOffset) { + date = date.zone(config.timezoneOffset) + } + + return date.format('HH:mm_YYYYMMDD'); }; this.match = function(targets, graphiteTargetStr) { diff --git a/src/config.js b/src/config.js index 35abc565831..5babff20a62 100644 --- a/src/config.js +++ b/src/config.js @@ -20,6 +20,12 @@ function (Settings) { default_route: '/dashboard/file/default.json', + /** + * If your graphite server has another timezone than you & users browsers specify the offset here + * Example: "-0500" (for UTC - 5 hours) + */ + timezoneOffset: null, + grafana_index: "grafana-dash", panel_names: [ diff --git a/src/vendor/moment.js b/src/vendor/moment.js index a5fa92f7ab2..4efd7d50b94 100644 --- a/src/vendor/moment.js +++ b/src/vendor/moment.js @@ -1,8 +1,8 @@ -// moment.js -// version : 2.1.0 -// author : Tim Wood -// license : MIT -// momentjs.com +//! moment.js +//! version : 2.5.1 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com (function (undefined) { @@ -11,41 +11,86 @@ ************************************/ var moment, - VERSION = "2.1.0", - round = Math.round, i, + VERSION = "2.5.1", + global = this, + round = Math.round, + i, + + YEAR = 0, + MONTH = 1, + DATE = 2, + HOUR = 3, + MINUTE = 4, + SECOND = 5, + MILLISECOND = 6, + // internal storage for language config files languages = {}, + // moment internal properties + momentProperties = { + _isAMomentObject: null, + _i : null, + _f : null, + _l : null, + _strict : null, + _isUTC : null, + _offset : null, // optional. Combine with _isUTC + _pf : null, + _lang : null // optional + }, + // check for nodeJS - hasModule = (typeof module !== 'undefined' && module.exports), + hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'), // ASP.NET json date format regex aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, - aspNetTimeSpanJsonRegex = /(\-)?(\d*)?\.?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/, + aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/, + + // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html + // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere + isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/, // format tokens - formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g, + formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g, localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g, // parsing token regexes parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 - parseTokenThreeDigits = /\d{3}/, // 000 - 999 - parseTokenFourDigits = /\d{1,4}/, // 0 - 9999 - parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999 + parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999 + parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999 + parseTokenDigits = /\d+/, // nonzero number of digits parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic. - parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z - parseTokenT = /T/i, // T (ISO seperator) + parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z + parseTokenT = /T/i, // T (ISO separator) parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 - // preliminary iso regex - // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 - isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/, + //strict parsing regexes + parseTokenOneDigit = /\d/, // 0 - 9 + parseTokenTwoDigits = /\d\d/, // 00 - 99 + parseTokenThreeDigits = /\d{3}/, // 000 - 999 + parseTokenFourDigits = /\d{4}/, // 0000 - 9999 + parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999 + parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf + + // iso 8601 regex + // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) + isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/, + isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', + isoDates = [ + ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], + ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], + ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], + ['GGGG-[W]WW', /\d{4}-W\d{2}/], + ['YYYY-DDD', /\d{4}-\d{3}/] + ], + // iso time formats and regexes isoTimes = [ - ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/], + ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/], ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], ['HH:mm', /(T| )\d\d:\d\d/], ['HH', /(T| )\d\d/] @@ -72,9 +117,24 @@ m : 'minute', h : 'hour', d : 'day', + D : 'date', w : 'week', + W : 'isoWeek', M : 'month', - y : 'year' + y : 'year', + DDD : 'dayOfYear', + e : 'weekday', + E : 'isoWeekday', + gg: 'weekYear', + GG: 'isoWeekYear' + }, + + camelFunctions = { + dayofyear : 'dayOfYear', + isoweekday : 'isoWeekday', + isoweek : 'isoWeek', + weekyear : 'weekYear', + isoweekyear : 'isoWeekYear' }, // format function strings @@ -127,11 +187,15 @@ YYYYY : function () { return leftZeroFill(this.year(), 5); }, + YYYYYY : function () { + var y = this.year(), sign = y >= 0 ? '+' : '-'; + return sign + leftZeroFill(Math.abs(y), 6); + }, gg : function () { return leftZeroFill(this.weekYear() % 100, 2); }, gggg : function () { - return this.weekYear(); + return leftZeroFill(this.weekYear(), 4); }, ggggg : function () { return leftZeroFill(this.weekYear(), 5); @@ -140,7 +204,7 @@ return leftZeroFill(this.isoWeekYear() % 100, 2); }, GGGG : function () { - return this.isoWeekYear(); + return leftZeroFill(this.isoWeekYear(), 4); }, GGGGG : function () { return leftZeroFill(this.isoWeekYear(), 5); @@ -170,14 +234,17 @@ return this.seconds(); }, S : function () { - return ~~(this.milliseconds() / 100); + return toInt(this.milliseconds() / 100); }, SS : function () { - return leftZeroFill(~~(this.milliseconds() / 10), 2); + return leftZeroFill(toInt(this.milliseconds() / 10), 2); }, SSS : function () { return leftZeroFill(this.milliseconds(), 3); }, + SSSS : function () { + return leftZeroFill(this.milliseconds(), 3); + }, Z : function () { var a = -this.zone(), b = "+"; @@ -185,7 +252,7 @@ a = -a; b = "-"; } - return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2); + return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2); }, ZZ : function () { var a = -this.zone(), @@ -194,7 +261,7 @@ a = -a; b = "-"; } - return b + leftZeroFill(~~(10 * a / 6), 4); + return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2); }, z : function () { return this.zoneAbbr(); @@ -204,8 +271,30 @@ }, X : function () { return this.unix(); + }, + Q : function () { + return this.quarter(); } + }, + + lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin']; + + function defaultParsingFlags() { + // We need to deep clone this object, and es5 standard is not very + // helpful. + return { + empty : false, + unusedTokens : [], + unusedInput : [], + overflow : -2, + charsLeftOver : 0, + nullInput : false, + invalidMonth : null, + invalidFormat : false, + userInvalidated : false, + iso: false }; + } function padToken(func, count) { return function (a) { @@ -239,36 +328,35 @@ // Moment prototype object function Moment(config) { + checkOverflow(config); extend(this, config); } // Duration Constructor function Duration(duration) { - var years = duration.years || duration.year || duration.y || 0, - months = duration.months || duration.month || duration.M || 0, - weeks = duration.weeks || duration.week || duration.w || 0, - days = duration.days || duration.day || duration.d || 0, - hours = duration.hours || duration.hour || duration.h || 0, - minutes = duration.minutes || duration.minute || duration.m || 0, - seconds = duration.seconds || duration.second || duration.s || 0, - milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0; - - // store reference to input for deterministic cloning - this._input = duration; + var normalizedInput = normalizeObjectUnits(duration), + years = normalizedInput.year || 0, + months = normalizedInput.month || 0, + weeks = normalizedInput.week || 0, + days = normalizedInput.day || 0, + hours = normalizedInput.hour || 0, + minutes = normalizedInput.minute || 0, + seconds = normalizedInput.second || 0, + milliseconds = normalizedInput.millisecond || 0; // representation for dateAddRemove - this._milliseconds = milliseconds + + this._milliseconds = +milliseconds + seconds * 1e3 + // 1000 minutes * 6e4 + // 1000 * 60 hours * 36e5; // 1000 * 60 * 60 // Because of dateAddRemove treats 24 hours as different from a // day when working around DST, we need to store them separately - this._days = days + + this._days = +days + weeks * 7; // It is impossible translate months into days without knowing // which months you are are talking about, so we have to store // it separately. - this._months = months + + this._months = +months + years * 12; this._data = {}; @@ -276,7 +364,6 @@ this._bubble(); } - /************************************ Helpers ************************************/ @@ -288,9 +375,29 @@ a[i] = b[i]; } } + + if (b.hasOwnProperty("toString")) { + a.toString = b.toString; + } + + if (b.hasOwnProperty("valueOf")) { + a.valueOf = b.valueOf; + } + return a; } + function cloneMoment(m) { + var result = {}, i; + for (i in m) { + if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) { + result[i] = m[i]; + } + } + + return result; + } + function absRound(number) { if (number < 0) { return Math.ceil(number); @@ -301,12 +408,14 @@ // left zero fill a number // see http://jsperf.com/left-zero-filling for performance comparison - function leftZeroFill(number, targetLength) { - var output = number + ''; + function leftZeroFill(number, targetLength, forceSign) { + var output = '' + Math.abs(number), + sign = number >= 0; + while (output.length < targetLength) { output = '0' + output; } - return output; + return (sign ? (forceSign ? '+' : '') : '-') + output; } // helper function for _.addTime and _.subtractTime @@ -315,8 +424,7 @@ days = duration._days, months = duration._months, minutes, - hours, - currentDate; + hours; if (milliseconds) { mom._d.setTime(+mom._d + milliseconds * isAdding); @@ -347,14 +455,20 @@ return Object.prototype.toString.call(input) === '[object Array]'; } + function isDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || + input instanceof Date; + } + // compare two arrays, return the number of differences - function compareArrays(array1, array2) { + function compareArrays(array1, array2, dontConvert) { var len = Math.min(array1.length, array2.length), lengthDiff = Math.abs(array1.length - array2.length), diffs = 0, i; for (i = 0; i < len; i++) { - if (~~array1[i] !== ~~array2[i]) { + if ((dontConvert && array1[i] !== array2[i]) || + (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { diffs++; } } @@ -362,16 +476,155 @@ } function normalizeUnits(units) { - return units ? unitAliases[units] || units.toLowerCase().replace(/(.)s$/, '$1') : units; + if (units) { + var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); + units = unitAliases[units] || camelFunctions[lowered] || lowered; + } + return units; } + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; + + for (prop in inputObject) { + if (inputObject.hasOwnProperty(prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } + + return normalizedInput; + } + + function makeList(field) { + var count, setter; + + if (field.indexOf('week') === 0) { + count = 7; + setter = 'day'; + } + else if (field.indexOf('month') === 0) { + count = 12; + setter = 'month'; + } + else { + return; + } + + moment[field] = function (format, index) { + var i, getter, + method = moment.fn._lang[field], + results = []; + + if (typeof format === 'number') { + index = format; + format = undefined; + } + + getter = function (i) { + var m = moment().utc().set(setter, i); + return method.call(moment.fn._lang, m, format || ''); + }; + + if (index != null) { + return getter(index); + } + else { + for (i = 0; i < count; i++) { + results.push(getter(i)); + } + return results; + } + }; + } + + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; + + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } + } + + return value; + } + + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } + + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; + } + + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + } + + function checkOverflow(m) { + var overflow; + if (m._a && m._pf.overflow === -2) { + overflow = + m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH : + m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE : + m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR : + m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE : + m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND : + m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND : + -1; + + if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } + + m._pf.overflow = overflow; + } + } + + function isValid(m) { + if (m._isValid == null) { + m._isValid = !isNaN(m._d.getTime()) && + m._pf.overflow < 0 && + !m._pf.empty && + !m._pf.invalidMonth && + !m._pf.nullInput && + !m._pf.invalidFormat && + !m._pf.userInvalidated; + + if (m._strict) { + m._isValid = m._isValid && + m._pf.charsLeftOver === 0 && + m._pf.unusedTokens.length === 0; + } + } + return m._isValid; + } + + function normalizeLanguage(key) { + return key ? key.toLowerCase().replace('_', '-') : key; + } + + // Return a moment from input, that is local/utc/zone equivalent to model. + function makeAs(input, model) { + return model._isUTC ? moment(input).zone(model._offset || 0) : + moment(input).local(); + } /************************************ Languages ************************************/ - Language.prototype = { + extend(Language.prototype, { + set : function (config) { var prop, i; for (i in config) { @@ -404,7 +657,7 @@ for (i = 0; i < 12; i++) { // make the regex if we don't have it already if (!this._monthsParse[i]) { - mom = moment([2000, i]); + mom = moment.utc([2000, i]); regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); } @@ -470,7 +723,9 @@ }, isPM : function (input) { - return ((input + '').toLowerCase()[0] === 'p'); + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return ((input + '').toLowerCase().charAt(0) === 'p'); }, _meridiemParse : /[ap]\.?m?\.?/i, @@ -537,11 +792,17 @@ week : function (mom) { return weekOfYear(mom, this._week.dow, this._week.doy).week; }, + _week : { dow : 0, // Sunday is the first day of the week. doy : 6 // The week that contains Jan 1st is the first week of the year. + }, + + _invalidDate: 'Invalid date', + invalidDate: function () { + return this._invalidDate; } - }; + }); // Loads a language definition into the `languages` cache. The function // takes a key and optionally values. If not in the browser and no values @@ -556,6 +817,11 @@ return languages[key]; } + // Remove a language from the `languages` cache. Mostly useful in tests. + function unloadLang(key) { + delete languages[key]; + } + // Determines which language definition to use and returns it. // // With no parameters, it will return the global language. If you @@ -563,20 +829,52 @@ // definition for 'en', so long as 'en' has already been loaded using // moment.lang. function getLangDefinition(key) { + var i = 0, j, lang, next, split, + get = function (k) { + if (!languages[k] && hasModule) { + try { + require('./lang/' + k); + } catch (e) { } + } + return languages[k]; + }; + if (!key) { return moment.fn._lang; } - if (!languages[key] && hasModule) { - try { - require('./lang/' + key); - } catch (e) { - // call with no params to set to default - return moment.fn._lang; - } - } - return languages[key]; - } + if (!isArray(key)) { + //short-circuit everything else + lang = get(key); + if (lang) { + return lang; + } + key = [key]; + } + + //pick the language from the array + //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each + //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root + while (i < key.length) { + split = normalizeLanguage(key[i]).split('-'); + j = split.length; + next = normalizeLanguage(key[i + 1]); + next = next ? next.split('-') : null; + while (j > 0) { + lang = get(split.slice(0, j).join('-')); + if (lang) { + return lang; + } + if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { + //the next array item is better than a shallower substring of this one + break; + } + j--; + } + i++; + } + return moment.fn._lang; + } /************************************ Formatting @@ -584,7 +882,7 @@ function removeFormattingTokens(input) { - if (input.match(/\[.*\]/)) { + if (input.match(/\[[\s\S]/)) { return input.replace(/^\[|\]$/g, ""); } return input.replace(/\\/g, ""); @@ -612,15 +910,12 @@ // format date using native date object function formatMoment(m, format) { - var i = 5; - function replaceLongDateFormatTokens(input) { - return m.lang().longDateFormat(input) || input; + if (!m.isValid()) { + return m.lang().invalidDate(); } - while (i-- && localFormattingTokens.test(format)) { - format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); - } + format = expandFormat(format, m.lang()); if (!formatFunctions[format]) { formatFunctions[format] = makeFormatFunction(format); @@ -629,6 +924,23 @@ return formatFunctions[format](m); } + function expandFormat(format, lang) { + var i = 5; + + function replaceLongDateFormatTokens(input) { + return lang.longDateFormat(input) || input; + } + + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } + + return format; + } + /************************************ Parsing @@ -637,16 +949,32 @@ // get the regex to find the next token function getParseRegexForToken(token, config) { + var a, strict = config._strict; switch (token) { case 'DDDD': return parseTokenThreeDigits; case 'YYYY': - return parseTokenFourDigits; + case 'GGGG': + case 'gggg': + return strict ? parseTokenFourDigits : parseTokenOneToFourDigits; + case 'Y': + case 'G': + case 'g': + return parseTokenSignedNumber; + case 'YYYYYY': case 'YYYYY': - return parseTokenSixDigits; + case 'GGGGG': + case 'ggggg': + return strict ? parseTokenSixDigits : parseTokenOneToSixDigits; case 'S': + if (strict) { return parseTokenOneDigit; } + /* falls through */ case 'SS': + if (strict) { return parseTokenTwoDigits; } + /* falls through */ case 'SSS': + if (strict) { return parseTokenThreeDigits; } + /* falls through */ case 'DDD': return parseTokenOneToThreeDigits; case 'MMM': @@ -665,13 +993,20 @@ return parseTokenTimezone; case 'T': return parseTokenT; + case 'SSSS': + return parseTokenDigits; case 'MM': case 'DD': case 'YY': + case 'GG': + case 'gg': case 'HH': case 'hh': case 'mm': case 'ss': + case 'ww': + case 'WW': + return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits; case 'M': case 'D': case 'd': @@ -679,16 +1014,23 @@ case 'h': case 'm': case 's': + case 'w': + case 'W': + case 'e': + case 'E': return parseTokenOneOrTwoDigits; default : - return new RegExp(token.replace('\\', '')); + a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i")); + return a; } } function timezoneMinutesFromString(string) { - var tzchunk = (parseTokenTimezone.exec(string) || [])[0], - parts = (tzchunk + '').match(parseTimezoneChunker) || ['-', 0, 0], - minutes = +(parts[1] * 60) + ~~parts[2]; + string = string || ""; + var possibleTzMatches = (string.match(parseTokenTimezone) || []), + tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [], + parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0], + minutes = +(parts[1] * 60) + toInt(parts[2]); return parts[0] === '+' ? -minutes : minutes; } @@ -701,34 +1043,43 @@ // MONTH case 'M' : // fall through to MM case 'MM' : - datePartArray[1] = (input == null) ? 0 : ~~input - 1; + if (input != null) { + datePartArray[MONTH] = toInt(input) - 1; + } break; case 'MMM' : // fall through to MMMM case 'MMMM' : a = getLangDefinition(config._l).monthsParse(input); // if we didn't find a month name, mark the date as invalid. if (a != null) { - datePartArray[1] = a; + datePartArray[MONTH] = a; } else { - config._isValid = false; + config._pf.invalidMonth = input; } break; // DAY OF MONTH - case 'D' : // fall through to DDDD - case 'DD' : // fall through to DDDD + case 'D' : // fall through to DD + case 'DD' : + if (input != null) { + datePartArray[DATE] = toInt(input); + } + break; + // DAY OF YEAR case 'DDD' : // fall through to DDDD case 'DDDD' : if (input != null) { - datePartArray[2] = ~~input; + config._dayOfYear = toInt(input); } + break; // YEAR case 'YY' : - datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000); + datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000); break; case 'YYYY' : case 'YYYYY' : - datePartArray[0] = ~~input; + case 'YYYYYY' : + datePartArray[YEAR] = toInt(input); break; // AM / PM case 'a' : // fall through to A @@ -740,23 +1091,24 @@ case 'HH' : // fall through to hh case 'h' : // fall through to hh case 'hh' : - datePartArray[3] = ~~input; + datePartArray[HOUR] = toInt(input); break; // MINUTE case 'm' : // fall through to mm case 'mm' : - datePartArray[4] = ~~input; + datePartArray[MINUTE] = toInt(input); break; // SECOND case 's' : // fall through to ss case 'ss' : - datePartArray[5] = ~~input; + datePartArray[SECOND] = toInt(input); break; // MILLISECOND case 'S' : case 'SS' : case 'SSS' : - datePartArray[6] = ~~ (('0.' + input) * 1000); + case 'SSSS' : + datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000); break; // UNIX TIMESTAMP WITH MS case 'X': @@ -768,11 +1120,29 @@ config._useUTC = true; config._tzm = timezoneMinutesFromString(input); break; - } - - // if the input is null, the date is not valid - if (input == null) { - config._isValid = false; + case 'w': + case 'ww': + case 'W': + case 'WW': + case 'd': + case 'dd': + case 'ddd': + case 'dddd': + case 'e': + case 'E': + token = token.substr(0, 1); + /* falls through */ + case 'gg': + case 'gggg': + case 'GG': + case 'GGGG': + case 'GGGGG': + token = token.substr(0, 2); + if (input) { + config._w = config._w || {}; + config._w[token] = input; + } + break; } } @@ -780,124 +1150,257 @@ // the array should mirror the parameters below // note: all values past the year are optional and will default to the lowest possible value. // [year, month, day , hour, minute, second, millisecond] - function dateFromArray(config) { - var i, date, input = []; + function dateFromConfig(config) { + var i, date, input = [], currentDate, + yearToUse, fixYear, w, temp, lang, weekday, week; if (config._d) { return; } - for (i = 0; i < 7; i++) { + currentDate = currentDateArray(config); + + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + fixYear = function (val) { + var int_val = parseInt(val, 10); + return val ? + (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) : + (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]); + }; + + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1); + } + else { + lang = getLangDefinition(config._l); + weekday = w.d != null ? parseWeekday(w.d, lang) : + (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0); + + week = parseInt(w.w, 10) || 1; + + //if we're parsing 'd', then the low day numbers may be next week + if (w.d != null && weekday < lang._week.dow) { + week++; + } + + temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow); + } + + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } + + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR]; + + if (config._dayOfYear > daysInYear(yearToUse)) { + config._pf._overflowDayOfYear = true; + } + + date = makeUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } + + // Default to current date. + // * if no year, month, day of month are given, default to today + // * if day of month is given, default month and year + // * if month is given, default only year + // * if year is given, don't default anything + for (i = 0; i < 3 && config._a[i] == null; ++i) { + config._a[i] = input[i] = currentDate[i]; + } + + // Zero out whatever was not defaulted, including time + for (; i < 7; i++) { config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; } // add the offsets to the time to be parsed so that we can have a clean array for checking isValid - input[3] += ~~((config._tzm || 0) / 60); - input[4] += ~~((config._tzm || 0) % 60); + input[HOUR] += toInt((config._tzm || 0) / 60); + input[MINUTE] += toInt((config._tzm || 0) % 60); - date = new Date(0); + config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); + } - if (config._useUTC) { - date.setUTCFullYear(input[0], input[1], input[2]); - date.setUTCHours(input[3], input[4], input[5], input[6]); - } else { - date.setFullYear(input[0], input[1], input[2]); - date.setHours(input[3], input[4], input[5], input[6]); + function dateFromObject(config) { + var normalizedInput; + + if (config._d) { + return; } - config._d = date; + normalizedInput = normalizeObjectUnits(config._i); + config._a = [ + normalizedInput.year, + normalizedInput.month, + normalizedInput.day, + normalizedInput.hour, + normalizedInput.minute, + normalizedInput.second, + normalizedInput.millisecond + ]; + + dateFromConfig(config); + } + + function currentDateArray(config) { + var now = new Date(); + if (config._useUTC) { + return [ + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate() + ]; + } else { + return [now.getFullYear(), now.getMonth(), now.getDate()]; + } } // date from string and format string function makeDateFromStringAndFormat(config) { - // This array is used to make a Date, either with `new Date` or `Date.UTC` - var tokens = config._f.match(formattingTokens), - string = config._i, - i, parsedInput; config._a = []; + config._pf.empty = true; + + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var lang = getLangDefinition(config._l), + string = '' + config._i, + i, parsedInput, tokens, token, skipped, + stringLength = string.length, + totalParsedInputLength = 0; + + tokens = expandFormat(config._f, lang).match(formattingTokens) || []; for (i = 0; i < tokens.length; i++) { - parsedInput = (getParseRegexForToken(tokens[i], config).exec(string) || [])[0]; + token = tokens[i]; + parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; if (parsedInput) { + skipped = string.substr(0, string.indexOf(parsedInput)); + if (skipped.length > 0) { + config._pf.unusedInput.push(skipped); + } string = string.slice(string.indexOf(parsedInput) + parsedInput.length); + totalParsedInputLength += parsedInput.length; } - // don't parse if its not a known token - if (formatTokenFunctions[tokens[i]]) { - addTimeToArrayFromToken(tokens[i], parsedInput, config); + // don't parse if it's not a known token + if (formatTokenFunctions[token]) { + if (parsedInput) { + config._pf.empty = false; + } + else { + config._pf.unusedTokens.push(token); + } + addTimeToArrayFromToken(token, parsedInput, config); + } + else if (config._strict && !parsedInput) { + config._pf.unusedTokens.push(token); } } - // add remaining unparsed input to the string - if (string) { - config._il = string; + // add remaining unparsed input length to the string + config._pf.charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + config._pf.unusedInput.push(string); } // handle am pm - if (config._isPm && config._a[3] < 12) { - config._a[3] += 12; + if (config._isPm && config._a[HOUR] < 12) { + config._a[HOUR] += 12; } // if is 12 am, change hours to 0 - if (config._isPm === false && config._a[3] === 12) { - config._a[3] = 0; + if (config._isPm === false && config._a[HOUR] === 12) { + config._a[HOUR] = 0; } - // return - dateFromArray(config); + + dateFromConfig(config); + checkOverflow(config); + } + + function unescapeFormat(s) { + return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + }); + } + + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function regexpEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } // date from string and array of format strings function makeDateFromStringAndArray(config) { var tempConfig, - tempMoment, bestMoment, - scoreToBeat = 99, + scoreToBeat, i, currentScore; + if (config._f.length === 0) { + config._pf.invalidFormat = true; + config._d = new Date(NaN); + return; + } + for (i = 0; i < config._f.length; i++) { + currentScore = 0; tempConfig = extend({}, config); + tempConfig._pf = defaultParsingFlags(); tempConfig._f = config._f[i]; makeDateFromStringAndFormat(tempConfig); - tempMoment = new Moment(tempConfig); - currentScore = compareArrays(tempConfig._a, tempMoment.toArray()); - - // if there is any input that was not parsed - // add a penalty for that format - if (tempMoment._il) { - currentScore += tempMoment._il.length; + if (!isValid(tempConfig)) { + continue; } - if (currentScore < scoreToBeat) { + // if there is any input that was not parsed add a penalty for that format + currentScore += tempConfig._pf.charsLeftOver; + + //or tokens + currentScore += tempConfig._pf.unusedTokens.length * 10; + + tempConfig._pf.score = currentScore; + + if (scoreToBeat == null || currentScore < scoreToBeat) { scoreToBeat = currentScore; - bestMoment = tempMoment; + bestMoment = tempConfig; } } - extend(config, bestMoment); + extend(config, bestMoment || tempConfig); } // date from iso format function makeDateFromString(config) { - var i, + var i, l, string = config._i, match = isoRegex.exec(string); if (match) { - // match[2] should be "T" or undefined - config._f = 'YYYY-MM-DD' + (match[2] || " "); - for (i = 0; i < 4; i++) { + config._pf.iso = true; + for (i = 0, l = isoDates.length; i < l; i++) { + if (isoDates[i][1].exec(string)) { + // match[5] should be "T" or undefined + config._f = isoDates[i][0] + (match[6] || " "); + break; + } + } + for (i = 0, l = isoTimes.length; i < l; i++) { if (isoTimes[i][1].exec(string)) { config._f += isoTimes[i][0]; break; } } - if (parseTokenTimezone.exec(string)) { - config._f += " Z"; + if (string.match(parseTokenTimezone)) { + config._f += "Z"; } makeDateFromStringAndFormat(config); - } else { + } + else { config._d = new Date(string); } } @@ -914,12 +1417,50 @@ makeDateFromString(config); } else if (isArray(input)) { config._a = input.slice(0); - dateFromArray(config); + dateFromConfig(config); + } else if (isDate(input)) { + config._d = new Date(+input); + } else if (typeof(input) === 'object') { + dateFromObject(config); } else { - config._d = input instanceof Date ? new Date(+input) : new Date(input); + config._d = new Date(input); } } + function makeDate(y, m, d, h, M, s, ms) { + //can't just apply() to create a date: + //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply + var date = new Date(y, m, d, h, M, s, ms); + + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); + } + return date; + } + + function makeUTCDate(y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); + } + return date; + } + + function parseWeekday(input, language) { + if (typeof input === 'string') { + if (!isNaN(input)) { + input = parseInt(input, 10); + } + else { + input = language.weekdaysParse(input); + if (typeof input !== 'number') { + return null; + } + } + } + return input; + } /************************************ Relative Time @@ -987,6 +1528,19 @@ }; } + //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday + function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { + var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear; + + weekday = weekday != null ? weekday : firstDayOfWeek; + daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); + dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; + + return { + year: dayOfYear > 0 ? year : year - 1, + dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; + } /************************************ Top Level Functions @@ -996,8 +1550,8 @@ var input = config._i, format = config._f; - if (input === null || input === '') { - return null; + if (input === null) { + return moment.invalid({nullInput: true}); } if (typeof input === 'string') { @@ -1005,7 +1559,8 @@ } if (moment.isMoment(input)) { - config = extend({}, input); + config = cloneMoment(input); + config._d = new Date(+input._d); } else if (format) { if (isArray(format)) { @@ -1020,24 +1575,48 @@ return new Moment(config); } - moment = function (input, format, lang) { - return makeMoment({ - _i : input, - _f : format, - _l : lang, - _isUTC : false - }); + moment = function (input, format, lang, strict) { + var c; + + if (typeof(lang) === "boolean") { + strict = lang; + lang = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._i = input; + c._f = format; + c._l = lang; + c._strict = strict; + c._isUTC = false; + c._pf = defaultParsingFlags(); + + return makeMoment(c); }; // creating with utc - moment.utc = function (input, format, lang) { - return makeMoment({ - _useUTC : true, - _isUTC : true, - _l : lang, - _i : input, - _f : format - }); + moment.utc = function (input, format, lang, strict) { + var c; + + if (typeof(lang) === "boolean") { + strict = lang; + lang = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._useUTC = true; + c._isUTC = true; + c._l = lang; + c._i = input; + c._f = format; + c._strict = strict; + c._pf = defaultParsingFlags(); + + return makeMoment(c).utc(); }; // creating with unix timestamp (in seconds) @@ -1047,34 +1626,60 @@ // duration moment.duration = function (input, key) { - var isDuration = moment.isDuration(input), - isNumber = (typeof input === 'number'), - duration = (isDuration ? input._input : (isNumber ? {} : input)), - matched = aspNetTimeSpanJsonRegex.exec(input), + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, sign, - ret; + ret, + parseIso; - if (isNumber) { + if (moment.isDuration(input)) { + duration = { + ms: input._milliseconds, + d: input._days, + M: input._months + }; + } else if (typeof input === 'number') { + duration = {}; if (key) { duration[key] = input; } else { duration.milliseconds = input; } - } else if (matched) { - sign = (matched[1] === "-") ? -1 : 1; + } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) { + sign = (match[1] === "-") ? -1 : 1; duration = { y: 0, - d: ~~matched[2] * sign, - h: ~~matched[3] * sign, - m: ~~matched[4] * sign, - s: ~~matched[5] * sign, - ms: ~~matched[6] * sign + d: toInt(match[DATE]) * sign, + h: toInt(match[HOUR]) * sign, + m: toInt(match[MINUTE]) * sign, + s: toInt(match[SECOND]) * sign, + ms: toInt(match[MILLISECOND]) * sign + }; + } else if (!!(match = isoDurationRegex.exec(input))) { + sign = (match[1] === "-") ? -1 : 1; + parseIso = function (inp) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; + }; + duration = { + y: parseIso(match[2]), + M: parseIso(match[3]), + d: parseIso(match[4]), + h: parseIso(match[5]), + m: parseIso(match[6]), + s: parseIso(match[7]), + w: parseIso(match[8]) }; } ret = new Duration(duration); - if (isDuration && input.hasOwnProperty('_lang')) { + if (moment.isDuration(input) && input.hasOwnProperty('_lang')) { ret._lang = input._lang; } @@ -1095,15 +1700,20 @@ // no arguments are passed in, it will simply return the current global // language key. moment.lang = function (key, values) { + var r; if (!key) { return moment.fn._lang._abbr; } if (values) { - loadLang(key, values); + loadLang(normalizeLanguage(key), values); + } else if (values === null) { + unloadLang(key); + key = 'en'; } else if (!languages[key]) { getLangDefinition(key); } - moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key); + r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key); + return r._abbr; }; // returns language data @@ -1116,7 +1726,8 @@ // compare moment object moment.isMoment = function (obj) { - return obj instanceof Moment; + return obj instanceof Moment || + (obj != null && obj.hasOwnProperty('_isAMomentObject')); }; // for typechecking Duration objects @@ -1124,13 +1735,36 @@ return obj instanceof Duration; }; + for (i = lists.length - 1; i >= 0; --i) { + makeList(lists[i]); + } + + moment.normalizeUnits = function (units) { + return normalizeUnits(units); + }; + + moment.invalid = function (flags) { + var m = moment.utc(NaN); + if (flags != null) { + extend(m._pf, flags); + } + else { + m._pf.userInvalidated = true; + } + + return m; + }; + + moment.parseZone = function (input) { + return moment(input).parseZone(); + }; /************************************ Moment Prototype ************************************/ - moment.fn = Moment.prototype = { + extend(moment.fn = Moment.prototype, { clone : function () { return moment(this); @@ -1145,7 +1779,7 @@ }, toString : function () { - return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); + return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); }, toDate : function () { @@ -1153,7 +1787,12 @@ }, toISOString : function () { - return formatMoment(moment(this).utc(), 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + var m = moment(this).utc(); + if (0 < m.year() && m.year() <= 9999) { + return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } else { + return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } }, toArray : function () { @@ -1170,14 +1809,24 @@ }, isValid : function () { - if (this._isValid == null) { - if (this._a) { - this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()); - } else { - this._isValid = !isNaN(this._d.getTime()); - } + return isValid(this); + }, + + isDSTShifted : function () { + + if (this._a) { + return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; } - return !!this._isValid; + + return false; + }, + + parsingFlags : function () { + return extend({}, this._pf); + }, + + invalidAt: function () { + return this._pf.overflow; }, utc : function () { @@ -1220,7 +1869,7 @@ }, diff : function (input, units, asFloat) { - var that = this._isUTC ? moment(input).zone(this._offset || 0) : moment(input).local(), + var that = makeAs(input, this), zoneDiff = (this.zone() - that.zone()) * 6e4, diff, output; @@ -1262,19 +1911,21 @@ }, calendar : function () { - var diff = this.diff(moment().startOf('day'), 'days', true), + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're zone'd or not. + var sod = makeAs(moment(), this).startOf('day'), + diff = this.diff(sod, 'days', true), format = diff < -6 ? 'sameElse' : - diff < -1 ? 'lastWeek' : - diff < 0 ? 'lastDay' : - diff < 1 ? 'sameDay' : - diff < 2 ? 'nextDay' : - diff < 7 ? 'nextWeek' : 'sameElse'; + diff < -1 ? 'lastWeek' : + diff < 0 ? 'lastDay' : + diff < 1 ? 'sameDay' : + diff < 2 ? 'nextDay' : + diff < 7 ? 'nextWeek' : 'sameElse'; return this.format(this.lang().calendar(format, this)); }, isLeapYear : function () { - var year = this.year(); - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + return isLeapYear(this.year()); }, isDST : function () { @@ -1285,12 +1936,7 @@ day : function (input) { var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); if (input != null) { - if (typeof input === 'string') { - input = this.lang().weekdaysParse(input); - if (typeof input !== 'number') { - return this; - } - } + input = parseWeekday(input, this.lang()); return this.add({ d : input - day }); } else { return day; @@ -1299,8 +1945,7 @@ month : function (input) { var utc = this._isUTC ? 'UTC' : '', - dayOfMonth, - daysInMonth; + dayOfMonth; if (input != null) { if (typeof input === 'string') { @@ -1334,6 +1979,7 @@ this.date(1); /* falls through */ case 'week': + case 'isoWeek': case 'day': this.hours(0); /* falls through */ @@ -1351,13 +1997,16 @@ // weeks are a special case if (units === 'week') { this.weekday(0); + } else if (units === 'isoWeek') { + this.isoWeekday(1); } return this; }, endOf: function (units) { - return this.startOf(units).add(units, 1).subtract('ms', 1); + units = normalizeUnits(units); + return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1); }, isAfter: function (input, units) { @@ -1371,8 +2020,8 @@ }, isSame: function (input, units) { - units = typeof units !== 'undefined' ? units : 'millisecond'; - return +this.clone().startOf(units) === +moment(input).startOf(units); + units = units || 'ms'; + return +this.clone().startOf(units) === +makeAs(input, this).startOf(units); }, min: function (other) { @@ -1413,8 +2062,28 @@ return this._isUTC ? "Coordinated Universal Time" : ""; }, + parseZone : function () { + if (this._tzm) { + this.zone(this._tzm); + } else if (typeof this._i === 'string') { + this.zone(this._i); + } + return this; + }, + + hasAlignedHourOffset : function (input) { + if (!input) { + input = 0; + } + else { + input = moment(input).zone(); + } + + return (this.zone() - input) % 60 === 0; + }, + daysInMonth : function () { - return moment.utc([this.year(), this.month() + 1, 0]).date(); + return daysInMonth(this.year(), this.month()); }, dayOfYear : function (input) { @@ -1422,6 +2091,10 @@ return input == null ? dayOfYear : this.add("d", (input - dayOfYear)); }, + quarter : function () { + return Math.ceil((this.month() + 1.0) / 3.0); + }, + weekYear : function (input) { var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year; return input == null ? year : this.add("y", (input - year)); @@ -1443,7 +2116,7 @@ }, weekday : function (input) { - var weekday = (this._d.getDay() + 7 - this.lang()._week.dow) % 7; + var weekday = (this.day() + 7 - this.lang()._week.dow) % 7; return input == null ? weekday : this.add("d", input - weekday); }, @@ -1454,6 +2127,19 @@ return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); }, + get : function (units) { + units = normalizeUnits(units); + return this[units](); + }, + + set : function (units, value) { + units = normalizeUnits(units); + if (typeof this[units] === 'function') { + this[units](value); + } + return this; + }, + // If passed a language key, it will set the language for this // instance. Otherwise, it will return the language configuration // variables for this instance. @@ -1465,7 +2151,7 @@ return this; } } - }; + }); // helper for adding shortcuts function makeGetterAndSetter(name, key) { @@ -1503,7 +2189,8 @@ ************************************/ - moment.duration.fn = Duration.prototype = { + extend(moment.duration.fn = Duration.prototype, { + _bubble : function () { var milliseconds = this._milliseconds, days = this._days, @@ -1542,7 +2229,7 @@ return this._milliseconds + this._days * 864e5 + (this._months % 12) * 2592e6 + - ~~(this._months / 12) * 31536e6; + toInt(this._months / 12) * 31536e6; }, humanize : function (withSuffix) { @@ -1591,8 +2278,34 @@ return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's'](); }, - lang : moment.fn.lang - }; + lang : moment.fn.lang, + + toIsoString : function () { + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var years = Math.abs(this.years()), + months = Math.abs(this.months()), + days = Math.abs(this.days()), + hours = Math.abs(this.hours()), + minutes = Math.abs(this.minutes()), + seconds = Math.abs(this.seconds() + this.milliseconds() / 1000); + + if (!this.asSeconds()) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } + + return (this.asSeconds() < 0 ? '-' : '') + + 'P' + + (years ? years + 'Y' : '') + + (months ? months + 'M' : '') + + (days ? days + 'D' : '') + + ((hours || minutes || seconds) ? 'T' : '') + + (hours ? hours + 'H' : '') + + (minutes ? minutes + 'M' : '') + + (seconds ? seconds + 'S' : ''); + } + }); function makeDurationGetter(name) { moment.duration.fn[name] = function () { @@ -1628,7 +2341,7 @@ moment.lang('en', { ordinal : function (number) { var b = number % 10, - output = (~~ (number % 100 / 10) === 1) ? 'th' : + output = (toInt(number % 100 / 10) === 1) ? 'th' : (b === 1) ? 'st' : (b === 2) ? 'nd' : (b === 3) ? 'rd' : 'th'; @@ -1636,27 +2349,52 @@ } }); + /* EMBED_LANGUAGES */ /************************************ Exposing Moment ************************************/ + function makeGlobal(deprecate) { + var warned = false, local_moment = moment; + /*global ender:false */ + if (typeof ender !== 'undefined') { + return; + } + // here, `this` means `window` in the browser, or `global` on the server + // add `moment` as a global object via a string identifier, + // for Closure Compiler "advanced" mode + if (deprecate) { + global.moment = function () { + if (!warned && console && console.warn) { + warned = true; + console.warn( + "Accessing Moment through the global scope is " + + "deprecated, and will be removed in an upcoming " + + "release."); + } + return local_moment.apply(null, arguments); + }; + extend(global.moment, local_moment); + } else { + global['moment'] = moment; + } + } // CommonJS module is defined if (hasModule) { module.exports = moment; - } - /*global ender:false */ - if (typeof ender === 'undefined') { - // here, `this` means `window` in the browser, or `global` on the server - // add `moment` as a global object via a string identifier, - // for Closure Compiler "advanced" mode - this['moment'] = moment; - } - /*global define:false */ - if (typeof define === "function" && define.amd) { - define("moment", [], function () { + makeGlobal(true); + } else if (typeof define === "function" && define.amd) { + define("moment", function (require, exports, module) { + if (module.config && module.config() && module.config().noGlobal !== true) { + // If user provided noGlobal, he is aware of global + makeGlobal(module.config().noGlobal === undefined); + } + return moment; }); + } else { + makeGlobal(); } }).call(this); \ No newline at end of file