var DateTimeFormatter = 
{
	dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],
	dayNamesInvariant:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],
	abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],
	abbreviatedDayNamesInvariant:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],
	monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],
	monthNamesInvariant:["January","February","March","April","May","June","July","August","September","October","November","December"],
	abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],
	abbreviatedMonthNamesInvariant:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],
	amDesignator:"AM",
	pmDesignator:"PM",
	dateSeparator:"/",
	timeSeparator:":",

	getFullFormat:function(formatChar)
	{
		switch (formatChar)
		{
			case "d": return "M/d/yyyy";
			case "D": return "dddd, MMMM dd, yyyy";
			case "f": return "dddd, MMMM dd, yyyy h:mm tt";
			case "F": return "dddd, MMMM dd, yyyy h:mm:ss tt";
			case "g": return "M/d/yyyy h:mm tt";
			case "G": return "M/d/yyyy h:mm:ss tt";
			case "m": case "M": return "MMMM dd";
			case "o": case "O": return "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK";
			case "r": case "R": return "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'";
			case "s": return "yyyy'-'MM'-'dd'T'HH':'mm':'ss";
			case "t": return "h:mm tt";
			case "T": return "h:mm:ss tt";
			case "u": return "yyyy'-'MM'-'dd HH':'mm':'ss'Z'";
			case "U": return "dddd, MMMM dd, yyyy h:mm:ss tt";
			case "y": case "Y": return "MMMM, yyyy";
		}
		return formatChar;
	},

	formatDate:function(date, format)
	{
		var useInvariant = (format == "r" || format == "R");
		format = DateTimeFormatter.parseFormat(format);
		
		var dateString = "";
		for (var i=0; i<format.length; i++)
		{
			if (format[i])
			{
				var formatToken = format[i].token;
				if (formatToken.indexOf("ffff") == 0) formatToken = "fff";
				if (formatToken.indexOf("FFFF") == 0) formatToken = "FFF";
				switch (formatToken)
				{
					case "/":
					{
						dateString += DateTimeFormatter.dateSeparator;
						break;
					}
					
					case ":":
					{
						dateString += DateTimeFormatter.timeSeparator;
						break;
					}
					
					case "d":
					{
						dateString += date.getDate().toString();
						break;
					}
					
					case "d":
					{
						dateString += date.getDate().toString();
						break;
					}
					
					case "dd":
					{
						dateString += DateTimeFormatter.padLeft(date.getDate().toString(), "0", 2);
						break;
					}

					case "ddd":
					{
						dateString += (useInvariant ? DateTimeFormatter.abbreviatedDayNamesInvariant[date.getDay()] : DateTimeFormatter.abbreviatedDayNames[date.getDay()]);
						break;
					}

					case "dddd":
					{
						dateString += (useInvariant ? DateTimeFormatter.dayNamesInvariant[date.getDay()] : DateTimeFormatter.dayNames[date.getDay()]);
						break;
					}
					
					case "f":
					case "ff":
					case "fff":
					{
						var value = DateTimeFormatter.padLeft(date.getMilliseconds().toString(), "0", 3);
						dateString += value.substr(0, formatToken.length);
						break;
					}

					case "F":
					case "FF":
					case "FFF":
					{
						var value = DateTimeFormatter.padLeft(date.getMilliseconds().toString(), "0", 3);
						dateString += DateTimeFormatter.removeTrailingZeroes(value.substr(0, formatToken.length));
						break;
					}
					
					case "h":
					case "hh":
					case "H":
					case "HH":
					{
						var value = date.getHours();
						if (formatToken.indexOf("h") != -1)
						{
							if (value == 0) value = 12;
							else if (value > 12) value -= 12;
						}
						value = value.toString();
						if (formatToken.toLowerCase() == "hh") value = DateTimeFormatter.padLeft(value, "0", 2);
						dateString += value;
						break;
					}
					
					case "m":
					case "mm":
					{
						var value = date.getMinutes().toString();
						if (formatToken == "mm") value = DateTimeFormatter.padLeft(value, "0", 2);
						dateString += value;
						break;
					}

					case "M":
					{
						dateString += (date.getMonth() + 1).toString();
						break;
					}

					case "MM":
					{
						dateString += DateTimeFormatter.padLeft((date.getMonth() + 1).toString(), "0", 2);
						break;
					}

					case "MMM":
					{
						dateString += (useInvariant ? DateTimeFormatter.abbreviatedMonthNamesInvariant[date.getMonth()] : DateTimeFormatter.abbreviatedMonthNames[date.getMonth()]);
						break;
					}

					case "MMMM":
					{
						dateString += (useInvariant ? DateTimeFormatter.monthNamesInvariant[date.getMonth()] : DateTimeFormatter.monthNames[date.getMonth()]);
						break;
					}
					
					case "s":
					case "ss":
					{
						var value = date.getSeconds().toString();
						if (formatToken == "ss") value = DateTimeFormatter.padLeft(value, "0", 2);
						dateString += value;
						break;
					}

					case "t":
					case "tt":
					{
						var value = (date.getHours() < 12 ? DateTimeFormatter.amDesignator : DateTimeFormatter.pmDesignator);
						dateString += (formatToken == "tt" ? value : value.substr(0, 1));
						break;
					}

					case "y":
					{
						var value = DateTimeFormatter.removeLeadingZeroes(DateTimeFormatter.padLeft(date.getFullYear().toString(), "0", 4).substr(2));
						if (value == "") value = 0;
						dateString += value;
						break;
					}

					case "yy":
					{
						dateString += DateTimeFormatter.padLeft(date.getFullYear().toString(), "0", 4).substr(2);
						break;
					}
					
					case "yyy":
					case "yyyy":
					{
						dateString += DateTimeFormatter.padLeft(date.getFullYear().toString(), "0", formatToken.length);
						break;
					}
					
					case "K":
					case "z":
					case "zz":
					case "zzz":
					{
						if (formatToken == "K") formatToken = "zzz";
						var value = 0 - date.getTimezoneOffset();
						var hour = parseInt(value / 60);
						if (formatToken == "zzz")
						{
							var minute = DateTimeFormatter.padLeft((60 * ((value / 60) - hour)).toString(), "0", 2);
							value = DateTimeFormatter.padLeft(hour.toString(), "0", 2) + ":" + minute;
						}
						else
						{
							hour = hour.toString();
							if (formatToken == "zz") hour = DateTimeFormatter.padLeft(hour, "0", 2);
							value = hour;
						}
						dateString += (value < 0 ? "-" : "+") + value;
						break;
					}
					
					default:
					{
						if (formatToken.charAt(0) == "'" && formatToken.charAt(formatToken.length - 1) == "'")
						{
							formatToken = formatToken.substr(1, formatToken.length - 2);
						}
						dateString += formatToken;
						break;
					}
				}
			}
		}
		return dateString;
	},
	
	charEscapeRegex:"'[^']+'",

	parseFormat:function(format)
	{
		if (!format) format = "F";
		if (format.length == 1)
		{
			format = DateTimeFormatter.getFullFormat(format);
		}
		format = format.replace(/%d/g, "d");
		format = format.replace(/%f/g, "f");
		format = format.replace(/%F/g, "F");
		format = format.replace(/%h/g, "h");
		format = format.replace(/%H/g, "H");
		format = format.replace(/%m/g, "m");
		format = format.replace(/%M/g, "M");
		format = format.replace(/%s/g, "s");
		format = format.replace(/%t/g, "t");
		format = format.replace(/%y/g, "y");
		format = format.replace(/%z/g, "z");
		var formatArray = [];

		format = DateTimeFormatter.buildFormatArray(format, formatArray, ["\\\\.{1}"]);
		format = DateTimeFormatter.buildFormatArray(format, formatArray, [DateTimeFormatter.charEscapeRegex]);
		format = DateTimeFormatter.buildFormatArray(format, formatArray, ["[,\\d\\s:/]"]);
		var patterns = [
			"d{1,4}",
			"f{1,7}",
			"F{1,7}",
			"h{1,2}",
			"H{1,2}",
			"m{1,2}",
			"M{1,4}",
			"s{1,2}",
			"t{1,2}",
			"y{1,6}",
			"z{1,3}"
		];
		format = DateTimeFormatter.buildFormatArray(format, formatArray, patterns);
		format = DateTimeFormatter.buildFormatArray(format, formatArray, ["[^~]|\D"]);

		return formatArray;
	},

	buildFormatArray:function(formatString, formatArray, regexes)
	{
		for (var i=0; i<regexes.length; i++)
		{
			var matches = formatString.match(new RegExp(regexes[i], "g"));
			if (matches)
			{
				var startIndex = 0;
				for (var j=0; j<matches.length; j++)
				{
					var match = matches[j];
					startIndex = formatString.indexOf(match, startIndex);
					formatArray[startIndex] = { token:match, regex:regexes[i] };
					formatString = formatString.replace(match, DateTimeFormatter.padRight("", "~", match.length));
					startIndex += match.length;
				}
			}
		}
		return formatString;
	},
	
	parseFormattedDate:function(dateString, format)
	{
		var year = null, month = null, date = null, hour = null, minute = null, second = null, millisecond = null, offset = null;
		var foundAMPM = false;
		var pm = false;
		var parseTime = false;
		var parseMilliseconds = false;
		format = DateTimeFormatter.parseFormat(format);
		for (var i=0; i<format.length; i++)
		{
			//dateString = DateTimeFormatter.trimString(dateString);
			if (format[i])
			{
				var formatToken = format[i].token;
				if (formatToken.indexOf("ffff") == 0) formatToken = "fff";
				if (formatToken.indexOf("FFFF") == 0) formatToken = "FFF";
				switch (formatToken)
				{
					case "d":
					case "dd":
					{
						if (date == null)
						{
							var matches = dateString.match(/\d{1,2}/);
							if (!matches) return NaN;
							dateString = dateString.substr(matches[0].length);
							var value = DateTimeFormatter.removeLeadingZeroes(matches[0]);
							if (value == "") value = 0;
							if (isNaN(value)) return NaN;
							date = parseInt(value);
							if (date < 0 || date > 31) return NaN;
						}
						break;
					}
					
					case "ddd":
					case "dddd":
					{
						var days = (formatToken == "ddd" ? DateTimeFormatter.abbreviatedDayNames : DateTimeFormatter.dayNames);
						for (var j=0; j<days.length; j++)
						{
							if (dateString.toLowerCase().indexOf(days[j].toLowerCase()) == 0)
							{
								dateString = dateString.substr(days[j].length);
								break;
							}
						}
						break;
					}

					case "f":
					case "ff":
					case "fff":
					case "F":
					case "FF":
					case "FFF":
					{
						parseTime = true;
						if (millisecond == null)
						{
							var value = dateString.substr(0, formatToken.length);
							if (isNaN(value)) return NaN;
							millisecond = parseInt(DateTimeFormatter.padRight(value, "0", 3));
						}
						break;
					}

					case "h":
					case "hh":
					case "H":
					case "HH":
					{
						parseTime = true;
						if (hour == null)
						{
							var matches = dateString.match(/\d{1,2}/);
							if (!matches) return NaN;
							dateString = dateString.substr(matches[0].length);
							var value = DateTimeFormatter.removeLeadingZeroes(matches[0]);
							if (value == "") value = 0;
							if (isNaN(value)) return NaN;
							hour = parseInt(value);
							if (hour < 0) return NaN;
							if (formatToken.indexOf("h") != -1)
							{
								if (hour == 12) hour = 0;
							}
						}
						break;
					}
					
					case "m":
					case "mm":
					{
						parseTime = true;
						if (minute == null)
						{
							var matches = dateString.match(/\d{1,2}/);
							if (!matches) return NaN;
							dateString = dateString.substr(matches[0].length);
							var value = DateTimeFormatter.removeLeadingZeroes(matches[0]);
							if (value == "") value = 0;
							if (isNaN(value)) return NaN;
							minute = parseInt(value);
							if (minute < 0 || minute > 59) return NaN;
						}
						break;
					}
					
					case "M":
					case "MM":
					{
						if (month == null)
						{
							var matches = dateString.match(/\d{1,2}/);
							if (!matches) return NaN;
							dateString = dateString.substr(matches[0].length);
							var value = DateTimeFormatter.removeLeadingZeroes(matches[0]);
							if (value == "") value = 0;
							if (isNaN(value)) return NaN;
							month = parseInt(value) - 1;
							if (month < 0 || month > 11) return NaN;
						}
						break;
					}

					case "MMM":
					case "MMMM":
					{
						if (month == null)
						{
							var months = (formatToken == "MMM" ? DateTimeFormatter.abbreviatedMonthNames : DateTimeFormatter.monthNames);
							for (var j=0; j<months.length; j++)
							{
								if (dateString.toLowerCase().indexOf(months[j].toLowerCase()) == 0)
								{
									month = j;
									dateString = dateString.substr(months[j].length);
									break;
								}
							}
							if (month == null) return NaN;
						}
						break;
					}
					
					case "s":
					case "ss":
					{
						parseTime = true;
						if (second == null)
						{
							var matches = dateString.match(/\d{1,2}/);
							if (!matches) return NaN;
							dateString = dateString.substr(matches[0].length);
							var value = DateTimeFormatter.removeLeadingZeroes(matches[0]);
							if (value == "") value = 0;
							if (isNaN(value)) return NaN;
							second = parseInt(value);
							if (second < 0 || minute > 59) return NaN;
						}
						break;
					}

					case "t":
					{
						foundAMPM = true;
						parseTime = true;
						var value = dateString.charAt(0);
						if (value != DateTimeFormatter.pmDesignator.charAt(0) && value != DateTimeFormatter.amDesignator.charAt(0))
						{
							return NaN;
						}
						pm = (dateString.charAt(0) == DateTimeFormatter.pmDesignator.charAt(0));
						dateString = dateString.substr(1);
						break;
					}
					
					case "tt":
					{
						foundAMPM = true;
						parseTime = true;
						var value = dateString.toLowerCase();
						if (value.indexOf(DateTimeFormatter.pmDesignator.toLowerCase()) != 0 && value.indexOf(DateTimeFormatter.amDesignator.toLowerCase()) != 0)
						{
							return NaN;
						}
						pm = (value.indexOf(DateTimeFormatter.pmDesignator.toLowerCase()) == 0);
						if (pm)
						{
							dateString = dateString.substr(DateTimeFormatter.pmDesignator.length);
						}
						else
						{
							dateString = dateString.substr(DateTimeFormatter.amDesignator.length);
						}
						break;
					}
										
					case "y":
					case "yy":
					{
						if (year == null)
						{
							var matches = dateString.match(/\d{1,2}/);
							if (!matches) return NaN;
							dateString = dateString.substr(matches[0].length);
							var value = DateTimeFormatter.removeLeadingZeroes(matches[0]);
							if (value == "") value = 0;
							if (isNaN(value)) return NaN;
							year = parseInt(value);
							year = (year < 30 ? 2000 : 1900) + year;
						}
						break;
					}

					case "yyy":
					{
						if (year == null)
						{
							if (format.length == 1) return NaN;
							var value = dateString.substr(0, 3);;
							dateString = dateString.substr(3);
							if (isNaN(value)) return NaN;
							year = parseInt(value);
							year = (year < 30 ? 2000 : 1900) + year;
						}
						break;
					}

					case "yyyy":
					{
						if (year == null)
						{
							var value = dateString.substr(0, 4);;
							dateString = dateString.substr(4);
							if (isNaN(value)) return NaN;
							year = parseInt(value);
						}
						break;
					}

					case "z":
					case "zz":
					case "zzz":
					{
						if (offset == null)
						{
							parseTime = true;
							var offsetChar = dateString.substr(0, 1);
							if (offsetChar != "+" && offsetChar != "-")
							{
								return NaN;
							}
							dateString = dateString.substr(1);
							
							var value;
							if (formatToken.length == 3)
							{
								value = dateString.substr(0, 5).split(":");
								dateString = dateString.substr(5);
								if (value.length != 2 || isNaN(value[0]) || isNaN(value[1])) return isNaN;
								offset = parseInt(value[0] * 60) + parseInt(value[1]);
							}
							else
							{
								var matches = dateString.match(/\d{1,2}/);
								if (!matches) return NaN;
								dateString = dateString.substr(matches[0].length);
								var value = DateTimeFormatter.removeLeadingZeroes(matches[0]);
								if (value == "") value = 0;
								if (isNaN(value)) return NaN;
								offset = parseInt(value * 60);
							}
							offset = (offsetChar == "-" ? 0 - offset : offset);
						}
						break;
					}
					
					
					default:
					{
						if (format[i].regex == DateTimeFormatter.charEscapeRegex)
						{
							dateString = dateString.substr(format[i].token.length - 2);
						}
						else
						{
							var matches = dateString.match(new RegExp(format[i].regex));
							if (matches)
							{
								dateString = dateString.substr(matches[0].length);
							}
						}
						break;
					}
				}
			}
		}
		
		var now = new Date();
		var finalDate = NaN;
		if (year != null || month != null || date != null || hour != null || minute != null || second != null || millisecond != null || offset != null || foundAMPM)
		{
			if (year != null)
			{
				if (month == null) month = 0;
				if (date == null) date = 1;
			}
			else
			{
				year = now.getFullYear();
				if (!parseTime)
				{
					if (month == null) month = 0;
					if (date == null) date = 1;
				}
				else
				{
					if (month == null) month = now.getMonth();
					if (date == null) date = now.getDate();
				}
			}
			if (!parseTime)
			{
				finalDate = new Date(year, month, date);
			}
			else
			{
				if (hour == null) hour = 0;
				else if (pm) hour += 12;
				if (minute == null) minute = 0;
				if (second == null) second = 0;
				finalDate = new Date(year, month, date, hour, minute, second);
			}
			var fullMilliseconds = finalDate.valueOf();
			if (millisecond != null) fullMilliseconds += millisecond;
			//if (offset != null) fullMilliseconds += (new Date().getTimezoneOffset() * 60 * 1000);
			return fullMilliseconds;
		}
		return finalDate;
	},

	padLeft:function(str, character, length)
	{
		while (str.length < length)
		{
			str = character + str;
		}
		return str;
	},

	padRight:function(str, character, length)
	{
		while (str.length < length)
		{
			str += character;
		}
		return str;
	},

	removeLeadingZeroes:function(str)
	{
		while (str.charAt(0) == "0")
		{
			str = str.substr(1);
		}
		return str;
	},

	removeTrailingZeroes:function(str)
	{
		while (str.charAt(str.length - 1) == "0")
		{
			str = str.substr(0, str.length - 1);
		}
		return str;
	},
	
	trimString:function(string)
	{
		return (string || "").replace(/^(\s|\u00A0)+|(\s|\u00A0)+$/g, "");
	}
};

/*** Add methods to standard Date object ***/
Date.prototype.toFormattedString = function(format)
{
	if (!format) return this.toString();
	else return DateTimeFormatter.formatDate(this, format);
};

Date.parseExact = function(value, format)
{
	if (!format) return NaN;
	else return DateTimeFormatter.parseFormattedDate(value, format);
}