UNPKG

sheetgo-timezone-guess

Version:

A take on original React Window Sortable that is a HOC for react-window that allows drag and drop sorting of lists by Sheetgo.

545 lines (446 loc) 12.4 kB
const data = require('./data/packed/latest.json') const Tz = {} const initTz = function() { var VERSION = "0.5.32", zones = {}, links = {}, countries = {}, names = {}, guesses = {}, cachedGuess /************************************ Unpacking ************************************/ function charCodeToInt(charCode) { if (charCode > 96) { return charCode - 87 } else if (charCode > 64) { return charCode - 29 } return charCode - 48 } function unpackBase60(string) { var i = 0, parts = string.split('.'), whole = parts[0], fractional = parts[1] || '', multiplier = 1, num, out = 0, sign = 1 // handle negative numbers if (string.charCodeAt(0) === 45) { i = 1 sign = -1 } // handle digits before the decimal for (i; i < whole.length; i++) { num = charCodeToInt(whole.charCodeAt(i)) out = 60 * out + num } // handle digits after the decimal for (i = 0; i < fractional.length; i++) { multiplier = multiplier / 60 num = charCodeToInt(fractional.charCodeAt(i)) out += num * multiplier } return out * sign } function arrayToInt (array) { for (var i = 0; i < array.length; i++) { array[i] = unpackBase60(array[i]) } } function intToUntil (array, length) { for (var i = 0; i < length; i++) { array[i] = Math.round((array[i - 1] || 0) + (array[i] * 60000)) // minutes to milliseconds } array[length - 1] = Infinity } function mapIndices (source, indices) { var out = [], i for (i = 0; i < indices.length; i++) { out[i] = source[indices[i]] } return out } function unpack (string) { var data = string.split('|'), offsets = data[2].split(' '), indices = data[3].split(''), untils = data[4].split(' ') arrayToInt(offsets) arrayToInt(indices) arrayToInt(untils) intToUntil(untils, indices.length) return { name : data[0], abbrs : mapIndices(data[1].split(' '), indices), offsets : mapIndices(offsets, indices), untils : untils, population : data[5] | 0 } } /************************************ Zone object ************************************/ function Zone (packedString) { if (packedString) { this._set(unpack(packedString)) } } Zone.prototype = { _set : function (unpacked) { this.name = unpacked.name this.abbrs = unpacked.abbrs this.untils = unpacked.untils this.offsets = unpacked.offsets this.population = unpacked.population }, _index : function (timestamp) { var target = +timestamp, untils = this.untils, i for (i = 0; i < untils.length; i++) { if (target < untils[i]) { return i } } }, countries : function () { var zone_name = this.name return Object.keys(countries).filter(function (country_code) { return countries[country_code].zones.indexOf(zone_name) !== -1 }) }, parse : function (timestamp) { var target = +timestamp, offsets = this.offsets, untils = this.untils, max = untils.length - 1, offset, offsetNext, offsetPrev, i for (i = 0; i < max; i++) { offset = offsets[i] offsetNext = offsets[i + 1] offsetPrev = offsets[i ? i - 1 : i] if (offset < offsetNext && tz.moveAmbiguousForward) { offset = offsetNext } else if (offset > offsetPrev && tz.moveInvalidForward) { offset = offsetPrev } if (target < untils[i] - (offset * 60000)) { return offsets[i] } } return offsets[max] }, abbr : function (mom) { return this.abbrs[this._index(mom)] }, offset : function (mom) { logError("zone.offset has been deprecated in favor of zone.utcOffset") return this.offsets[this._index(mom)] }, utcOffset : function (mom) { return this.offsets[this._index(mom)] } } /************************************ Country object ************************************/ function Country (country_name, zone_names) { this.name = country_name this.zones = zone_names } /************************************ Current Timezone ************************************/ function OffsetAt(at) { var timeString = at.toTimeString() var abbr = timeString.match(/\([a-z ]+\)/i) if (abbr && abbr[0]) { // 17:56:31 GMT-0600 (CST) // 17:56:31 GMT-0600 (Central Standard Time) abbr = abbr[0].match(/[A-Z]/g) abbr = abbr ? abbr.join('') : undefined } else { // 17:56:31 CST // 17:56:31 GMT+0800 (台北標準時間) abbr = timeString.match(/[A-Z]{3,5}/g) abbr = abbr ? abbr[0] : undefined } if (abbr === 'GMT') { abbr = undefined } this.at = +at this.abbr = abbr this.offset = at.getTimezoneOffset() } function ZoneScore(zone) { this.zone = zone this.offsetScore = 0 this.abbrScore = 0 } ZoneScore.prototype.scoreOffsetAt = function (offsetAt) { this.offsetScore += Math.abs(this.zone.utcOffset(offsetAt.at) - offsetAt.offset) if (this.zone.abbr(offsetAt.at).replace(/[^A-Z]/g, '') !== offsetAt.abbr) { this.abbrScore++ } } function findChange(low, high) { var mid, diff while ((diff = ((high.at - low.at) / 12e4 | 0) * 6e4)) { mid = new OffsetAt(new Date(low.at + diff)) if (mid.offset === low.offset) { low = mid } else { high = mid } } return low } function userOffsets() { var startYear = new Date().getFullYear() - 2, last = new OffsetAt(new Date(startYear, 0, 1)), offsets = [last], change, next, i for (i = 1; i < 48; i++) { next = new OffsetAt(new Date(startYear, i, 1)) if (next.offset !== last.offset) { change = findChange(last, next) offsets.push(change) offsets.push(new OffsetAt(new Date(change.at + 6e4))) } last = next } for (i = 0; i < 4; i++) { offsets.push(new OffsetAt(new Date(startYear + i, 0, 1))) offsets.push(new OffsetAt(new Date(startYear + i, 6, 1))) } return offsets } function sortZoneScores (a, b) { if (a.offsetScore !== b.offsetScore) { return a.offsetScore - b.offsetScore } if (a.abbrScore !== b.abbrScore) { return a.abbrScore - b.abbrScore } if (a.zone.population !== b.zone.population) { return b.zone.population - a.zone.population } return b.zone.name.localeCompare(a.zone.name) } function addToGuesses (name, offsets) { var i, offset arrayToInt(offsets) for (i = 0; i < offsets.length; i++) { offset = offsets[i] guesses[offset] = guesses[offset] || {} guesses[offset][name] = true } } function guessesForUserOffsets (offsets) { var offsetsLength = offsets.length, filteredGuesses = {}, out = [], i, j, guessesOffset for (i = 0; i < offsetsLength; i++) { guessesOffset = guesses[offsets[i].offset] || {} for (j in guessesOffset) { if (guessesOffset.hasOwnProperty(j)) { filteredGuesses[j] = true } } } for (i in filteredGuesses) { if (filteredGuesses.hasOwnProperty(i)) { out.push(names[i]) } } return out } function rebuildGuess () { // use Intl API when available and returning valid time zone try { var intlName = Intl.DateTimeFormat().resolvedOptions().timeZone if (intlName && intlName.length > 3) { console.warn(names) var name = names[normalizeName(intlName)] console.warn(name) if (name) { return name } logError("Found " + intlName + " from the Intl API, but did not have that data loaded.") } else { console.log('Intl unavailable, fall back to manual guessing') } } catch (e) { // Intl unavailable, fall back to manual guessing. } var offsets = userOffsets(), offsetsLength = offsets.length, guesses = guessesForUserOffsets(offsets), zoneScores = [], zoneScore, i, j for (i = 0; i < guesses.length; i++) { zoneScore = new ZoneScore(getZone(guesses[i]), offsetsLength) for (j = 0; j < offsetsLength; j++) { zoneScore.scoreOffsetAt(offsets[j]) } zoneScores.push(zoneScore) } zoneScores.sort(sortZoneScores) return zoneScores.length > 0 ? zoneScores[0].zone.name : undefined } function guess (ignoreCache) { if (!cachedGuess || ignoreCache) { cachedGuess = rebuildGuess() } return cachedGuess } /************************************ Global Methods ************************************/ function normalizeName (name) { return (name || '').toLowerCase().replace(/\//g, '_') } function addZone (packed) { var i, name, split, normalized if (typeof packed === "string") { packed = [packed] } for (i = 0; i < packed.length; i++) { split = packed[i].split('|') name = split[0] normalized = normalizeName(name) zones[normalized] = packed[i] names[normalized] = name addToGuesses(normalized, split[2].split(' ')) } } function getZone (name, caller) { name = normalizeName(name) var zone = zones[name] var link if (zone instanceof Zone) { return zone } if (typeof zone === 'string') { zone = new Zone(zone) zones[name] = zone return zone } // Pass getZone to prevent recursion more than 1 level deep if (links[name] && caller !== getZone && (link = getZone(links[name], getZone))) { zone = zones[name] = new Zone() zone._set(link) zone.name = names[name] return zone } return null } function getNames () { var i, out = [] for (i in names) { if (names.hasOwnProperty(i) && (zones[i] || zones[links[i]]) && names[i]) { out.push(names[i]) } } return out.sort() } function getCountryNames () { return Object.keys(countries) } function addLink (aliases) { var i, alias, normal0, normal1 if (typeof aliases === "string") { aliases = [aliases] } for (i = 0; i < aliases.length; i++) { alias = aliases[i].split('|') normal0 = normalizeName(alias[0]) normal1 = normalizeName(alias[1]) links[normal0] = normal1 names[normal0] = alias[0] links[normal1] = normal0 names[normal1] = alias[1] } } function addCountries (data) { var i, country_code, country_zones, split if (!data || !data.length) return for (i = 0; i < data.length; i++) { split = data[i].split('|') country_code = split[0].toUpperCase() country_zones = split[1].split(' ') countries[country_code] = new Country( country_code, country_zones ) } } function getCountry (name) { name = name.toUpperCase() return countries[name] || null } function zonesForCountry(country, with_offset) { country = getCountry(country) if (!country) return null var zones = country.zones.sort() if (with_offset) { return zones.map(function (zone_name) { var zone = getZone(zone_name) return { name: zone_name, offset: zone.utcOffset(new Date()) } }) } return zones } function loadData (data) { addZone(data.zones) addLink(data.links) addCountries(data.countries) Tz.dataVersion = data.version } function zoneExists (name) { if (!zoneExists.didShowError) { zoneExists.didShowError = true } return !!getZone(name) } function needsOffset (m) { var isUnixTimestamp = (m._f === 'X' || m._f === 'x') return !!(m._a && (m._tzm === undefined) && !isUnixTimestamp) } function logError (message) { if (typeof console !== 'undefined' && typeof console.error === 'function') { console.error(message) } } Tz.version = VERSION Tz.dataVersion = '' Tz._zones = zones Tz._links = links Tz._names = names Tz._countries = countries Tz.add = addZone Tz.link = addLink Tz.load = loadData Tz.zone = getZone Tz.guess = guess Tz.names = getNames Tz.Zone = Zone Tz.unpack = unpack Tz.unpackBase60 = unpackBase60 Tz.needsOffset = needsOffset Tz.moveInvalidForward = true Tz.moveAmbiguousForward = false Tz.countries = getCountryNames Tz.zonesForCountry = zonesForCountry Tz.load(data) } initTz() export { Tz }