moment-timezone
Version:
Timezone plugin for Moment.js.
501 lines (414 loc) • 11.1 kB
JavaScript
// moment-timezone.js
// version : 0.0.1
// author : Tim Wood
// license : MIT
// github.com/timrwood/moment-timezone
(function () {
var VERSION = "0.0.1";
function onload(moment) {
var oldZoneName = moment.fn.zoneName,
oldZoneAbbr = moment.fn.zoneAbbr,
defaultRule,
rules = {},
ruleSets = {},
zones = {},
zoneSets = {},
links = {},
TIME_RULE_WALL_CLOCK = 0,
TIME_RULE_UTC = 1,
TIME_RULE_STANDARD = 2,
DAY_RULE_DAY_OF_MONTH = 7,
DAY_RULE_LAST_WEEKDAY = 8;
// converts time in the HH:mm:ss format to absolute number of minutes
function parseMinutes (input) {
input = input + '';
var output = input.split(':'),
sign = ~input.indexOf('-') ? -1 : 1,
hour = Math.abs(+output[0]),
minute = parseInt(output[1], 10) || 0,
second = parseInt(output[2], 10) || 0;
return sign * ((hour * 60) + (minute) + (second / 60));
}
/************************************
Rules
************************************/
function Rule (name, startYear, endYear, month, day, dayRule, time, timeRule, offset, letters) {
this.name = name;
this.startYear = +startYear;
this.endYear = +endYear;
this.month = +month;
this.day = +day;
this.dayRule = +dayRule;
this.time = parseMinutes(time);
this.timeRule = +timeRule;
this.offset = parseMinutes(offset);
this.letters = letters || '';
}
Rule.prototype = {
contains : function (year) {
return (year >= this.startYear && year <= this.endYear);
},
start : function (year) {
year = Math.min(Math.max(year, this.startYear), this.endYear);
return moment.utc([year, this.month, this.date(year), 0, this.time]);
},
date : function (year) {
if (this.dayRule === DAY_RULE_DAY_OF_MONTH) {
return this.day;
} else if (this.dayRule === DAY_RULE_LAST_WEEKDAY) {
return this.lastWeekday(year);
}
return this.weekdayAfter(year);
},
weekdayAfter : function (year) {
var day = this.day,
firstDayOfWeek = moment([year, this.month, 1]).day(),
output = this.dayRule + 1 - firstDayOfWeek;
while (output < day) {
output += 7;
}
return output;
},
lastWeekday : function (year) {
var day = this.day,
dow = day % 7,
lastDowOfMonth = moment([year, this.month + 1, 1]).day(),
daysInMonth = moment([year, this.month, 1]).daysInMonth(),
output = daysInMonth + (dow - (lastDowOfMonth - 1)) - (~~(day / 7) * 7);
if (dow >= lastDowOfMonth) {
output -= 7;
}
return output;
}
};
/************************************
Rule Year
************************************/
function RuleYear (year, rule) {
this.rule = rule;
this.start = rule.start(year);
}
RuleYear.prototype = {
equals : function (other) {
if (!other || other.rule !== this.rule) {
return false;
}
return Math.abs(other.start - this.start) < 86400000; // 24 * 60 * 60 * 1000
}
};
function sortRuleYears (a, b) {
if (a.isLast) {
return -1;
}
if (b.isLast) {
return 1;
}
return b.start - a.start;
}
/************************************
Rule Sets
************************************/
function RuleSet (name) {
this.name = name;
this.rules = [];
}
RuleSet.prototype = {
add : function (rule) {
this.rules.push(rule);
},
ruleYears : function (mom, lastZone) {
var i, j,
year = mom.year(),
rule,
lastZoneRule,
rules = [];
for (i = 0; i < this.rules.length; i++) {
rule = this.rules[i];
if (rule.contains(year)) {
rules.push(new RuleYear(year, rule));
} else if (rule.contains(year + 1)) {
rules.push(new RuleYear(year + 1, rule));
}
}
rules.push(new RuleYear(year - 1, this.lastYearRule(year - 1)));
if (lastZone) {
lastZoneRule = new RuleYear(year - 1, lastZone.lastRule());
lastZoneRule.start = lastZone.until.clone().utc();
lastZoneRule.isLast = lastZone.ruleSet !== this;
rules.push(lastZoneRule);
}
rules.sort(sortRuleYears);
return rules;
},
rule : function (mom, offset, lastZone) {
var rules = this.ruleYears(mom, lastZone),
lastOffset = 0,
rule,
lastZoneOffset,
lastZoneOffsetAbs,
lastRule,
i;
if (lastZone) {
lastZoneOffset = lastZone.offset + lastZone.lastRule().offset;
lastZoneOffsetAbs = Math.abs(lastZoneOffset) * 90000;
}
// make sure to include the previous rule's offset
for (i = rules.length - 1; i > -1; i--) {
lastRule = rule;
rule = rules[i];
if (rule.equals(lastRule)) {
continue;
}
if (lastZone && !rule.isLast && Math.abs(rule.start - lastZone.until) <= lastZoneOffsetAbs) {
lastOffset += lastZoneOffset - offset;
}
if (rule.rule.timeRule === TIME_RULE_STANDARD) {
lastOffset = offset;
}
if (rule.rule.timeRule !== TIME_RULE_UTC) {
rule.start.add('m', -lastOffset);
}
lastOffset = rule.rule.offset + offset;
}
for (i = 0; i < rules.length; i++) {
rule = rules[i];
if (mom >= rule.start && !rule.isLast) {
return rule.rule;
}
}
return defaultRule;
},
lastYearRule : function (year) {
var i,
rule,
start,
bestRule = defaultRule,
largest = -1e30;
for (i = 0; i < this.rules.length; i++) {
rule = this.rules[i];
if (year >= rule.startYear) {
start = rule.start(year);
if (start > largest) {
largest = start;
bestRule = rule;
}
}
}
return bestRule;
}
};
/************************************
Zone
************************************/
function Zone (name, offset, ruleSet, letters, until, untilOffset) {
var i,
untilArray = typeof until === 'string' ? until.split('_') : [9999];
this.name = name;
this.offset = parseMinutes(offset);
this.ruleSet = ruleSet;
this.letters = letters;
for (i = 0; i < untilArray.length; i++) {
untilArray[i] = +untilArray[i];
}
this.until = moment.utc(untilArray).subtract('m', parseMinutes(untilOffset));
}
Zone.prototype = {
rule : function (mom, lastZone) {
return this.ruleSet.rule(mom, this.offset, lastZone);
},
lastRule : function () {
if (!this._lastRule) {
this._lastRule = this.rule(this.until);
}
return this._lastRule;
},
format : function (rule) {
return this.letters.replace("%s", rule.letters);
}
};
/************************************
Zone Set
************************************/
function sortZones (a, b) {
return a.until - b.until;
}
function ZoneSet (name) {
this.name = normalizeName(name);
this.displayName = name;
this.zones = [];
}
ZoneSet.prototype = {
zoneAndRule : function (mom) {
var i,
zone,
lastZone;
mom = mom.clone().utc();
for (i = 0; i < this.zones.length; i++) {
zone = this.zones[i];
if (mom < zone.until) {
break;
}
lastZone = zone;
}
return [zone, zone.rule(mom, lastZone)];
},
add : function (zone) {
this.zones.push(zone);
this.zones.sort(sortZones);
},
format : function (mom) {
var zoneAndRule = this.zoneAndRule(mom);
return zoneAndRule[0].format(zoneAndRule[1]);
},
offset : function (mom) {
var zoneAndRule = this.zoneAndRule(mom);
return -(zoneAndRule[0].offset + zoneAndRule[1].offset);
}
};
/************************************
Global Methods
************************************/
function addRules (rules) {
var i, j, rule;
for (i in rules) {
rule = rules[i];
for (j = 0; j < rule.length; j++) {
addRule(i + '\t' + rule[j]);
}
}
}
function addRule (ruleString) {
// don't duplicate rules
if (rules[ruleString]) {
return rules[ruleString];
}
var p = ruleString.split(/\s/),
name = normalizeName(p[0]),
rule = new Rule(name, p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9], p[10]);
// cache the rule so we don't add it again
rules[ruleString] = rule;
// add to the ruleset
getRuleSet(name).add(rule);
return rule;
}
function normalizeName (name) {
return (name || '').toLowerCase().replace(/\//g, '_');
}
function addZones (zones) {
var i, j, zone;
for (i in zones) {
zone = zones[i];
for (j = 0; j < zone.length; j++) {
addZone(i + '\t' + zone[j]);
}
}
}
function addLinks (linksToAdd) {
var i;
for (i in linksToAdd) {
links[normalizeName(i)] = normalizeName(linksToAdd[i]);
}
}
function addZone (zoneString) {
// don't duplicate zones
if (zones[zoneString]) {
return zones[zoneString];
}
var p = zoneString.split(/\s/),
name = normalizeName(p[0]),
zone = new Zone(name, p[1], getRuleSet(p[2]), p[3], p[4], p[5]);
// cache the zone so we don't add it again
zones[zoneString] = zone;
// add to the zoneset
getZoneSet(p[0]).add(zone);
return zone;
}
function getRuleSet (name) {
name = normalizeName(name);
if (!ruleSets[name]) {
ruleSets[name] = new RuleSet(name);
}
return ruleSets[name];
}
function getZoneSet (name) {
var machineName = normalizeName(name);
if (links[machineName]) {
machineName = links[machineName];
}
if (!zoneSets[machineName]) {
zoneSets[machineName] = new ZoneSet(name);
}
return zoneSets[machineName];
}
function add (data) {
if (!data) {
return;
}
if (data.zones) {
addZones(data.zones);
}
if (data.rules) {
addRules(data.rules);
}
if (data.links) {
addLinks(data.links);
}
}
// overwrite moment.updateOffset
moment.updateOffset = function (mom) {
var offset;
if (mom._z) {
offset = mom._z.offset(mom);
if (Math.abs(offset) < 16) {
offset = offset / 60;
}
mom.zone(offset);
}
};
moment.fn.tz = function (name) {
if (name) {
this._z = getZoneSet(name);
if (this._z) {
moment.updateOffset(this);
}
return this;
}
if (this._z) {
return this._z.displayName;
}
};
moment.fn.zoneName = function () {
if (this._z) {
return this._z.format(this);
}
return oldZoneName.call(this);
};
moment.fn.zoneAbbr = function () {
if (this._z) {
return this._z.format(this);
}
return oldZoneAbbr.call(this);
};
moment.tz = function () {
var args = [], i, len = arguments.length - 1;
for (i = 0; i < len; i++) {
args[i] = arguments[i];
}
return moment.apply(null, args).tz(arguments[len]);
};
moment.tz.add = add;
moment.tz.addRule = addRule;
moment.tz.addZone = addZone;
moment.tz.version = VERSION;
// add default rule
defaultRule = addRule("- 0 9999 0 0 0 0 0 0");
return moment;
}
if (typeof define === "function" && define.amd) {
define("moment-timezone", ["moment"], onload);
} else if (typeof window !== "undefined" && window.moment) {
onload(window.moment);
} else if (typeof module !== 'undefined') {
module.exports = onload(require('moment'));
}
}).apply(this);