ultra-strftime
Version:
Ultra fast realization of javascript strftime function without memory leak
730 lines (649 loc) • 18.2 kB
JavaScript
/*
Copyright (c) 2014 Petr Shalkov
This is solution without memory leak
Based on code by Sami Samhuri
Copyright 2010 - 2014 Sami Samhuri under the terms of the MIT license found
at http://sjs.mit-license.org/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
(function() {
"use strict";
// constants
var DEFAULT_LOCALE =
{ days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
shortDays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
shortMonths: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
AM: "AM",
PM: "PM",
am: "am",
pm: "pm"
};
// you can change this parameter to achieve best performance
var FAST_CACHE_LEN = 10;
// caching variables
// cache
var fastCacheP = 0;
var fastCache = new Array(FAST_CACHE_LEN);
(function createFastCache() {
for (var i =0; i < FAST_CACHE_LEN; ++i) {
fastCache[i] = { fmt: null, ts: null, locale: null, opt: null, res: null};
}
})();
// formatting functions cache
var fmtCache = [];
// cached date
var utcDate = new Date();
var tmId = null;
var cachedDate;
var cachedTime;
var curTimestamp;
function dateTimeout() {
tmId = null;
};
function getCachedDate() {
if (tmId === null) {
cachedDate = new Date();
cachedTime = cachedDate.getTime();
// if you need accurate timestamp then you can comment this line
tmId = setTimeout(dateTimeout, 1);
}
return cachedDate;
}
// cached ordinal
var cachedOrd = new Array(31);
(function createCachedOrd() {
for (var i = 0; i< cachedOrd.length; ++i) {
cachedOrd[i] = '' + (i + 1) + "th";
}
cachedOrd[0] = "1st";
cachedOrd[1] = "2nd";
cachedOrd[2] = "3rd";
cachedOrd[20] = "21st";
cachedOrd[21] = "22nd";
cachedOrd[22] = "23rd";
cachedOrd[30] = "31st";
})();
// cached timezones from -1200 to +1400
var cachedTZ = new Array(1561);
// minimal cached timezones
(function createMinCachedTZ() {
for (var i =0; i < cachedTZ.length; ++i) {
cachedTZ[i] = null;
}
// -1200
cachedTZ[-12*60+720] = "-1200";
// -1100
cachedTZ[-11*60+720] = "-1100";
// -1000
cachedTZ[-10*60+720] = "-1000";
// -0930
cachedTZ[-9*60-30+720] = "-0930";
// -0900
cachedTZ[-9*60+720] = "-0900";
// -0800
cachedTZ[-8*60+720] = "-0800";
// -0700
cachedTZ[-7*60+720] = "-0700";
// -0600
cachedTZ[-6*60+720] = "-0600";
// -0500
cachedTZ[-5*60+720] = "-0500";
// -0400
cachedTZ[-4*60+720] = "-0400";
// -0300
cachedTZ[-3*60-30+720] = "-0330";
// -0300
cachedTZ[-3*60+720] = "-0300";
// -0200
cachedTZ[-2*60+720] = "-0200";
// -0100
cachedTZ[-1*60+720] = "-0100";
// +0000
cachedTZ[0+720] = "+0000";
// +0100
cachedTZ[1*60+720] = "+0100";
// +0200
cachedTZ[2*60+720] = "+0200";
// +0300
cachedTZ[3*60+720] = "+0300";
// +0330
cachedTZ[3*60+30+720] = "+0330";
// +0400
cachedTZ[4*60+720] = "+0400";
// +0430
cachedTZ[4*60+30+720] = "+0430";
// +0500
cachedTZ[5*60+720] = "+0500";
// +0530
cachedTZ[5*60+30+720] = "+0530";
// +0545
cachedTZ[5*60+45+720] = "+0545";
// +0600
cachedTZ[6*60+720] = "+0600";
// +0630
cachedTZ[6*60+30+720] = "+0630";
// +0700
cachedTZ[7*60+720] = "+0700";
// +0800
cachedTZ[8*60+720] = "+0800";
// +0845
cachedTZ[8*60+45+720] = "+0845";
// +0900
cachedTZ[9*60+720] = "+0900";
// +0930
cachedTZ[9*60+30+720] = "+0930";
// +1000
cachedTZ[10*60+720] = "+1000";
// +1030
cachedTZ[10*60+30+720] = "+1030";
// +1100
cachedTZ[11*60+720] = "+1100";
// +1130
cachedTZ[11*60+30+720] = "+1130";
// +1200
cachedTZ[12*60+720] = "+1200";
// +1245
cachedTZ[12*60+45+720] = "+1245";
// +1300
cachedTZ[13*60+720] = "+1300";
// +1400
cachedTZ[14*60+720] = "+1400";
})();
// formatting elements
var elements = new Array(256);
var dummyObj = {};
(function createFmtElements() {
for (var i=0; i < elements.length; ++i) {
elements[i] = { val: null, pad: false };
}
// setup padding
// '19'
// C
elements[0x43].pad =
// '01/01/70'
// D
elements[0x44].pad =
// '01'
// d
elements[0x64].pad =
// '1970-01-01'
// F
elements[0x46].pad =
// '00'
// H
elements[0x48].pad =
// '12'
// I
elements[0x49].pad =
// ' 0'
// k
// controlled padding
elements[0x6B].pad =
// '12'
// l
// controlled padding
elements[0x6C].pad =
// '00'
// M
elements[0x4D].pad =
// '01'
// m
elements[0x6D].pad =
// '00:00'
// R
elements[0x52].pad =
// '12:00:00 AM'
// r
elements[0x72].pad =
// '00'
// S
elements[0x53].pad =
// '00:00:00'
// T
elements[0x54].pad =
// '00'
// U
elements[0x55].pad =
// '00'
// W
elements[0x57].pad = true;
// formatting code
// 'Thursday'
// A
elements[0x41].val = "( locale.days[d.getDay()] )\n";
// 'Thu'
// a
elements[0x61].val= "( locale.shortDays[d.getDay()] )\n";
// 'January'
// B
elements[0x42].val = "( locale.months[d.getMonth()] )\n";
// 'Jan'
// b
elements[0x62].val = "( locale.shortMonths[d.getMonth()] )\n";
// '19'
// C
// use padding
elements[0x43].val = "( ($1 =(d.getFullYear() / 100 | 0)), ($1 > 9) ? $1 : ( ";
// '01/01/70'
// D
// use padding
elements[0x44].val = "( (formats.D !== void 0) ? this.strftime(formats.D, d, locale): ";
// '01'
// d
// use padding
elements[0x64].val = "( ($1 = d.getDate()), ($1 > 9) ? $1 : ( ";
// '01'
// e
elements[0x65].val = "( d.getDate() )\n";
// '1970-01-01'
// F
// use padding
elements[0x46].val = "( (formats.F !== void 0) ? this.strftime(formats.F, d, locale): ";
// '00'
// H
// use padding
elements[0x48].val = "( ($1=d.getHours()), ($1 > 9) ? $1 : ( ";
// 'Jan'
// h
elements[0x68].val = "( locale.shortMonths[d.getMonth()] )\n";
// '12'
// I
// use padding
// elements[0x49].val = "( ($1 =this.hours12(d)), ($1 > 9) ? $1 : ( ";
elements[0x49].val = "((($1=d.getHours()),($1===0)? $1=12:(($1>12)?($1=$1-12):$1)), ($1 > 9) ? $1 : ( ";
// '000'
// j
elements[0x6A].val = "( ($1=new Date(d.getFullYear(), 0, 1)), $1=Math.ceil((d.getTime() - $1.getTime())/86400000), ($1 < 100 ? ($1 < 10 ? ('00' + $1) : ('0' + $1) ) : $1) )\n";
// ' 0'
// k
// control padding
// use padding
elements[0x6B].val = "( ($1 =d.getHours()), ($1 > 9) ? $1 : ( ";
// '000'
// L
elements[0x4C].val = "( ($1 = ((ts % 1000) | 0)), $1 < 100 ? ($1 < 10 ? ('00' + $1) : ('0' + $1)) : $1)";
// '12'
// l
// control padding
// use padding
elements[0x6C].val = "( (($1=d.getHours()),($1===0)? $1=12:(($1>12)?($1=$1-12):$1)), ($1 > 9) ? $1 : ( ";
// '00'
// M
// use padding
elements[0x4D].val = "( ($1=d.getMinutes()), ($1 > 9) ? $1 : ( ";
// '01'
// m
// use padding
elements[0x6D].val = "( ($1=d.getMonth() + 1), ($1 > 9) ? $1 : ( ";
// '\n'
// n
elements[0x6E].val = "'\\n'";
// '1st'
// o
elements[0x6F].val = "( this.cachedOrd[d.getDate() - 1] )\n";
// 'am'
// P
elements[0x50].val = "( d.getHours() < 12 ? locale.am : locale.pm )\n";
// 'AM'
// p
elements[0x70].val = "( d.getHours() < 12 ? locale.AM : locale.PM )\n";
// '00:00'
// R
// use padding
elements[0x52].val = "( (formats.R !== void 0) ? this.strftime(formats.R, d, locale) : ";
// '12:00:00 AM'
// r
// use padding
elements[0x72].val = "( (formats.r !== void 0) ? this.strftime(formats.r, d, locale) : ";
// '00'
// S
// use padding
elements[0x53].val = "( ($1=d.getSeconds()), ($1 > 9) ? $1 : ( ";
// '0'
// s
elements[0x73].val = "( (ts / 1000) | 0 )\n";
// '00:00:00'
// T
// use padding
elements[0x54].val = "( (formats.T !== void 0) ? this.strftime(formats.T, d, locale) : ";
// '\t'
// t
elements[0x74].val = "'\\t'";
// '00'
// U
// use padding
elements[0x55].val = "( ($1=this.weekNumber(d, 'sunday')), ($1 > 9) ? $1 : ( ";
// '4'
// u
elements[0x75].val = " ( ($1=d.getDay()), ($1 === 0) ? 7 : $1 )\n"; // 1 - 7, Monday is first day of the week
// '1-Jan-1970'
// v
// use padding
elements[0x76].val = "( (formats.v !== void 0) ? this.strftime(locale.formats.v, d, locale) : ('' + d.getDate() + '-' + locale.shortMonths[d.getMonth()] + '-' + d.getFullYear()) ) ";
// '00'
// W
// use padding
elements[0x57].val = "( ($1=this.weekNumber(d, 'monday')), ($1 > 9) ? $1 : ( ";
// '4'
// w
elements[0x77].val = "( d.getDay() )\n"; // 0 - 6, Sunday is first day of the week
// '1970'
// Y
elements[0x59].val = "( d.getFullYear() )\n";
// '70'
// y
elements[0x79].val = "( $1=String(d.getFullYear()), $1.slice($1.length - 2) )\n";
// 'GMT'
// Z
elements[0x5A].val = "( utc ? 'GMT' : ( $1 = d.toString().match(\/\\((\\w+)\\)\/), $1 && $1[1] || ''\n ) )";
// '+0000'
// z
elements[0x7A].val = "( utc ? '+0000' : this.computeTZ(tz !== void 0 ? tz : -d.getTimezoneOffset() ) )\n";
})();
// could be used prototype but I limited 'this' context
var strftimeContext = {
"strftime" : strftime,
"cachedOrd" : cachedOrd,
"computeTZ" : computeTZ,
"weekNumber": weekNumber,
};
// servant functions
// '01/01/70'
// D
function opt0x44(pad)
{
return "(($1=d.getMonth() + 1), ($1 > 9) ? $1 : "+pad+"+$1) + '/' + (($1=d.getDate()), ($1 > 9) ? $1 : "+pad+"+$1) + '/' + ((d.getFullYear() % 100) |0)";
}
// '1970-01-01'
// F
function opt0x46(pad)
{
return "(d.getFullYear() + '-' + (($1=d.getMonth()+1), ($1 > 9)?$1: "+pad+"+$1) + '-' + (($1=d.getDate()), ($1 > 9)?$1: "+pad+"+$1))";
}
// '00:00'
// R
function opt0x52(pad)
{
return "((($1 = d.getHours()), ($1 > 9)? $1: "+pad+"+$1) + ':' + (($1=d.getMinutes()), ($1 > 9)?$1: "+pad+"+$1))";
}
// '12:00:00 AM'
// r
function opt0x72(pad)
{
return "(((($1=d.getHours()),($1===0)? $1=12:(($1>12)?($1=$1-12):$1)), ($1>9)?$1:"+pad+"+$1) + ':' + (($1=d.getMinutes()), ($1>9)?$1:"+pad+"+$1) + ':' +(($1=d.getSeconds()), ($1>9)?$1:"+pad+"+$1) + ' ' + (d.getHours() < 12 ? locale.AM : locale.PM))";
}
// '00:00:00'
// T
function opt0x54(d, pad)
{
return "((($1=d.getHours()), ($1>9)?$1:'"+pad+"'+$1) + ':' + (($1=d.getMinutes()), ($1>9)?$1:'"+pad+"'+$1) + ':' + (($1=d.getSeconds()), ($1>9)?$1:'"+pad+"'+$1))";
}
function computeTZ(offsetTZ)
{
var id = (offsetTZ + 720) | 0;
var val = cachedTZ[id];
if (val === null) {
var hours = Math.abs(offsetTZ / 60) | 0,
minutes = offsetTZ % 60 | 0;
val = (offsetTZ < 0 ? '-' : '+') + (hours < 10 ? "0" + hours: hours) + (minutes < 10 ? "0" + minutes: minutes);
cachedTZ[id] = val;
}
return val;
}
function dateToUTC(d) {
var msDelta = (d.getTimezoneOffset() || 0) * 60000;
utcDate.setTime(d.getTime() + msDelta);
return utcDate;
// return new Date(d.getTime() + msDelta);
}
// firstWeekday: 'sunday' or 'monday', default is 'sunday'
//
// Pilfered & ported from Ruby's strftime implementation.
function weekNumber(d, firstWeekday) {
firstWeekday = firstWeekday || 'sunday';
// This works by shifting the weekday back by one day if we
// are treating Monday as the first day of the week.
var wday = d.getDay();
if (firstWeekday == 'monday') {
if (wday == 0) // Sunday
wday = 6;
else
wday--;
}
var firstDayOfYear = new Date(d.getFullYear(), 0, 1)
, yday = (d - firstDayOfYear) / 86400000
, weekNum = (yday + 7 - wday) / 7
;
// return Math.floor(weekNum);
return weekNum | 0;
}
// formatter
function buildFormatter(fmt) {
var element;
var body = "";
var fmtPos = 0, ch;
var len = fmt.length;
var str = "";
var padding = "'0'";
var fmtFound = false;
function pushBodyElement(code, pad) {
element = elements[code];
if (element !== null) {
if (element.pad) {
switch(code) {
// optimized elements
case 0x44:
body += element.val + opt0x44(pad) + ")\n";
break;
case 0x46:
body += element.val + opt0x46(pad) + ")\n";
break;
case 0x52:
body += element.val + opt0x52(pad) + ")\n";
break;
case 0x72:
body += element.val + opt0x72(pad) + ")\n";
break;
case 0x54:
body += element.val + opt0x54(pad) + ")\n";
break;
default:
body += element.val + pad + "+ $1))\n";
}
}
else {
body += element.val;
}
}
else {
str += String.fromCharCode(code);
}
}
while (fmtPos < len) {
//%
ch = fmt.charCodeAt(fmtPos);
//%
if (ch === 0x25) {
if (str.length > 0) {
body += "'" + str + "'+";
str = "";
}
padding = "'0'";
// found %% symbol
if (fmtFound) {
body += "'%'";
}
fmtFound = true;
++fmtPos;
continue;
}
if (fmtFound) {
switch (ch) {
//"-"
case 0x2D:
// ''
padding = "''";
break;
// "_"
case 0x5F:
// ' '
padding = "' '";
break;
// "0"
case 0x30:
// '0'
// microhack
padding = " '0'";
break;
default:
fmtFound = false;
if (((ch === 0x6B) || (ch === 0x6C)) && padding === "'0'") {
padding = "' '";
}
pushBodyElement(ch, padding);
if (fmtPos !== len - 1) {
body += "+";
}
break;
}
}
// ' and \
else if (ch === 0x27 || ch === 0x5C) {
str += "\\" + String.fromCharCode(ch);
}
else {
str += String.fromCharCode(ch);
}
++fmtPos;
if (fmtPos === len) {
if (str.length > 0) {
body += "'"+str + "';";
}
else {
body += ";";
}
}
}
// console.log("BODY:" + body);
return new Function("tz", "d", "locale", "formats", "ts", "utc", "var $1; \n return '' +" + body);
}
// main function
// d, locale, and options are optional, but you can't leave
// holes in the argument list. If you pass options you have to pass
// in all the preceding args as well.
//
// options:
// - locale [object] an object with the same structure as DefaultLocale
// - timezone [number] timezone offset in minutes from GMT
function strftime(fmt, d, locale, options) {
// d and locale are optional so check if d is really the locale
if ((d !== void 0) && !(d instanceof Date)) {
locale = d;
d = void 0;
}
if (d !== void 0) {
curTimestamp = d.getTime();
}
else {
curTimestamp = cachedTime;
d = getCachedDate();
}
// fast caching
var cache;
for (var i=0; i < FAST_CACHE_LEN; ++i) {
cache = fastCache[i];
if ((cache.fmt === fmt) && (cache.ts === curTimestamp) && (cache.locale === locale) && (cache.opt === options)) {
return cache.res;
}
}
if (fastCacheP === FAST_CACHE_LEN) {
fastCacheP = 0;
}
cache = fastCache[fastCacheP];
cache.fmt = fmt;
// save real options and locale in cache
cache.opt = options;
cache.locale = locale;
fastCacheP++;
options = (options !== void 0) ? options : dummyObj;
locale = (locale) ? locale : DEFAULT_LOCALE;
locale.formats = (locale.formats !== void 0) ? locale.formats : dummyObj;
var tz = options.timezone;
var utc = options.utc;
if (options.utc || tz !== void 0) {
d = dateToUTC(d);
}
if (tz !== void 0) {
var tzType = typeof tz;
var tzIsString = (tzType === 'string');
if (tz) {
// ISO 8601 format timezone string, [-+]HHMM
//
// Convert to the number of minutes and it'll be applied to the date below.
if (tzIsString) {
var hours = (tz.charCodeAt(1)-0x30)*10 + (tz.charCodeAt(2)-0x30);
var mins = (tz.charCodeAt(3)-0x30)*10 + (tz.charCodeAt(4)-0x30);
if (tz.charCodeAt(0) === 0x2D) {
tz = -( (60 * hours) + mins);
}
else {
tz = (60 * hours) + mins;
}
}
d.setTime(d.getTime() + (tz * 60000));
}
}
// slow cache
if ((locale === DEFAULT_LOCALE)) {
var func = fmtCache[fmt];
if (func !== void 0) {
cache.res = func.call(strftimeContext, tz, d, locale, locale.formats, curTimestamp, options.utc);
cache.ts = curTimestamp;
return cache.res;
// return func.call(strftimeContext, tz, d, locale, locale.formats, curTimestamp, options.utc);
}
}
var buildedFmt = buildFormatter.call(strftimeContext, fmt);
fmtCache[fmt] = buildedFmt;
cache.res = buildedFmt.call(strftimeContext, tz, d, locale, locale.formats, curTimestamp, options.utc);
cache.ts = curTimestamp;
return cache.res;
// return buildedFmt.call(strftimeContext, tz, d, locale, locale.formats, curTimestamp, options.utc);
}
// compatibility with original javascript strftime
strftime.strftimeUTC = function(fmt, d, locale) {
return strftime(fmt, d, locale, { utc: true });
};
strftime.strftimeTZ= function(fmt, d, locale, timezone) {
var tLoc = typeof locale;
if ((tLoc === "number" || tLoc === "string") && timezone === void 0) {
timezone = locale;
locale = DEFAULT_LOCALE;
}
else if (locale === void 0)
locale = DEFAULT_LOCALE;
return strftime(fmt, d, locale, { timezone: timezone });
};
strftime.localizedStrftime = function(locale) {
return function(fmt, d, options) {
return strftime(fmt, d, locale, options);
}
}
strftime.strftime = strftime;
module.exports = strftime;
}());