Skip to content

Instantly share code, notes, and snippets.

@mattjohnsonpint
Last active October 5, 2015 16:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattjohnsonpint/e39c77151008e70348d3 to your computer and use it in GitHub Desktop.
Save mattjohnsonpint/e39c77151008e70348d3 to your computer and use it in GitHub Desktop.
WIP for guessing the current time zone using moment.js & moment-timezone
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
];
@mattjohnsonpint
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment