Skip to content

Commit

Permalink
add strftime implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
bluesmoon committed Sep 14, 2011
1 parent ebd8531 commit ef9b600
Show file tree
Hide file tree
Showing 2 changed files with 324 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/prettydate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
exports.strftime = strftime = require('./strftime');

322 changes: 322 additions & 0 deletions lib/strftime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
/**
* strftime formatters for javascript based on the Open Group specification defined at
* http://www.opengroup.org/onlinepubs/007908799/xsh/strftime.html
* This implementation does not include modified conversion specifiers (i.e., Ex and Ox)
*/

/**
* Pad a number with leading spaces, zeroes or something else
* @method xPad
* @param x {Number} The number to be padded
* @param pad {String} The character to pad the number with
* @param r {Number} (optional) The base of the pad, eg, 10 implies to two digits, 100 implies to 3 digits.
* @private
*/
function xPad(x, pad, r)
{
if(typeof r === "undefined") {
r=10;
}
pad = pad.toString();
for( ; parseInt(x, 10)<r && r>1; r/=10) {
x = pad + x;
}
return x.toString();
}

var formats = {
a: function (d, l) { return l.a[d.getDay()]; },
A: function (d, l) { return l.A[d.getDay()]; },
b: function (d, l) { return l.b[d.getMonth()]; },
B: function (d, l) { return l.B[d.getMonth()]; },
C: function (d) { return xPad(parseInt(d.getFullYear()/100, 10), 0); },
d: ["getDate", "0"],
e: ["getDate", " "],
g: function (d) { return xPad(parseInt(formats.G(d)%100, 10), 0); },
G: function (d) {
var y = d.getFullYear();
var V = parseInt(formats.V(d), 10);
var W = parseInt(formats.W(d), 10);

if(W > V) {
y++;
} else if(W===0 && V>=52) {
y--;
}

return y;
},
H: ["getHours", "0"],
I: function (d) { var I=d.getHours()%12; return xPad(I===0?12:I, 0); },
j: function (d) {
var gmd_1 = new Date("" + d.getFullYear() + "/1/1 GMT");
var gmdate = new Date("" + d.getFullYear() + "/" + (d.getMonth()+1) + "/" + d.getDate() + " GMT");
var ms = gmdate - gmd_1;
var doy = parseInt(ms/60000/60/24, 10)+1;
return xPad(doy, 0, 100);
},
k: ["getHours", " "],
l: function (d) { var I=d.getHours()%12; return xPad(I===0?12:I, " "); },
m: function (d) { return xPad(d.getMonth()+1, 0); },
M: ["getMinutes", "0"],
p: function (d, l) { return l.p[d.getHours() >= 12 ? 1 : 0 ]; },
P: function (d, l) { return l.P[d.getHours() >= 12 ? 1 : 0 ]; },
s: function (d, l) { return parseInt(d.getTime()/1000, 10); },
S: ["getSeconds", "0"],
u: function (d) { var dow = d.getDay(); return dow===0?7:dow; },
U: function (d) {
var doy = parseInt(formats.j(d), 10);
var rdow = 6-d.getDay();
var woy = parseInt((doy+rdow)/7, 10);
return xPad(woy, 0);
},
V: function (d) {
var woy = parseInt(formats.W(d), 10);
var dow1_1 = (new Date("" + d.getFullYear() + "/1/1")).getDay();
// First week is 01 and not 00 as in the case of %U and %W,
// so we add 1 to the final result except if day 1 of the year
// is a Monday (then %W returns 01).
// We also need to subtract 1 if the day 1 of the year is
// Friday-Sunday, so the resulting equation becomes:
var idow = woy + (dow1_1 > 4 || dow1_1 <= 1 ? 0 : 1);
if(idow === 53 && (new Date("" + d.getFullYear() + "/12/31")).getDay() < 4)
{
idow = 1;
}
else if(idow === 0)
{
idow = formats.V(new Date("" + (d.getFullYear()-1) + "/12/31"));
}

return xPad(idow, 0);
},
w: "getDay",
W: function (d) {
var doy = parseInt(formats.j(d), 10);
var rdow = 7-formats.u(d);
var woy = parseInt((doy+rdow)/7, 10);
return xPad(woy, 0, 10);
},
y: function (d) { return xPad(d.getFullYear()%100, 0); },
Y: "getFullYear",
z: function (d) {
var o = d.getTimezoneOffset();
var H = xPad(parseInt(Math.abs(o/60), 10), 0);
var M = xPad(Math.abs(o%60), 0);
return (o>0?"-":"+") + H + M;
},
Z: function (d) {
var tz = d.toString().replace(/^.*:\d\d( GMT[+-]\d+)? \(?([A-Za-z ]+)\)?\d*$/, "$2").replace(/[a-z ]/g, "");
if(tz.length > 4) {
tz = formats.z(d);
}
return tz;
},
"%": function (d) { return "%"; }
};

var aggregates = {
c: "locale",
D: "%m/%d/%y",
F: "%Y-%m-%d",
h: "%b",
n: "\n",
r: "%I:%M:%S %p",
R: "%H:%M",
t: "\t",
T: "%H:%M:%S",
x: "locale",
X: "locale"
//"+": "%a %b %e %T %Z %Y"
};

/**
* Takes a native JavaScript Date and formats it as a string for display to user.
*
* @method strftime
* @param oDate {Date} Date.
* @param sFormat {Object} (Required) Format specifier
* <p>
* Any strftime string is supported, such as "%I:%M:%S %p". strftime has several format specifiers defined by the Open group at
* <a href="http://www.opengroup.org/onlinepubs/007908799/xsh/strftime.html">http://www.opengroup.org/onlinepubs/007908799/xsh/strftime.html</a>
* PHP added a few of its own, defined at <a href="http://www.php.net/strftime">http://www.php.net/strftime</a>
* </p>
* <p>
* This javascript implementation supports all the PHP specifiers and a few more. The full list is below.
* </p>
* <dl>
* <dt>%a</dt> <dd>abbreviated weekday name according to the current locale</dd>
* <dt>%A</dt> <dd>full weekday name according to the current locale</dd>
* <dt>%b</dt> <dd>abbreviated month name according to the current locale</dd>
* <dt>%B</dt> <dd>full month name according to the current locale</dd>
* <dt>%c</dt> <dd>preferred date and time representation for the current locale</dd>
* <dt>%C</dt> <dd>century number (the year divided by 100 and truncated to an integer, range 00 to 99)</dd>
* <dt>%d</dt> <dd>day of the month as a decimal number (range 01 to 31)</dd>
* <dt>%D</dt> <dd>same as %m/%d/%y</dd>
* <dt>%e</dt> <dd>day of the month as a decimal number, a single digit is preceded by a space (range " 1" to "31")</dd>
* <dt>%F</dt> <dd>same as %Y-%m-%d (ISO 8601 date format)</dd>
* <dt>%g</dt> <dd>like %G, but without the century</dd>
* <dt>%G</dt> <dd>The 4-digit year corresponding to the ISO week number</dd>
* <dt>%h</dt> <dd>same as %b</dd>
* <dt>%H</dt> <dd>hour as a decimal number using a 24-hour clock (range 00 to 23)</dd>
* <dt>%I</dt> <dd>hour as a decimal number using a 12-hour clock (range 01 to 12)</dd>
* <dt>%j</dt> <dd>day of the year as a decimal number (range 001 to 366)</dd>
* <dt>%k</dt> <dd>hour as a decimal number using a 24-hour clock (range 0 to 23); single digits are preceded by a blank. (See also %H.)</dd>
* <dt>%l</dt> <dd>hour as a decimal number using a 12-hour clock (range 1 to 12); single digits are preceded by a blank. (See also %I.) </dd>
* <dt>%m</dt> <dd>month as a decimal number (range 01 to 12)</dd>
* <dt>%M</dt> <dd>minute as a decimal number</dd>
* <dt>%n</dt> <dd>newline character</dd>
* <dt>%p</dt> <dd>either "AM" or "PM" according to the given time value, or the corresponding strings for the current locale</dd>
* <dt>%P</dt> <dd>like %p, but lower case</dd>
* <dt>%r</dt> <dd>time in a.m. and p.m. notation equal to %I:%M:%S %p</dd>
* <dt>%R</dt> <dd>time in 24 hour notation equal to %H:%M</dd>
* <dt>%s</dt> <dd>number of seconds since the Epoch, ie, since 1970-01-01 00:00:00 UTC</dd>
* <dt>%S</dt> <dd>second as a decimal number</dd>
* <dt>%t</dt> <dd>tab character</dd>
* <dt>%T</dt> <dd>current time, equal to %H:%M:%S</dd>
* <dt>%u</dt> <dd>weekday as a decimal number [1,7], with 1 representing Monday</dd>
* <dt>%U</dt> <dd>week number of the current year as a decimal number, starting with the
* first Sunday as the first day of the first week</dd>
* <dt>%V</dt> <dd>The ISO 8601:1988 week number of the current year as a decimal number,
* range 01 to 53, where week 1 is the first week that has at least 4 days
* in the current year, and with Monday as the first day of the week.</dd>
* <dt>%w</dt> <dd>day of the week as a decimal, Sunday being 0</dd>
* <dt>%W</dt> <dd>week number of the current year as a decimal number, starting with the
* first Monday as the first day of the first week</dd>
* <dt>%x</dt> <dd>preferred date representation for the current locale without the time</dd>
* <dt>%X</dt> <dd>preferred time representation for the current locale without the date</dd>
* <dt>%y</dt> <dd>year as a decimal number without a century (range 00 to 99)</dd>
* <dt>%Y</dt> <dd>year as a decimal number including the century</dd>
* <dt>%z</dt> <dd>numerical time zone representation</dd>
* <dt>%Z</dt> <dd>time zone name or abbreviation</dd>
* <dt>%%</dt> <dd>a literal "%" character</dd>
* </dl>
* @param sLocale {String} (Optional)</dt>
* The locale to use when displaying days of week, months of the year, and other locale specific
* strings. If not specified, this defaults to "en" (though this may be overridden by the deprecated Y.config.locale).
* The following locales are built in:
* <dl>
* <dt>en</dt>
* <dd>English</dd>
* <dt>en-US</dt>
* <dd>US English</dd>
* <dt>en-GB</dt>
* <dd>British English</dd>
* <dt>en-AU</dt>
* <dd>Australian English (identical to British English)</dd>
* </dl>
* @return {String} Formatted date for display.
*/
function strftime(oDate, sFormat, sLocale) {
var locale;

if(!oDate) {
return "";
}

if(!sFormat) {
sFormat="";
}

if(!sLocale) {
sLocale = 'en-US';
}

sLocale = sLocale.replace(/_/g, "-");

// Make sure we have a definition for the requested locale, or default to en.
if(!LOCALES[sLocale]) {
console.warn("selected locale " + sLocale + " not found, trying alternatives");
var tmpLocale = sLocale.replace(/-[a-zA-Z]+$/, "");
if(tmpLocale in LOCALES) {
sLocale = tmpLocale;
} else {
sLocale = "en";
}
console.info("falling back to " + sLocale);
}

locale = LOCALES[sLocale];

var replace_aggs = function (m0, m1) {
var f = aggregates[m1];
return (f === "locale" ? locale[m1] : f);
};

var replace_formats = function (m0, m1) {
var f = formats[m1];
if(typeof f == 'string') // string => built in date function
return oDate[f]();
else if(typeof f == 'function') // function => our own function
return f.call(oDate, oDate, locale);
else if(f instanceof Array && typeof f[0] === 'string') // built in function with padding
return xPad(oDate[f[0]](), f[1]);
else {
console.warn("unrecognised replacement type, please file a bug (format: " + sFormat + ")");
return m1;
}
};

// Preprocess by replacing %% with %<BS>, then later we'll replace %<BS> with %%
// XXX this is a hack. There's a very low, but non-0 probability that an actual %<BS>
// will show up in a string, and this will break when that happens. -- Philip 2011/01/10
sFormat = sFormat.replace(/%%/g, "%\b");

// First replace aggregates (run in a loop because an agg may be made up of other aggs)
while(sFormat.match(/%[cDFhnrRtTxX]/)) {
sFormat = sFormat.replace(/%([cDFhnrRtTxX])/g, replace_aggs);
}

// Now replace formats (do not run in a loop otherwise %%a will be replaced with the value of %a)
var str = sFormat.replace(/%([aAbBCdegGHIjklmMpPsSuUVwWyYzZ])/g, replace_formats);

// Post-process now to change %<BS> back to %%
str = str.replace("%\b", '%%');

replace_aggs = replace_formats = undefined;

return str;
}

var LOCALES = {};

LOCALES['en'] = {
a: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
A: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
b: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
B: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
c: "%a %d %b %Y %T %Z",
p: ["AM", "PM"],
P: ["am", "pm"],
r: "%I:%M:%S %p",
x: "%d/%m/%y",
X: "%T"
};

function addLocale(sName, oLocale, sBase) {
if(!sBase)
sBase = 'en';
LOCALES[sName] = oLocale;

for(k in LOCALES[sBase]) {
if(!(k in oLocale)) {
LOCALES[sName][k] = LOCALES[sBase][k];
}
}
}

addLocale('en-US', {
c: "%a %d %b %Y %I:%M:%S %p %Z",
x: "%m/%d/%Y",
X: "%I:%M:%S %p"
});

addLocale('en-GB', {
r: "%l:%M:%S %P %Z"
});

addLocale('en-AU', {});

strftime.addLocale=addLocale;
module.exports = strftime;

0 comments on commit ef9b600

Please sign in to comment.