Last active
October 5, 2015 16:02
-
-
Save mattjohnsonpint/e39c77151008e70348d3 to your computer and use it in GitHub Desktop.
WIP for guessing the current time zone using moment.js & moment-timezone
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function guessTimeZone() { | |
// First try ECMA-402 time zone detection, unless true is passed for testing the rest of this function. | |
if (!arguments[0] && typeof Intl === "object" && typeof Intl.DateTimeFormat === "function") { | |
var tz = Intl.DateTimeFormat().resolvedOptions().timeZone; | |
if (tz === "UTC") return "Etc/UTC"; | |
if (tz && tz.indexOf('/') > -1) return tz; | |
} | |
// get the current timestamp - used throughout | |
var ts = moment().valueOf(); | |
// check the unambiguous list | |
var jan = moment(ts).startOf('year'); | |
var jul = moment(jan).month(6); | |
var janOffset = jan.utcOffset(); | |
var julOffset = jul.utcOffset(); | |
for (var i = 0; i < unambiguousZones.length; i++) { | |
var zone = unambiguousZones[i]; | |
if (jan.tz(zone).utcOffset() === janOffset && jul.tz(zone).utcOffset() === julOffset) | |
return zone; | |
}; | |
// get last four transitions | |
var transitions = []; | |
var t = ts; | |
for (var i = 0; i < 4; i++) { | |
t = findPriorTransition(t); | |
if (t === null) break; | |
transitions.unshift(t); | |
t -= 1; | |
}; | |
// get next four transitions | |
t = ts; | |
for (var i = 0; i < 4; i++) { | |
t = findNextTransition(t); | |
if (t === null) break; | |
transitions.push(t); | |
t += 1; | |
}; | |
// If there are no transitions, we can't do anything but assume we're in a fixed offset zone. | |
// That is probably true, but it's also possible we are in a zone that *used* to have DST, | |
// but no longer does and we can't detect the transition due to ES5/15.9.1.8. | |
if (transitions.length === 0) { | |
// first see if the offset is found in our list of fixed-offset zones | |
var offset = moment(ts).utcOffset(); | |
for (var i = 0; i < fixedZones.length; i++) { | |
if (fixedZones[i].o === offset) | |
return fixedZones[i].z; | |
}; | |
// Use the POSIX-compatible zone entries, such as Etc/GMT+7 | |
// note that sign is intentionally inverted to match TZDB rules | |
// (we assume all of these are whole-hour offsets at this point) | |
var o = offset / -60; | |
if (o === 0) return "Etc/GMT"; | |
return "Etc/GMT" + (o >= 0 ? '+' : '') + o; | |
} | |
// At this point, there we know of one or more local transitions. | |
// They may or may not be reliable, due mismatches between host tzdata and moment tzdata, | |
// and also due to the effects of ES5/15.9.1.8, which some environments may or may not have. | |
// Therefore, we will test all of the transitions we've gathered against all known zone definitions, | |
// and score them based on the number of matching transitions. | |
// First filter the input list to the "normal" zones. | |
var re = /^(?:America|Africa|Asia|Australia|Europe|Atlantic|Pacific|Indian|Antarctica)\//; | |
var list = []; | |
var names = moment.tz.names(); | |
for (var i = 0; i < names.length; i++) { | |
if (re.test(names[i])) | |
list.push(names[i]); | |
}; | |
// Now test transitions to find candidates. | |
var candidates = []; | |
for (var i = 0; i < list.length; i++) { | |
var zone = list[i]; | |
var tz = moment.tz.zone(zone); | |
var score = 0; | |
for (var j = 0; j < transitions.length; j++) { | |
var t = transitions[j]; | |
if (tz.untils.indexOf(t) > -1) { | |
var m = moment(t); | |
var o1 = m.utcOffset(); | |
var o2 = m.tz(zone).utcOffset(); | |
if (o1 === o2) score++; | |
} | |
} | |
// reduce the score slightly for each inverse mismatch to resolve a few edge cases | |
if (score > 0) { | |
for (var j = tz.untils.length - 1; j >= 0; j--) { | |
if (isFinite(tz.untils[j]) && transitions.indexOf(tz.untils[j]) === -1) | |
score -= 0.1; | |
}; | |
} | |
// At least one transition has to match to be considered a candidate. | |
if (score > 0) | |
candidates.push({zone: zone, score: score}); | |
}; | |
// If we have no candidates, then we have transitions that aren't found in the loaded data. | |
if (candidates.length === 0) { | |
// Special case: "E. Europe Standard Time" on Windows has no exact mapping. "Europe/Chisinau" is the closest, by one hour. | |
if (janOffset === 120 && julOffset === 180 && transitions.indexOf(1445731200000) > -1) | |
return "Europe/Chisinau"; | |
var s = transitions.map(function(t) { return t + '|' + moment(t).utcOffset()}); | |
throw "Time zone not found in " + moment.tz.dataVersion + ". Please report to https://github.com/moment/moment-timezone/issues with: [" + s + "]"; | |
} | |
// If there's only one candidate, return it. | |
if (candidates.length === 1) | |
return candidates[0].zone; | |
candidates.sort(function(a,b) { return a.score > b.score ? -1 : 1 }); | |
console.log("Candidates: " + candidates.map(function (x) { return x.zone + '(' + x.score + ')';})); | |
// Keep only the candidates with the top scores. | |
for (var i = 1; i < candidates.length; i++) { | |
if (candidates[i].score !== candidates[0].score) { | |
candidates = candidates.slice(0, i); | |
break; | |
} | |
} | |
// If there's now only one candidate, return it. | |
if (candidates.length === 1) | |
return candidates[0].zone; | |
// When there is more than one candidate with the same score, use the first candidate found in the preferred list. | |
var candidateZones = candidates.map(function(c) { return c.zone; }); | |
for (var i = 0; i < preferredZones.length; i++) { | |
if (candidateZones.indexOf(preferredZones[i]) > -1) | |
return preferredZones[i]; | |
}; | |
// At this point, there is more than one candidate, but we haven't set a preference, so just return the first one. | |
return candidates[0].zone; | |
} | |
function findPriorTransition(t) { | |
if (!t) t == moment().valueOf(); | |
return findTransition(t, true); | |
} | |
function findNextTransition(t) { | |
if (!t) t == moment().valueOf(); | |
return findTransition(t, false); | |
} | |
function findTransition(from, last) { | |
var t, ts = (from / 6e4 | 0) * 6e4; // trunate to whole minutes | |
var stop = ts + (last ? -1 : 1) * 864e5 * 1461; // search up to aprox 4 years for a transition | |
var steps = [30 * 864e5, 864e5, 36e5, 6e4]; | |
var dt = new Date(ts); | |
var offset = dt.getTimezoneOffset(); | |
for (var i = 0; i < steps.length; i++) { | |
while ((last ? ts > stop : ts < stop) && dt.getTimezoneOffset() === offset) { | |
t = ts; | |
ts = ts + (last ? -1 : 1) * steps[i]; | |
dt.setTime(ts); | |
} | |
if (i === 0 && dt.getTimezoneOffset() === offset) | |
return null; // no transitions found | |
dt.setTime(t); | |
ts = t; | |
} | |
return last ? ts : ts + 6e4; | |
} | |
var fixedZones = [ | |
// No DST in these zones, and the offset is mostly used in a single region. | |
// All zones with fractional-hour offsets and no DST must be in this list. | |
// Note that we keep the offsets here to avoid picking the wrong zone when the host environment is not fully updated. | |
// Example, Asia/Pyongyang might otherwise be picked for Tokyo and other +09:00 zones. | |
{z: "Pacific/Marquesas", o: -570}, // -09:30 | |
{z: "Pacific/Gambier", o: -540}, // -09:00 | |
{z: "America/Caracas", o: -270}, // -04:30 | |
{z: "Atlantic/Cape_Verde", o: -60}, // -01:00 | |
{z: "Asia/Kabul", o: 270}, // +04:30 | |
{z: "Asia/Kolkata", o: 330}, // +05:30 (also "Asia/Colombo") | |
{z: "Asia/Katmandu", o: 345}, // +05:45 | |
{z: "Asia/Rangoon", o: 390}, // +06:30 (also "Indian/Cocos") | |
{z: "Asia/Pyongyang", o: 510}, // +08:30 | |
{z: "Australia/Eucla", o: 525}, // +08:45 | |
{z: "Australia/Darwin", o: 570}, // +09:30 | |
{z: "Pacific/Kiritimati", o: 840} // +14:00 | |
]; | |
var unambiguousZones = [ | |
// These zones have DST, and there are no ambiguities. | |
// Jan Jul | |
"America/Adak", // -10:00 / -09:00 | |
"America/Anchorage", // -09:00 / -08:00 | |
"America/Halifax", // -04:00 / -03:00 | |
"America/St_Johns", // -03:30 / -02:30 | |
"Antarctica/Troll", // +00:00 / +02:00 | |
"Atlantic/Azores", // -01:00 / +00:00 | |
"Europe/Paris", // +01:00 / +02:00 (EU CET/CEST) | |
"Africa/Windhoek", // +02:00 / +01:00 | |
"Asia/Tehran", // +03:30 / +04:30 | |
"Asia/Baku", // +04:00 / +05:00 | |
"Asia/Hovd", // +07:00 / +08:00 | |
"Asia/Ulaanbaatar", // +08:00 / +09:00 | |
"Australia/Adelaide", // +10:30 / +09:30 | |
"Australia/Sydney", // +11:00 / +10:00 | |
"Australia/Lord_Howe", // +11:00 / +10:30 | |
"Pacific/Apia" // +14:00 / +13:00 | |
]; | |
var preferredZones = [ | |
// These zones have DST, but transitions differ. | |
// Jan Jul | |
"America/Los_Angeles", // -08:00 / -07:00 US | |
"America/Santa_Isabel", // -08:00 / -07:00 MX | |
"America/Denver", // -07:00 / -06:00 US | |
"America/Chihuahua", // -07:00 / -06:00 MX | |
"America/Chicago", // -06:00 / -05:00 US | |
"America/Mexico_City", // -06:00 / -05:00 MX | |
"America/New_York", // -05:00 / -04:00 US | |
"America/Havana", // -05:00 / -04:00 CU | |
"America/Cuiaba", // -03:00 / -04:00 BR | |
"America/Asuncion", // -03:00 / -04:00 PY | |
"America/Godthab", // -03:00 / -02:00 GL | |
"America/Miquelon", // -03:00 / -02:00 PM | |
"America/Sao_Paulo", // -02:00 / -03:00 BR | |
"America/Montevideo", // -02:00 / -03:00 UY | |
"Europe/London", // +00:00 / +01:00 GB | |
"Africa/Casablanca", // +00:00 / +01:00 MA | |
"Europe/Bucharest", // +02:00 / +03:00 RO (EU EET/EEST) | |
"Europe/Istanbul", // +02:00 / +03:00 TR 77.7M // Note: In some environments, we might not be able to distinguish and will pick Bucharest instead. | |
"Asia/Damascus", // +02:00 / +03:00 SY 17.9M | |
"Asia/Jerusalem", // +02:00 / +03:00 IL 8.2M | |
"Asia/Amman", // +02:00 / +03:00 JO 8.0M | |
"Asia/Hebron", // +02:00 / +03:00 PS 4.5M | |
"Asia/Beirut", // +02:00 / +03:00 LB 5.8M | |
"Europe/Chisinau", // +02:00 / +03:00 MD 2.9M | |
"Pacific/Auckland", // +13:00 / +12:00 NZ | |
"Pacific/Fiji", // +13:00 / +12:00 FJ | |
// Additional preferences found in testing go here. | |
"Asia/Vladivostok" // Otherwise picks Asia/Sakhalin | |
]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
http://jsfiddle.net/rs6hdh5z/2/