mirror of
https://github.com/samsonjs/SwiftTimeZoneLookup.git
synced 2026-03-25 08:25:49 +00:00
187 lines
7.4 KiB
Swift
187 lines
7.4 KiB
Swift
@_implementationOnly import CZoneDetect
|
|
import Foundation
|
|
|
|
public enum SwiftTimeZoneLookupError: Error {
|
|
case couldNotFindTimezone21bin
|
|
case couldNotOpenDatabase
|
|
}
|
|
|
|
public struct SwiftTimeZoneLookupResult {
|
|
/// Timezone identifier like `Europe/Berlin`
|
|
public let timezone: String
|
|
|
|
/// Country name like `Germany`
|
|
public let countryName: String?
|
|
|
|
/// 2 character country code like `DE` for Germany
|
|
public let countryAlpha2: String?
|
|
}
|
|
|
|
public final class SwiftTimeZoneLookup {
|
|
/// 0.00017 degrees (~20m) resolution
|
|
private let database21: OpaquePointer
|
|
|
|
/// 0.0055 degrees (~0.5km) resolution
|
|
private let database16: OpaquePointer
|
|
|
|
/// Throws if the timezone database could not be opened
|
|
/// If an optional `databasePath` is provided, it tries first to uses this path and then uses the default bundle resources path
|
|
public init(databasePath: String? = nil) throws {
|
|
if let databasePath = databasePath,
|
|
let database21 = ZDOpenDatabase("\(databasePath)timezone21.bin"),
|
|
let database16 = ZDOpenDatabase("\(databasePath)timezone16.bin")
|
|
{
|
|
self.database21 = database21
|
|
self.database16 = database16
|
|
return
|
|
}
|
|
|
|
guard let timezone21 = Bundle.module.path(forResource: "timezone21", ofType: "bin") else {
|
|
throw SwiftTimeZoneLookupError.couldNotFindTimezone21bin
|
|
}
|
|
guard let timezone16 = Bundle.module.path(forResource: "timezone16", ofType: "bin") else {
|
|
throw SwiftTimeZoneLookupError.couldNotFindTimezone21bin
|
|
}
|
|
|
|
guard let database21 = ZDOpenDatabase(timezone21) else {
|
|
throw SwiftTimeZoneLookupError.couldNotOpenDatabase
|
|
}
|
|
guard let database16 = ZDOpenDatabase(timezone16) else {
|
|
throw SwiftTimeZoneLookupError.couldNotOpenDatabase
|
|
}
|
|
|
|
self.database21 = database21
|
|
self.database16 = database16
|
|
}
|
|
|
|
/// Try with lower resolution first and use high resolution database if too close to the border
|
|
private func highResLookup(latitude: Float, longitude: Float) -> UnsafeMutablePointer<ZoneDetectResult>? {
|
|
var safezone: Float = .nan
|
|
guard let result = ZDLookup(database16, latitude, longitude, &safezone) else {
|
|
return nil
|
|
}
|
|
if safezone >= 0.0055*2 {
|
|
return result
|
|
}
|
|
ZDFreeResults(result)
|
|
|
|
guard let result21 = ZDLookup(database21, latitude, longitude, &safezone) else {
|
|
return nil
|
|
}
|
|
return result21
|
|
}
|
|
|
|
/// Resolve timezone by coordinate and return timezone, country name and alpha2
|
|
public func lookup(latitude: Float, longitude: Float) -> SwiftTimeZoneLookupResult? {
|
|
guard let result = highResLookup(latitude: latitude, longitude: longitude) else {
|
|
return nil
|
|
}
|
|
defer { ZDFreeResults(result) }
|
|
var countryName: String? = nil
|
|
var countryAlpha2: String? = nil
|
|
var timezoneIdPrefix: UnsafeMutablePointer<CChar>? = nil
|
|
var timezoneId: UnsafeMutablePointer<CChar>? = nil
|
|
for i in 0..<result.pointee.numFields {
|
|
guard let field = result.pointee.fieldNames.advanced(by: Int(i)).pointee else {
|
|
continue
|
|
}
|
|
guard let value = result.pointee.data.advanced(by: Int(i)).pointee else {
|
|
continue
|
|
}
|
|
if strcmp(field, "CountryAlpha2") == 0 {
|
|
countryAlpha2 = String(cString: value)
|
|
}
|
|
if strcmp(field, "CountryName") == 0 {
|
|
countryName = String(cString: value)
|
|
}
|
|
if strcmp(field, "TimezoneIdPrefix") == 0 {
|
|
timezoneIdPrefix = value
|
|
}
|
|
if strcmp(field, "TimezoneId") == 0 {
|
|
timezoneId = value
|
|
}
|
|
}
|
|
guard let timezoneIdPrefix = timezoneIdPrefix, let timezoneId = timezoneId else {
|
|
return nil
|
|
}
|
|
let timezone = String(cString: timezoneIdPrefix) + String(cString: timezoneId)
|
|
return SwiftTimeZoneLookupResult(timezone: timezone, countryName: countryName, countryAlpha2: countryAlpha2)
|
|
}
|
|
|
|
/// Resolve the timezone
|
|
public func simple(latitude: Float, longitude: Float) -> String? {
|
|
if (36.2443...36.7389).contains(latitude) && (26.0019...26.7957).contains(longitude) {
|
|
// Astypalaia island in Greece does not resolve any timezone and would return nil
|
|
// Reasons unknown, could be an invalid polygon
|
|
return "Europe/Athens"
|
|
}
|
|
if (11.865393...12.474443).contains(latitude) && (-69.312710...(-68.613387)).contains(longitude) {
|
|
// Curacao island
|
|
return "America/Curacao"
|
|
}
|
|
|
|
if let timezone = simpleUncorrected(latitude: latitude, longitude: longitude) {
|
|
return timezone
|
|
}
|
|
for delta in stride(from: Float(0.1), through: 2.35, by: 0.25) {
|
|
if let timezone = simpleUncorrected(latitude: latitude + delta, longitude: longitude) {
|
|
return timezone
|
|
}
|
|
if let timezone = simpleUncorrected(latitude: latitude - delta, longitude: longitude) {
|
|
return timezone
|
|
}
|
|
if let timezone = simpleUncorrected(latitude: latitude, longitude: longitude + delta) {
|
|
return timezone
|
|
}
|
|
if let timezone = simpleUncorrected(latitude: latitude, longitude: longitude - delta) {
|
|
return timezone
|
|
}
|
|
if let timezone = simpleUncorrected(latitude: latitude + delta, longitude: longitude + delta) {
|
|
return timezone
|
|
}
|
|
if let timezone = simpleUncorrected(latitude: latitude - delta, longitude: longitude + delta) {
|
|
return timezone
|
|
}
|
|
if let timezone = simpleUncorrected(latitude: latitude + delta, longitude: longitude - delta) {
|
|
return timezone
|
|
}
|
|
if let timezone = simpleUncorrected(latitude: latitude - delta, longitude: longitude - delta) {
|
|
return timezone
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Resolve the timezone without any corrections
|
|
private func simpleUncorrected(latitude: Float, longitude: Float) -> String? {
|
|
guard let result = highResLookup(latitude: latitude, longitude: longitude) else {
|
|
return nil
|
|
}
|
|
defer { ZDFreeResults(result) }
|
|
var timezoneIdPrefix: UnsafeMutablePointer<CChar>? = nil
|
|
var timezoneId: UnsafeMutablePointer<CChar>? = nil
|
|
for i in 0..<result.pointee.numFields {
|
|
guard let field = result.pointee.fieldNames.advanced(by: Int(i)).pointee else {
|
|
continue
|
|
}
|
|
guard let value = result.pointee.data.advanced(by: Int(i)).pointee else {
|
|
continue
|
|
}
|
|
if strcmp(field, "TimezoneIdPrefix") == 0 {
|
|
timezoneIdPrefix = value
|
|
}
|
|
if strcmp(field, "TimezoneId") == 0 {
|
|
timezoneId = value
|
|
}
|
|
}
|
|
guard let timezoneIdPrefix = timezoneIdPrefix, let timezoneId = timezoneId else {
|
|
return nil
|
|
}
|
|
let timezone = String(cString: timezoneIdPrefix) + String(cString: timezoneId)
|
|
return timezone
|
|
}
|
|
|
|
deinit {
|
|
ZDCloseDatabase(database21)
|
|
}
|
|
}
|