time
Version:
"time.h" bindings for Node.js
629 lines (572 loc) • 20.7 kB
JavaScript
/**
* Module dependencies.
*/
var debug = require('debug')('time')
, fs = require('fs')
, path = require('path')
, bindings = require('bindings')('time.node')
, MILLIS_PER_SECOND = 1000
, DAYS_OF_WEEK = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']
, MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']
, TZ_BLACKLIST = [ 'SystemV', 'Etc' ];
/**
* Extends a "Date" constructor with node-time's extensions.
* By default, `time.Date` is extended with this function.
* If you want the global your your module-specific Date to be extended,
* then invoke this function on the Date constructor.
*/
exports = module.exports = function (Date) {
debug('extending Date constructor');
var p = Date.prototype;
p.getTimezone = getTimezone;
p.setTimezone = setTimezone;
p.getTimezoneAbbr = getTimezoneAbbr;
return exports;
}
/**
* The initial timezone of the process. This env var may initially be undefined,
* in which case node-time will attempt to resolve and set the variable.
*/
exports.currentTimezone = process.env.TZ;
/**
* Export the raw functions from the bindings.
*/
exports.time = bindings.time;
exports.localtime = bindings.localtime;
exports.mktime = bindings.mktime;
/**
* A "hack" of sorts to force getting our own Date instance.
* Otherwise, in normal cases, the global Natives are shared between
* contexts (not what we want)...
*/
var _Date = process.env.NODE_MODULE_CONTEXTS
? Date
: require('vm').runInNewContext("Date");
/**
* Add the node-time extensions (setTimezone(), etc.)
*/
exports(_Date);
/**
* During startup, we synchronously attempt to determine the location of the
* timezone dir, or TZDIR on some systems. This isn't necessary for the
* C bindings, however it's needed for the `listTimezones()` function and for
* resolving the 'initial' timezone to use.
*/
debug('attempting to resolve timezone directory.');
var possibleTzdirs = [
'/usr/share/zoneinfo'
, '/usr/lib/zoneinfo'
, '/usr/share/lib/zoneinfo'
];
var TZDIR = process.env.TZDIR;
if (TZDIR) {
debug('got env-defined TZDIR:', TZDIR);
possibleTzdirs.unshift(TZDIR);
}
while (possibleTzdirs.length > 0) {
var d = possibleTzdirs.shift();
debug('checking if directory exists:', d);
try {
if (fs.statSync(d).isDirectory()) {
TZDIR = d;
break;
}
} catch (e) {
debug(e);
}
}
possibleTzdirs = null; // garbage collect
if (TZDIR) {
debug('found timezone directory at:', TZDIR);
} else {
debug('WARN: Could not find timezone directory. listTimezones() won\'t work');
}
/**
* Older versions of node-time would require the user to have the TZ
* environment variable set, otherwise undesirable results would happen. Now
* node-time tries to automatically determine the current timezone for you.
*/
if (!exports.currentTimezone) {
debug('`process.env.TZ` not initially set, attempting to resolve');
try {
var currentTimezonePath = fs.readlinkSync('/etc/localtime');
if (currentTimezonePath.substring(0, TZDIR.length) === TZDIR) {
// Got It!
var zone = currentTimezonePath.substring(TZDIR.length + 1);
exports.currentTimezone = process.env.TZ = zone;
debug('resolved initial timezone:', zone);
}
} catch (e) {
debug(e);
}
}
if (!exports.currentTimezone) {
debug('"currentTimezone" still not set. Checking "/etc/timezone"');
try {
var zone = fs.readFileSync('/etc/timezone', 'utf8').trim();
exports.currentTimezone = process.env.TZ = zone;
debug('resolved initial timezone:', zone);
} catch (e) {
debug(e);
}
}
/**
* The user-facing 'tzset' function is a thin wrapper around the native binding to
* 'tzset()'. This function accepts a timezone String to set the process' timezone
* to. Returns an object with the zoneinfo for the timezone.
*
* Throws (on *some* platforms) when the desired timezone could not be loaded.
*
* Sets the `currentTimezone` property on the exports.
*/
function tzset (tz) {
if (tz) {
process.env.TZ = tz;
}
var usedTz = process.env.TZ;
var rtn = bindings.tzset();
debug('set the current timezone to:', usedTz);
if (!rtn.tzname[1] && rtn.timezone === 0) {
debug('got bad zoneinfo object:', rtn);
var err = new Error("Unknown Timezone: '" + usedTz + "'");
for (var i in rtn) {
err[i] = rtn[i];
}
throw err;
}
exports.currentTimezone = usedTz;
exports._currentZoneinfo = rtn;
return rtn;
}
exports.tzset = tzset;
/**
* Lists the timezones that the current system can accept. It does this by going
* on a recursive walk through the timezone dir and collecting filenames.
*/
function listTimezones () {
if (arguments.length == 0) {
throw new Error("You must set a callback");
}
if (typeof arguments[arguments.length - 1] != "function") {
throw new Error("You must set a callback");
}
var cb = arguments[arguments.length - 1]
, subset = (arguments.length > 1 ? arguments[0] : null)
return listTimezonesFolder(subset ? subset + "/" : "", subset ? path.join(TZDIR, "/" + subset) : TZDIR, function (err, tzs) {
if (err) return cb(err);
cb(null, tzs.sort());
});
}
exports.listTimezones = listTimezones;
function listTimezonesFolder(prefix, folder, cb) {
var timezones = [];
fs.readdir(folder, function (err, files) {
if (err) return cb(err);
var pending_stats = files.length;
for (var i = 0; i < files.length; i++) {
if (~TZ_BLACKLIST.indexOf(files[i])
|| files[i].indexOf(".") >= 0
|| files[i][0].toUpperCase() != files[i][0]) {
pending_stats--;
continue
}
fs.stat(path.join(folder, files[i]), (function (file) {
return function (err, stats) {
if (!err) {
if (stats.isDirectory()) {
listTimezonesFolder(prefix + file + "/", path.join(folder, file), function (err, tzs) {
if (!err) {
timezones = timezones.concat(tzs);
}
pending_stats--;
if (pending_stats == 0) cb(null, timezones);
});
return;
}
if (prefix.length > 0) timezones.push(prefix + file);
}
pending_stats--;
if (pending_stats == 0) cb(null, timezones);
};
})(files[i]));
}
});
}
/**
* The "setTimezone" function is the "entry point" for a Date instance.
* It must be called after an instance has been created. After, the 'getSeconds()',
* 'getHours()', 'getDays()', etc. functions will return values relative
* to the time zone specified.
*/
function setTimezone (timezone, relative) {
debug('Date#setTimezone(%s, %s)', timezone, relative);
// If `true` is passed in as the second argument, then the Date instance
// will have it's timezone set, but it's current local values will remain
// the same (i.e. the Date's internal time value will be changed)
var ms, s, m, h, d, mo, y
if (relative) {
y = this.getFullYear()
mo = this.getMonth()
d = this.getDate()
h = this.getHours()
m = this.getMinutes()
s = this.getSeconds()
ms = this.getMilliseconds()
}
// If the current process timezone doesn't match the desired timezone, then call
// tzset() to change the current timezone of the process.
var oldTz = exports.currentTimezone
, tz = exports._currentZoneinfo;
if (!tz || oldTz !== timezone) {
debug('current timezone is not "%s", calling tzset()', timezone);
tz = exports.tzset(timezone);
}
// Get the zoneinfo for this Date instance's time value
var zoneInfo = exports.localtime(this.getTime() / 1000);
// Change the timezone back if we changed it originally
if (oldTz != timezone) {
debug('setting timezone back to "%s"', oldTz);
exports.tzset(oldTz);
}
oldTz = null;
// If we got to here without throwing an Error, then
// a valid timezone was requested, and we should have
// a valid zoneInfo Object.
this.getTimezone = function getTimezone() {
return timezone;
}
// Returns the day of the month (1-31) for the specified date according to local time.
this.getDate = function getDate() {
return zoneInfo.dayOfMonth;
}
// Returns the day of the week (0-6) for the specified date according to local time.
this.getDay = function getDay() {
return zoneInfo.dayOfWeek;
}
// Deprecated. Returns the year (usually 2-3 digits) in the specified date according
// to local time. Use `getFullYear()` instead.
this.getYear = function getYear() {
return zoneInfo.year;
}
// Returns the year (4 digits for 4-digit years) of the specified date according to local time.
this.getFullYear = function getFullYear() {
return zoneInfo.year + 1900;
}
// Returns the hour (0-23) in the specified date according to local time.
this.getHours = function getHours() {
return zoneInfo.hours;
}
// Returns the minutes (0-59) in the specified date according to local time.
this.getMinutes = function getMinutes() {
return zoneInfo.minutes;
}
// Returns the month (0-11) in the specified date according to local time.
this.getMonth = function getMonth() {
return zoneInfo.month;
}
// Returns the seconds (0-59) in the specified date according to local time.
this.getSeconds = function getSeconds() {
return zoneInfo.seconds;
}
// Returns the timezone offset from GMT the Date instance currently is in,
// in minutes. Also, left of GMT is positive, right of GMT is negative.
this.getTimezoneOffset = function getTimezoneOffset() {
return -zoneInfo.gmtOffset / 60;
}
// NON-STANDARD: Returns the abbreviation (e.g. EST, EDT) for the specified time zone.
this.getTimezoneAbbr = function getTimezoneAbbr() {
return tz.tzname[zoneInfo.isDaylightSavings ? 1 : 0];
}
// Sets day, month and year at once
this.setAllDateFields = function setAllDateFields(y,mo,d) {
return this.setFullYear(y,mo,d);
}
// Sets the day of the month (from 1-31) in the current timezone
this.setDate = function setDate(d) {
zoneInfo.dayOfMonth = d;
return mktime.call(this);
}
// Sets the year (four digits) in the current timezone
this.setFullYear = function setFullYear(y,mo,d) {
zoneInfo.year = y - 1900;
if(arguments.length > 1)
zoneInfo.month = mo;
if(arguments.length > 2)
zoneInfo.dayOfMonth = d;
return mktime.call(this);
}
// Sets the hour (from 0-23) in the current timezone
this.setHours = function setHours(h,m,s,ms) {
zoneInfo.hours = h;
if(arguments.length > 1)
zoneInfo.minutes = m;
if(arguments.length > 2)
zoneInfo.seconds = s;
if(arguments.length > 3) {
mktime.call(this);
var diff = ms - this.getMilliseconds();
return this.setTime(this.getTime() + diff);
} else
return mktime.call(this);
}
// Sets the milliseconds (from 0-999) in the current timezone
this.setMilliseconds = function setMilliseconds(ms) {
var diff = ms - this.getMilliseconds();
return this.setTime(this.getTime() + diff);
}
// Set the minutes (from 0-59) in the current timezone
this.setMinutes = function setMinutes(m,s,ms) {
zoneInfo.minutes = m;
if(arguments.length > 1)
zoneInfo.seconds = s;
if(arguments.length > 2) {
mktime.call(this);
var diff = ms - this.getMilliseconds();
return this.setTime(this.getTime() + diff);
} else
return mktime.call(this);
}
// Sets the month (from 0-11) in the current timezone
this.setMonth = function setMonth(mo,d) {
zoneInfo.month = mo;
if(arguments.length > 1)
zoneInfo.dayOfMonth = d;
return mktime.call(this);
}
// Sets the seconds (from 0-59) in the current timezone
this.setSeconds = function setSeconds(s,ms) {
zoneInfo.seconds = s;
if(arguments.length > 1) {
mktime.call(this);
var diff = ms - this.getMilliseconds();
return this.setTime(this.getTime() + diff);
} else
return mktime.call(this);
}
// Sets a date and time by adding or subtracting a specified number of
// milliseconds to/from midnight January 1, 1970.
this.setTime = function setTime(v) {
var rtn = _Date.prototype.setTime.call(this, v);
// Since this function changes the internal UTC epoch date value, we need to
// re-setup these timezone translation functions to reflect the new value
reset.call(this);
return rtn;
}
// Sets the day of the month, according to universal time (from 1-31)
this.setUTCDate = function setUTCDate(d) {
var rtn = _Date.prototype.setUTCDate.call(this, d);
reset.call(this);
return rtn;
}
// Sets the year, according to universal time (four digits)
this.setUTCFullYear = function setUTCFullYear(y,mo,d) {
var rtn;
switch(arguments.length) {
case 1:
rtn = _Date.prototype.setUTCFullYear.call(this, y); break;
case 2:
rtn = _Date.prototype.setUTCFullYear.call(this, y,mo); break;
case 3:
rtn = _Date.prototype.setUTCFullYear.call(this, y,mo,d); break;
}
reset.call(this);
return rtn;
}
// Sets the hour, according to universal time (from 0-23)
this.setUTCHours = function setUTCHours(h,m,s,ms) {
var rtn;
switch(arguments.length) {
case 1:
rtn = _Date.prototype.setUTCHours.call(this, h); break;
case 2:
rtn = _Date.prototype.setUTCHours.call(this, h,m); break;
case 3:
rtn = _Date.prototype.setUTCHours.call(this, h,m,s); break;
case 4:
rtn = _Date.prototype.setUTCHours.call(this, h,m,s,ms); break;
}
reset.call(this);
return rtn;
}
// Sets the milliseconds, according to universal time (from 0-999)
this.setUTCMilliseconds = function setUTCMillseconds(ms) {
var rtn = _Date.prototype.setUTCMilliseconds.call(this, ms);
reset.call(this);
return rtn;
}
// Set the minutes, according to universal time (from 0-59)
this.setUTCMinutes = function setUTCMinutes(m,s,ms) {
var rtn;
switch(arguments.length) {
case 1:
rtn = _Date.prototype.setUTCMinutes.call(this, m); break;
case 2:
rtn = _Date.prototype.setUTCMinutes.call(this, m,s); break;
case 3:
rtn = _Date.prototype.setUTCMinutes.call(this, m,s,ms); break;
}
reset.call(this);
return rtn;
}
// Sets the month, according to universal time (from 0-11)
this.setUTCMonth = function setUTCMonth(mo,d) {
var rtn;
switch(arguments.length) {
case 1:
rtn = _Date.prototype.setUTCMonth.call(this, mo); break;
case 2:
rtn = _Date.prototype.setUTCMonth.call(this, mo,d); break;
}
reset.call(this);
return rtn;
}
// Set the seconds, according to universal time (from 0-59)
this.setUTCSeconds = function setUTCSeconds(s,ms) {
var rtn;
switch(arguments.length) {
case 1:
rtn = _Date.prototype.setUTCSeconds.call(this, s); break;
case 2:
rtn = _Date.prototype.setUTCSeconds.call(this, s,ms); break;
}
reset.call(this);
return rtn;
}
this.toDateString = function toDateString() {
return DAYS_OF_WEEK[this.getDay()].substring(0, 3) + ' ' + MONTHS[this.getMonth()].substring(0, 3) + ' ' + pad(this.getDate(), 2) + ' ' + this.getFullYear();
}
this.toTimeString = function toTimeString() {
var offset = Math.abs(zoneInfo.gmtOffset / 60); // total minutes
// split into HHMM:
var hours = pad(Math.floor(offset / 60), 2);
var minutes = pad(offset % 60, 2);
return this.toLocaleTimeString() + ' GMT' + (zoneInfo.gmtOffset >= 0 ? '+' : '-') + hours + minutes
+ ' (' + tz.tzname[zoneInfo.isDaylightSavings ? 1 : 0] + ')';
}
this.toString = function toString() {
return this.toDateString() + ' ' + this.toTimeString();
}
this.toLocaleDateString = function toLocaleDateString() {
return DAYS_OF_WEEK[this.getDay()] + ', ' + MONTHS[this.getMonth()] + ' ' + pad(this.getDate(), 2) + ', ' + this.getFullYear();
}
this.toLocaleTimeString = function toLocaleTimeString() {
return pad(this.getHours(), 2) + ':' + pad(this.getMinutes(), 2) + ':' + pad(this.getSeconds(), 2);
}
this.toLocaleString = this.toString;
if (relative) {
this.setAllDateFields(y,mo,d)
this.setHours(h)
this.setMinutes(m)
this.setSeconds(s)
this.setMilliseconds(ms)
ms = s = m = h = d = mo = y = null
}
// Used internally by the 'set*' functions above...
function reset () {
this.setTimezone(this.getTimezone());
}
// 'mktime' calls 'reset' implicitly through 'setTime()'
function mktime () {
var oldTz = process.env.TZ;
exports.tzset(this.getTimezone());
zoneInfo.isDaylightSavings = -1; // Auto-detect the timezone
var t = exports.mktime(zoneInfo);
if (oldTz) {
exports.tzset(oldTz);
oldTz = null;
}
return this.setTime( (t * MILLIS_PER_SECOND) + this.getMilliseconds() );
}
return this;
}
// Returns a "String" of the last value set in "setTimezone".
// TODO: Return something when 'setTimezone' hasn't been called yet.
function getTimezone () {
throw new Error('You must call "setTimezone(tz)" before "getTimezone()" may be called');
}
// NON-STANDARD: Returns the abbreviated timezone name, also taking daylight
// savings into consideration. Useful for the presentation layer of a Date
// instance.
function getTimezoneAbbr () {
var str = this.toString().match(/\([A-Z]+\)/)[0];
return str.substring(1, str.length-1);
}
// Export the modified 'Date' instance. Users should either use this with the
// 'new' operator, or extend an already existing Date instance with 'extend()'.
// An optional, NON-STANDARD, "timezone" argument may be appended as the final
// argument, in order to specify the initial timezone the Date instance should
// be created with.
function Date (year, month, day, hour, minute, second, millisecond, timezone) {
if (!(this instanceof Date)) {
return new Date(year, month, day, hour, minute, second, millisecond, timezone).toString();
}
var argc = arguments.length
, d;
// So that we don't have to do the switch block below twice!
while (argc > 0 && typeof arguments[argc-1] === 'undefined') {
argc--;
}
// An optional 'timezone' argument may be passed as the final argument
if (argc >= 2 && typeof arguments[argc - 1] === 'string') {
timezone = arguments[argc - 1];
argc--;
}
// Ugly, but the native Date constructor depends on arguments.length in order
// to create a Date instance in the intended fashion.
switch (argc) {
case 0:
d = new _Date(); break;
case 1:
d = new _Date(year); break;
case 2:
d = new _Date(year, month); break;
case 3:
d = new _Date(year, month, day); break;
case 4:
d = new _Date(year, month, day, hour); break;
case 5:
d = new _Date(year, month, day, hour, minute); break;
case 6:
d = new _Date(year, month, day, hour, minute, second); break;
case 7:
d = new _Date(year, month, day, hour, minute, second, millisecond); break;
}
if (timezone) {
// set time given timezone relative to the currently set local time
// (changing the internal "time" milliseconds value unless ms specified)
d.setTimezone(timezone, !(argc == 1 && typeof year === 'number'));
} else {
d.setTimezone(exports.currentTimezone);
}
return d;
}
Date.prototype = _Date.prototype;
exports.Date = Date;
// We also overwrite `Date.parse()`. It can accept an optional 'timezone'
// second argument.
function parse (dateStr, timezone) {
return new Date(dateStr, timezone).getTime();
}
exports.parse = parse;
// 'now()', 'parse()', and 'UTC()' all need to be re-defined on Date as don't enum
Object.defineProperty(Date, 'now', { value: _Date.now, writable: true, enumerable: false });
Object.defineProperty(Date, 'parse', { value: parse, writable: true, enumerable: false });
Object.defineProperty(Date, 'UTC', { value: _Date.UTC, writable: true, enumerable: false });
// Turns a "regular" Date instance into one of our "extended" Date instances.
// The return value is negligible, as the original Date instance is modified.
// DEPRECATED: Just extend the Date's prototype using the Date-extend function.
exports.extend = function extend (date) {
if (!date) return date;
date.getTimezone = getTimezone;
date.setTimezone = setTimezone;
date.getTimezoneAbbr = getTimezoneAbbr;
return date;
}
/**
* Pads a number with 0s if required.
*/
function pad (num, padLen) {
var padding = '0000';
num = String(num);
return padding.substring(0, padLen - num.length) + num;
}