YUI DateMath on JavaScript Date Object

In the Date Functions article, I mentioned that you could apply the methods on YAHOO.util.DateMath to the Date object direclty, instead of implementing a static class. A few people wrote in and requested that I do just that, so that is what I will write about today.

For reference, please check out the DateMath class at YAHOO:

DateMath

The first step is to remove the constants from the static class and augment the Date object with them.

Example 1: Date Augmentation

YAHOO.lang.augmentObject(Date, {

	/**
	* Constant field representing Day
	* @property DAY
	* @static
	* @final
	* @type String
	*/
	DAY : "D",

	/**
	* Constant field representing Week
	* @property WEEK
	* @static
	* @final
	* @type String
	*/
	WEEK : "W",

	/**
	* Constant field representing Year
	* @property YEAR
	* @static
	* @final
	* @type String
	*/
	YEAR : "Y",

	/**
	* Constant field representing Month
	* @property MONTH
	* @static
	* @final
	* @type String
	*/
	MONTH : "M",

	/**
	* Constant field representing one day, in milliseconds
	* @property ONE_DAY_MS
	* @static
	* @final
	* @type Number
	*/
	ONE_DAY_MS : 1000*60*60*24,
	
	/**
	* Retrieves a JavaScript Date object representing January 1 of any given year.
	* @method getJan1
	* @param {Number} calendarYear		The calendar year for which to retrieve January 1
	* @return {Date}	January 1 of the calendar year specified.
	*/
	getJan1 : function(calendarYear) {
		return Date.getDate(calendarYear,0,1);
	},

	/**
	 * Returns a new JavaScript Date object, representing the given year, month and date. Time fields (hr, min, sec, ms) on the new Date object
	 * are set to 0. The method allows Date instances to be created with the a year less than 100. "new Date(year, month, date)" implementations 
	 * set the year to 19xx if a year (xx) which is less than 100 is provided.
	 * 
	 * NOTE:Validation on argument values is not performed. It is the callers responsibility to ensure
	 * arguments are valid as per the ECMAScript-262 Date object specification for the new Date(year, month[, date]) constructor.
	 * 
	 * @method getDate
	 * @param {Number} y Year.
	 * @param {Number} m Month index from 0 (Jan) to 11 (Dec).
	 * @param {Number} d (optional) Date from 1 to 31. If not provided, defaults to 1.
	 * @return {Date} The JavaScript date object with year, month, date set as provided.
	 */
	getDate : function(y, m, d) {
		var dt = null;
		if (YAHOO.lang.isUndefined(d)) {
			d = 1;
		}
		if (y >= 100) {
			dt = new Date(y, m, d);
		} else {
			dt = new Date();
			dt.setFullYear(y);
			dt.setMonth(m);
			dt.setDate(d);
			dt.setHours(0,0,0,0);
		}
		return dt;
	}
});

Most of what we have moved here are constant values, but I did move two static methods. We moved "getJan1", because the parameter required is not a Date object, but instead a year integer and therefore, doesnt really belong on each instance of Date. The "getDate" method is a shortcut method to retrieve a Date object from 3 integers and therefore also does not belong on "Date.prototype".

The following methods have been transformed to interact with the current Date object instantiation instead of requiring Date as a parameter.

Example 2: Date.prototype Augmentation

YAHOO.lang.augmentObject(Date.prototype, {

	/**
	* Adds the specified amount of time to the this instance.
	* @method add
	* @param {String} field	The field constant to be used for performing addition.
	* @param {Number} amount	The number of units (measured in the field constant) to add to the date.
	* @return {Date} The resulting Date object
	*/
	add : function(field, amount) {
		var d = new Date(this.getTime());
		
		switch (field) {
			case Date.MONTH:
				var newMonth = this.getMonth() + amount;
				var years = 0;

				if (newMonth < 0) {
					while (newMonth < 0) {
						newMonth += 12;
						years -= 1;
					}
				} else if (newMonth > 11) {
					while (newMonth > 11) {
						newMonth -= 12;
						years += 1;
					}
				}
				
				d.setMonth(newMonth);
				d.setFullYear(this.getFullYear() + years);
				break;
			case Date.DAY:
				d.setDate(this.getDate() + amount);
				break;
			case Date.YEAR:
				d.setFullYear(this.getFullYear() + amount);
				break;
			case Date.WEEK:
				d.setDate(this.getDate() + (amount * 7));
				break;
		}
		return d;
	},

	/**
	* Subtracts the specified amount of time from the this instance.
	* @method subtract
	* @param {Number} field	The this field constant to be used for performing subtraction.
	* @param {Number} amount	The number of units (measured in the field constant) to subtract from the date.
	* @return {Date} The resulting Date object
	*/
	subtract : function(field, amount) {
		return this.add(field, (amount*-1));
	},

	/**
	* Determines whether a given date is before another date on the calendar.
	* @method before
	* @param {Date} compareTo	The Date object to use for the comparison
	* @return {Boolean} true if the date occurs before the compared date; false if not.
	*/
	before : function(compareTo) {
		return (that.getTime() < compareTo.getTime());
	},

	/**
	* Determines whether a given date is after another date on the calendar.
	* @method after
	* @param {Date} compareTo	The Date object to use for the comparison
	* @return {Boolean} true if the date occurs after the compared date; false if not.
	*/
	after : function(compareTo) {
		return (that.getTime() > compareTo.getTime());
	},

	/**
	* Determines whether a given date is between two other dates on the calendar.
	* @method between
	* @param {Date} dateBegin	The start of the range
	* @param {Date} dateEnd		The end of the range
	* @return {Boolean} true if the date occurs between the compared dates; false if not.
	*/
	between : function(dateBegin, dateEnd) {
		return (this.after(dateBegin) && this.before(dateEnd));
	},

	/**
	* Calculates the number of days the specified date is from January 1 of the specified calendar year.
	* Passing January 1 to this function would return an offset value of zero.
	* @method getDayOffset
	* @return {Number}	The number of days since January 1 of the given year
	*/
	getDayOffset : function() {
		var beginYear = Date.getJan1(this.getFullYear()); // Find the start of the year. This will be in week 1.
		
		// Find the number of days the passed in date is away from the calendar year start
		return Math.ceil((this.getTime() - beginYear.getTime()) / Date.ONE_DAY_MS);
	},

	/**
	* Calculates the week number for the given date. This function assumes that week 1 is the
	* week in which January 1 appears, regardless of whether the week consists of a full 7 days.
	* The calendar year can be specified to help find what a the week number would be for a given
	* date if the date overlaps years. For instance, a week may be considered week 1 of 2005, or
	* week 53 of 2004. Specifying the optional calendarYear allows one to make this distinction
	* easily.
	* @method getWeekNumber
	* @return {Number}	The week number of the given date.
	*/
	getWeekNumber : function() {
		var date = this.clearTime();
		var nearestThurs = new Date(date.getTime() + (4 * Date.ONE_DAY_MS) - ((date.getDay()) * Date.ONE_DAY_MS));

		var jan1 = Date.getJan1(nearestThurs.getFullYear());
		var dayOfYear = ((nearestThurs.getTime() - jan1.getTime()) / Date.ONE_DAY_MS) - 1;

		return Math.ceil((dayOfYear)/ 7);
	},

	/**
	* Determines if a given week overlaps two different years.
	* @method isYearOverlapWeek
	* @return {Boolean}	true if the date overlaps two different years.
	*/
	isYearOverlapWeek : function() {
		var nextWeek = this.add(Date.DAY, 6);
		return (nextWeek.getFullYear() != weekBeginDate.getFullYear());
	},

	/**
	* Determines if a given week overlaps two different months.
	* @method isMonthOverlapWeek
	* @return {Boolean}	true if the date overlaps two different months.
	*/
	isMonthOverlapWeek : function() {
		var nextWeek = this.add(Date.DAY, 6);
		return (nextWeek.getMonth() != weekBeginDate.getMonth());
	},

	/**
	* Gets the first day of a month containing a given date.
	* @method findMonthStart
	* @return {Date}		The JavaScript Date representing the first day of the month
	*/
	findMonthStart : function(date) {
		return Date.getDate(this.getFullYear(), this.getMonth(), 1);
	},

	/**
	* Gets the last day of a month containing a given date.
	* @method findMonthEnd
	* @return {Date}		The JavaScript Date representing the last day of the month
	*/
	findMonthEnd : function() {
		var start = this.findMonthStart();
		var nextMonth = start.add(Date.MONTH, 1);
		return nextMonth.subtract(Date.DAY, 1);
	},

	/**
	* Clears the time fields from a given date, effectively setting the time to 12 noon.
	* @method clearTime
	* @return {Date}		The JavaScript Date cleared of all time fields
	*/
	clearTime : function() {
		var date = new Date(this.getTime());
		date.setHours(12,0,0,0);
		return date;
	}
});

Generally these functions do not modify the current date, but instead return a new instance of the Date object after a transformation has been made. So if you wanted to update the current Date instantiation, then you would need to assign it to the returned value:

Example 3: Assignment

var d = new Date(); // d is now the current time Date object
d = d.findMonthStart(); // d is now the first of the month Date object
// versus
var date = new Date(); // date is now the current time Date object
var newdate = date.findMonthStart(); // newdate is now the first of the month Date object, but date is still the current time

The other functions return boolean values and should be straight forward in meaning. I also took the liberty to simplify the code. YUI tends to be more verbose than necessary, most likely as a result of Douglas Crockfords JavaScript standards (no function chaining and always assigning a value to a variable).

I have not created a test page for this code, but it does compile and my quick function test seemed to work as expected. Please let me know if you notice anything that does not work as expected.