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 prototype object
function Moment(config) {
+ checkOverflow(config);
extend(this, config);
}
return value;
}
+ function daysInMonth(year, month) {
+ return moment.utc([year, month + 1, 0]).date();
+ }
+
+ function isLeapYear(year) {
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+ }
+
+ function checkOverflow(m) {
+ if (m._a && m._pf.overflow == -2) {
+ m._pf.overflow =
+ m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
+ m._a[DATE] < 1 || m._a[DATE] >
+ (m._pf.overflowMonthOk ? (isLeapYear(m._a[YEAR]) ? 364 : 365) : 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;
+
+ }
+ }
+
+ function initializeParsingFlags(config) {
+ config._pf = {
+ empty : false,
+ unusedTokens : [],
+ trailingInput : '',
+ overflowMonthOk : false,
+ dstShifted : false,
+ overflow : -2,
+ charsLeftOver: 0
+ };
+ }
+
+ function isValid(m) {
+ if (m._isValid == null) {
+ m._isValid = !isNaN(m._d.getTime()) && m._pf.overflow < 0 && !m._pf.empty;
+
+ if (m._strict) {
+ m._isValid = m._isValid &&
+ m._pf.charsLeftOver === 0 &&
+ m._pf.trailingInput.length === 0;
+ }
+ }
+ return m._isValid;
+ }
+
/************************************
Languages
************************************/
case 's':
return parseTokenOneOrTwoDigits;
default :
- return new RegExp(regexpEscape(token.replace('\\', '')));
+ return null;
}
}
+ function getDefaultParserRegex(token) {
+ return new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
+ }
+
function timezoneMinutesFromString(string) {
var tzchunk = (parseTokenTimezone.exec(string) || [])[0],
parts = (tzchunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
config._isValid = false;
}
break;
- // DAY OF MONTH
+ // DATE OF MONTH
case 'D' : // fall through to DD
case 'DD' :
if (input != null) {
- datePartArray[2] = toInt(input);
+ a = toInt(input);
+ datePartArray[DATE] = a;
+ if (a < 1) {
+ config._pf.overflow = DATE;
+ }
}
break;
- // DAY OF YEAR
+ // DATE OF YEAR
case 'DDD' : // fall through to DDDD
case 'DDDD' :
if (input != null) {
- datePartArray[1] = 0;
- datePartArray[2] = toInt(input);
+ datePartArray[MONTH] = 0;
+ datePartArray[DATE] = toInt(input);
+ config._pf.overflowMonthOk = true;
}
+
break;
// YEAR
case 'YY' :
case 'a' : // fall through to A
case 'A' :
config._isPm = getLangDefinition(config._l).isPM(input);
- return;
+ break;
// 24 HOUR
case 'H' : // fall through to hh
case 'HH' : // fall through to hh
config._tzm = timezoneMinutesFromString(input);
break;
}
-
- // if the input is null, the date is not valid
- if (input == null) {
- config._isValid = false;
- }
}
// convert an array to a date.
// date from string and format string
function makeDateFromStringAndFormat(config) {
- if (config._strict) {
- makeDateFromStringAndStrictFormat(config);
- return;
- }
+ config._pf.empty = true;
+ config._a = [];
+
// 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,
+ i, parsedInput, tokens, regex, defaulted, token,
stringLength = string.length,
totalParsedInputLength = 0;
tokens = expandFormat(config._f, lang).match(formattingTokens);
- config._a = [];
+ if (!tokens) {
+ dateFromArray(config);
+ checkOverflow(config);
+ return;
+ }
+
for (i = 0; i < tokens.length; i++) {
- parsedInput = (getParseRegexForToken(tokens[i], config).exec(string) || [])[0];
+ token = tokens[i];
+ defaulted = false;
+ regex = getParseRegexForToken(token, config);
+ if (regex === null) {
+ regex = getDefaultParserRegex(token);
+ defaulted = true;
+ }
+ if (config._strict) {
+ regex = new RegExp("^" + regex.source, regex.ignoreCase ? "i" : "");
+ }
+ parsedInput = (regex.exec(string) || [])[0];
if (parsedInput) {
string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
totalParsedInputLength += parsedInput.length;
+ if (!defaulted) {
+ config._pf.empty = false;
+ }
}
+ else {
+ if (!defaulted) {
+ config._pf.unusedTokens.push(token);
+ }
+ }
+
// don't parse if its not a known token
- if (formatTokenFunctions[tokens[i]]) {
- addTimeToArrayFromToken(tokens[i], parsedInput, config);
+ if (formatTokenFunctions[token]) {
+ addTimeToArrayFromToken(token, parsedInput, config);
}
}
+
// add remaining unparsed input length to the string
- config._il = stringLength - totalParsedInputLength;
+ config._pf.charsLeftOver = stringLength - totalParsedInputLength;
+ config._pf.trailingInput = string;
// handle am pm
if (config._isPm && config._a[3] < 12) {
if (config._isPm === false && config._a[3] === 12) {
config._a[3] = 0;
}
- // return
- dateFromArray(config);
- }
-
- function makeDateFromStringAndStrictFormat(config) {
- var regexp = '', non_token_start = 0,
- tokens = config._f.match(formattingTokens),
- match, i, tokenIndex;
-
- // We're not interested in the result. Just the tokens and their
- // starting positions.
- config._f.replace(formattingTokens, function (token) {
- var offset = arguments[arguments.length - 2],
- tokenRegexp;
-
- if (formatTokenFunctions[token]) {
- tokenRegexp = getParseRegexForToken(token).toString();
- // Do not remember groups
- tokenRegexp = tokenRegexp.replace(/\(/g, '(?:');
-
- // regexp-escape strings in-between tokens
- if (offset > non_token_start) {
- regexp += regexpEscape(unescapeFormat(config._f.substring(non_token_start, offset)));
- }
- non_token_start = offset + token.length;
-
- // add token regexp
- regexp += '(' + tokenRegexp.substring(1, tokenRegexp.lastIndexOf('/')) + ')';
- }
-
- return token;
- });
-
- // Handle stuff after last formatting token.
- if (non_token_start !== config._f.length) {
- regexp += regexpEscape(unescapeFormat(config._f.substring(non_token_start)));
- }
- regexp = new RegExp('^' + regexp + '$');
- match = config._i.match(regexp);
- if (match === null) {
- config._a = [];
- config._d = new Date(0);
- config._isValid = false;
- return;
- }
-
- config._a = [];
- for (i = tokenIndex = 0; i < tokens.length; ++i) {
- if (formatTokenFunctions[tokens[i]]) {
- addTimeToArrayFromToken(tokens[i], match[tokenIndex + 1], config);
- ++tokenIndex;
- }
- }
+ // return
dateFromArray(config);
+ checkOverflow(config);
}
function unescapeFormat(s) {
// date from string and array of format strings
function makeDateFromStringAndArray(config) {
var tempConfig,
- tempMoment,
bestMoment,
- scoreToBeat = 99,
+ scoreToBeat = 1000,
i,
currentScore;
for (i = 0; i < config._f.length; i++) {
+ currentScore = 0;
tempConfig = extend({}, config);
+ initializeParsingFlags(tempConfig);
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
- currentScore += tempMoment._il || 0;
+ if (!isValid(tempConfig)) {
+ currentScore += 100;
+ }
+
+ // 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 (currentScore < scoreToBeat) {
scoreToBeat = currentScore;
- bestMoment = tempMoment;
+ bestMoment = tempConfig;
}
}
- extend(config, bestMoment);
+ extend(config, bestMoment || tempConfig);
}
// date from iso format
var input = config._i,
format = config._f;
+ if (typeof config._pf === 'undefined') {
+ initializeParsingFlags(config);
+ }
+
if (input === null ||
(typeof input === 'string' &&
input.replace(/^\s+|\s+$/g, '') === '')) {
_i : input,
_f : format,
_strict : strict
- });
- if (m != null) {
- m = m.utc();
- }
+ }).utc();
return m;
};
},
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) {
+ //this is best-effort. What if it's a valid overflow AND DST-shifted?
+ return !this._pf.overflowMonthOk && !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
}
- return !!this._isValid;
+
+ return false;
+ },
+
+ parsingFlags : function () {
+ return extend({}, this._pf);
},
invalidAt: function () {
- var i, arr1 = this._a, arr2 = (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray();
- for (i = 6; i >= 0 && arr1[i] === arr2[i]; --i) {
- // empty loop body
- }
- return i;
+ return this._pf.overflow;
},
utc : function () {
},
isLeapYear : function () {
- var year = this.year();
- return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+ return isLeapYear(this.year());
},
isDST : function () {
},
daysInMonth : function () {
- return moment.utc([this.year(), this.month() + 1, 0]).date();
+ return daysInMonth(this.year(), this.month());
},
dayOfYear : function (input) {
test.equal(moment('05/1/2012 12:25:00 am', 'MM/DD/YYYY h:m:s a').format('MM/DD/YYYY'), '05/01/2012', 'should not break if am/pm is left off from the parsing tokens');
test.equal(moment('05/1/2012 12:25:00 pm', 'MM/DD/YYYY h:m:s a').format('MM/DD/YYYY'), '05/01/2012', 'should not break if am/pm is left off from the parsing tokens');
- test.ok(moment('05/1/2012 12:25:00', 'MM/DD/YYYY h:m:s a').isValid())
- test.ok(moment('05/1/2012 12:25:00 am', 'MM/DD/YYYY h:m:s a').isValid())
- test.ok(moment('05/1/2012 12:25:00 pm', 'MM/DD/YYYY h:m:s a').isValid())
+ test.ok(moment('05/1/2012 12:25:00', 'MM/DD/YYYY h:m:s a').isValid());
+ test.ok(moment('05/1/2012 12:25:00 am', 'MM/DD/YYYY h:m:s a').isValid());
+ test.ok(moment('05/1/2012 12:25:00 pm', 'MM/DD/YYYY h:m:s a').isValid());
test.done();
},
test.equal(moment(' ', 'DD').format('YYYY-MM-DD HH:mm:ss'), 'Invalid date');
test.equal(moment(' ', ['MM', "DD"]).format('YYYY-MM-DD HH:mm:ss'), 'Invalid date');
- test.ok(!moment('', 'MM').isValid())
- test.ok(!moment(' ', 'MM').isValid())
- test.ok(!moment(' ', 'DD').isValid())
- test.ok(!moment(' ', ['MM', "DD"]).isValid())
+ test.ok(!moment('', 'MM').isValid());
+ test.ok(!moment(' ', 'MM').isValid());
+ test.ok(!moment(' ', 'DD').isValid());
+ test.ok(!moment(' ', ['MM', "DD"]).isValid());
test.done();
},
['LLLL', 'Thursday, September 2 1999 12:30 AM'],
['llll', 'Thu, Sep 2 1999 12:30 AM']
],
+ m,
i;
test.expect(2 * a.length);
test.equal(moment('13-11-1999', ['MM/DD/YYYY', 'DD/MM/YYYY']).format('MM DD YYYY'), '11 13 1999', 'second must be month');
test.equal(moment('11-13-1999', ['MM/DD/YYYY', 'DD/MM/YYYY']).format('MM DD YYYY'), '11 13 1999', 'first must be month');
- test.equal(moment('13-14-1999', ['MM/DD/YYYY', 'DD/MM/YYYY']).format('MM DD YYYY'), '01 14 2000', 'either can be a month, month first format');
- test.equal(moment('13-14-1999', ['DD/MM/YYYY', 'MM/DD/YYYY']).format('MM DD YYYY'), '02 13 2000', 'either can be a month, day first format');
+ test.equal(moment('01-02-2000', ['MM/DD/YYYY', 'DD/MM/YYYY']).format('MM DD YYYY'), '01 02 2000', 'either can be a month, month first format');
+ test.equal(moment('02-01-2000', ['DD/MM/YYYY', 'MM/DD/YYYY']).format('MM DD YYYY'), '01 02 2000', 'either can be a month, day first format');
test.equal(moment('11-02-10', ['MM/DD/YY', 'YY MM DD', 'DD-MM-YY']).format('MM DD YYYY'), '02 11 2010', 'all unparsed substrings have influence on format penalty');
test.equal(moment('11-02-10', ['MM.DD.YY', 'DD-MM-YY']).format('MM DD YYYY'), '02 11 2010', 'escape RegExp special characters on comparing');
"null" : function (test) {
test.expect(6);
- test.Ok(!moment('').isValid());
- test.Ok(!moment(null).isValid());
- test.Ok(!moment('', 'YYYY-MM-DD').isValid());
+ test.ok(!moment('').isValid());
+ test.ok(!moment(null).isValid());
+ test.ok(!moment('', 'YYYY-MM-DD').isValid());
- test.Ok(!moment.utc('').isValid(), "Calling moment.utc('')");
- test.Ok(!moment.utc(null).isValid(), "Calling moment.utc(null)");
- test.Ok(!moment.utc('', 'YYYY-MM-DD').isValid(), "Calling moment.utc('', 'YYYY-MM-DD')");
+ test.ok(!moment.utc('').isValid(), "Calling moment.utc('')");
+ test.ok(!moment.utc(null).isValid(), "Calling moment.utc(null)");
+ test.ok(!moment.utc('', 'YYYY-MM-DD').isValid(), "Calling moment.utc('', 'YYYY-MM-DD')");
test.done();
},
exports.is_valid = {
"array bad month" : function (test) {
- test.expect(2);
+ var underflow = moment([2010, -1]),
+ overflow = moment([2100, 12]);
+
+ test.expect(4);
+ test.equal(underflow.isValid(), false, 'month -1 invalid');
+ test.equal(underflow.parsingFlags().overflow, 1, 'month -1 overflow');
- test.equal(moment([2010, -1]).isValid(), false, 'month -1');
- test.equal(moment([2100, 12]).isValid(), false, 'month 12');
+ test.equal(overflow.isValid(), false, 'month 12 invalid');
+ test.equal(overflow.parsingFlags().overflow, 1, 'month 12 invalid');
test.done();
},
"array good month" : function (test) {
- test.expect(24);
+ test.expect(12 * 2);
for (var i = 0; i < 12; i++) {
test.equal(moment([2010, i]).isValid(), true, 'month ' + i);
},
"array bad date" : function (test) {
- test.expect(4);
+ var tests = [
+ moment([2010, 0, 0]),
+ moment([2100, 0, 32]),
+ moment.utc([2010, 0, 0]),
+ moment.utc([2100, 0, 32])
+ ],
+ i, m;
- test.equal(moment([2010, 0, 0]).isValid(), false, 'date 0');
- test.equal(moment([2100, 0, 32]).isValid(), false, 'date 32');
+ test.expect(tests.length * 2);
- test.equal(moment.utc([2010, 0, 0]).isValid(), false, 'utc date 0');
- test.equal(moment.utc([2100, 0, 32]).isValid(), false, 'utc date 32');
+ for (i in tests) {
+ m = tests[i];
+ test.equal(m.isValid(), false);
+ test.equal(m.parsingFlags().overflow, 2);
+ }
test.done();
},
test.expect(2);
test.equal(moment('fail', "MM-DD-YYYY").isValid(), false, 'string "fail" with format "MM-DD-YYYY"');
- test.equal(moment("xx-xx-2001", 'DD-MM-YYY').isValid(), false, 'string "xx-xx-2001" with format "MM-DD-YYYY"');
+ test.equal(moment("xx-xx-2001", 'DD-MM-YYY').isValid(), true, 'string "xx-xx-2001" with format "MM-DD-YYYY"');
test.done();
},