// Copyright © 2015 Abhishek Banthia import Cocoa import CoreLocation class TimezoneDataOperations: NSObject { private var dataObject: TimezoneData! private lazy var nsCalendar: Calendar = Calendar.autoupdatingCurrent private static var gregorianCalendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian) private static var swiftyCalendar = Calendar(identifier: .gregorian) @objc init(with timezone: TimezoneData) { dataObject = timezone } } extension TimezoneDataOperations { func time(with sliderValue: Int) -> String { guard let newDate = TimezoneDataOperations.gregorianCalendar?.date(byAdding: .minute, value: sliderValue, to: Date(), options: .matchFirst) else { assertionFailure("Data was unexpectedly nil") return CLEmptyString } let dateFormatter = DateFormatterManager.dateFormatterWithFormat(with: .none, format: dataObject.timezoneFormat(), timezoneIdentifier: dataObject.timezone(), locale: Locale.autoupdatingCurrent) return dateFormatter.string(from: newDate) } func compactMenuHeader() -> String { var subtitle = CLEmptyString let shouldDayBeShown = DataStore.shared().shouldShowDateInMenubar() if shouldDayBeShown { let substring = date(with: 0, displayType: CLDateDisplayType.menuDisplay) subtitle.append(substring) } subtitle.isEmpty ? subtitle.append(time(with: 0)) : subtitle.append(" \(time(with: 0))") return subtitle } func menuTitle() -> String { var menuTitle = CLEmptyString let dataStore = DataStore.shared() let shouldCityBeShown = dataStore.shouldDisplay(.placeInMenubar) let shouldDayBeShown = dataStore.shouldShowDateInMenubar() let shouldDateBeShown = dataStore.shouldDisplay(.dateInMenubar) if shouldCityBeShown { if let address = dataObject.formattedAddress, address.isEmpty == false { if let label = dataObject.customLabel { label.isEmpty == false ? menuTitle.append(label) : menuTitle.append(address) } else { menuTitle.append(address) } } else { if let label = dataObject.customLabel { label.isEmpty == false ? menuTitle.append(label) : menuTitle.append(dataObject.timezone()) } else { menuTitle.append(dataObject.timezone()) } } } if shouldDayBeShown { var substring = date(with: 0, displayType: CLDateDisplayType.menuDisplay) let endIndex = substring.index(substring.startIndex, offsetBy: 2) substring = String(substring[substring.startIndex ... endIndex]) if menuTitle.isEmpty == false { menuTitle.append(" \(substring.capitalized)") } else { menuTitle.append(substring.capitalized) } } if shouldDateBeShown { let date = Date().formatter(with: "MMM d", timeZone: dataObject.timezone()) if menuTitle.isEmpty == false { menuTitle.append(" \(date)") } else { menuTitle.append("\(date)") } } menuTitle.isEmpty == false ? menuTitle.append(" \(time(with: 0))") : menuTitle.append(time(with: 0)) return menuTitle } private func timezoneDate(with sliderValue: Int, _ calendar: Calendar) -> Date { let source = timezoneDateByAdding(minutesToAdd: sliderValue, calendar) let sourceTimezone = TimeZone.current let destinationTimezone = TimeZone(identifier: dataObject.timezone()) let sourceGMTOffset: Double = Double(sourceTimezone.secondsFromGMT(for: source)) let destinationGMTOffset: Double = Double(destinationTimezone?.secondsFromGMT(for: source) ?? 0) let interval = destinationGMTOffset - sourceGMTOffset return Date(timeInterval: interval, since: source) } // calendar.dateByAdding takes a 0.1% or 0.2% according to TimeProfiler // Let's not use it unless neccesary! private func timezoneDateByAdding(minutesToAdd: Int, _ calendar: Calendar?) -> Date { if minutesToAdd == 0 { return Date() } return calendar?.date(byAdding: .minute, value: minutesToAdd, to: Date()) ?? Date() } func date(with sliderValue: Int, displayType: CLDateDisplayType) -> String { var currentCalendar = Calendar(identifier: .gregorian) currentCalendar.locale = Locale.autoupdatingCurrent let convertedDate = timezoneDate(with: sliderValue, currentCalendar) guard let relativeDayPreference = DataStore.shared().retrieve(key: CLRelativeDateKey) as? NSNumber else { assertionFailure("Data was unexpectedly nil") return CLEmptyString } if displayType == CLDateDisplayType.panelDisplay { // Yesterday, tomorrow, etc if relativeDayPreference.intValue == 0 { let localFormatter = DateFormatterManager.localizedSimpleFormatter("EEEE") let local = localFormatter.date(from: localeDate(with: "EEEE")) // Gets local week day number and timezone's week day number for comparison let weekDay = currentCalendar.component(.weekday, from: local!) let timezoneWeekday = currentCalendar.component(.weekday, from: convertedDate) if weekDay == timezoneWeekday + 1 { return "Yesterday\(timeDifference())" } else if weekDay == timezoneWeekday { return "Today\(timeDifference())" } else if weekDay + 1 == timezoneWeekday || weekDay - 6 == timezoneWeekday { return "Tomorrow\(timeDifference())" } else { return "\(weekdayText(from: convertedDate))\(timeDifference())" } } // Day name: Thursday, Friday etc if relativeDayPreference.intValue == 1 { return "\(weekdayText(from: convertedDate))\(timeDifference())" } // Date in mmm/dd if relativeDayPreference.intValue == 2 { return "\(todaysDate(with: sliderValue))\(timeDifference())" } let errorDictionary: [String: Any] = ["Timezone" : dataObject.timezone(), "Current Locale": Locale.autoupdatingCurrent.identifier, "Slider Value": sliderValue, "Today's Date": Date()] Logger.log(object: errorDictionary, for: "Unable to get date") return "Error" } else { return "\(shortWeekdayText(convertedDate))" } } // Returns shortened weekday given a date // For eg. Thu or Thursday, Tues for Tuesday etc private func shortWeekdayText(_ date: Date) -> String { let localizedFormatter = DateFormatterManager.localizedSimpleFormatter("E") return localizedFormatter.string(from: date) } // Returns proper weekday given a date // For eg. Thursday, Sunday, Friday etc private func weekdayText(from date: Date) -> String { let dateFormatter = DateFormatterManager.localizedFormatter(with: "EEEE", for: TimeZone.current.identifier) return dateFormatter.string(from: date) } // Exposed to public for tests! public func timeDifference() -> String { let localFormatter = DateFormatterManager.localizedSimpleFormatter("d MMM yyyy HH:mm:ss") let local = localFormatter.date(from: localeDate(with: "d MMM yyyy HH:mm:ss"))! let newDate = timezoneDateByAdding(minutesToAdd: 0, TimezoneDataOperations.swiftyCalendar) let dateFormatter = DateFormatterManager.localizedFormatter(with: "d MMM yyyy HH:mm:ss", for: dataObject.timezone()) guard let timezoneDate = localFormatter.date(from: dateFormatter.string(from: newDate)) else { let unableToConvertDateParameters = [ "New Date": newDate, "Timezone": dataObject.timezone(), "Locale": dateFormatter.locale.identifier ] as [String: Any] Logger.log(object: unableToConvertDateParameters, for: "Date conversion failure - New Date is nil") return CLEmptyString } let timeDifference = local.timeAgo(since: timezoneDate) if timeDifference.contains("Just now") { return CLEmptyString } if (local as NSDate).earlierDate(timezoneDate) == local { var replaceAgo = CLEmptyString replaceAgo.append(", ") let agoString = timezoneDate.timeAgo(since: local, numericDates: true) replaceAgo.append(agoString.replacingOccurrences(of: "ago", with: CLEmptyString)) let minuteDifference = calculateTimeDifference(with: local as NSDate, timezoneDate: timezoneDate as NSDate) minuteDifference == 0 ? replaceAgo.append("ahead") : replaceAgo.append("\(minuteDifference) mins ahead") return replaceAgo.lowercased() } var replaceAgo = CLEmptyString replaceAgo.append(", ") let replaced = timeDifference.replacingOccurrences(of: "ago", with: CLEmptyString) replaceAgo.append(replaced) let minuteDifference = calculateTimeDifference(with: local as NSDate, timezoneDate: timezoneDate as NSDate) minuteDifference == 0 ? replaceAgo.append("behind") : replaceAgo.append("\(minuteDifference) mins behind") return replaceAgo.lowercased() } private func initializeSunriseSunset(with sliderValue: Int) { let currentDate = nsCalendar.date(byAdding: .minute, value: sliderValue, to: Date()) guard let lat = dataObject.latitude, let long = dataObject.longitude else { assertionFailure("Data was unexpectedly nil.") return } let coordinates: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: lat, longitude: long) guard let dateForCalculation = currentDate, let solar = Solar(for: dateForCalculation, coordinate: coordinates) else { return } if let sunrise = solar.sunrise, let sunset = solar.sunset { dataObject.sunriseTime = sunrise dataObject.sunsetTime = sunset dataObject.isSunriseOrSunset = solar.isNighttime } else { Logger.log(object: ["Unable to fetch sunrise/sunset": dataObject.formattedTimezoneLabel()], for: "Sunrise/Sunset Error") } } private func calculateTimeDifference(with localDate: NSDate, timezoneDate: NSDate) -> Int { let earliest = localDate.earlierDate(timezoneDate as Date) let latest = earliest == localDate as Date ? timezoneDate : localDate // if timeAgo < 24h => compare DateTime else compare Date only let upToHours: Set = [.second, .minute, .hour] let difference = nsCalendar.dateComponents(upToHours, from: earliest, to: latest as Date) return difference.minute! } func formattedSunriseTime(with sliderValue: Int) -> String { /* We have to call this everytime so that we get an updated value everytime! */ if dataObject.selectionType == .timezone || (dataObject.latitude == nil || dataObject.longitude == nil) { return CLEmptyString } initializeSunriseSunset(with: sliderValue) if let sunrise = dataObject.sunriseTime, let sunset = dataObject.sunsetTime { let correct = dataObject.isSunriseOrSunset ? sunrise : sunset let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US") dateFormatter.timeZone = TimeZone(identifier: dataObject.timezone()) dateFormatter.dateFormat = DataStore.shared().shouldDisplay(ViewType.twelveHour) ? "hh:mm a" : "HH:mm" return dateFormatter.string(from: correct) } return CLEmptyString } func todaysDate(with sliderValue: Int, locale: Locale = Locale(identifier: "en-US")) -> String { let newDate = TimezoneDataOperations.gregorianCalendar?.date(byAdding: .minute, value: sliderValue, to: Date(), options: .matchFirst) let date = newDate!.formatter(with: "MMM d", timeZone: dataObject.timezone(), locale: locale) return date } private func localDate() -> String { let dateFormatter = DateFormatterManager.dateFormatter(with: .medium, for: TimeZone.autoupdatingCurrent.identifier) return dateFormatter.string(from: Date()) } private func localeDate(with format: String) -> String { let dateFormatter = DateFormatterManager.localizedFormatter(with: format, for: TimeZone.autoupdatingCurrent.identifier) return dateFormatter.string(from: Date()) } @objc func saveObject(at index: Int = -1) { var defaults = DataStore.shared().timezones() let encodedObject = NSKeyedArchiver.archivedData(withRootObject: dataObject as Any) index == -1 ? defaults.append(encodedObject) : defaults.insert(encodedObject, at: index) DataStore.shared().setTimezones(defaults) } } extension Date { func formatter(with format: String, timeZone: String, locale: Locale = Locale(identifier: "en-US")) -> String { let dateFormatter = DateFormatterManager.dateFormatterWithFormat(with: .medium, format: format, timezoneIdentifier: timeZone, locale: locale) return dateFormatter.string(from: self) } }