moment-timezone
Version:
Timezone plugin for Moment.js.
562 lines (461 loc) • 12.4 kB
JavaScript
var moment = require('../moment-timezone'),
TIME_RULE_WALL_CLOCK = 0,
TIME_RULE_UTC = 1,
TIME_RULE_STANDARD = 2,
DAY_RULE_WEEKDAY_AFTER = 0, // 0 - 6
DAY_RULE_DAY_OF_MONTH = 7,
DAY_RULE_LAST_WEEKDAY = 8,
DAYS_OF_WEEK = 'sun mon tue wed thu fri sat'.split(' '),
ZONE_FILES = "africa antarctica asia australasia europe northamerica southamerica".split(' '),
rIsRuleset = /\d+:\d+/;
/******************************
Helpers
******************************/
function replacer(key, value) {
if (value.constructor !== Object) {
return value;
}
return Object.keys(value).sort().reduce(function(sorted, key) {
sorted[key] = value[key];
return sorted;
}, {});
}
function parseLatLong (input, isLong) {
var sign = input[0] === '+' ? 1 : -1,
deg = ~~input.substr(1, 2 + isLong) * sign,
min = ~~input.substr(3 + isLong, 2),
sec = ~~input.substr(5 + isLong, 2);
min += sec / 60;
deg += min / 60;
return parseFloat(deg.toFixed(4), 10);
}
// converts time in the HH:mm format to absolute number of minutes
function parseMinutes (input) {
var output = input.split(':'),
sign = ~input.indexOf('-') ? -1 : 1,
hour = Math.abs(+output[0]),
minute = parseInt(output[1], 10) || 0;
return sign * ((hour * 60) + minute);
}
function parseSeconds (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 * 60) + (minute * 60) + second);
}
function trimMinutes (input) {
var output = input.split(':'),
hour = output[0],
minute = parseInt(output[1], 10) || 0,
second = parseInt(output[2], 10) || 0;
if (second) {
return [hour, minute, second].join(':');
}
if (minute) {
return [hour, minute].join(':');
}
return hour + '';
}
function formatSeconds(input) {
var abs = Math.abs(input),
seconds = abs % 60,
minutes = Math.floor(abs / 60) % 60,
hours = Math.floor(abs / 3600) % 60;
if (input < 0) {
hours = '-' + hours;
}
return trimMinutes([hours, minutes, seconds].join(':'));
}
/******************************
Task
******************************/
module.exports = function (grunt) {
grunt.registerTask('zones', 'Generate the zone data files based on the olson database.', function () {
var file = new File();
ZONE_FILES.forEach(function (filename) {
file.load('tz/' + filename);
});
file.loadZoneTab('tz/zone.tab');
file.save();
});
/******************************
Files
******************************/
function File (filename) {
this.filename = filename;
this.rules = {};
this.zones = {};
this.meta = {};
this.links = {};
}
File.prototype = {
loadZoneTab : function (filename) {
var i,
lines = grunt.file.read(filename).split('\n');
for (i = 0; i < lines.length; i++) {
this.parseZoneTabLine(lines[i]);
}
},
parseZoneTabLine : function (line) {
var sanitized = this.sanitizeLine(line),
name,
latlon;
// ignore comment lines
if (!sanitized) {
return;
}
name = sanitized[2];
latlon = sanitized[1].match(/[+-]\d+/g);
if (!this.meta[name]) {
this.meta[name] = {};
}
this.meta[name].lat = parseLatLong(latlon[0], 0);
this.meta[name].lon = parseLatLong(latlon[1], 1);
},
load : function (filename) {
var i,
lines = grunt.file.read(filename).split('\n');
for (i = 0; i < lines.length; i++) {
this.parseLine(lines[i]);
}
this.updateZonesUntil();
},
updateZonesUntil : function () {
var i, j, zone;
for (i in this.zones) {
zone = this.zones[i];
for (j = 0; j < zone.length; j++) {
zone[j].findUntilRule(this.rules);
}
}
},
// convert a line to an array of data
sanitizeLine : function (line) {
// ignore comments
line = line.split("#")[0];
// if the line is only whitespace, ignore it
if (!line.match(/\S/)) {
return;
}
// assume lines that start with whitespace are zones
// and use the last known zone name
if (line.match(/^\s/)) {
line = "Zone " + this.lastZone + line;
}
return line.split(/\s+/);
},
parseLine : function (line) {
var sanitized = this.sanitizeLine(line),
type;
// ignore comment lines
if (!sanitized) {
return;
}
// the type is the first element
type = sanitized.shift().toLowerCase();
switch (type) {
case 'zone':
return this.parseZone(sanitized);
case 'rule':
return this.parseRule(sanitized);
case 'link':
return this.parseLink(sanitized);
}
},
parseZone : function (line) {
var zone = new Zone(line),
name = line[0];
if (!this.zones[name]) {
this.zones[name] = [];
}
if (!this.meta[name]) {
this.meta[name] = {};
}
if (!this.meta[name].rules) {
this.meta[name].rules = {};
}
this.meta[name].rules[zone.ruleset] = true;
this.zones[name].push(zone);
this.lastZone = zone.name;
},
parseRule : function (line) {
var rule = new Rule(line);
if (!this.rules[line[0]]) {
this.rules[line[0]] = [];
}
this.rules[line[0]].push(rule);
},
parseLink : function (line) {
var link = line[1],
zone = line[0];
this.links[link] = zone;
},
formatObject : function (obj) {
var o = {},
name,
i;
for (name in obj) {
o[name] = [];
for (i = 0; i < obj[name].length; i++) {
o[name].push(obj[name][i].format());
}
}
return o;
},
format : function () {
var o = {
meta : this.meta,
links : this.links,
rules : this.formatObject(this.rules),
zones : this.formatObject(this.zones)
};
return JSON.stringify(o, replacer, '\t').replace(/\\t/g, '\t');
},
trimMeta : function () {
var name, zone,
rule, ruleset,
rules;
for (name in this.meta) {
zone = this.meta[name];
ruleset = zone.rules;
rules = [];
for (rule in ruleset) {
if (rule !== '-') {
rules.push(rule);
}
}
zone.rules = rules.join(' ');
}
},
save : function () {
this.trimMeta();
grunt.file.write('moment-timezone.json', this.format());
}
};
/******************************
Zones
******************************/
function Zone (line) {
this.name = line[0];
this.offset = parseSeconds(line[1]);
this.ruleAsOffset = 0;
this.parseRuleset(line[2]);
this.letters = line[3];
this.zone = moment.tz.addZone(this.name + " " + this.format());
this.parseUntil(line.slice(4));
}
Zone.prototype = {
parseRuleset : function (ruleset) {
if (rIsRuleset.exec(ruleset)) {
this.ruleAsOffset = parseSeconds(ruleset);
this.ruleset = '-';
} else {
this.ruleset = ruleset;
}
},
findUntilRule : function (rules) {
var name;
// no need for this if there is no until data
if (!this.untilYear) {
return;
}
// if (this.name !== "Pacific/Tahiti") return;
for (name in rules) {
if (name === this.ruleset) {
this.findUntilRuleFromSet(rules[name], name);
return;
}
}
// for one-off zones, use an empty array
this.findUntilRuleFromSet([], name);
},
findUntilRuleFromSet : function (set, name) {
var until = this.untilMoment,
last = until.clone().subtract(1, 'd'),
untilRule = this.zone.rule(until),
lastRule = this.zone.rule(last);
if (this.untilTimeRule === TIME_RULE_STANDARD) {
this.untilOffset = this.offset;
} else if (this.untilTimeRule === TIME_RULE_WALL_CLOCK) {
this.untilOffset = this.offset + this.ruleAsOffset + (lastRule.offset * 60);
}
},
parseUntil : function (input) {
var time, minute, second;
this.untilYear = +input[0] || 0;
this.untilMoment = moment.utc([this.untilYear]);
if (~(input[3] || '').indexOf('u')) {
this.untilTimeRule = TIME_RULE_UTC;
} else if (~(input[3] || '').indexOf('s')) {
this.untilTimeRule = TIME_RULE_STANDARD;
} else {
this.untilTimeRule = TIME_RULE_WALL_CLOCK;
}
if (input[1]) {
this.untilMonth = input[1] = moment(input[1], "MMM").month();
this.untilMoment.month(this.untilMonth);
}
if (input[2]) {
this.untilDay = input[2] = this.date(input[2].toLowerCase(), this.untilYear);
this.untilMoment.date(this.untilDay);
}
if (input[3]) {
time = input[3].split(':');
input[3] = time[0];
this.untilMoment.hour(time[0]);
minute = parseInt(time[1], 10);
second = parseInt(time[2], 10);
if (minute) {
input[4] = minute;
this.untilMoment.minute(minute);
}
if (second) {
input[4] = input[4] || 0;
input[5] = second;
this.untilMoment.second(second);
}
this.untilMoment.subtract(this.offset + this.ruleAsOffset, 's');
}
this.until = input.join('_').replace(/_$/, '');
},
parseDay : function (input) {
var lastWeekdayIndex = DAYS_OF_WEEK.indexOf(input.slice(4, 7)),
weekdayAfterIndex = DAYS_OF_WEEK.indexOf(input.slice(0, 3));
if (~lastWeekdayIndex) {
this.dayRule = DAY_RULE_LAST_WEEKDAY;
this.day = lastWeekdayIndex;
} else if (~weekdayAfterIndex) {
this.dayRule = weekdayAfterIndex;
this.day = input.slice(5);
} else {
this.dayRule = DAY_RULE_DAY_OF_MONTH;
this.day = input;
}
},
date : function (input, year) {
this.parseDay(input);
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.untilMonth, 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.untilMonth + 1, 1]).day(),
daysInMonth = moment([year, this.untilMonth, 1]).daysInMonth(),
output = daysInMonth + (dow - (lastDowOfMonth - 1)) - (~~(day / 7) * 7);
if (dow >= lastDowOfMonth) {
output -= 7;
}
return output;
},
format : function () {
var o = [
formatSeconds(this.offset + this.ruleAsOffset),
this.ruleset,
this.letters
];
if (this.until) {
o.push(this.until);
if (this.untilOffset) {
o.push(formatSeconds(this.untilOffset));
}
}
return o.join(' ');
},
formatMeta : function () {
return {
};
}
};
/******************************
Rules
******************************/
function Rule (line) {
this.name = line[0];
this.startYear = line[1];
this.endYear = this.parseEndYear(line[2]);
this.month = this.parseMonth(line[4]);
this.offset = trimMinutes(line[7]);
this.letters = line[8] === '-' ? '' : line[8];
this.parseDay(line[5].toLowerCase());
this.parseTime(line[6]);
this.debugType(line[3]);
moment.tz.addRule(this.name + " " + this.format());
}
Rule.prototype = {
parseMonth : function (input) {
return moment(input, "MMM").month();
},
parseEndYear : function (input) {
if (input === 'only') {
return this.startYear;
} else if (input === 'max') {
return '9999';
}
return input;
},
parseDay : function (input) {
var lastWeekdayIndex = DAYS_OF_WEEK.indexOf(input.slice(4, 7)),
weekdayAfterIndex = DAYS_OF_WEEK.indexOf(input.slice(0, 3));
if (~lastWeekdayIndex) {
this.dayRule = DAY_RULE_LAST_WEEKDAY;
this.day = lastWeekdayIndex;
} else if (~weekdayAfterIndex) {
this.dayRule = weekdayAfterIndex;
this.day = input.slice(5);
} else {
this.dayRule = DAY_RULE_DAY_OF_MONTH;
this.day = input;
}
},
parseTime : function (input) {
if (~input.indexOf('u')) {
this.timeRule = TIME_RULE_UTC;
} else if (~input.indexOf('s')) {
this.timeRule = TIME_RULE_STANDARD;
} else {
this.timeRule = TIME_RULE_WALL_CLOCK;
}
this.time = trimMinutes(input);
},
debugType : function (type) {
if (type !== '-') {
console.log('\n\n');
console.log('new type:', type);
console.log(this);
console.log('\n\n');
}
},
format : function () {
var o = [
this.startYear,
this.endYear,
this.month,
this.day,
this.dayRule,
this.time,
this.timeRule,
this.offset
];
if (this.letters) {
o.push(this.letters);
}
return o.join(' ');
}
};
};