/** * @fileOverview Date parsing and formatting operations without extending the Date built-in object. * @author Chris Leonello * @version #VERSION# * @date #DATE# */ (function($) { /** * @description *
Object with extended date parsing and formatting capabilities. * This library borrows many concepts and ideas from the Date Instance * Methods by Ken Snyder along with some parts of Ken's actual code.
* *jsDate takes a different approach by not extending the built-in * Date Object, improving date parsing, allowing for multiple formatting * syntaxes and multiple and more easily expandable localization.
* * @author Chris Leonello * @date #date# * @version #VERSION# * @copyright (c) 2010-2013 Chris Leonello * jsDate is currently available for use in all personal or commercial projects * under both the MIT and GPL version 2.0 licenses. This means that you can * choose the license that best suits your project and use it accordingly. * *Ken's original Date Instance Methods and copyright notice:
*
* Ken Snyder (ken d snyder at gmail dot com)
* 2008-09-10
* version 2.0.2 (http://kendsnyder.com/sandbox/date/)
* Creative Commons Attribution License 3.0 (http://creativecommons.org/licenses/by/3.0/)
*
*
* @class
* @name jsDate
* @param {String | Number | Array | Date Object | Options Object} arguments Optional arguments, either a parsable date/time string,
* a JavaScript timestamp, an array of numbers of form [year, month, day, hours, minutes, seconds, milliseconds],
* a Date object, or an options object of form {syntax: "perl", date:some Date} where all options are optional.
*/
var jsDate = function () {
this.syntax = jsDate.config.syntax;
this._type = "jsDate";
this.proxy = new Date();
this.options = {};
this.locale = jsDate.regional.getLocale();
this.formatString = '';
this.defaultCentury = jsDate.config.defaultCentury;
switch ( arguments.length ) {
case 0:
break;
case 1:
// other objects either won't have a _type property or,
// if they do, it shouldn't be set to "jsDate", so
// assume it is an options argument.
if (get_type(arguments[0]) == "[object Object]" && arguments[0]._type != "jsDate") {
var opts = this.options = arguments[0];
this.syntax = opts.syntax || this.syntax;
this.defaultCentury = opts.defaultCentury || this.defaultCentury;
this.proxy = jsDate.createDate(opts.date);
}
else {
this.proxy = jsDate.createDate(arguments[0]);
}
break;
default:
var a = [];
for ( var i=0; iLocalizations must be an object and have the following properties defined: monthNames, monthNamesShort, dayNames, dayNamesShort and Localizations are added like:
*
* jsDate.regional['en'] = {
* monthNames : 'January February March April May June July August September October November December'.split(' '),
* monthNamesShort : 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' '),
* dayNames : 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday'.split(' '),
* dayNamesShort : 'Sun Mon Tue Wed Thu Fri Sat'.split(' ')
* };
*
* After adding localizations, call jsDate.regional.getLocale(); to update the locale setting with the
* new localizations.
strftime formatting can be accomplished without creating a jsDate object by calling jsDate.strftime():
*
* var formattedDate = jsDate.strftime('Feb 8, 2006 8:48:32', '%Y-%m-%d %H:%M:%S');
*
* @param {String | Number | Array | jsDate Object | Date Object} date A parsable date string, JavaScript time stamp, Array of form [year, month, day, hours, minutes, seconds, milliseconds], jsDate Object or Date object.
* @param {String} formatString String with embedded date formatting codes.
* See: {@link jsDate.formats}.
* @param {String} syntax Optional syntax to use [default perl].
* @param {String} locale Optional locale to use.
* @returns {String} Formatted representation of the date.
*/
//
// Logic as implemented here is very similar to Ken Snyder's Date Instance Methods.
//
jsDate.strftime = function(d, formatString, syntax, locale) {
var syn = 'perl';
var loc = jsDate.regional.getLocale();
// check if syntax and locale are available or reversed
if (syntax && jsDate.formats.hasOwnProperty(syntax)) {
syn = syntax;
}
else if (syntax && jsDate.regional.hasOwnProperty(syntax)) {
loc = syntax;
}
if (locale && jsDate.formats.hasOwnProperty(locale)) {
syn = locale;
}
else if (locale && jsDate.regional.hasOwnProperty(locale)) {
loc = locale;
}
if (get_type(d) != "[object Object]" || d._type != "jsDate") {
d = new jsDate(d);
d.locale = loc;
}
if (!formatString) {
formatString = d.formatString || jsDate.regional[loc]['formatString'];
}
// default the format string to year-month-day
var source = formatString || '%Y-%m-%d',
result = '',
match;
// replace each format code
while (source.length > 0) {
if (match = source.match(jsDate.formats[syn].codes.matcher)) {
result += source.slice(0, match.index);
result += (match[1] || '') + format(d, match[2], syn);
source = source.slice(match.index + match[0].length);
} else {
result += source;
source = '';
}
}
return result;
};
/**
* @namespace
* Namespace to hold format codes and format shortcuts. "perl" and "php" format codes
* and shortcuts are defined by default. Additional codes and shortcuts can be
* added like:
*
*
* jsDate.formats["perl"] = {
* "codes": {
* matcher: /someregex/,
* Y: "fullYear", // name of "get" method without the "get",
* ..., // more codes
* },
* "shortcuts": {
* F: '%Y-%m-%d',
* ..., // more shortcuts
* }
* };
*
*
* Additionally, ISO and SQL shortcuts are defined and can be accesses via:
* jsDate.formats.ISO and jsDate.formats.SQL
*/
jsDate.formats = {
ISO:'%Y-%m-%dT%H:%M:%S.%N%G',
SQL:'%Y-%m-%d %H:%M:%S'
};
/**
* Perl format codes and shortcuts for strftime.
*
* A hash (object) of codes where each code must be an array where the first member is
* the name of a Date.prototype or jsDate.prototype function to call
* and optionally a second member indicating the number to pass to addZeros()
*
*
The following format codes are defined:
* *
* Code Result Description
* == Years ==
* %Y 2008 Four-digit year
* %y 08 Two-digit year
*
* == Months ==
* %m 09 Two-digit month
* %#m 9 One or two-digit month
* %B September Full month name
* %b Sep Abbreviated month name
*
* == Days ==
* %d 05 Two-digit day of month
* %#d 5 One or two-digit day of month
* %e 5 One or two-digit day of month
* %A Sunday Full name of the day of the week
* %a Sun Abbreviated name of the day of the week
* %w 0 Number of the day of the week (0 = Sunday, 6 = Saturday)
*
* == Hours ==
* %H 23 Hours in 24-hour format (two digits)
* %#H 3 Hours in 24-hour integer format (one or two digits)
* %I 11 Hours in 12-hour format (two digits)
* %#I 3 Hours in 12-hour integer format (one or two digits)
* %p PM AM or PM
*
* == Minutes ==
* %M 09 Minutes (two digits)
* %#M 9 Minutes (one or two digits)
*
* == Seconds ==
* %S 02 Seconds (two digits)
* %#S 2 Seconds (one or two digits)
* %s 1206567625723 Unix timestamp (Seconds past 1970-01-01 00:00:00)
*
* == Milliseconds ==
* %N 008 Milliseconds (three digits)
* %#N 8 Milliseconds (one to three digits)
*
* == Timezone ==
* %O 360 difference in minutes between local time and GMT
* %Z Mountain Standard Time Name of timezone as reported by browser
* %G 06:00 Hours and minutes between GMT
*
* == Shortcuts ==
* %F 2008-03-26 %Y-%m-%d
* %T 05:06:30 %H:%M:%S
* %X 05:06:30 %H:%M:%S
* %x 03/26/08 %m/%d/%y
* %D 03/26/08 %m/%d/%y
* %#c Wed Mar 26 15:31:00 2008 %a %b %e %H:%M:%S %Y
* %v 3-Sep-2008 %e-%b-%Y
* %R 15:31 %H:%M
* %r 03:31:00 PM %I:%M:%S %p
*
* == Characters ==
* %n \n Newline
* %t \t Tab
* %% % Percent Symbol
*
*
* Formatting shortcuts that will be translated into their longer version. * Be sure that format shortcuts do not refer to themselves: this will cause an infinite loop.
* *Format codes and format shortcuts can be redefined after the jsDate * module is imported.
* *Note that if you redefine the whole hash (object), you must supply a "matcher" * regex for the parser. The default matcher is:
* */()%(#?(%|[a-z]))/i
*
* which corresponds to the Perl syntax used by default.
* *By customizing the matcher and format codes, nearly any strftime functionality is possible.
*/ jsDate.formats.perl = { codes: { // // 2-part regex matcher for format codes // // first match must be the character before the code (to account for escaping) // second match must be the format code character(s) // matcher: /()%(#?(%|[a-z]))/i, // year Y: 'FullYear', y: 'ShortYear.2', // month m: 'MonthNumber.2', '#m': 'MonthNumber', B: 'MonthName', b: 'AbbrMonthName', // day d: 'Date.2', '#d': 'Date', e: 'Date', A: 'DayName', a: 'AbbrDayName', w: 'Day', // hours H: 'Hours.2', '#H': 'Hours', I: 'Hours12.2', '#I': 'Hours12', p: 'AMPM', // minutes M: 'Minutes.2', '#M': 'Minutes', // seconds S: 'Seconds.2', '#S': 'Seconds', s: 'Unix', // milliseconds N: 'Milliseconds.3', '#N': 'Milliseconds', // timezone O: 'TimezoneOffset', Z: 'TimezoneName', G: 'GmtOffset' }, shortcuts: { // date F: '%Y-%m-%d', // time T: '%H:%M:%S', X: '%H:%M:%S', // local format date x: '%m/%d/%y', D: '%m/%d/%y', // local format extended '#c': '%a %b %e %H:%M:%S %Y', // local format short v: '%e-%b-%Y', R: '%H:%M', r: '%I:%M:%S %p', // tab and newline t: '\t', n: '\n', '%': '%' } }; /** * PHP format codes and shortcuts for strftime. * * A hash (object) of codes where each code must be an array where the first member is * the name of a Date.prototype or jsDate.prototype function to call * and optionally a second member indicating the number to pass to addZeros() * *The following format codes are defined:
* *
* Code Result Description
* === Days ===
* %a Sun through Sat An abbreviated textual representation of the day
* %A Sunday - Saturday A full textual representation of the day
* %d 01 to 31 Two-digit day of the month (with leading zeros)
* %e 1 to 31 Day of the month, with a space preceding single digits.
* %j 001 to 366 Day of the year, 3 digits with leading zeros
* %u 1 - 7 (Mon - Sun) ISO-8601 numeric representation of the day of the week
* %w 0 - 6 (Sun - Sat) Numeric representation of the day of the week
*
* === Week ===
* %U 13 Full Week number, starting with the first Sunday as the first week
* %V 01 through 53 ISO-8601:1988 week number, starting with the first week of the year
* with at least 4 weekdays, with Monday being the start of the week
* %W 46 A numeric representation of the week of the year,
* starting with the first Monday as the first week
* === Month ===
* %b Jan through Dec Abbreviated month name, based on the locale
* %B January - December Full month name, based on the locale
* %h Jan through Dec Abbreviated month name, based on the locale (an alias of %b)
* %m 01 - 12 (Jan - Dec) Two digit representation of the month
*
* === Year ===
* %C 19 Two digit century (year/100, truncated to an integer)
* %y 09 for 2009 Two digit year
* %Y 2038 Four digit year
*
* === Time ===
* %H 00 through 23 Two digit representation of the hour in 24-hour format
* %I 01 through 12 Two digit representation of the hour in 12-hour format
* %l 1 through 12 Hour in 12-hour format, with a space preceeding single digits
* %M 00 through 59 Two digit representation of the minute
* %p AM/PM UPPER-CASE 'AM' or 'PM' based on the given time
* %P am/pm lower-case 'am' or 'pm' based on the given time
* %r 09:34:17 PM Same as %I:%M:%S %p
* %R 00:35 Same as %H:%M
* %S 00 through 59 Two digit representation of the second
* %T 21:34:17 Same as %H:%M:%S
* %X 03:59:16 Preferred time representation based on locale, without the date
* %z -0500 or EST Either the time zone offset from UTC or the abbreviation
* %Z -0500 or EST The time zone offset/abbreviation option NOT given by %z
*
* === Time and Date ===
* %D 02/05/09 Same as %m/%d/%y
* %F 2009-02-05 Same as %Y-%m-%d (commonly used in database datestamps)
* %s 305815200 Unix Epoch Time timestamp (same as the time() function)
* %x 02/05/09 Preferred date representation, without the time
*
* === Miscellaneous ===
* %n --- A newline character (\n)
* %t --- A Tab character (\t)
* %% --- A literal percentage character (%)
*
*/
jsDate.formats.php = {
codes: {
//
// 2-part regex matcher for format codes
//
// first match must be the character before the code (to account for escaping)
// second match must be the format code character(s)
//
matcher: /()%((%|[a-z]))/i,
// day
a: 'AbbrDayName',
A: 'DayName',
d: 'Date.2',
e: 'Date',
j: 'DayOfYear.3',
u: 'DayOfWeek',
w: 'Day',
// week
U: 'FullWeekOfYear.2',
V: 'IsoWeek.2',
W: 'WeekOfYear.2',
// month
b: 'AbbrMonthName',
B: 'MonthName',
m: 'MonthNumber.2',
h: 'AbbrMonthName',
// year
C: 'Century.2',
y: 'ShortYear.2',
Y: 'FullYear',
// time
H: 'Hours.2',
I: 'Hours12.2',
l: 'Hours12',
p: 'AMPM',
P: 'AmPm',
M: 'Minutes.2',
S: 'Seconds.2',
s: 'Unix',
O: 'TimezoneOffset',
z: 'GmtOffset',
Z: 'TimezoneAbbr'
},
shortcuts: {
D: '%m/%d/%y',
F: '%Y-%m-%d',
T: '%H:%M:%S',
X: '%H:%M:%S',
x: '%m/%d/%y',
R: '%H:%M',
r: '%I:%M:%S %p',
t: '\t',
n: '\n',
'%': '%'
}
};
//
// Conceptually, the logic implemented here is similar to Ken Snyder's Date Instance Methods.
// I use his idea of a set of parsers which can be regular expressions or functions,
// iterating through those, and then seeing if Date.parse() will create a date.
// The parser expressions and functions are a little different and some bugs have been
// worked out. Also, a lot of "pre-parsing" is done to fix implementation
// variations of Date.parse() between browsers.
//
jsDate.createDate = function(date) {
// if passing in multiple arguments, try Date constructor
if (date == null) {
return new Date();
}
// If the passed value is already a date object, return it
if (date instanceof Date) {
return date;
}
// if (typeof date == 'number') return new Date(date * 1000);
// If the passed value is an integer, interpret it as a javascript timestamp
if (typeof date == 'number') {
return new Date(date);
}
// Before passing strings into Date.parse(), have to normalize them for certain conditions.
// If strings are not formatted staccording to the EcmaScript spec, results from Date parse will be implementation dependent.
//
// For example:
// * FF and Opera assume 2 digit dates are pre y2k, Chome assumes <50 is pre y2k, 50+ is 21st century.
// * Chrome will correctly parse '1984-1-25' into localtime, FF and Opera will not parse.
// * Both FF, Chrome and Opera will parse '1984/1/25' into localtime.
// remove leading and trailing spaces
var parsable = String(date).replace(/^\s*(.+)\s*$/g, '$1');
// replace dahses (-) with slashes (/) in dates like n[nnn]/n[n]/n[nnn]
parsable = parsable.replace(/^([0-9]{1,4})-([0-9]{1,2})-([0-9]{1,4})/, "$1/$2/$3");
/////////
// Need to check for '15-Dec-09' also.
// FF will not parse, but Chrome will.
// Chrome will set date to 2009 as well.
/////////
// first check for 'dd-mmm-yyyy' or 'dd/mmm/yyyy' like '15-Dec-2010'
parsable = parsable.replace(/^(3[01]|[0-2]?\d)[-\/]([a-z]{3,})[-\/](\d{4})/i, "$1 $2 $3");
// Now check for 'dd-mmm-yy' or 'dd/mmm/yy' and normalize years to default century.
var match = parsable.match(/^(3[01]|[0-2]?\d)[-\/]([a-z]{3,})[-\/](\d{2})\D*/i);
if (match && match.length > 3) {
var m3 = parseFloat(match[3]);
var ny = jsDate.config.defaultCentury + m3;
ny = String(ny);
// now replace 2 digit year with 4 digit year
parsable = parsable.replace(/^(3[01]|[0-2]?\d)[-\/]([a-z]{3,})[-\/](\d{2})\D*/i, match[1] +' '+ match[2] +' '+ ny);
}
// Check for '1/19/70 8:14PM'
// where starts with mm/dd/yy or yy/mm/dd and have something after
// Check if 1st postiion is greater than 31, assume it is year.
// Assme all 2 digit years are 1900's.
// Finally, change them into US style mm/dd/yyyy representations.
match = parsable.match(/^([0-9]{1,2})[-\/]([0-9]{1,2})[-\/]([0-9]{1,2})[^0-9]/);
function h1(parsable, match) {
var m1 = parseFloat(match[1]);
var m2 = parseFloat(match[2]);
var m3 = parseFloat(match[3]);
var cent = jsDate.config.defaultCentury;
var ny, nd, nm, str;
if (m1 > 31) { // first number is a year
nd = m3;
nm = m2;
ny = cent + m1;
}
else { // last number is the year
nd = m2;
nm = m1;
ny = cent + m3;
}
str = nm+'/'+nd+'/'+ny;
// now replace 2 digit year with 4 digit year
return parsable.replace(/^([0-9]{1,2})[-\/]([0-9]{1,2})[-\/]([0-9]{1,2})/, str);
}
if (match && match.length > 3) {
parsable = h1(parsable, match);
}
// Now check for '1/19/70' with nothing after and do as above
var match = parsable.match(/^([0-9]{1,2})[-\/]([0-9]{1,2})[-\/]([0-9]{1,2})$/);
if (match && match.length > 3) {
parsable = h1(parsable, match);
}
var i = 0;
var length = jsDate.matchers.length;
var pattern,
ms,
current = parsable,
obj;
while (i < length) {
ms = Date.parse(current);
if (!isNaN(ms)) {
return new Date(ms);
}
pattern = jsDate.matchers[i];
if (typeof pattern == 'function') {
obj = pattern.call(jsDate, current);
if (obj instanceof Date) {
return obj;
}
} else {
current = parsable.replace(pattern[0], pattern[1]);
}
i++;
}
return NaN;
};
/**
* @static
* Handy static utility function to return the number of days in a given month.
* @param {Integer} year Year
* @param {Integer} month Month (1-12)
* @returns {Integer} Number of days in the month.
*/
//
// handy utility method Borrowed right from Ken Snyder's Date Instance Mehtods.
//
jsDate.daysInMonth = function(year, month) {
if (month == 2) {
return new Date(year, 1, 29).getDate() == 29 ? 29 : 28;
}
return [undefined,31,undefined,31,30,31,30,31,31,30,31,30,31][month];
};
//
// An Array of regular expressions or functions that will attempt to match the date string.
// Functions are called with scope of a jsDate instance.
//
jsDate.matchers = [
// convert dd.mmm.yyyy to mm/dd/yyyy (world date to US date).
[/(3[01]|[0-2]\d)\s*\.\s*(1[0-2]|0\d)\s*\.\s*([1-9]\d{3})/, '$2/$1/$3'],
// convert yyyy-mm-dd to mm/dd/yyyy (ISO date to US date).
[/([1-9]\d{3})\s*-\s*(1[0-2]|0\d)\s*-\s*(3[01]|[0-2]\d)/, '$2/$3/$1'],
// Handle 12 hour or 24 hour time with milliseconds am/pm and optional date part.
function(str) {
var match = str.match(/^(?:(.+)\s+)?([012]?\d)(?:\s*\:\s*(\d\d))?(?:\s*\:\s*(\d\d(\.\d*)?))?\s*(am|pm)?\s*$/i);
// opt. date hour opt. minute opt. second opt. msec opt. am or pm
if (match) {
if (match[1]) {
var d = this.createDate(match[1]);
if (isNaN(d)) {
return;
}
} else {
var d = new Date();
d.setMilliseconds(0);
}
var hour = parseFloat(match[2]);
if (match[6]) {
hour = match[6].toLowerCase() == 'am' ? (hour == 12 ? 0 : hour) : (hour == 12 ? 12 : hour + 12);
}
d.setHours(hour, parseInt(match[3] || 0, 10), parseInt(match[4] || 0, 10), ((parseFloat(match[5] || 0)) || 0)*1000);
return d;
}
else {
return str;
}
},
// Handle ISO timestamp with time zone.
function(str) {
var match = str.match(/^(?:(.+))[T|\s+]([012]\d)(?:\:(\d\d))(?:\:(\d\d))(?:\.\d+)([\+\-]\d\d\:\d\d)$/i);
if (match) {
if (match[1]) {
var d = this.createDate(match[1]);
if (isNaN(d)) {
return;
}
} else {
var d = new Date();
d.setMilliseconds(0);
}
var hour = parseFloat(match[2]);
d.setHours(hour, parseInt(match[3], 10), parseInt(match[4], 10), parseFloat(match[5])*1000);
return d;
}
else {
return str;
}
},
// Try to match ambiguous strings like 12/8/22.
// Use FF date assumption that 2 digit years are 20th century (i.e. 1900's).
// This may be redundant with pre processing of date already performed.
function(str) {
var match = str.match(/^([0-3]?\d)\s*[-\/.\s]{1}\s*([a-zA-Z]{3,9})\s*[-\/.\s]{1}\s*([0-3]?\d)$/);
if (match) {
var d = new Date();
var cent = jsDate.config.defaultCentury;
var m1 = parseFloat(match[1]);
var m3 = parseFloat(match[3]);
var ny, nd, nm;
if (m1 > 31) { // first number is a year
nd = m3;
ny = cent + m1;
}
else { // last number is the year
nd = m1;
ny = cent + m3;
}
var nm = inArray(match[2], jsDate.regional[jsDate.regional.getLocale()]["monthNamesShort"]);
if (nm == -1) {
nm = inArray(match[2], jsDate.regional[jsDate.regional.getLocale()]["monthNames"]);
}
d.setFullYear(ny, nm, nd);
d.setHours(0,0,0,0);
return d;
}
else {
return str;
}
}
];
//
// I think John Reisig published this method on his blog, ejohn.
//
function inArray( elem, array ) {
if ( array.indexOf ) {
return array.indexOf( elem );
}
for ( var i = 0, length = array.length; i < length; i++ ) {
if ( array[ i ] === elem ) {
return i;
}
}
return -1;
}
//
// Thanks to Kangax, Christian Sciberras and Stack Overflow for this method.
//
function get_type(thing){
if(thing===null) return "[object Null]"; // special case
return Object.prototype.toString.call(thing);
}
$.jsDate = jsDate;
})(jQuery);