// Copyright © 2015 Abhishek Banthia import Cocoa import CoreLoggerKit struct ModelConstants { static let customLabel = "customLabel" static let timezoneName = "formattedAddress" static let placeIdentifier = "place_id" static let timezoneID = "timezoneID" static let emptyString = "" static let latitude = "latitude" static let longitude = "longitude" static let note = "note" } public enum DateFormat { public static let twelveHour = "h:mm a" public static let twelveHourWithSeconds = "h:mm:ss a" public static let twentyFourHour = "HH:mm" public static let twentyFourHourWithSeconds = "HH:mm:ss" public static let twelveHourWithZero = "hh:mm a" public static let twelveHourWithZeroSeconds = "hh:mm:ss a" public static let twelveHourWithoutSuffix = "hh:mm" public static let twelveHourWithoutSuffixAndSeconds = "hh:mm:ss" public static let epochTime = "epoch" } // Non-class type cannot conform to NSCoding! public class TimezoneData: NSObject, NSCoding { public enum SelectionType: Int { case city case timezone } public enum DateDisplayType: Int { case panel case menu } public enum TimezoneOverride: Int { case globalFormat = 0 case twelveHourFormat = 1 case twentyFourFormat = 2 case twelveHourWithSeconds = 4 case twentyHourWithSeconds = 5 case twelveHourPrecedingZero = 7 case twelveHourPrecedingZeroSeconds = 8 case twelveHourWithoutSuffix = 10 case twelveHourWithoutSuffixAndSeconds = 11 case epochTime = 12 } static let values = [ NSNumber(integerLiteral: 0): DateFormat.twelveHour, NSNumber(integerLiteral: 1): DateFormat.twentyFourHour, // Seconds NSNumber(integerLiteral: 3): DateFormat.twelveHourWithSeconds, NSNumber(integerLiteral: 4): DateFormat.twentyFourHourWithSeconds, // Preceding Zero NSNumber(integerLiteral: 6): DateFormat.twelveHourWithZero, NSNumber(integerLiteral: 7): DateFormat.twelveHourWithZeroSeconds, // Suffix NSNumber(integerLiteral: 9): DateFormat.twelveHourWithoutSuffix, NSNumber(integerLiteral: 10): DateFormat.twelveHourWithoutSuffixAndSeconds, NSNumber(integerLiteral: 11): DateFormat.epochTime, ] public var customLabel: String? public var formattedAddress: String? public var placeID: String? public var timezoneID: String? = ModelConstants.emptyString public var latitude: Double? public var longitude: Double? public var note: String? = ModelConstants.emptyString public var nextUpdate: Date? = Date() public var sunriseTime: Date? public var sunsetTime: Date? public var isFavourite: Int = 0 public var isSunriseOrSunset = false public var selectionType: SelectionType = .city public var isSystemTimezone = false public var overrideFormat: TimezoneOverride = .globalFormat public override init() { selectionType = .timezone isFavourite = 0 note = ModelConstants.emptyString isSystemTimezone = false overrideFormat = .globalFormat placeID = UUID().uuidString } public init(with dictionary: [String: Any]) { customLabel = dictionary[ModelConstants.customLabel] as? String timezoneID = (dictionary[ModelConstants.timezoneID] as? String) ?? "Error" latitude = dictionary[ModelConstants.latitude] as? Double ?? -0.0 longitude = dictionary[ModelConstants.longitude] as? Double ?? -0.0 placeID = (dictionary[ModelConstants.placeIdentifier] as? String) ?? "Error" formattedAddress = (dictionary[ModelConstants.timezoneName] as? String) ?? "Error" isFavourite = 0 selectionType = .city note = (dictionary[ModelConstants.note] as? String) ?? ModelConstants.emptyString isSystemTimezone = false overrideFormat = .globalFormat } public required init?(coder aDecoder: NSCoder) { customLabel = aDecoder.decodeObject(forKey: "customLabel") as? String formattedAddress = aDecoder.decodeObject(forKey: "formattedAddress") as? String placeID = aDecoder.decodeObject(forKey: "place_id") as? String timezoneID = aDecoder.decodeObject(forKey: "timezoneID") as? String latitude = aDecoder.decodeObject(forKey: "latitude") as? Double longitude = aDecoder.decodeObject(forKey: "longitude") as? Double note = aDecoder.decodeObject(forKey: "note") as? String nextUpdate = aDecoder.decodeObject(forKey: "nextUpdate") as? Date sunriseTime = aDecoder.decodeObject(forKey: "sunriseTime") as? Date sunsetTime = aDecoder.decodeObject(forKey: "sunsetTime") as? Date isFavourite = aDecoder.decodeInteger(forKey: "isFavourite") let selection = aDecoder.decodeInteger(forKey: "selectionType") selectionType = SelectionType(rawValue: selection)! isSystemTimezone = aDecoder.decodeBool(forKey: "isSystemTimezone") let override = aDecoder.decodeInteger(forKey: "overrideFormat") overrideFormat = TimezoneOverride(rawValue: override)! } public class func customObject(from encodedData: Data?) -> TimezoneData? { guard let dataObject = encodedData else { return TimezoneData() } if let timezoneObject = NSKeyedUnarchiver.unarchiveObject(with: dataObject) as? TimezoneData { return timezoneObject } return nil } public func encode(with aCoder: NSCoder) { aCoder.encode(placeID, forKey: "place_id") aCoder.encode(formattedAddress, forKey: "formattedAddress") aCoder.encode(customLabel, forKey: "customLabel") aCoder.encode(timezoneID, forKey: "timezoneID") aCoder.encode(nextUpdate, forKey: "nextUpdate") aCoder.encode(latitude, forKey: "latitude") aCoder.encode(longitude, forKey: "longitude") aCoder.encode(isFavourite, forKey: "isFavourite") aCoder.encode(sunriseTime, forKey: "sunriseTime") aCoder.encode(sunsetTime, forKey: "sunsetTime") aCoder.encode(selectionType.rawValue, forKey: "selectionType") aCoder.encode(note, forKey: "note") aCoder.encode(isSystemTimezone, forKey: "isSystemTimezone") aCoder.encode(overrideFormat.rawValue, forKey: "overrideFormat") } public func formattedTimezoneLabel() -> String { // First check if there's an user preferred custom label set if let label = customLabel, !label.isEmpty { return label } // No custom label, return the formatted address/timezone if let address = formattedAddress, !address.isEmpty { return address } // No formatted address, return the timezoneID if let timezone = timezoneID, !timezone.isEmpty { let hashSeperatedString = timezone.components(separatedBy: "/") // Return the second component! if let first = hashSeperatedString.first { return first } // Second component not available, return the whole thing! return timezone } // Return error return "Error" } public func setLabel(_ label: String) { customLabel = !label.isEmpty ? label : ModelConstants.emptyString } public func setShouldOverrideGlobalTimeFormat(_ shouldOverride: Int) { if shouldOverride == 0 { overrideFormat = .globalFormat } else if shouldOverride == 1 { overrideFormat = .twelveHourFormat } else if shouldOverride == 2 { overrideFormat = .twentyFourFormat } else if shouldOverride == 4 { overrideFormat = .twelveHourWithSeconds } else if shouldOverride == 5 { print("Setting override format to five") overrideFormat = .twentyHourWithSeconds } else if shouldOverride == 7 { overrideFormat = .twelveHourPrecedingZero } else if shouldOverride == 8 { overrideFormat = .twelveHourPrecedingZeroSeconds } else if shouldOverride == 10 { overrideFormat = .twelveHourWithoutSuffix } else if shouldOverride == 11 { overrideFormat = .twelveHourWithoutSuffixAndSeconds } else if shouldOverride == 12 { overrideFormat = .epochTime } else { assertionFailure("Chosen a wrong timezone format") } } public func timezone() -> String { if isSystemTimezone { timezoneID = TimeZone.autoupdatingCurrent.identifier formattedAddress = TimeZone.autoupdatingCurrent.identifier return TimeZone.autoupdatingCurrent.identifier } if let timezone = timezoneID { return timezone } if let name = formattedAddress, let placeIdentifier = placeID, let timezoneIdentifier = timezoneID { let errorDictionary = [ "Formatted Address": name, "Place Identifier": placeIdentifier, "TimezoneID": timezoneIdentifier, ] Logger.log(object: errorDictionary, for: "Error fetching timezone() in TimezoneData") } return TimeZone.autoupdatingCurrent.identifier } public func timezoneFormat(_ currentFormat: NSNumber) -> String { let chosenDefault = currentFormat let timeFormat = TimezoneData.values[chosenDefault] ?? DateFormat.twelveHour switch overrideFormat { case .globalFormat: return timeFormat case .twelveHourFormat: return DateFormat.twelveHour case .twentyFourFormat: return DateFormat.twentyFourHour case .twelveHourWithSeconds: return DateFormat.twelveHourWithSeconds case .twentyHourWithSeconds: return DateFormat.twentyFourHourWithSeconds case .twelveHourPrecedingZero: return DateFormat.twelveHourWithZero case .twelveHourPrecedingZeroSeconds: return DateFormat.twelveHourWithZeroSeconds case .twelveHourWithoutSuffix: return DateFormat.twelveHourWithoutSuffix case .twelveHourWithoutSuffixAndSeconds: return DateFormat.twelveHourWithoutSuffixAndSeconds case .epochTime: return DateFormat.epochTime } } public func shouldShowSeconds(_ currentFormat: NSNumber) -> Bool { if overrideFormat == .globalFormat { let formatInString = TimezoneData.values[currentFormat] ?? DateFormat.twelveHour return formatInString.contains("ss") } let formatInString = TimezoneData.values[NSNumber(integerLiteral: overrideFormat.rawValue)] ?? DateFormat.twelveHour return formatInString.contains("ss") } public override var hash: Int { guard let placeIdentifier = placeID, let timezone = timezoneID else { return -1 } return placeIdentifier.hashValue ^ timezone.hashValue } static func == (lhs: TimezoneData, rhs: TimezoneData) -> Bool { return lhs.placeID == rhs.placeID } public override func isEqual(to object: Any?) -> Bool { if let other = object as? TimezoneData { return placeID == other.placeID } return false } public override func isEqual(_ object: Any?) -> Bool { guard let compared = object as? TimezoneData else { return false } // Plain timezones might have similar placeID. Adding another check for timezone identifier. return placeID == compared.placeID && timezoneID == compared.timezoneID } } public extension TimezoneData { override var description: String { return objectDescription() } override var debugDescription: String { return objectDescription() } private func objectDescription() -> String { let customString = """ TimezoneID: \(timezoneID ?? "Error") Formatted Address: \(formattedAddress ?? "Error") Custom Label: \(customLabel ?? "Error") Latitude: \(latitude ?? -0.0) Longitude: \(longitude ?? -0.0) Place Identifier: \(placeID ?? "Error") Is Favourite: \(isFavourite) Sunrise Time: \(sunriseTime?.debugDescription ?? "N/A") Sunset Time: \(sunsetTime?.debugDescription ?? "N/A") Selection Type: \(selectionType.rawValue) Note: \(note ?? "Error") Is System Timezone: \(isSystemTimezone) Override: \(overrideFormat) """ return customString } }