﻿/***********************************************************
SUMMARY: 
     ghDate has a few prototypes on the Date global object.
     The heart of ghDate is its toFormat() method which can generate a string formatting a date instance using format codes from the ubiquitous strftime function, the PHP date function, common  format codes like 'yyyy', and format codes unique to ghDate. ghDate uses code to generate code a la http://www.xaprb.com/blog/2005/12/12/javascript-closures-for-runtime-efficiency/ but does more than single character format codes.
COPYRIGHT:
    Creative Commons
    http://www.georgehernandez.com/h/About/#Copyright
DEV ONLY:
    http://www.georgehernandez.com/h/xComputers/JavaScript/Tests/includes/ghLibrary.js
MOD LOG:
    20081007 1324 Released version 20081007 1324. -gh@georgehernandez.com
    20081008 1336 Added getElapsed() and put prototype in ifs. -gh@georgehernandez.com
    20081017 1722 Tweaked using JSLint with these options: {bitwise: true, browser: true, eqeqeq: true, evil: true, nomen: true, onevar: true, plusplus: true, regexp: true, undef: true, white: true}. There are 3 ++ "errors" but no parentheses needed. -gh@georgehernandez.com
    20090106 1323 Removed Date Static and Date Utils. -gh@georgehernandez.com
CONTENTS:
    Date Formatting: formatFunctions{}, toFormat(), createNewFormat()(static), getFormatCode()    
***********************************************************/
/*jslint bitwise: true, browser: true, eqeqeq: true, evil: true, nomen: true, onevar: true, plusplus: true, regexp: true, undef: true, white: true */


//Date Formatting/////////////////////////////////////////////////////////

Date.formatFunctions = {count: 0};

if (!Date.prototype.toFormat) {
    Date.prototype.toFormat = function (format) {
        //ATTRIBUTION: Derived by georgehernandez.com from http://www.xaprb.com/blog/2005/12/12/javascript-closures-for-runtime-efficiency/ and http://dren.ch/strftime/
        if (!Date.formatFunctions[format]) {
            Date.createNewFormat(format);
        }
        var func = Date.formatFunctions[format];
        return this[func]();
    };
}
Date.createNewFormat = function (format) {
    //NOTES: Dependent on Number.prototype.pad, Number.prototype.getSuffix
    var funcName, code, k = [], i;
    funcName = "format" + Date.formatFunctions.count++;
    Date.formatFunctions[format] = funcName;
    code = "Date.prototype." + funcName + " = function (){return   "; 
    format.replace(/\{([^\}]+|\})\}/g, function (sub, formatKey, index, str) {
        k[k.length] = {key: formatKey, idx: index};
        return '[' + k[k.length - 1].idx + ']';
    });
    for (i = 0; i < k.length; i++) {
        if (i === 0) {
            code += "'" + format.slice(0, k[i].idx).replace(/'/g, "\\'") + "' + ";
        } else {
            code += "'" + format.slice(k[i - 1].idx + k[i - 1].key.length + 2, k[i].idx).replace(/'/g, "\\'") + "' + ";
        }
        code += Date.getFormatCode(k[i].key);
        if (i === k.length - 1) {
            code += "'" + format.slice(k[i].idx + k[i].key.length + 2, format.length).replace(/'/g, "\\'") + "' + ";
        }
    }
    //document.write(code.substring(0, code.length - 3) + ";}"); //DEV ONLY
    eval(code.substring(0, code.length - 3) + ";}");
};
Date.getFormatCode = function (formatKey) {
    switch (formatKey) {
    case "{":
        return "'{' + ";
    case "}":
        return "'}' + ";
    case "dim_28":
    case "t":
        //NOTES: Days in the month. 
        //VALUES: 28-31
        return "this.getDaysInMonth() + ";
    case "dom":
    case "j":
        //NOTES: Day of the month, padded not. 
        //VALUES: 1-31
        return "this.getDate() + ";
    case "dom_pad":
    case "d":
    case "%d":
    case "dd":
        //NOTES: Day of the month, padded. 
        //VALUES: 01-31
        return "this.getDate().pad(2) + ";
    case "dom_pads":
    case "%e":
        //NOTES: Day of the month, padded space. 
        //VALUES: ' 1'-'31'
        return "this.getDate().pad(2, ' ') + ";
    case "dom_suffix":
    case "S":
        //NOTES: Day of the month, English suffix. 
        //VALUES: st, nd, rd, th
        return "this.getDate().getSuffix() + ";
    case "dow_0":
    case "w":
    case "%w":
        //NOTES: Day of the week, 0=Sun. 
        //VALUES: 0-6 with 0=Sun
        return "this.getDay() + ";
    case "dow_1":
    case "N":
    case "%u":
        //NOTES: Day of the week, 1=Mon, ISO-860. 
        //VALUES: 1-7 with 1=Mon
        return "(this.getDay() || 7) + ";
    case "dow_nam":
    case "D":
    case "%a":
    case "ddd":
        //NOTES: Day of the week, abbreviated. 
        //VALUES: Mon-Sun
        return "Date.dayNames[this.getDay()].substring(0, 3) + ";
    case "doy_0":
    case "z":
        //NOTES: Day of the year, padded not. 
        //VALUES: 0-365
        return "this.getDayOfYear() + ";
    case "doy_1":
        //Day of the year, padded not. 
        //VALUES: 1-366
        return "(this.getDayOfYear()+1) + ";
    case "doy_1pad":
    case "%j":
        //NOTES: Day of the year, padded. 
        //VALUES: 001-366
        return "(this.getDayOfYear()+1).pad(3) + ";
    case "dt_loc":
    case "%x":
        //NOTES: Date, locale. 
        //EG: Thursday, January 31, 2008
        return "this.toLocaleDateString() + ";
    case "dt_mdy":
    case "%D":
        //NOTES: Date, MM/dd/yy. 
        //EG: 01/31/08
        return "(this.getMonth()+1).pad(2) + '/' + this.getDate().pad(2) + '/' + (this.getFullYear() % 100).pad(2) + ";
    case "dt_ymd":
        //NOTES: Date, yyyy-MM-dd. 
        //EG: 2008-01-31
        return "this.getFullYear() + '-' + (this.getMonth()+1).pad(2) + '-' + this.getDate().pad(2) + ";
    case "dtm_iso":
    case "c":
        //NOTES: Datetime, ISO 8601: yyyy-MM-ddTHH-mm-ss
        //EG: 1970-01-01T00:00:00+00:00
        return "this.getFullYear() + '-' + (this.getMonth()+1).pad(2) + '-' + this.getDate().pad(2) + 'T' + this.getHours().pad(2) + ':' + this.getMinutes().pad(2) + ':' + this.getSeconds().pad(2) + ";
    case "dtm_loc":
    case "%c":
        //NOTES: Datetime, locale. 
        //EG: Thursday, January 31, 2008
        return "this.toLocaleString() + ";
    case "dtm_ms":
        //NOTES: Datetime, milliseconds since midnight 01 January, 1970 UTC (Unix epoch)
        //EG: 1970-01-01T00:00:01+00:00 GMT is 1000
        return "this.getTime() + ";
    case "dtm_rfc":
    case "r":
        //NOTES: Datetime, RFC 2822: ddd, dd MMM yyyy HH:mm:ss +zzzz
        //EG: 1970-01-01T00:00:00+00:00 is 
        return "Date.dayNames[this.getDay()].substring(0, 3) + ', ' + this.getDate().pad(2) + ' ' + Date.monthNames[this.getMonth()].substring(0, 3) + ' ' + this.getFullYear() + ' ' + this.getHours().pad(2) + ':' + this.getMinutes().pad(2) + ':' + this.getSeconds().pad(2) + this.getGMTOffset() + ";
    case "dtm_s":
    case "U":
        //NOTES: Datetime, seconds since midnight 01 January, 1970 UTC (Unix epoch)
        //EG: 1970-01-01T00:00:01+00:00 GMT is 1
        return "Math.round(this.getTime() / 1000) + ";
    case "dtm_ymd_hm":
        //NOTES: Datetime, yyyy-MM-dd HH:mm
        //EG: 1970-01-01 00:00
        return "this.getFullYear() + '-' + (this.getMonth()+1).pad(2) + '-' + this.getDate().pad(2) + ' ' + this.getHours().pad(2) + ':' + this.getMinutes().pad(2) + ";
    case "mo_0":
        //NOTES: Month, 0=Jan, padded not
        //VALUES: 0-12
        return "this.getMonth() + ";
    case "mo_0pad":
        //NOTES: Month, 0=Jan, padded
        //VALUES: 00-12
        return "this.getMonth().pad(2) + ";
    case "mo_1":
    case "n":
        //NOTES: Month, 1=Jan, padded not
        //VALUES: 1-12
        return "(this.getMonth()+1) + ";
    case "mo_1pad":
    case "m":
    case "%m":
    case "MM":
        //NOTES: Month, 1=Jan, padded. 
        //VALUES: 01-12
        return "(this.getMonth()+1).pad(2) + ";
    case "mo_nam":
    case "M":
    case "%b":
    case "%h":
    case "MMM":
        //NOTES: Month, name, abbreviated
        //VALUES: Jan-Dec
        return "Date.monthNames[this.getMonth()].substring(0, 3) + ";
    case "mo_name":
    case "F":
    case "%B":
    case "%h":
    case "MMMM":
        //NOTES: Month, name, abbreviated not
        //VALUES: January-December
        return "Date.monthNames[this.getMonth()] + ";
    case "t_hm":
    case "%R":
        //NOTES: Time, HH:mm
        //EG: 13:32
        return "this.getHours().pad(2) + ':' + this.getMinutes().pad(2) + ";
    case "t_hma":
        //NOTES: Time, hh:mm ap
        //EG: 01:32 pm
        return "((this.getHours() %12) ? this.getHours() % 12 : 12).pad(2) + ':' + this.getMinutes().pad(2) + ' ' + (this.getHours() < 12 ? 'am' : 'pm') + ";
    case "t_hms":
    case "%T":
        //NOTES: Time, HH:mm:ss
        //EG: 13:32:43
        return "this.getHours().pad(2) + ':' + this.getMinutes().pad(2) + ':' + this.getSeconds().pad(2) + ";
    case "t_hmsa":
    case "%r":
        //NOTES: Time, hh:mm:ss ap
        //EG: 01:32:43 pm
        return "((this.getHours() %12) ? this.getHours() % 12 : 12).pad(2) + ':' + this.getMinutes().pad(2) + ':' + this.getSeconds().pad(2) + ' ' + (this.getHours() < 12 ? 'am' : 'pm') + ";
    case "t_loc":
    case "%X":
        //NOTES: Time, locale. 
        //EG: 13:32:43
        return "this.toLocaleTimeString() + ";
    //case "t_swatch":
    //case "B":
        //NOTES: Time, Swatch Internet, padded. 1000 beats per solar day = 00:01:26.4. One timezone: CET or UTC+1. Usually expressed like "@BBB".
        //VALUES: 000-999
        //return "";
    case "ta_AM":
    case "A":
    case "AM":
        //NOTES: Time, am/pm
        //VALUES: am, pm
        return "(this.getHours() < 12 ? 'AM' : 'PM') + ";
    case "ta_am":
    case "a":
    case "am":
        //NOTES: Time, am/pm
        //VALUES: am, pm
        return "(this.getHours() < 12 ? 'am' : 'pm') + ";
    case "ta_loc":
    case "%p":
        //NOTES: Time, a.m./p.m., locale. No real way to implement this, so I'm using my locale here.
        //VALUES: a.m., p.m.
        return "(this.getHours() < 12 ? 'a.m.' : 'p.m.') + ";
    case "th_12":
    case "g":
        //NOTES: Time, hour, 12 hour format, padded not
        //VALUES: 1-12
        return "((this.getHours() %12) ? this.getHours() % 12 : 12) + ";
    case "th_12pad":
    case "h":
    case "%I":
    case "hh":
        //NOTES: Time, hour, 12 hour format, padded
        //VALUES: 01-12
        return "((this.getHours() %12) ? this.getHours() % 12 : 12).pad(2) + ";
    case "th_24":
    case "G":
        //NOTES: Time, hour, 24 hour format, padded not
        //VALUES: 0-23
        return "this.getHours() + ";
    case "th_24pad":
    case "H":
    case "%H":
    case "HH":
        //NOTES: Time, hour, 24 hour format, padded
        //VALUES: 00-23
        return "this.getHours().pad(2) + ";
    case "tm":
        //NOTES: Time, minutes, padded not
        //VALUES: 0-59
        return "this.getMinutes() + ";
    case "tm_pad":
    case "i":
    case "%M":
    case "mm":
        //NOTES: Time, minutes, padded
        //VALUES: 00-59
        return "this.getMinutes().pad(2) + ";
    case "tms":
    case "u":
        //NOTES: Time, milliseconds, padded not
        //VALUES: 0-999
        return "this.getMilliseconds() + ";
    case "tms_pad":
    case "ms":
        //NOTES: Time, milliseconds, padded
        //VALUES: 000-999
        return "this.getMilliseconds().pad(3) + ";
    case "ts":
        //NOTES: Time, minutes, padded not
        //VALUES: 0-59
        return "this.getSeconds() + ";
    case "ts_pad":
    case "s":
    case "%S":
    case "SS":
        //NOTES: Time, minutes, padded
        //VALUES: 00-59
        return "this.getSeconds().pad(2) + ";
    case "tz_dst":
    case "I":
        //NOTES: Timezone, is daylight saving time. Checks if the locale is in DST for the datetime.
        //EG: For CDT (UTC-05:00), returns 1. For CST (UTC-06:00), returns 0.
        return "( (this.getDSTOffset() != 0) ? 1 : 0 ) + ";
    case "tz_hm":
    case "O":
    case "zzzz":
        //NOTES: Timezone, GMT diff, in hhmm
        //EG: For CDT (UTC-05:00), returns '-0500'
        return "(this.getTimezoneOffset() > 0 ? '-' : '+') + Math.floor(this.getTimezoneOffset() / 60).pad(2) + (this.getTimezoneOffset() % 60).pad(2) + ";
    case "tz_h_m":
    case "P":
        //NOTES: Timezone, GMT diff, in hh:mmgetTimezoneOffset().
        //EG: For CDT (UTC-05:00), returns '-05:00'
        return "(this.getTimezoneOffset() > 0 ? '-' : '+') + Math.floor(this.getTimezoneOffset() / 60).pad(2) + ':' + (this.getTimezoneOffset() % 60).pad(2) + ";
    case "tz_m":
        //NOTES: Timezone, GMT diff, in hh:mm
        //NOTES: Timezone, GMT diff, in Minutes.
        return "-this.getTimezoneOffset() + ";
    case "tz_nam":
    case "T":
        //NOTES: Timezone, name abbreviated
        //EG: EST, CST
        return "((this.toString().replace(/\\w{3} \\w{3} \\d{1,2}/, '').replace(/\\d{2}:\\d{2}:\\d{2}/, '').replace(/GMT[+-]\\d+/, '').replace(/[-]*\\d+/, '').replace(/ B\\.C\\./, '').trim().length == 3) ? this.toString().replace(/\\w{3} \\w{3} \\d{1,2}/, '').replace(/\\d{2}:\\d{2}:\\d{2}/, '').replace(/GMT[+-]\\d+/ , '').replace(/[-]*\\d+/ , '').replace(/ B\\.C\\./, '').trim() : this.toString().replace(/\\w{3} \\w{3} \\d{1,2}/, '').replace(/\\d{2}:\\d{2}:\\d{2}/, '').replace(/GMT[+-]\\d+/ , '').replace(/[-]*\\d+/ , '').replace(/ B\\.C\\./, '').replace(/(\\w)\\w+ (\\w)\\w+ (\\w)\\w+/, '$1$2$3').replace(/\\(|\\)/g, '')) + ";
    case "tz_name":
    case "e":
    case "%Z":
    case "Zzzz":
        //NOTES: Timezone, name abbreviated not
        //EG: UTC, GMT, Atlantic/Azores
        return "this.toString().replace(/\\w{3} \\w{3} \\d{1,2}/, '').replace(/\\d{2}:\\d{2}:\\d{2}/, '').replace(/[^+-]\\d+/ , '') + ";
    case "tz_s":
    case "Z":
        //NOTES: Timezone, GMT diff, in Seconds. 
        //EG: For CDT (UTC-05:00), returns '-18000'
        return "(this.getTimezoneOffset() * -60) + ";
    case "wmon_iso":
    case "W":
    case "%V":
    case "week":
        //NOTES: Week, of the year, Mon first day of the week, padded. ISO 8601. If the week with Jan 1 has 4+ days (i.e. Thu at latest), then it is week 1 of that year. Otherwise it is the last week of the previous year.
        //VALUES: 01-53
        return "this.getWeek('mon_iso') + ";
    case "wmon_jan":
        //NOTES: Week, of the year, Mon first day of the week, padded. Week that has Jan 1 is week 1 of that year.
        //VALUES: 01-53
        return "this.getWeek('mon_jan') + ";
    case "wmon_mon":
    case "%W":
        //NOTES: Week, of the year, Mon first day of the week, padded. All days in a new year preceding the 1st Mon are considered to be week 0.
        //VALUES: 00-53
        return "this.getWeek('mon_mon') + ";
    case "wsun_jan":
        //NOTES: Week, of the year, Sun first day of the week, padded. Week that has Jan 1 is week 1 of that year.
        //VALUES: 01-53
        return "this.getWeek('sun_jan') + ";
    case "wsun_sun":
    case "%U":
        //NOTES: Week, of the year, Sun first day of the week, padded. All days in a new year preceding the 1st Sun are considered to be week 0.
        //VALUES: 00-53
        return "this.getWeek('sun_jan') + ";
    case "y_c":
        //NOTES: Year, century, padded not
        //EG: 0-99
        return "Math.floor(this.getFullYear() / 100) + ";
    case "y_cpad":
    case "%C":
        //NOTES: Year, century, padded
        //EG: 0-99
        return "Math.floor(this.getFullYear() / 100).pad(2) + ";
    case "y_leap":
    case "L":
        //NOTES: Year, is leap?
        //VALUES: 0-1
        //EG: Given 2008, return 1
        return "( (this.isLeapYear()) ? 1 : 0) + ";
    case "y_yyyy":
    case "y_yy":
    case "y":
    case "%y":
    case "yy":
        //NOTES: Year, abbreviated. 
        //EG: 99
        return "(this.getFullYear() % 100).pad(2) + ";
    case "y_yyyy":
    case "Y":
    case "%Y":
    case "yyyy":
        //NOTES: Year, abbreviated not. 
        //EG: 1999
        return "this.getFullYear() + ";
    case "y_yyyy_iso":
    case "o":
        //NOTES: Year, abbreviated not. ISO-8601. If the ISO week belongs to the next or previous year, then that year otherwise current year.
        //EG: For 2006-01-01, returns 2005
        return " ( (this.getWeek('mon_iso')>50 && this.getMonth()==0) ? this.getFullYear()-1 : this.getFullYear() ) + ";
    default:
        return "'" + formatKey.replace(/'/g, "\\'") + "' + ";
    }
};
