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
JavaScript
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 }