Explorar el Código

break apart date_utils.js

Adam Shaw hace 12 años
padre
commit
1f5b6490a3
Se han modificado 6 ficheros con 514 adiciones y 489 borrados
  1. 2 1
      lumbar.json
  2. 214 0
      src/date-formatting.js
  3. 0 487
      src/date_util.js
  4. 264 0
      src/moment-ext.js
  5. 31 0
      src/util.js
  6. 3 1
      tests/date_util_test.html

+ 2 - 1
lumbar.json

@@ -20,7 +20,8 @@
         "src/Header.js",
         "src/EventManager.js",
         "src/util.js",
-        "src/date_util.js",
+        "src/moment-ext.js",
+        "src/date-formatting.js",
         "src/basic/MonthView.js",
         "src/basic/BasicWeekView.js",
         "src/basic/BasicDayView.js",

+ 214 - 0
src/date-formatting.js

@@ -0,0 +1,214 @@
+
+// Single Date Formatting
+// -------------------------------------------------------------------------------------------------
+
+
+// call this if you want Moment's original format method to be used
+function momentFormat(mom, formatStr) {
+	return moment.fn.format.call(mom, formatStr);
+}
+
+
+// Formats `date` with a Moment formatting string, but allow our non-zero areas and
+// additional token.
+function formatDate(date, formatStr) {
+	return formatDateWithChunks(date, getFormatStringChunks(formatStr));
+}
+
+
+function formatDateWithChunks(date, chunks) {
+	var s = '';
+	var i;
+
+	for (i=0; i<chunks.length; i++) {
+		s += formatDateWithChunk(date, chunks[i]);
+	}
+
+	return s;
+}
+
+
+// addition formatting tokens we want recognized
+var tokenOverrides = {
+	t: function(date) { // "a" or "p"
+		return momentFormat(date, 'a').charAt(0);
+	},
+	T: function(date) { // "A" or "P"
+		return momentFormat(date, 'A').charAt(0);
+	}
+};
+
+
+function formatDateWithChunk(date, chunk) {
+	var token;
+	var maybeStr;
+
+	if (typeof chunk === 'string') { // a literal string
+		return chunk;
+	}
+	else if ((token = chunk.token)) { // a token, like "YYYY"
+		if (tokenOverrides[token]) {
+			return tokenOverrides[token](date); // use our custom token
+		}
+		return momentFormat(date, token);
+	}
+	else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
+		maybeStr = formatDateWithChunks(date, chunk.maybe);
+		if (maybeStr.match(/[1-9]/)) {
+			return maybeStr;
+		}
+	}
+
+	return '';
+}
+
+
+// Date Range Formatting
+// -------------------------------------------------------------------------------------------------
+// TODO: make it work with timezone offset
+
+// Using a formatting string meant for a single date, generate a range string, like
+// "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
+// If the dates are the same as far as the format string is concerned, just return a single
+// rendering of one date, without any separator.
+function formatRange(date1, date2, formatStr, separator, isRTL) {
+
+	// Expand localized format strings, like "LL" -> "MMMM D YYYY"
+	formatStr = date1.lang().longDateFormat(formatStr) || formatStr;
+	// BTW, this is not important for `formatDate` because it is impossible to put custom tokens
+	// or non-zero areas in Moment's localized format strings.
+
+	separator = separator || ' - ';
+
+	return formatRangeWithChunks(
+		date1,
+		date2,
+		getFormatStringChunks(formatStr),
+		separator,
+		isRTL
+	);
+}
+fc.formatRange = formatRange; // expose
+
+
+function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
+	var chunkStr; // the rendering of the chunk
+	var leftI;
+	var leftStr = '';
+	var rightI;
+	var rightStr = '';
+	var middleI;
+	var middleStr1 = '';
+	var middleStr2 = '';
+	var middleStr = '';
+
+	// Start at the leftmost side of the formatting string and continue until you hit a token
+	// that is not the same between dates.
+	for (leftI=0; leftI<chunks.length; leftI++) {
+		chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]);
+		if (chunkStr === false) {
+			break;
+		}
+		leftStr += chunkStr;
+	}
+
+	// Similarly, start at the rightmost side of the formatting string and move left
+	for (rightI=chunks.length-1; rightI>leftI; rightI--) {
+		chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]);
+		if (chunkStr === false) {
+			break;
+		}
+		rightStr = chunkStr + rightStr;
+	}
+
+	// The area in the middle is different for both of the dates.
+	// Collect them distinctly so we can jam them together later.
+	for (middleI=leftI; middleI<=rightI; middleI++) {
+		middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
+		middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
+	}
+
+	if (middleStr1 || middleStr2) {
+		if (isRTL) {
+			middleStr = middleStr2 + separator + middleStr1;
+		}
+		else {
+			middleStr = middleStr1 + separator + middleStr2;
+		}
+	}
+
+	return leftStr + middleStr + rightStr;
+}
+
+
+var similarUnitMap = {
+	Y: 'year',
+	M: 'month',
+	D: 'day', // day of month
+	d: 'day' // day of week
+};
+// don't go any further than day, because we don't want to break apart times like "12:30:00"
+// TODO: week maybe?
+
+
+// Given a formatting chunk, and given that both dates are similar in the regard the
+// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
+function formatSimilarChunk(date1, date2, chunk) {
+	var token;
+	var unit;
+
+	if (typeof chunk === 'string') { // a literal string
+		return chunk;
+	}
+	else if ((token = chunk.token)) {
+		unit = similarUnitMap[token.charAt(0)];
+		// are the dates the same for this unit of measurement?
+		if (unit && date1.isSame(date2, unit)) {
+			return momentFormat(date1, token); // would be the same if we used `date2`
+			// BTW, don't support custom tokens
+		}
+	}
+
+	return false; // the chunk is NOT the same for the two dates
+	// BTW, don't support splitting on non-zero areas
+}
+
+
+// Chunking Utils
+// -------------------------------------------------------------------------------------------------
+
+
+var formatStringChunkCache = {};
+
+
+function getFormatStringChunks(formatStr) {
+	if (formatStr in formatStringChunkCache) {
+		return formatStringChunkCache[formatStr];
+	}
+	return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
+}
+
+
+// Break the formatting string into an array of chunks
+function chunkFormatString(formatStr) {
+	var chunks = [];
+	var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|((\w)\4*o?T?)|([^\w\[\(]+)/g; // TODO: more descrimination
+	var match;
+
+	while ((match = chunker.exec(formatStr))) {
+		if (match[1]) { // a literal string instead [ ... ]
+			chunks.push(match[1]);
+		}
+		else if (match[2]) { // non-zero formatting inside ( ... )
+			chunks.push({ maybe: chunkFormatString(match[2]) });
+		}
+		else if (match[3]) { // a formatting token
+			chunks.push({ token: match[3] });
+		}
+		else if (match[5]) { // an unenclosed literal string
+			chunks.push(match[5]);
+		}
+	}
+
+	return chunks;
+}

+ 0 - 487
src/date_util.js

@@ -1,487 +0,0 @@
-
-var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
-var ambigTimeRegex = /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))$/;
-var ambigZoneRegex = /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;
-var momentCloneMethod = moment.fn.clone;
-var momentFormatMethod = moment.fn.format;
-var momentToISOStringMethod = moment.fn.toISOString;
-
-
-// diffs the two moments into a Duration where full-days are recorded first,
-// then the remaining time.
-function dayishDiff(d1, d0) {
-	return moment.duration({
-		days: d1.clone().stripTime().diff(d0.clone().stripTime(), 'days'),
-		ms: d1.time() - d0.time()
-	});
-}
-
-function isNativeDate(input) {
-	return  Object.prototype.toString.call(input) === '[object Date]' ||
-		input instanceof Date;
-}
-
-
-// MOMENT: creating
-// -------------------------------------------------------------------------------------------------
-
-// Creates a moment in the local timezone, similar to the vanilla moment(...) constructor,
-// but with extra features:
-// - ambiguous times
-// - enhanced formatting (TODO)
-fc.moment = function() {
-	return buildMoment(arguments);
-};
-
-// Sames as fc.moment, but creates a moment in the UTC timezone.
-fc.moment.utc = function() {
-	return buildMoment(arguments, true);
-};
-
-// Creates a moment and preserves the timezone offset of the ISO8601 string,
-// allowing for ambigous timezones. If the string is not an ISO8601 string,
-// the moment is processed in UTC-mode (a departure from moment's method).
-fc.moment.parseZone = function() {
-	return buildMoment(arguments, true, true);
-};
-
-// when parseZone==true, if can't figure it out, fall back to parseUTC
-function buildMoment(args, parseUTC, parseZone) {
-	var isSingleArg = args.length == 1;
-	var isSingleMoment = isSingleArg && moment.isMoment(args[0]);
-	var isSingleString = isSingleArg && typeof args[0] === 'string';
-	var isSingleArray = isSingleArg && $.isArray(args[0]);
-	var isSingleNativeDate = isSingleArg && isNativeDate(args[0]);
-	var isAmbigTime = isSingleString && ambigTimeRegex.test(args[0]);
-	var isAmbigZone = isAmbigTime || isSingleArray || isSingleString && ambigZoneRegex.test(args[0]);
-	var mom;
-
-	if (parseUTC || parseZone || isAmbigTime) {
-		mom = moment.utc.apply(moment, args);
-	}
-	else {
-		mom = moment.apply(null, args);
-	}
-
-	// if we are essentially cloning a moment, transfer over existing _ambig properties
-	if (isSingleMoment) {
-		transferAmbigs(args[0], mom);
-	}
-
-	if (isAmbigTime) {
-		mom._ambigTime = true;
-		mom._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
-	}
-
-	if (parseZone) {
-		if (isAmbigZone) {
-			mom._ambigZone = true;
-		}
-		else if (isSingleString) {
-			mom.zone(args[0]); // if fails, will set it to 0, which it already was
-		}
-		else if (isSingleNativeDate || args[0] === undefined) {
-			// native Date object?
-			// specified with no arguments?
-			// then consider the moment to be local
-			mom.local();
-		}
-	}
-
-	mom._fc = true; // flag for use other extended functionality (only formatting at this point)
-
-	return mom;
-}
-
-// we need to patch moment's clone method because it will not copy our _ambig properties
-moment.fn.clone = function() {
-	var res = momentCloneMethod.apply(this, arguments);
-	transferAmbigs(this, res);
-	return res;
-};
-
-// transfers our internal _ambig properties from one moment to another, but only if true
-function transferAmbigs(srcMoment, destMoment) {
-	if (srcMoment._ambigTime) {
-		destMoment._ambigTime = true;
-	}
-	if (srcMoment._ambigZone) {
-		destMoment._ambigZone = true;
-	}
-}
-
-
-// MOMENT: time-of-day
-// -------------------------------------------------------------------------------------------------
-
-
-// GETTER
-// Returns a Duration with the hours/minutes/seconds/ms values of the moment.
-// If the moment has an ambiguous time, a duration of 00:00 will be returned.
-//
-// SETTER
-// You can supply a Duration, a Moment, or a Duration-like argument.
-// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
-moment.fn.time = function(time) {
-	if (time === undefined) { // getter
-		return moment.duration({
-			hours: this.hours(),
-			minutes: this.minutes(),
-			seconds: this.seconds(),
-			milliseconds: this.milliseconds()
-		});
-	}
-	else { // setter
-
-		delete this._ambigTime; // mark that the moment now has a time
-
-		if (!moment.isDuration(time) && !moment.isMoment(time)) {
-			time = moment.duration(time);
-		}
-
-		return this.hours(time.hours() + time.days() * 24) // day value will cause overflow (so 24 hours becomes 00:00:00 of next day)
-			.minutes(time.minutes())
-			.seconds(time.seconds())
-			.milliseconds(time.milliseconds());
-	}
-};
-
-// Converts the moment to UTC, stripping out its time-of-day and timezone offset,
-// but preserving its YMD. A moment with a stripped time will display no time
-// nor timezone offset when .format() is called.
-moment.fn.stripTime = function() {
-	var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
-
-	this._ambigTime = true;
-	this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
-
-	return this.utc()
-		.year(a[0])
-		.month(a[1])
-		.date(a[2])
-		.hours(0)
-		.minutes(0)
-		.seconds(0)
-		.milliseconds(0);
-};
-
-// Returns if the moment has a non-ambiguous time (boolean)
-moment.fn.hasTime = function() {
-	return !this._ambigTime;
-};
-
-
-// MOMENT: timezone offset
-// -------------------------------------------------------------------------------------------------
-
-// Converts the moment to UTC, stripping out its timezone offset, but preserving its
-// YMD and time-of-day. A moment with a stripped timezone offset will display no
-// timezone offset when .format() is called.
-moment.fn.stripZone = function() {
-	var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
-
-	this._ambigZone = true;
-
-	return this.utc()
-		.year(a[0])
-		.month(a[1])
-		.date(a[2])
-		.hours(a[3])
-		.minutes(a[4])
-		.seconds(a[5])
-		.milliseconds(a[6]);
-};
-
-// Returns of the moment has a non-ambiguous timezone offset (boolean)
-moment.fn.hasZone = function() {
-	return !this._ambigZone;
-};
-
-
-// MOMENT: formatting mods
-// -------------------------------------------------------------------------------------------------
-
-moment.fn.format = function() {
-	if (!arguments[0]) {
-		if (this._ambigTime) {
-			return momentFormat(this, 'YYYY-MM-DD');
-		}
-		if (this._ambigZone) {
-			return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
-		}
-	}
-	if (this._fc) {
-		return formatDate(this, arguments[0]); // our extended formatting
-	}
-	else {
-		return momentFormatMethod.apply(this, arguments); // pass along all arguments
-	}
-};
-
-moment.fn.toISOString = function() {
-	if (this._ambigTime) {
-		return momentFormat(this, 'YYYY-MM-DD');
-	}
-	if (this._ambigZone) {
-		return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
-	}
-	return momentToISOStringMethod.apply(this, arguments); // pass along all arguments
-};
-
-// call this if you want Moment's original format method to be used
-function momentFormat(moment, formatStr) {
-	return momentFormatMethod.call(moment, formatStr);
-}
-
-
-// MOMENT: misc utils
-// -------------------------------------------------------------------------------------------------
-
-// Is the moment within the specified range? `end` is exclusive.
-// TODO: rename for collision reasons?
-moment.fn.isWithin = function(start, end) {
-	return this >= moment(start) && this < moment(end);
-};
-
-$.each([
-	'isBefore',
-	'isAfter',
-	//'isSame', // nevermind. moment handles normalization to UTC
-	'isWithin'
-], function(i, methodName) {
-	var origMethod = moment.fn[methodName];
-	var momentCount = methodName == 'isWithin' ? 2 : 1;
-
-	moment.fn[methodName] = function() {
-		var newThis;
-		var args = Array.prototype.slice.call(arguments);
-		var i;
-
-		if (this._ambigZone) {
-			for (i=0; i<momentCount; i++) {
-				if (typeof args[i] === 'string') {
-					args[i] = fc.moment.parseZone(args[i]);
-				}
-			}
-		}
-
-		for (i=0; i<momentCount; i++) {
-			if (moment.isMoment(args[i]) && args[i]._ambigZone !== this._ambigZone) {
-				newThis = newThis || this.clone().stripZone();
-				args[i] = args[i].clone().stripZone();
-			}
-		}
-
-		return origMethod.apply(newThis || this, args);
-	};
-});
-
-
-// Single Date Formatting
-// -------------------------------------------------------------------------------------------------
-
-
-// Formats `date` with a Moment formatting string, but allow our non-zero areas and
-// additional token.
-function formatDate(date, formatStr) {
-	return formatDateWithChunks(date, getFormatStringChunks(formatStr));
-}
-
-
-function formatDateWithChunks(date, chunks) {
-	var s = '';
-	var i;
-
-	for (i=0; i<chunks.length; i++) {
-		s += formatDateWithChunk(date, chunks[i]);
-	}
-
-	return s;
-}
-
-
-// addition formatting tokens we want recognized
-var tokenOverrides = {
-	t: function(date) { // "a" or "p"
-		return momentFormat(date, 'a').charAt(0);
-	},
-	T: function(date) { // "A" or "P"
-		return momentFormat(date, 'A').charAt(0);
-	}
-};
-
-
-function formatDateWithChunk(date, chunk) {
-	var token;
-	var maybeStr;
-
-	if (typeof chunk === 'string') { // a literal string
-		return chunk;
-	}
-	else if ((token = chunk.token)) { // a token, like "YYYY"
-		if (tokenOverrides[token]) {
-			return tokenOverrides[token](date); // use our custom token
-		}
-		return momentFormat(date, token);
-	}
-	else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
-		maybeStr = formatDateWithChunks(date, chunk.maybe);
-		if (maybeStr.match(/[1-9]/)) {
-			return maybeStr;
-		}
-	}
-
-	return '';
-}
-
-
-// Date Range Formatting
-// -------------------------------------------------------------------------------------------------
-// TODO: make it work with timezone offset
-
-// Using a formatting string meant for a single date, generate a range string, like
-// "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
-// If the dates are the same as far as the format string is concerned, just return a single
-// rendering of one date, without any separator.
-function formatRange(date1, date2, formatStr, separator, isRTL) {
-
-	// Expand localized format strings, like "LL" -> "MMMM D YYYY"
-	formatStr = date1.lang().longDateFormat(formatStr) || formatStr;
-	// BTW, this is not important for `formatDate` because it is impossible to put custom tokens
-	// or non-zero areas in Moment's localized format strings.
-
-	separator = separator || ' - ';
-
-	return formatRangeWithChunks(
-		date1,
-		date2,
-		getFormatStringChunks(formatStr),
-		separator,
-		isRTL
-	);
-}
-fc.formatRange = formatRange; // expose
-
-
-function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
-	var chunkStr; // the rendering of the chunk
-	var leftI;
-	var leftStr = '';
-	var rightI;
-	var rightStr = '';
-	var middleI;
-	var middleStr1 = '';
-	var middleStr2 = '';
-	var middleStr = '';
-
-	// Start at the leftmost side of the formatting string and continue until you hit a token
-	// that is not the same between dates.
-	for (leftI=0; leftI<chunks.length; leftI++) {
-		chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]);
-		if (chunkStr === false) {
-			break;
-		}
-		leftStr += chunkStr;
-	}
-
-	// Similarly, start at the rightmost side of the formatting string and move left
-	for (rightI=chunks.length-1; rightI>leftI; rightI--) {
-		chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]);
-		if (chunkStr === false) {
-			break;
-		}
-		rightStr = chunkStr + rightStr;
-	}
-
-	// The area in the middle is different for both of the dates.
-	// Collect them distinctly so we can jam them together later.
-	for (middleI=leftI; middleI<=rightI; middleI++) {
-		middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
-		middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
-	}
-
-	if (middleStr1 || middleStr2) {
-		if (isRTL) {
-			middleStr = middleStr2 + separator + middleStr1;
-		}
-		else {
-			middleStr = middleStr1 + separator + middleStr2;
-		}
-	}
-
-	return leftStr + middleStr + rightStr;
-}
-
-
-var similarUnitMap = {
-	Y: 'year',
-	M: 'month',
-	D: 'day', // day of month
-	d: 'day' // day of week
-};
-// don't go any further than day, because we don't want to break apart times like "12:30:00"
-// TODO: week maybe?
-
-
-// Given a formatting chunk, and given that both dates are similar in the regard the
-// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
-function formatSimilarChunk(date1, date2, chunk) {
-	var token;
-	var unit;
-
-	if (typeof chunk === 'string') { // a literal string
-		return chunk;
-	}
-	else if ((token = chunk.token)) {
-		unit = similarUnitMap[token.charAt(0)];
-		// are the dates the same for this unit of measurement?
-		if (unit && date1.isSame(date2, unit)) {
-			return momentFormat(date1, token); // would be the same if we used `date2`
-			// BTW, don't support custom tokens
-		}
-	}
-
-	return false; // the chunk is NOT the same for the two dates
-	// BTW, don't support splitting on non-zero areas
-}
-
-
-// Chunking Utils
-// -------------------------------------------------------------------------------------------------
-
-
-var formatStringChunkCache = {};
-
-
-function getFormatStringChunks(formatStr) {
-	if (formatStr in formatStringChunkCache) {
-		return formatStringChunkCache[formatStr];
-	}
-	return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
-}
-
-
-// Break the formatting string into an array of chunks
-function chunkFormatString(formatStr) {
-	var chunks = [];
-	var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|((\w)\4*o?T?)|([^\w\[\(]+)/g; // TODO: more descrimination
-	var match;
-
-	while ((match = chunker.exec(formatStr))) {
-		if (match[1]) { // a literal string instead [ ... ]
-			chunks.push(match[1]);
-		}
-		else if (match[2]) { // non-zero formatting inside ( ... )
-			chunks.push({ maybe: chunkFormatString(match[2]) });
-		}
-		else if (match[3]) { // a formatting token
-			chunks.push({ token: match[3] });
-		}
-		else if (match[5]) { // an unenclosed literal string
-			chunks.push(match[5]);
-		}
-	}
-
-	return chunks;
-}
-

+ 264 - 0
src/moment-ext.js

@@ -0,0 +1,264 @@
+
+var ambigTimeRegex = /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))$/;
+var ambigZoneRegex = /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;
+
+
+// MOMENT: creating
+// -------------------------------------------------------------------------------------------------
+
+// Creates a moment in the local timezone, similar to the vanilla moment(...) constructor,
+// but with extra features:
+// - ambiguous times
+// - enhanced formatting (TODO)
+fc.moment = function() {
+	return makeMoment(arguments);
+};
+
+// Sames as fc.moment, but creates a moment in the UTC timezone.
+fc.moment.utc = function() {
+	return makeMoment(arguments, true);
+};
+
+// Creates a moment and preserves the timezone offset of the ISO8601 string,
+// allowing for ambigous timezones. If the string is not an ISO8601 string,
+// the moment is processed in UTC-mode (a departure from moment's method).
+fc.moment.parseZone = function() {
+	return makeMoment(arguments, true, true);
+};
+
+function FCMoment(config) {
+	extend(this, config);
+}
+
+FCMoment.prototype = createObject(moment.fn);
+
+FCMoment.prototype.clone = function() {
+	return makeMoment([ this ]);
+};
+
+// when parseZone==true, if can't figure it out, fall back to parseUTC
+function makeMoment(args, parseUTC, parseZone) {
+	var isSingleArg = args.length == 1;
+	var isSingleMoment = isSingleArg && moment.isMoment(args[0]);
+	var isSingleString = isSingleArg && typeof args[0] === 'string';
+	var isSingleArray = isSingleArg && $.isArray(args[0]);
+	var isSingleNativeDate = isSingleArg && isNativeDate(args[0]);
+	var isAmbigTime = isSingleString && ambigTimeRegex.test(args[0]);
+	var isAmbigZone = isAmbigTime || isSingleArray || isSingleString && ambigZoneRegex.test(args[0]);
+	var mom;
+
+	if (parseUTC || parseZone || isAmbigTime) {
+		mom = moment.utc.apply(moment, args);
+	}
+	else {
+		mom = moment.apply(null, args);
+	}
+
+	if (isSingleMoment) {
+		transferAmbigs(args[0], mom);
+	}
+
+	if (isAmbigTime) {
+		mom._ambigTime = true;
+		mom._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
+	}
+
+	if (parseZone) {
+		if (isAmbigZone) {
+			mom._ambigZone = true;
+		}
+		else if (isSingleString) {
+			mom.zone(args[0]); // if fails, will set it to 0, which it already was
+		}
+		else if (isSingleNativeDate || args[0] === undefined) {
+			// native Date object?
+			// specified with no arguments?
+			// then consider the moment to be local
+			mom.local();
+		}
+	}
+
+	return new FCMoment(mom);
+}
+
+// transfers our internal _ambig properties from one moment to another
+function transferAmbigs(src, dest) {
+	if (src._ambigTime) {
+		dest._ambigTime = true;
+	}
+	else if (dest._ambigTime) {
+		delete dest._ambigTime;
+	}
+
+	if (src._ambigZone) {
+		dest._ambigZone = true;
+	}
+	else if (dest._ambigZone) {
+		delete dest._ambigZone;
+	}
+}
+
+function makeMomentAs(input, model) {
+	var output = fc.moment(input);
+	if (model._ambigTime) {
+		output.stripTime();
+	}
+	else if (model._ambigZone) {
+		output.stripZone();
+	}
+	return output;
+}
+
+
+
+// MOMENT: time-of-day
+// -------------------------------------------------------------------------------------------------
+
+
+// GETTER
+// Returns a Duration with the hours/minutes/seconds/ms values of the moment.
+// If the moment has an ambiguous time, a duration of 00:00 will be returned.
+//
+// SETTER
+// You can supply a Duration, a Moment, or a Duration-like argument.
+// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
+FCMoment.prototype.time = function(time) {
+	if (time === undefined) { // getter
+		return moment.duration({
+			hours: this.hours(),
+			minutes: this.minutes(),
+			seconds: this.seconds(),
+			milliseconds: this.milliseconds()
+		});
+	}
+	else { // setter
+
+		delete this._ambigTime; // mark that the moment now has a time
+
+		if (!moment.isDuration(time) && !moment.isMoment(time)) {
+			time = moment.duration(time);
+		}
+
+		return this.hours(time.hours() + time.days() * 24) // day value will cause overflow (so 24 hours becomes 00:00:00 of next day)
+			.minutes(time.minutes())
+			.seconds(time.seconds())
+			.milliseconds(time.milliseconds());
+	}
+};
+
+// Converts the moment to UTC, stripping out its time-of-day and timezone offset,
+// but preserving its YMD. A moment with a stripped time will display no time
+// nor timezone offset when .format() is called.
+FCMoment.prototype.stripTime = function() {
+	var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
+
+	this._ambigTime = true;
+	this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
+
+	return this.utc()
+		.year(a[0])
+		.month(a[1])
+		.date(a[2])
+		.hours(0)
+		.minutes(0)
+		.seconds(0)
+		.milliseconds(0);
+};
+
+// Returns if the moment has a non-ambiguous time (boolean)
+FCMoment.prototype.hasTime = function() {
+	return !this._ambigTime;
+};
+
+
+// MOMENT: timezone offset
+// -------------------------------------------------------------------------------------------------
+
+// Converts the moment to UTC, stripping out its timezone offset, but preserving its
+// YMD and time-of-day. A moment with a stripped timezone offset will display no
+// timezone offset when .format() is called.
+FCMoment.prototype.stripZone = function() {
+	var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
+
+	this._ambigZone = true;
+
+	return this.utc()
+		.year(a[0])
+		.month(a[1])
+		.date(a[2])
+		.hours(a[3])
+		.minutes(a[4])
+		.seconds(a[5])
+		.milliseconds(a[6]);
+};
+
+// Returns of the moment has a non-ambiguous timezone offset (boolean)
+FCMoment.prototype.hasZone = function() {
+	return !this._ambigZone;
+};
+
+FCMoment.prototype.zone = function(tzo) {
+	if (tzo != undefined) {
+		this._ambigZone = false;
+	}
+	return moment.fn.zone.apply(this, arguments);
+};
+
+FCMoment.prototype.local = function() {
+	this._ambigZone = false;
+	return moment.fn.local.apply(this, arguments);
+};
+
+FCMoment.prototype.utc = function() {
+	this._ambigZone = false;
+	return moment.fn.utc.apply(this, arguments);
+};
+
+
+// MOMENT: formatting mods
+// -------------------------------------------------------------------------------------------------
+
+FCMoment.prototype.format = function() {
+	if (arguments[0]) {
+		return formatDate(this, arguments[0]); // our extended formatting
+	}
+	if (this._ambigTime) {
+		return momentFormat(this, 'YYYY-MM-DD');
+	}
+	if (this._ambigZone) {
+		return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
+	}
+	return momentFormat(this); // default moment original formatting
+};
+
+FCMoment.prototype.toISOString = function() {
+	if (this._ambigTime) {
+		return momentFormat(this, 'YYYY-MM-DD');
+	}
+	if (this._ambigZone) {
+		return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
+	}
+	return moment.fn.toISOString.apply(this, arguments);
+};
+
+
+// MOMENT: misc utils
+// -------------------------------------------------------------------------------------------------
+
+// Is the moment within the specified range? `end` is exclusive.
+FCMoment.prototype.isWithin = function(start, end) {
+	return this >= makeMomentAs(start, this) && this < makeMomentAs(end, this);
+};
+
+$.each([
+	'isBefore',
+	'isAfter'
+], function(i, methodName) {
+	FCMoment.prototype[methodName] = function(input, units) {
+		moment.fn[methodName].call(
+			this,
+			makeMomentAs(input, this),
+			units
+		);
+	};
+});

+ 31 - 0
src/util.js

@@ -9,6 +9,37 @@ function createObject(proto) { // like Object.create
 	return new f();
 }
 
+function extend(a, b) {
+	for (var i in b) {
+		if (b.hasOwnProperty(i)) {
+			a[i] = b[i];
+		}
+	}
+}
+
+
+
+/* Date
+-----------------------------------------------------------------------------*/
+
+
+var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
+
+
+// diffs the two moments into a Duration where full-days are recorded first,
+// then the remaining time.
+function dayishDiff(d1, d0) {
+	return moment.duration({
+		days: d1.clone().stripTime().diff(d0.clone().stripTime(), 'days'),
+		ms: d1.time() - d0.time()
+	});
+}
+
+
+function isNativeDate(input) {
+	return  Object.prototype.toString.call(input) === '[object Date]' ||
+		input instanceof Date;
+}
 
 
 

+ 3 - 1
tests/date_util_test.html

@@ -5,7 +5,9 @@
 <script src='../lib/moment/moment.js'></script>
 <script src='../lib/moment/min/langs.min.js'></script>
 <script>fc = {}</script>
-<script src='../src/date_util.js'></script>
+<script src='../src/util.js'></script>
+<script src='../src/moment-ext.js'></script>
+<script src='../src/date-formatting.js'></script>
 </head>
 <body>
 </body>