@_exported import CoreModels import Dependencies import DependenciesMacros import Foundation public extension DependencyValues { var locationClient: LocationClient { get { self[LocationClient.self] } set { self[LocationClient.self] = newValue } } } @DependencyClient public struct LocationClient: Sendable { /// Estimate the climate zone by the heating design temperature and /// the state code (ex. "OH"). public var estimatedClimateZone: @Sendable (_ heatingTemperature: Double, _ stateCode: String) async throws -> ClimateZone /// Estimate the design temperatures based on location's coordinates. public var estimatedDesignTemperatures: @Sendable (Coordinates) async throws -> DesignTemperatures /// Get location details from a zip code. public var search: @Sendable (_ zipCode: Int) async throws -> [Location] public func estimateDesignTemperaturesAndClimateZone( coordinates: Coordinates, stateCode: String ) async throws -> (DesignTemperatures, ClimateZone) { let designTemperatures = try await estimatedDesignTemperatures(coordinates) let climateZone = try await estimatedClimateZone( heatingTemperature: designTemperatures.heating, stateCode: stateCode ) return (designTemperatures, climateZone) } } struct DecodingError: Error {} extension LocationClient: TestDependencyKey { public static let testValue: LocationClient = Self() } // Intermediate response returned from 'https://app.zipcodebase.com' private struct IntermediateResponse: Codable, Equatable, Sendable { let city: String let latitude: String let longitude: String let zipCode: String let state: String let stateCode: String let county: String func toLocation() throws -> Location { guard let longitude = Double(longitude), let latitude = Double(latitude) else { throw DecodingError() } return .init( city: city, state: state, stateCode: stateCode, zipCode: zipCode, county: county, coordinates: .init(latitude: latitude, longitude: longitude) ) } private enum CodingKeys: String, CodingKey { case city case latitude case longitude case zipCode = "postal_code" case state case stateCode = "state_code" case county = "province" } } // TODO: Need to add climate zone 7 (-17). private func estimatedHeatingTemperature(_ coordinates: Coordinates) -> Double { let latitude = coordinates.latitude let longitude = coordinates.longitude // Gulf coast region if latitude < 31, longitude > -95 && longitude < -89 { return 35 } // Florida if latitude < 31, longitude > -87.5 && longitude < -80 { return 40 } let absLat = abs(latitude) if absLat < 30 { return 35 } if absLat < 33 { return 30 } if absLat < 36 { return 25 } if absLat < 40 { return 15 } if absLat < 45 { return 5 } return 0 } // TODO: Need to add climate zone 7 (89). private func estimatedCoolingTemperature(_ coordinates: Coordinates) -> Double { let latitude = coordinates.latitude let longitude = coordinates.longitude // Gulf coast and Florida if latitude < 31, longitude > -95 && longitude < -80 { return 95 } let absLat = abs(latitude) if absLat < 30 { return 95 } if absLat < 33 { return 92 } if absLat < 36 { return 90 } if absLat < 40 { return 90 } if absLat < 45 { return 88 } return 85 } // TODO: Need to add climate zone 7. private func determineClimateZone(heatingTemperature: Double, stateCode: String) -> ClimateZone { let hotHumidStates = ["FL", "LA", "TX", "MS", "AL", "GA", "SC"] if hotHumidStates.contains(stateCode.uppercased()) { return .two } if heatingTemperature >= 45 { return .one } if heatingTemperature >= 35 { return .two } if heatingTemperature >= 25 { return .three } if heatingTemperature >= 15 { return .four } if heatingTemperature >= 5 { return .five } return .six } #if DEBUG extension IntermediateResponse { static let mock = Self( city: "Monroe", latitude: "39.4413000", longitude: "-84.3652000", zipCode: "45050", state: "Ohio", stateCode: "OH", county: "Butler" ) } #endif